From d69549b74773e889b52ce1e80e49aadf0e31166b Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:12:49 +0700 Subject: [PATCH] feat: implement server-side search, filter state management, and clearable inputs for data tables with associated API transformer updates. --- .../components/table-filter-drawer.tsx | 9 +- .../components/data-table/index.tsx | 104 ++++++++++++++++-- .../components/module-page-header/index.tsx | 3 +- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx index 1550ea0..059be51 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/components/table-filter-drawer.tsx @@ -92,7 +92,14 @@ export function TableFilterDrawer({ const handleReset = useCallback(() => { if (hasForm) { setPreviousValues(form.getValues()); - form.reset(config?.defaultValues ?? {}); + + // Ensure all fields are explicitly cleared + const cleared = Object.keys(form.getValues()).reduce((acc, key) => { + acc[key] = ''; + return acc; + }, {} as Record); + + form.reset({ ...cleared, ...(config?.defaultValues || {}) }); setHasReset(true); } }, [hasForm, form, config]); diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx index 62a047e..e647e1c 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx @@ -11,7 +11,7 @@ import { DefaultMenuItem, StatusBar, } from 'ag-grid-community'; -import { Box, Group, TextInput } from '@mantine/core'; +import { Box, Group, TextInput, Indicator, CloseButton } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { Search, Filter, Settings } from 'lucide-react'; @@ -122,6 +122,12 @@ export interface EnterpriseDataTableProps extends Omit void; onBulkClickHold?: (data: E[]) => void; + /** + * The query parameter key used for search. + * @default 'q' + */ + searchKey?: string; + /** * Configuration for the Filter Drawer */ @@ -165,6 +171,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT cancelModalConfig, rollbackModalConfig, holdModalConfig, + searchKey = 'q', filterConfig, // Bulk action props @@ -203,6 +210,58 @@ export function EnterpriseDataTable(props: EnterpriseDataT // Reference to the AG Grid API for programmatic interaction const gridApiRef = useRef | null>(null); + // --------------------------------------------------------------------------- + // Search & Filter State + // --------------------------------------------------------------------------- + const searchRef = useRef((filterData?.[searchKey] as string) || ''); + const filterRef = useRef>((() => { + if (!filterData) return {}; + const copy = { ...filterData }; + delete copy[searchKey]; + return copy; + })()); + + const [searchValue, setSearchValue] = useState(searchRef.current); + + const handleSearchChange = useCallback((e: React.ChangeEvent) => { + setSearchValue(e.currentTarget.value); + }, []); + + const handleSearchKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + searchRef.current = searchValue; + gridApiRef.current?.refreshServerSide({ purge: true }); + } + }, [searchValue]); + + const handleSearchClear = useCallback(() => { + setSearchValue(''); + searchRef.current = ''; + gridApiRef.current?.refreshServerSide({ purge: true }); + }, []); + + const activeFilterCount = useMemo(() => { + if (!filterData) return 0; + + const filterKeys = filterConfig?.defaultValues + ? Object.keys(filterConfig.defaultValues) + : Object.keys(filterData).filter( + (key) => key !== searchKey && !['page', 'limit', 'order_by', 'order_type'].includes(key) + ); + + return filterKeys.filter((key) => { + const val = filterData[key]; + if (val === undefined || val === null || val === '') return false; + if (Array.isArray(val) && val.length === 0) return false; + return true; + }).length; + }, [filterData, searchKey, filterConfig]); + + const handleFilterApply = useCallback((data: any) => { + filterRef.current = data || {}; + gridApiRef.current?.refreshServerSide({ purge: true }); + }, []); + // --------------------------------------------------------------------------- // Action Handlers & Modal State // --------------------------------------------------------------------------- @@ -619,8 +678,8 @@ export function EnterpriseDataTable(props: EnterpriseDataT try { const request = params.request; - // Calculate the current page based on the start row and per-page limit - const page = Math.floor((request.startRow ?? 0) / perPage) + 1; + const limit = perPage + const page = Math.floor((request.startRow ?? 0) / limit) + 1; // Extract sorting information from the request const sortModel = request.sortModel[0]; @@ -628,7 +687,18 @@ export function EnterpriseDataTable(props: EnterpriseDataT const orderType = sortModel?.sort?.toUpperCase(); // Prepare the request parameters for the API call - const requestParams = { page, limit: perPage, order_by: orderBy, order_type: orderType }; + const requestParams: Record = { + page, + limit, + order_by: orderBy, + order_type: orderType, + ...filterRef.current + }; + + if (searchRef.current) { + requestParams[searchKey] = searchRef.current; + } + const response = await dataServices.getMany({ params: requestParams }); if (!response.data?.data) throw new Error('Invalid response'); @@ -639,6 +709,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT // Update the global metadata state setMetaData(meta); + setFilterData({ ...filterRef.current, [searchKey]: searchRef.current }); // Pass the retrieved data back to AG Grid params.success({ rowData, rowCount }); @@ -649,7 +720,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT } }, }), - [dataServices, perPage, setMetaData, t], + [dataServices, perPage, setMetaData, setFilterData, searchKey, t], ); // --------------------------------------------------------------------------- @@ -713,13 +784,30 @@ export function EnterpriseDataTable(props: EnterpriseDataT w={{ base: '100%', sm: 350 }} placeholder={t('common:searchPlaceholder')} leftSection={} + rightSection={ + searchValue ? ( + e.preventDefault()} + onClick={handleSearchClear} + aria-label={t('common:actions.clear')} + /> + ) : null + } + value={searchValue} + onChange={handleSearchChange} + onKeyDown={handleSearchKeyDown} /> , + icon: ( + + + + ), variant: 'default', showLabel: false, tooltipLabel: t('common:actions.filter'), @@ -799,9 +887,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT title={t('common:filterTitle', { module: moduleTitle })} config={filterConfig} currentFilterData={filterData} - onFilter={(data) => { - console.log('Filter applied:', data); - }} + onFilter={handleFilterApply} /> + {item.label} ) : (