feat: implement server-side search, filter state management, and clearable inputs for data tables with associated API transformer updates.
This commit is contained in:
@@ -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<E extends BaseEntity> extends Omit<AgG
|
||||
onBulkClickRollback?: (data: E[]) => 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<E extends BaseEntity>(props: EnterpriseDataT
|
||||
cancelModalConfig,
|
||||
rollbackModalConfig,
|
||||
holdModalConfig,
|
||||
searchKey = 'q',
|
||||
filterConfig,
|
||||
|
||||
// Bulk action props
|
||||
@@ -203,6 +210,58 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
// Reference to the AG Grid API for programmatic interaction
|
||||
const gridApiRef = useRef<GridApi<E> | null>(null);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search & Filter State
|
||||
// ---------------------------------------------------------------------------
|
||||
const searchRef = useRef<string>((filterData?.[searchKey] as string) || '');
|
||||
const filterRef = useRef<Record<string, any>>((() => {
|
||||
if (!filterData) return {};
|
||||
const copy = { ...filterData };
|
||||
delete copy[searchKey];
|
||||
return copy;
|
||||
})());
|
||||
|
||||
const [searchValue, setSearchValue] = useState(searchRef.current);
|
||||
|
||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchValue(e.currentTarget.value);
|
||||
}, []);
|
||||
|
||||
const handleSearchKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
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<E extends BaseEntity>(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<E extends BaseEntity>(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<string, any> = {
|
||||
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<E extends BaseEntity>(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<E extends BaseEntity>(props: EnterpriseDataT
|
||||
}
|
||||
},
|
||||
}),
|
||||
[dataServices, perPage, setMetaData, t],
|
||||
[dataServices, perPage, setMetaData, setFilterData, searchKey, t],
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -713,13 +784,30 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
w={{ base: '100%', sm: 350 }}
|
||||
placeholder={t('common:searchPlaceholder')}
|
||||
leftSection={<Search size={16} />}
|
||||
rightSection={
|
||||
searchValue ? (
|
||||
<CloseButton
|
||||
size="sm"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={handleSearchClear}
|
||||
aria-label={t('common:actions.clear')}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
value={searchValue}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
/>
|
||||
<PageActions
|
||||
actions={[
|
||||
{ type: 'divider', key: 'search-divider' },
|
||||
{
|
||||
key: 'filter',
|
||||
icon: <Filter size={16} />,
|
||||
icon: (
|
||||
<Indicator disabled={activeFilterCount === 0} size={8} offset={2}>
|
||||
<Filter size={16} />
|
||||
</Indicator>
|
||||
),
|
||||
variant: 'default',
|
||||
showLabel: false,
|
||||
tooltipLabel: t('common:actions.filter'),
|
||||
@@ -799,9 +887,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
||||
title={t('common:filterTitle', { module: moduleTitle })}
|
||||
config={filterConfig}
|
||||
currentFilterData={filterData}
|
||||
onFilter={(data) => {
|
||||
console.log('Filter applied:', data);
|
||||
}}
|
||||
onFilter={handleFilterApply}
|
||||
/>
|
||||
|
||||
<TableSettingDrawer
|
||||
|
||||
Reference in New Issue
Block a user