From 32b2edaa49ea24a97896745478b7a6998e2e36c5 Mon Sep 17 00:00:00 2001 From: shancheas Date: Mon, 31 Aug 2026 14:38:13 +0700 Subject: [PATCH] feat: enhance EnterpriseDataTable with server-side pagination utilities and refresh logic - Introduced new utility functions for handling server-side pagination, including `resolveServerSideInitialRowCount` and `shouldRestorePaginationPage`. - Updated `EnterpriseDataTable` to utilize these utilities for improved pagination behavior, ensuring proper row count handling and page restoration. - Added a new test file for the pagination utilities to validate their functionality and ensure reliability. - Modified navigation logic to reset pagination state after creating or duplicating entries, enhancing user experience. These changes improve the data table's handling of server-side pagination, ensuring a smoother experience when navigating through data entries. --- .../components/data-table/index.tsx | 22 +++++++--- .../server-side-index.utils.test.ts | 40 +++++++++++++++++++ .../data-table/server-side-index.utils.ts | 33 +++++++++++++++ .../enterprise-module/entities/entity.ts | 9 ++++- .../providers/form-page.provider.tsx | 4 +- .../providers/module.provider.tsx | 13 +++++- 6 files changed, 112 insertions(+), 9 deletions(-) create mode 100644 packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.test.ts create mode 100644 packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.ts diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx index ef86bdf..6cdca4a 100644 --- a/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/index.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback, useRef, useState } from 'react'; +import { useMemo, useCallback, useRef, useState, useEffect } from 'react'; import { AgGridReactProps } from 'ag-grid-react'; import { ColDef, @@ -53,6 +53,10 @@ import { TableFilterDrawer, TableFilterConfig } from './components/table-filter- import { EntityId } from '../../../../../../core-api/src/data-services/types'; import { computeActionColumnWidth } from './action-column.utils'; import { formatAuditActor, formatAuditTimestamp, resolveAuditValue } from './audit-column.utils'; +import { + resolveServerSideInitialRowCount, + shouldRestorePaginationPage, +} from './server-side-index.utils'; export * from 'ag-grid-community'; export * from 'ag-grid-react'; @@ -223,6 +227,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT setTableConfig, } = useEnterpriseModuleSelectionContext(); const navigation = useEnterpriseModuleNavigationContext(); + const { indexRefreshKey } = navigation; const { config, privileges } = useEnterpriseModuleConfigContext(); const { moduleType } = config; @@ -302,6 +307,11 @@ export function EnterpriseDataTable(props: EnterpriseDataT gridApiRef.current?.refreshServerSide({ purge: true }); }, []); + useEffect(() => { + if (indexRefreshKey === 0) return; + gridApiRef.current?.refreshServerSide({ purge: true }); + }, [indexRefreshKey]); + // --------------------------------------------------------------------------- // Action Handlers & Modal State // --------------------------------------------------------------------------- @@ -788,7 +798,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT const rowData = response.data.data; const meta = response.data.meta; - const rowCount = meta?.total || 0; + const rowCount = meta?.total ?? 0; // Update the global metadata state setMetaData(meta); @@ -833,9 +843,11 @@ export function EnterpriseDataTable(props: EnterpriseDataT // This ensures the grid doesn't reset our page back to 1. // NOTE: This relies on `serverSideInitialRowCount` being provided so the grid knows // there are enough pages to jump to! - if (isPaginated && metaData?.page && metaData.page > 1) { - params.api.paginationGoToPage(metaData.page - 1); + if (isPaginated && shouldRestorePaginationPage(metaData)) { + params.api.paginationGoToPage((metaData?.page ?? 1) - 1); } + + params.api.refreshServerSide({ purge: true }); } }, [datasource, setSelectedRows, isPaginated, metaData, tableConfig], @@ -1002,7 +1014,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT pagination={isPaginated} paginationPageSize={isPaginated ? perPage : undefined} paginationPageSizeSelector={isPaginated ? [10, 15, 20, 50] : undefined} - serverSideInitialRowCount={metaData?.total ?? undefined} + serverSideInitialRowCount={resolveServerSideInitialRowCount(metaData)} columnDefs={finalColumnDefs} defaultColDef={defaultColDef} animateRows={true} diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.test.ts b/packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.test.ts new file mode 100644 index 0000000..dca60dc --- /dev/null +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { + paginationMetaAfterCreate, + resolveServerSideInitialRowCount, + shouldRestorePaginationPage, +} from './server-side-index.utils'; + +describe('resolveServerSideInitialRowCount', () => { + it('does not pin the previous total on page 1 so a newly created row is not truncated', () => { + expect(resolveServerSideInitialRowCount({ page: 1, total: 5 })).toBeUndefined(); + expect(resolveServerSideInitialRowCount({ total: 5 })).toBeUndefined(); + expect(resolveServerSideInitialRowCount({ limit: 15 })).toBeUndefined(); + expect(resolveServerSideInitialRowCount(null)).toBeUndefined(); + }); + + it('keeps the known total only when restoring a page after the first', () => { + expect(resolveServerSideInitialRowCount({ page: 3, total: 40 })).toBe(40); + }); +}); + +describe('shouldRestorePaginationPage', () => { + it('restores only when the stored page is greater than 1', () => { + expect(shouldRestorePaginationPage({ page: 1 })).toBe(false); + expect(shouldRestorePaginationPage({ page: 2 })).toBe(true); + expect(shouldRestorePaginationPage(null)).toBe(false); + }); +}); + +describe('paginationMetaAfterCreate', () => { + it('returns to page 1 and drops the stale total so the next index load can grow', () => { + expect(paginationMetaAfterCreate({ page: 2, limit: 15, total: 5, totalPages: 1 })).toEqual({ + limit: 15, + page: 1, + }); + }); + + it('keeps the default page size when meta is missing', () => { + expect(paginationMetaAfterCreate(null)).toEqual({ limit: 15, page: 1 }); + }); +}); diff --git a/packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.ts b/packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.ts new file mode 100644 index 0000000..6a94884 --- /dev/null +++ b/packages/ui/src/foundations/enterprise-module/components/data-table/server-side-index.utils.ts @@ -0,0 +1,33 @@ +import type { StandardPaginationMeta } from '@repo/core-api/data-services'; + +const DEFAULT_PAGE_SIZE = 15; + +/** + * AG Grid SSRM uses `serverSideInitialRowCount` as a React-reactive store size. + * Binding it to the previous visit's `meta.total` pins the grid to that count, so a + * newly created row (total + 1) is truncated until a full reload resets zustand. + * + * Only pass a count when we must restore a page after the first. + */ +export function resolveServerSideInitialRowCount( + meta: Pick | null | undefined, +): number | undefined { + if (!shouldRestorePaginationPage(meta)) return undefined; + return meta?.total; +} + +export function shouldRestorePaginationPage( + meta: Pick | null | undefined, +): boolean { + return Boolean(meta?.page && meta.page > 1); +} + +/** Drop stale total/page after create so the next index fetch can grow by one row. */ +export function paginationMetaAfterCreate( + meta: StandardPaginationMeta | null | undefined, +): StandardPaginationMeta { + return { + limit: meta?.limit ?? DEFAULT_PAGE_SIZE, + page: 1, + }; +} diff --git a/packages/ui/src/foundations/enterprise-module/entities/entity.ts b/packages/ui/src/foundations/enterprise-module/entities/entity.ts index 25d55f2..b633410 100644 --- a/packages/ui/src/foundations/enterprise-module/entities/entity.ts +++ b/packages/ui/src/foundations/enterprise-module/entities/entity.ts @@ -165,12 +165,19 @@ export interface EnterpriseModuleState< setPrivileges: (privileges: string[]) => void; } +export type NavigateToIndexOptions = { + /** After create, return to page 1 and drop the stale total so the new row can appear. */ + resetPage?: boolean; +}; + export interface NavigationSlice { - navigateToIndex: () => void; + navigateToIndex: (options?: NavigateToIndexOptions) => void; navigateToCreate: () => void; navigateToEdit: (id: string) => void; navigateToDetail: (id: string) => void; navigateToDuplicate: (id: string) => void; + /** Bumped on every index navigation so a still-mounted table purges its SSRM cache. */ + indexRefreshKey: number; } export interface ModalSlice { diff --git a/packages/ui/src/foundations/enterprise-module/providers/form-page.provider.tsx b/packages/ui/src/foundations/enterprise-module/providers/form-page.provider.tsx index 37a2acc..2876caf 100644 --- a/packages/ui/src/foundations/enterprise-module/providers/form-page.provider.tsx +++ b/packages/ui/src/foundations/enterprise-module/providers/form-page.provider.tsx @@ -244,7 +244,9 @@ export function EnterpriseFormPageProvider(pr color: 'green', }); } - navigation.navigateToIndex(); + navigation.navigateToIndex({ + resetPage: formPageType === 'CREATE' || formPageType === 'DUPLICATE', + }); } catch (error: any) { const message = error?.response?.data?.message; notifications.show({ diff --git a/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx b/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx index 970111c..91910fb 100644 --- a/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx +++ b/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx @@ -10,7 +10,9 @@ import { SinglePageModalState, EnterpriseModuleState, PrivilegeEntity, + NavigateToIndexOptions, } from '../entities/entity'; +import { paginationMetaAfterCreate } from '../components/data-table/server-side-index.utils'; import { EnterpriseConfigContext, EnterpriseDataServiceContext, @@ -130,11 +132,18 @@ export function EnterpriseModuleProvider< // --------------------------------------------------------------------------- // Navigation Slice // --------------------------------------------------------------------------- + const [indexRefreshKey, setIndexRefreshKey] = useState(0); + const navigationSlice = useMemo(() => { const isSingle = config.moduleCategory === 'SINGLE_PAGE'; return { - navigateToIndex: () => { + indexRefreshKey, + navigateToIndex: (options?: NavigateToIndexOptions) => { + setIndexRefreshKey((key) => key + 1); + if (options?.resetPage) { + store.getState().setMetaData(paginationMetaAfterCreate(store.getState().metaData)); + } if (isSingle) { setFormState({ open: false, formType: 'CREATE' }); setDetailState({ open: false }); @@ -171,7 +180,7 @@ export function EnterpriseModuleProvider< } }, }; - }, [config.moduleCategory, config.webUrl, navigate]); + }, [config.moduleCategory, config.webUrl, navigate, store, indexRefreshKey]); // --------------------------------------------------------------------------- // Render