feat: add EnterpriseDataTable component, integrate standard pagination, and update AgGridProvider for enhanced data handling

This commit is contained in:
Firman Ramdhani
2026-07-21 18:52:37 +07:00
parent bd88ed53a4
commit 058a571866
6 changed files with 251 additions and 13 deletions
@@ -0,0 +1,234 @@
import { useMemo, useCallback, useRef } from 'react';
import { AgGridReactProps } from 'ag-grid-react';
import {
ColDef,
GridReadyEvent,
GridApi,
IServerSideDatasource,
IServerSideGetRowsParams,
SelectionChangedEvent,
GetContextMenuItemsParams,
MenuItemDef,
DefaultMenuItem,
StatusBar,
} from 'ag-grid-community';
import { Box } from '@mantine/core';
import { DataGrid } from '../../../../components';
import {
useEnterpriseModuleDataServiceContext,
useEnterpriseModuleSelectionContext,
useEnterpriseModuleTranslationContext,
} from '../../hooks/use-module.context';
import { BaseEntity } from '@repo/core-api/data-services';
import { notifications } from '@mantine/notifications';
export * from 'ag-grid-community';
export * from 'ag-grid-react';
// ---------------------------------------------------------------------------
// Types & Interfaces
// ---------------------------------------------------------------------------
export type PaginationMode = 'pagination' | 'infinite-scroll';
export interface EnterpriseDataTableProps<E extends BaseEntity> extends Omit<AgGridReactProps<E>, 'rowData'> {
columnDefs: ColDef<E>[];
gridHeight?: number | string;
/**
* Controls the data navigation strategy:
* - `'pagination'` Manual page navigation with Previous/Next buttons (default).
* - `'infinite-scroll'` Rows are lazily loaded as the user scrolls, leveraging
* AG Grid's server-side infinite row model.
*
* Both modes use the same server-side datasource under the hood.
* @default 'pagination'
*/
paginationMode?: PaginationMode;
/**
* Custom message displayed in the loading overlay while data is being fetched.
* @default 'Loading data...'
*/
loadingMessage?: string;
/**
* Custom message displayed when the datasource returns zero rows.
* @default 'No records found'
*/
noRowsMessage?: string;
showStatusbar?: boolean;
}
// ---------------------------------------------------------------------------
// Main Component
// ---------------------------------------------------------------------------
export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataTableProps<E>) {
// ---------------------------------------------------------------------------
// Component Props Destructuring
// ---------------------------------------------------------------------------
const {
columnDefs,
gridHeight = 600,
paginationMode = 'pagination',
loadingMessage = 'Loading data...',
noRowsMessage = 'No records found',
showStatusbar,
...restAgGridProps
} = props;
// ---------------------------------------------------------------------------
// Context & Hooks
// ---------------------------------------------------------------------------
const { t } = useEnterpriseModuleTranslationContext();
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
const { setSelectedRows, setFilterData, metaData, setMetaData } = useEnterpriseModuleSelectionContext<E>();
// ---------------------------------------------------------------------------
// Local UI State
// ---------------------------------------------------------------------------
// Reference to the AG Grid API for programmatic interaction
const gridApiRef = useRef<GridApi<E> | null>(null);
// ---------------------------------------------------------------------------
// Derived State & Configuration
// ---------------------------------------------------------------------------
// Determine the number of rows per page based on metadata, defaulting to 10
const perPage = useMemo(() => {
return metaData?.limit ?? 10;
}, [metaData]);
// Boolean flag to check if the current mode is pagination
const isPaginated = paginationMode === 'pagination';
// Ensure column definitions are referentially stable
const finalColumnDefs = useMemo<ColDef<E>[]>(() => {
return columnDefs;
}, [columnDefs]);
// Default configuration applied to all columns in the grid
const defaultColDef = useMemo<ColDef>(() => ({ flex: 1, minWidth: 100, sortable: true, resizable: true }), []);
// ---------------------------------------------------------------------------
// Data Source (Server-Side Row Model)
// ---------------------------------------------------------------------------
// Configures the server-side datasource to handle data fetching, pagination, and sorting
const datasource: IServerSideDatasource = useMemo(
() => ({
getRows: async (params: IServerSideGetRowsParams) => {
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;
// Extract sorting information from the request
const sortModel = request.sortModel[0];
const orderBy = sortModel?.colId;
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 response = await dataServices.getMany({ params: requestParams });
if (!response.data?.data) throw new Error('Invalid response');
const rowData = response.data.data;
const meta = response.data.meta;
const rowCount = meta?.total || 0;
// Update the global metadata state
setMetaData(meta);
// Pass the retrieved data back to AG Grid
params.success({ rowData, rowCount });
} catch (error: any) {
// Display an error notification if the request fails
notifications.show({ title: t('common:notifications.errorTitle'), message: error?.message, color: 'red' });
params.fail();
}
},
}),
[dataServices, perPage, setMetaData, t],
);
// ---------------------------------------------------------------------------
// Event Handlers
// ---------------------------------------------------------------------------
// Triggered when the grid is initialized and ready
const onGridReady = useCallback(
(params: GridReadyEvent<E>) => {
gridApiRef.current = params.api;
setSelectedRows([]);
if (params.api) {
// Attach the server-side datasource to the grid API
params.api.setGridOption('serverSideDatasource', datasource);
}
},
[datasource, setSelectedRows],
);
// Triggered whenever the row selection in the grid changes
const handleSelectionChanged = useCallback(
(event: SelectionChangedEvent<E>) => {
const selectedData = event.api.getSelectedRows();
// Forward full rows to the context for backward compatibility with consumers
setSelectedRows(selectedData);
},
[setSelectedRows],
);
// Configures the context menu items available when right-clicking a cell
const getContextMenuItems = useCallback(
(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
params: GetContextMenuItemsParams,
): (DefaultMenuItem | MenuItemDef)[] | Promise<(DefaultMenuItem | MenuItemDef)[]> => {
return ['copy', 'copyWithHeaders'];
},
[],
);
const statusBar = useMemo<StatusBar | undefined>(() => {
if (!showStatusbar) return undefined;
return { statusPanels: [{ statusPanel: 'agSelectedRowCountComponent', align: 'left' }] };
}, [showStatusbar]);
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
return (
<Box>
{/* GRID CONTAINER */}
<Box style={{ height: gridHeight, width: '100%' }} className="erp-data-grid-container">
<DataGrid<E>
rowModelType="serverSide"
getRowId={(v) => v.data.id as string}
cacheBlockSize={perPage}
pagination={isPaginated}
paginationPageSize={isPaginated ? perPage : undefined}
paginationPageSizeSelector={isPaginated ? [10, 20, 50] : undefined}
columnDefs={finalColumnDefs}
defaultColDef={defaultColDef}
animateRows={true}
rowSelection={{ mode: 'multiRow', checkboxes: true, copySelectedRows: false, headerCheckbox: false }}
enableCellTextSelection={true}
onSelectionChanged={handleSelectionChanged}
onGridReady={onGridReady}
getContextMenuItems={getContextMenuItems}
statusBar={statusBar}
overlayLoadingTemplate={`<span style="padding:10px">${loadingMessage}</span>`}
overlayNoRowsTemplate={`<span style="padding:10px;color:var(--ag-foreground-color,#868e96)">${noRowsMessage}</span>`}
{...restAgGridProps}
/>
</Box>
</Box>
);
}