feat: search by keyword on AccessList, CertificateList, WorkflowList
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
EditOutlined as EditOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
ReloadOutlined as ReloadOutlinedIcon,
|
||||
SnippetsOutlined as SnippetsOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Avatar, Button, Empty, Modal, Space, Table, type TableProps, Tooltip, Typography, notification } from "antd";
|
||||
import { Avatar, Button, Card, Empty, Flex, Input, Modal, Space, Table, type TableProps, Tooltip, Typography, notification } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
@@ -20,6 +22,8 @@ import { useAccessesStore } from "@/stores/access";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
const AccessList = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
@@ -116,6 +120,12 @@ const AccessList = () => {
|
||||
const [tableData, setTableData] = useState<AccessModel[]>([]);
|
||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
};
|
||||
});
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
@@ -134,14 +144,21 @@ const AccessList = () => {
|
||||
() => {
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const items = accesses.slice(startIndex, endIndex);
|
||||
const list = accesses.filter((e) => {
|
||||
const keyword = (filters["keyword"] as string | undefined)?.trim();
|
||||
if (keyword) {
|
||||
return e.name.includes(keyword);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return Promise.resolve({
|
||||
items,
|
||||
totalItems: accesses.length,
|
||||
items: list.slice(startIndex, endIndex),
|
||||
totalItems: list.length,
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [accesses, page, pageSize],
|
||||
refreshDeps: [accesses, filters, page, pageSize],
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
setTableTotal(res.totalItems);
|
||||
@@ -149,6 +166,16 @@ const AccessList = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setFilters((prev) => ({ ...prev, keyword: value }));
|
||||
};
|
||||
|
||||
const handleReloadClick = () => {
|
||||
if (loading) return;
|
||||
|
||||
fetchAccesses();
|
||||
};
|
||||
|
||||
const handleDeleteClick = async (data: AccessModel) => {
|
||||
modalApi.confirm({
|
||||
title: t("access.action.delete"),
|
||||
@@ -186,30 +213,43 @@ const AccessList = () => {
|
||||
]}
|
||||
/>
|
||||
|
||||
<Table<AccessModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={!loadedAtOnce || loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("access.nodata")} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
<Card size="small">
|
||||
<div className="mb-4">
|
||||
<Flex gap="small">
|
||||
<div className="flex-1">
|
||||
<Input.Search allowClear defaultValue={filters["keyword"] as string} placeholder={t("access.search.placeholder")} onSearch={handleSearch} />
|
||||
</div>
|
||||
<div>
|
||||
<Button icon={<ReloadOutlinedIcon spin={loading} />} onClick={handleReloadClick} />
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Table<AccessModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={!loadedAtOnce || loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("access.nodata")} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons";
|
||||
import { DeleteOutlined as DeleteOutlinedIcon, ReloadOutlined as ReloadOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useRequest } from "ahooks";
|
||||
import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Empty,
|
||||
Flex,
|
||||
Input,
|
||||
Menu,
|
||||
type MenuProps,
|
||||
Modal,
|
||||
Radio,
|
||||
Space,
|
||||
Table,
|
||||
type TableProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
notification,
|
||||
theme,
|
||||
} from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
@@ -191,6 +209,7 @@ const CertificateList = () => {
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
state: searchParams.get("state"),
|
||||
};
|
||||
});
|
||||
@@ -205,9 +224,10 @@ const CertificateList = () => {
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listCertificate({
|
||||
keyword: filters["keyword"] as string,
|
||||
state: filters["state"] as ListCertificateRequest["state"],
|
||||
page: page,
|
||||
perPage: pageSize,
|
||||
state: filters["state"] as ListCertificateRequest["state"],
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -229,6 +249,16 @@ const CertificateList = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setFilters((prev) => ({ ...prev, keyword: value.trim() }));
|
||||
};
|
||||
|
||||
const handleReloadClick = () => {
|
||||
if (loading) return;
|
||||
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleDeleteClick = (certificate: CertificateModel) => {
|
||||
modalApi.confirm({
|
||||
title: t("certificate.action.delete"),
|
||||
@@ -255,30 +285,43 @@ const CertificateList = () => {
|
||||
|
||||
<PageHeader title={t("certificate.page.title")} />
|
||||
|
||||
<Table<CertificateModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("certificate.nodata"))} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
<Card size="small">
|
||||
<div className="mb-4">
|
||||
<Flex gap="small">
|
||||
<div className="flex-1">
|
||||
<Input.Search allowClear defaultValue={filters["keyword"] as string} placeholder={t("certificate.search.placeholder")} onSearch={handleSearch} />
|
||||
</div>
|
||||
<div>
|
||||
<Button icon={<ReloadOutlinedIcon spin={loading} />} onClick={handleReloadClick} />
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Table<CertificateModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("certificate.nodata"))} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
DeleteOutlined as DeleteOutlinedIcon,
|
||||
EditOutlined as EditOutlinedIcon,
|
||||
PlusOutlined as PlusOutlinedIcon,
|
||||
ReloadOutlined as ReloadOutlinedIcon,
|
||||
StopOutlined as StopOutlinedIcon,
|
||||
SyncOutlined as SyncOutlinedIcon,
|
||||
} from "@ant-design/icons";
|
||||
@@ -16,8 +17,11 @@ import { PageHeader } from "@ant-design/pro-components";
|
||||
import { useRequest } from "ahooks";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Empty,
|
||||
Flex,
|
||||
Input,
|
||||
Menu,
|
||||
type MenuProps,
|
||||
Modal,
|
||||
@@ -235,6 +239,7 @@ const WorkflowList = () => {
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
state: searchParams.get("state"),
|
||||
};
|
||||
});
|
||||
@@ -249,9 +254,10 @@ const WorkflowList = () => {
|
||||
} = useRequest(
|
||||
() => {
|
||||
return listWorkflow({
|
||||
keyword: filters["keyword"] as string,
|
||||
enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined,
|
||||
page: page,
|
||||
perPage: pageSize,
|
||||
enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined,
|
||||
});
|
||||
},
|
||||
{
|
||||
@@ -273,10 +279,20 @@ const WorkflowList = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setFilters((prev) => ({ ...prev, keyword: value.trim() }));
|
||||
};
|
||||
|
||||
const handleCreateClick = () => {
|
||||
navigate("/workflows/new");
|
||||
};
|
||||
|
||||
const handleReloadClick = () => {
|
||||
if (loading) return;
|
||||
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
||||
try {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
@@ -345,30 +361,43 @@ const WorkflowList = () => {
|
||||
]}
|
||||
/>
|
||||
|
||||
<Table<WorkflowModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("workflow.nodata"))} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
<Card size="small">
|
||||
<div className="mb-4">
|
||||
<Flex gap="small">
|
||||
<div className="flex-1">
|
||||
<Input.Search allowClear defaultValue={filters["keyword"] as string} placeholder={t("workflow.search.placeholder")} onSearch={handleSearch} />
|
||||
</div>
|
||||
<div>
|
||||
<Button icon={<ReloadOutlinedIcon spin={loading} />} onClick={handleReloadClick} />
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Table<WorkflowModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("workflow.nodata"))} />,
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: pageSize,
|
||||
total: tableTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
onShowSizeChange: (page: number, pageSize: number) => {
|
||||
setPage(page);
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user