feat: add debounced search input and clear filter functionality to PersonalDocumentsCard

This commit is contained in:
estifanos
2026-08-28 11:28:41 +00:00
parent bf083bff82
commit 664558146d
3 changed files with 66 additions and 25 deletions

View File

@@ -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.')
}
/>