feat: add EnterpriseDataTable component, integrate standard pagination, and update AgGridProvider for enhanced data handling
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user