mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 07:11:00 +00:00
feat: add debounced search input and clear filter functionality to PersonalDocumentsCard
This commit is contained in:
@@ -6,13 +6,16 @@ import {
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { IconEdit, IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconEdit, IconPlus, IconSearch, IconTrash, IconX } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AdvancedTable,
|
||||
@@ -41,6 +44,8 @@ type DraftRequirement = Omit<DocumentRequirement, 'id' | 'licenseTypeId' | 'isAc
|
||||
/** Filter value for "the documents every licence asks for". */
|
||||
const GLOBAL_ONLY = 'GLOBAL';
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 300;
|
||||
|
||||
/** `application/vnd.openxmlformats-…-document` is nobody's idea of a column. */
|
||||
const MIME_LABELS: Record<string, string> = {
|
||||
'application/pdf': 'PDF',
|
||||
@@ -93,14 +98,17 @@ export function PersonalDocumentsCard() {
|
||||
/** null = any licence, GLOBAL_ONLY = the all-licence ones, else a type id. */
|
||||
const [licenseTypeFilter, setLicenseTypeFilter] = useState<string | null>(null);
|
||||
|
||||
const { q, setQ, pageIndex, setPageIndex, pageSize, setPageSize } = useServerTable({
|
||||
const { pageIndex, setPageIndex, pageSize, setPageSize } = useServerTable({
|
||||
pageSize: 10,
|
||||
});
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
// Typing must not fire a request per keystroke; same 300ms as the queue.
|
||||
const [search] = useDebouncedValue(searchInput, SEARCH_DEBOUNCE_MS);
|
||||
|
||||
// Every facet goes to the server: it filters and searches in SQL, groups the
|
||||
// rows into documents, then pages the documents.
|
||||
const { data, isFetching, refetch } = useGetPersonalDocumentsQuery({
|
||||
search: q.trim() || undefined,
|
||||
search: search.trim() || undefined,
|
||||
licenseTypeId:
|
||||
licenseTypeFilter && licenseTypeFilter !== GLOBAL_ONLY
|
||||
? licenseTypeFilter
|
||||
@@ -126,7 +134,13 @@ export function PersonalDocumentsCard() {
|
||||
);
|
||||
|
||||
/** Filters are the server's business now; an empty page is its answer. */
|
||||
const isFiltered = q.trim() !== '' || licenseTypeFilter !== null;
|
||||
const isFiltered = search.trim() !== '' || licenseTypeFilter !== null;
|
||||
|
||||
function clearFilters() {
|
||||
setSearchInput('');
|
||||
setLicenseTypeFilter(null);
|
||||
setPageIndex(0);
|
||||
}
|
||||
|
||||
const typeName = (id: string) => {
|
||||
const found = (licenseTypes?.items ?? []).find((lt) => lt.id === id);
|
||||
@@ -298,25 +312,24 @@ export function PersonalDocumentsCard() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<AdvancedTable
|
||||
tableName={t('certReq.personal.title', 'Personal documents')}
|
||||
columns={columns}
|
||||
data={groups}
|
||||
itemCount={data?.total ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
onSearchChange={setQ}
|
||||
refresh={refetch}
|
||||
isLoading={isFetching}
|
||||
emptyText={
|
||||
isFiltered
|
||||
? t('certReq.personal.noMatch', 'No personal document matches those filters.')
|
||||
: t('certReq.personal.empty', 'No personal documents configured yet.')
|
||||
}
|
||||
toolbar={
|
||||
{/* Facets, in the shape the licence-review queue uses. No date range:
|
||||
a configuration row has no submission date to filter on. */}
|
||||
<Paper withBorder p="sm">
|
||||
<Group gap="sm" align="flex-end" wrap="wrap">
|
||||
<TextInput
|
||||
label={t('certReq.personal.search', 'Search')}
|
||||
placeholder={t('certReq.personal.searchPlaceholder', 'Search by name or key')}
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={searchInput}
|
||||
onChange={(e) => {
|
||||
const { value } = e.currentTarget;
|
||||
setSearchInput(value);
|
||||
setPageIndex(0);
|
||||
}}
|
||||
w={240}
|
||||
/>
|
||||
<Select
|
||||
label={t('certReq.doc.scope', 'Applies to')}
|
||||
placeholder={t('certReq.personal.filterAny', 'Any licence type')}
|
||||
data={[
|
||||
{
|
||||
@@ -336,9 +349,35 @@ export function PersonalDocumentsCard() {
|
||||
}}
|
||||
searchable
|
||||
clearable
|
||||
size="sm"
|
||||
w={240}
|
||||
/>
|
||||
{isFiltered && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
leftSection={<IconX size={14} />}
|
||||
onClick={clearFilters}
|
||||
>
|
||||
{t('certReq.personal.clearFilters', 'Clear')}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<AdvancedTable
|
||||
tableName={t('certReq.personal.title', 'Personal documents')}
|
||||
columns={columns}
|
||||
data={groups}
|
||||
itemCount={data?.total ?? 0}
|
||||
pageIndex={pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
refresh={refetch}
|
||||
isLoading={isFetching}
|
||||
emptyText={
|
||||
isFiltered
|
||||
? t('certReq.personal.noMatch', 'No personal document matches those filters.')
|
||||
: t('certReq.personal.empty', 'No personal documents configured yet.')
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1374,6 +1374,7 @@ export const am: Translations = {
|
||||
"አመልካቹ በእያንዳንዱ ማመልከቻ ላይ ከመስቀል ይልቅ በ\u2018ሰነዶቼ\u2019 ውስጥ አንድ ጊዜ የሚያስቀምጣቸው የማንነትና የትምህርት ሰነዶች። ለተወሰኑ የፈቃድ አይነቶች ከወሰኑት፣ እነዚያን የሥራ ዘርፍ ያሳወቁ አመልካቾች ብቻ ይጠየቃሉ።",
|
||||
add: "የግል ሰነድ ጨምር",
|
||||
empty: "እስካሁን የተዋቀረ የግል ሰነድ የለም።",
|
||||
search: "ፍለጋ",
|
||||
searchPlaceholder: "በስም ወይም በቁልፍ ይፈልጉ",
|
||||
moreTypes: " +{{count}} ተጨማሪ",
|
||||
columns: {
|
||||
@@ -1383,7 +1384,7 @@ export const am: Translations = {
|
||||
filterAny: "ማንኛውም የፈቃድ አይነት",
|
||||
filterGlobal: "ለሁሉም ፈቃዶች የሚሆኑ ብቻ",
|
||||
noMatch: "በእነዚህ ማጣሪያዎች የሚመጣጠን የግል ሰነድ የለም።",
|
||||
clearFilters: "ማጣሪያዎችን አጽዳ",
|
||||
clearFilters: "አጽዳ",
|
||||
fileCount_one: "{{count}} ፋይል",
|
||||
fileCount_other: "{{count}} ፋይሎች",
|
||||
deleteWarning:
|
||||
|
||||
@@ -1380,6 +1380,7 @@ export const en = {
|
||||
'Identity and education documents an applicant keeps once, in My Documents, instead of uploading with every application. Scope one to licence types and only applicants who declared those modes of operation are asked for it.',
|
||||
add: 'Add personal document',
|
||||
empty: 'No personal documents configured yet.',
|
||||
search: 'Search',
|
||||
searchPlaceholder: 'Search by name or key',
|
||||
moreTypes: ' +{{count}} more',
|
||||
columns: {
|
||||
@@ -1389,7 +1390,7 @@ export const en = {
|
||||
filterAny: 'Any licence type',
|
||||
filterGlobal: 'All-licence documents only',
|
||||
noMatch: 'No personal document matches those filters.',
|
||||
clearFilters: 'Clear filters',
|
||||
clearFilters: 'Clear',
|
||||
fileCount_one: '{{count}} file',
|
||||
fileCount_other: '{{count}} files',
|
||||
deleteWarning:
|
||||
|
||||
Reference in New Issue
Block a user