feat: add EnterpriseDataTable component, integrate standard pagination, and update AgGridProvider for enhanced data handling
This commit is contained in:
@@ -19,4 +19,4 @@ export type {
|
|||||||
DataServicesConfig,
|
DataServicesConfig,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export type { IDataTransformer } from './base-data.transformer';
|
export type { IDataTransformer, StandardPaginationMeta } from './base-data.transformer';
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { createContext, useContext, useEffect, useRef } from 'react';
|
import React, { createContext, useContext, useEffect, useRef } from 'react';
|
||||||
import { useMantineColorScheme } from '@mantine/core';
|
import { useMantineColorScheme } from '@mantine/core';
|
||||||
import type { Theme } from 'ag-grid-community';
|
import { AllCommunityModule, type Theme } from 'ag-grid-community';
|
||||||
import { initAgGrid, type AgGridInitOptions } from './ag-grid-setup';
|
import { initAgGrid, type AgGridInitOptions } from './ag-grid-setup';
|
||||||
import { agGridMantineTheme } from './ag-grid-theme';
|
import { agGridMantineTheme } from './ag-grid-theme';
|
||||||
|
import { AgGridProvider as BaseAgGridProvider } from 'ag-grid-react';
|
||||||
|
import { AllEnterpriseModule } from 'ag-grid-enterprise';
|
||||||
|
|
||||||
/* ─── Context ──────────────────────────────────────────────────────── */
|
/* ─── Context ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@@ -68,10 +70,12 @@ export function AgGridProvider({ licenseKey, bypassLicense, children }: AgGridPr
|
|||||||
const agThemeMode = colorScheme === 'dark' ? 'dark' : 'light';
|
const agThemeMode = colorScheme === 'dark' ? 'dark' : 'light';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgGridContext.Provider value={{ theme: agGridMantineTheme }}>
|
<BaseAgGridProvider modules={[AllCommunityModule, AllEnterpriseModule]}>
|
||||||
<div data-ag-theme-mode={agThemeMode} style={{ display: 'contents' }}>
|
<AgGridContext.Provider value={{ theme: agGridMantineTheme }}>
|
||||||
{children}
|
<div data-ag-theme-mode={agThemeMode} style={{ display: 'contents' }}>
|
||||||
</div>
|
{children}
|
||||||
</AgGridContext.Provider>
|
</div>
|
||||||
|
</AgGridContext.Provider>
|
||||||
|
</BaseAgGridProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const agGridMantineTheme = themeQuartz
|
|||||||
selectedRowBackgroundColor: 'var(--mantine-primary-color-light)',
|
selectedRowBackgroundColor: 'var(--mantine-primary-color-light)',
|
||||||
|
|
||||||
/* ── Radius ─────────────────────────────────────────────────── */
|
/* ── Radius ─────────────────────────────────────────────────── */
|
||||||
borderRadius: 'var(--mantine-radius-xs)',
|
borderRadius: 'var(--mantine-radius-sm)',
|
||||||
wrapperBorderRadius: 'var(--mantine-radius-sm)',
|
wrapperBorderRadius: 'var(--mantine-radius-sm)',
|
||||||
|
|
||||||
/* ── Spacing ────────────────────────────────────────────────── */
|
/* ── Spacing ────────────────────────────────────────────────── */
|
||||||
@@ -88,8 +88,7 @@ export const agGridMantineTheme = themeQuartz
|
|||||||
/* ── Row Styling ──────────────────────────────────────────── */
|
/* ── Row Styling ──────────────────────────────────────────── */
|
||||||
oddRowBackgroundColor: 'var(--mantine-color-dark-6)',
|
oddRowBackgroundColor: 'var(--mantine-color-dark-6)',
|
||||||
rowHoverColor: 'color-mix(in srgb, var(--mantine-primary-color-filled) 12%, transparent)',
|
rowHoverColor: 'color-mix(in srgb, var(--mantine-primary-color-filled) 12%, transparent)',
|
||||||
selectedRowBackgroundColor:
|
selectedRowBackgroundColor: 'color-mix(in srgb, var(--mantine-primary-color-filled) 18%, transparent)',
|
||||||
'color-mix(in srgb, var(--mantine-primary-color-filled) 18%, transparent)',
|
|
||||||
},
|
},
|
||||||
'dark',
|
'dark',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import type { UseFormReturn } from 'react-hook-form';
|
import type { UseFormReturn } from 'react-hook-form';
|
||||||
import type { ZodType } from 'zod';
|
import type { ZodType } from 'zod';
|
||||||
import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
import type { BaseEntity, BaseRemoteDataServices, StandardPaginationMeta } from '@repo/core-api/data-services';
|
||||||
import type { ModulePageHeaderProps } from '../components/module-page-header';
|
import type { ModulePageHeaderProps } from '../components/module-page-header';
|
||||||
import { PageActionsProps } from '../../../components';
|
import { PageActionsProps } from '../../../components';
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ export interface DataServiceSlice<
|
|||||||
export interface SelectionSlice<
|
export interface SelectionSlice<
|
||||||
E extends BaseEntity = BaseEntity,
|
E extends BaseEntity = BaseEntity,
|
||||||
TFilter = Record<string, unknown>,
|
TFilter = Record<string, unknown>,
|
||||||
TMeta = Record<string, unknown>,
|
TMeta = StandardPaginationMeta,
|
||||||
> {
|
> {
|
||||||
selectedRows: E[];
|
selectedRows: E[];
|
||||||
setSelectedRows: (rows: E[]) => void;
|
setSelectedRows: (rows: E[]) => void;
|
||||||
@@ -148,7 +148,7 @@ export interface SelectionSlice<
|
|||||||
export interface EnterpriseModuleState<
|
export interface EnterpriseModuleState<
|
||||||
E extends BaseEntity = BaseEntity,
|
E extends BaseEntity = BaseEntity,
|
||||||
TFilter = Record<string, unknown>,
|
TFilter = Record<string, unknown>,
|
||||||
TMeta = Record<string, unknown>,
|
TMeta = StandardPaginationMeta,
|
||||||
> {
|
> {
|
||||||
metaData: TMeta | null;
|
metaData: TMeta | null;
|
||||||
setMetaData: (data: TMeta | null) => void;
|
setMetaData: (data: TMeta | null) => void;
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export * from './providers/index-page.provider';
|
|||||||
export * from './providers/detail-page.provider';
|
export * from './providers/detail-page.provider';
|
||||||
export * from './components/module-page-header';
|
export * from './components/module-page-header';
|
||||||
export * from './components/action-confirmation-modal';
|
export * from './components/action-confirmation-modal';
|
||||||
|
export * from './components/data-table';
|
||||||
|
|||||||
Reference in New Issue
Block a user