Files
trackgo-fe/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx
T

235 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}