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.
This commit is contained in:
@@ -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<E extends BaseEntity>(props: EnterpriseDataT
|
||||
setTableConfig,
|
||||
} = useEnterpriseModuleSelectionContext<E>();
|
||||
const navigation = useEnterpriseModuleNavigationContext();
|
||||
const { indexRefreshKey } = navigation;
|
||||
|
||||
const { config, privileges } = useEnterpriseModuleConfigContext();
|
||||
const { moduleType } = config;
|
||||
@@ -302,6 +307,11 @@ export function EnterpriseDataTable<E extends BaseEntity>(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<E extends BaseEntity>(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<E extends BaseEntity>(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<E extends BaseEntity>(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}
|
||||
|
||||
+40
@@ -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 });
|
||||
});
|
||||
});
|
||||
+33
@@ -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<StandardPaginationMeta, 'page' | 'total'> | null | undefined,
|
||||
): number | undefined {
|
||||
if (!shouldRestorePaginationPage(meta)) return undefined;
|
||||
return meta?.total;
|
||||
}
|
||||
|
||||
export function shouldRestorePaginationPage(
|
||||
meta: Pick<StandardPaginationMeta, 'page'> | 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,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -244,7 +244,9 @@ export function EnterpriseFormPageProvider<E extends BaseEntity = BaseEntity>(pr
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
navigation.navigateToIndex();
|
||||
navigation.navigateToIndex({
|
||||
resetPage: formPageType === 'CREATE' || formPageType === 'DUPLICATE',
|
||||
});
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message;
|
||||
notifications.show({
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user