235 lines
8.7 KiB
TypeScript
235 lines
8.7 KiB
TypeScript
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>
|
||
);
|
||
}
|