feat: implement server-side search, filter state management, and clearable inputs for data tables with associated API transformer updates.
This commit is contained in:
+8
-1
@@ -92,7 +92,14 @@ export function TableFilterDrawer({
|
|||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
if (hasForm) {
|
if (hasForm) {
|
||||||
setPreviousValues(form.getValues());
|
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<string, unknown>);
|
||||||
|
|
||||||
|
form.reset({ ...cleared, ...(config?.defaultValues || {}) });
|
||||||
setHasReset(true);
|
setHasReset(true);
|
||||||
}
|
}
|
||||||
}, [hasForm, form, config]);
|
}, [hasForm, form, config]);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
DefaultMenuItem,
|
DefaultMenuItem,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
} from 'ag-grid-community';
|
} 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 { useDisclosure } from '@mantine/hooks';
|
||||||
import { Search, Filter, Settings } from 'lucide-react';
|
import { Search, Filter, Settings } from 'lucide-react';
|
||||||
|
|
||||||
@@ -122,6 +122,12 @@ export interface EnterpriseDataTableProps<E extends BaseEntity> extends Omit<AgG
|
|||||||
onBulkClickRollback?: (data: E[]) => void;
|
onBulkClickRollback?: (data: E[]) => void;
|
||||||
onBulkClickHold?: (data: E[]) => void;
|
onBulkClickHold?: (data: E[]) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query parameter key used for search.
|
||||||
|
* @default 'q'
|
||||||
|
*/
|
||||||
|
searchKey?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the Filter Drawer
|
* Configuration for the Filter Drawer
|
||||||
*/
|
*/
|
||||||
@@ -165,6 +171,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
cancelModalConfig,
|
cancelModalConfig,
|
||||||
rollbackModalConfig,
|
rollbackModalConfig,
|
||||||
holdModalConfig,
|
holdModalConfig,
|
||||||
|
searchKey = 'q',
|
||||||
filterConfig,
|
filterConfig,
|
||||||
|
|
||||||
// Bulk action props
|
// Bulk action props
|
||||||
@@ -203,6 +210,58 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
// Reference to the AG Grid API for programmatic interaction
|
// Reference to the AG Grid API for programmatic interaction
|
||||||
const gridApiRef = useRef<GridApi<E> | null>(null);
|
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
|
// Action Handlers & Modal State
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -619,8 +678,8 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
try {
|
try {
|
||||||
const request = params.request;
|
const request = params.request;
|
||||||
|
|
||||||
// Calculate the current page based on the start row and per-page limit
|
const limit = perPage
|
||||||
const page = Math.floor((request.startRow ?? 0) / perPage) + 1;
|
const page = Math.floor((request.startRow ?? 0) / limit) + 1;
|
||||||
|
|
||||||
// Extract sorting information from the request
|
// Extract sorting information from the request
|
||||||
const sortModel = request.sortModel[0];
|
const sortModel = request.sortModel[0];
|
||||||
@@ -628,7 +687,18 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
const orderType = sortModel?.sort?.toUpperCase();
|
const orderType = sortModel?.sort?.toUpperCase();
|
||||||
|
|
||||||
// Prepare the request parameters for the API call
|
// 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 });
|
const response = await dataServices.getMany({ params: requestParams });
|
||||||
|
|
||||||
if (!response.data?.data) throw new Error('Invalid response');
|
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
|
// Update the global metadata state
|
||||||
setMetaData(meta);
|
setMetaData(meta);
|
||||||
|
setFilterData({ ...filterRef.current, [searchKey]: searchRef.current });
|
||||||
|
|
||||||
// Pass the retrieved data back to AG Grid
|
// Pass the retrieved data back to AG Grid
|
||||||
params.success({ rowData, rowCount });
|
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 }}
|
w={{ base: '100%', sm: 350 }}
|
||||||
placeholder={t('common:searchPlaceholder')}
|
placeholder={t('common:searchPlaceholder')}
|
||||||
leftSection={<Search size={16} />}
|
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
|
<PageActions
|
||||||
actions={[
|
actions={[
|
||||||
{ type: 'divider', key: 'search-divider' },
|
{ type: 'divider', key: 'search-divider' },
|
||||||
{
|
{
|
||||||
key: 'filter',
|
key: 'filter',
|
||||||
icon: <Filter size={16} />,
|
icon: (
|
||||||
|
<Indicator disabled={activeFilterCount === 0} size={8} offset={2}>
|
||||||
|
<Filter size={16} />
|
||||||
|
</Indicator>
|
||||||
|
),
|
||||||
variant: 'default',
|
variant: 'default',
|
||||||
showLabel: false,
|
showLabel: false,
|
||||||
tooltipLabel: t('common:actions.filter'),
|
tooltipLabel: t('common:actions.filter'),
|
||||||
@@ -799,9 +887,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
title={t('common:filterTitle', { module: moduleTitle })}
|
title={t('common:filterTitle', { module: moduleTitle })}
|
||||||
config={filterConfig}
|
config={filterConfig}
|
||||||
currentFilterData={filterData}
|
currentFilterData={filterData}
|
||||||
onFilter={(data) => {
|
onFilter={handleFilterApply}
|
||||||
console.log('Filter applied:', data);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableSettingDrawer
|
<TableSettingDrawer
|
||||||
|
|||||||
+2
-1
@@ -13,6 +13,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
||||||
import { PageActions, PageActionsProps } from '../../../../components';
|
import { PageActions, PageActionsProps } from '../../../../components';
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ function BreadcrumbBar({ breadcrumbs }: BreadcrumbBarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return !isText ? (
|
return !isText ? (
|
||||||
<Anchor key={index} {...sharedProps} href={item.href}>
|
<Anchor component={Link} key={index} {...sharedProps} to={item.href || '#'}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user