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 { AgGridReactProps } from 'ag-grid-react';
|
||||||
import {
|
import {
|
||||||
ColDef,
|
ColDef,
|
||||||
@@ -53,6 +53,10 @@ import { TableFilterDrawer, TableFilterConfig } from './components/table-filter-
|
|||||||
import { EntityId } from '../../../../../../core-api/src/data-services/types';
|
import { EntityId } from '../../../../../../core-api/src/data-services/types';
|
||||||
import { computeActionColumnWidth } from './action-column.utils';
|
import { computeActionColumnWidth } from './action-column.utils';
|
||||||
import { formatAuditActor, formatAuditTimestamp, resolveAuditValue } from './audit-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-community';
|
||||||
export * from 'ag-grid-react';
|
export * from 'ag-grid-react';
|
||||||
@@ -223,6 +227,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
setTableConfig,
|
setTableConfig,
|
||||||
} = useEnterpriseModuleSelectionContext<E>();
|
} = useEnterpriseModuleSelectionContext<E>();
|
||||||
const navigation = useEnterpriseModuleNavigationContext();
|
const navigation = useEnterpriseModuleNavigationContext();
|
||||||
|
const { indexRefreshKey } = navigation;
|
||||||
|
|
||||||
const { config, privileges } = useEnterpriseModuleConfigContext();
|
const { config, privileges } = useEnterpriseModuleConfigContext();
|
||||||
const { moduleType } = config;
|
const { moduleType } = config;
|
||||||
@@ -302,6 +307,11 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
gridApiRef.current?.refreshServerSide({ purge: true });
|
gridApiRef.current?.refreshServerSide({ purge: true });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (indexRefreshKey === 0) return;
|
||||||
|
gridApiRef.current?.refreshServerSide({ purge: true });
|
||||||
|
}, [indexRefreshKey]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Action Handlers & Modal State
|
// Action Handlers & Modal State
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -788,7 +798,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
|
|
||||||
const rowData = response.data.data;
|
const rowData = response.data.data;
|
||||||
const meta = response.data.meta;
|
const meta = response.data.meta;
|
||||||
const rowCount = meta?.total || 0;
|
const rowCount = meta?.total ?? 0;
|
||||||
|
|
||||||
// Update the global metadata state
|
// Update the global metadata state
|
||||||
setMetaData(meta);
|
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.
|
// This ensures the grid doesn't reset our page back to 1.
|
||||||
// NOTE: This relies on `serverSideInitialRowCount` being provided so the grid knows
|
// NOTE: This relies on `serverSideInitialRowCount` being provided so the grid knows
|
||||||
// there are enough pages to jump to!
|
// there are enough pages to jump to!
|
||||||
if (isPaginated && metaData?.page && metaData.page > 1) {
|
if (isPaginated && shouldRestorePaginationPage(metaData)) {
|
||||||
params.api.paginationGoToPage(metaData.page - 1);
|
params.api.paginationGoToPage((metaData?.page ?? 1) - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.api.refreshServerSide({ purge: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[datasource, setSelectedRows, isPaginated, metaData, tableConfig],
|
[datasource, setSelectedRows, isPaginated, metaData, tableConfig],
|
||||||
@@ -1002,7 +1014,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
pagination={isPaginated}
|
pagination={isPaginated}
|
||||||
paginationPageSize={isPaginated ? perPage : undefined}
|
paginationPageSize={isPaginated ? perPage : undefined}
|
||||||
paginationPageSizeSelector={isPaginated ? [10, 15, 20, 50] : undefined}
|
paginationPageSizeSelector={isPaginated ? [10, 15, 20, 50] : undefined}
|
||||||
serverSideInitialRowCount={metaData?.total ?? undefined}
|
serverSideInitialRowCount={resolveServerSideInitialRowCount(metaData)}
|
||||||
columnDefs={finalColumnDefs}
|
columnDefs={finalColumnDefs}
|
||||||
defaultColDef={defaultColDef}
|
defaultColDef={defaultColDef}
|
||||||
animateRows={true}
|
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;
|
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 {
|
export interface NavigationSlice {
|
||||||
navigateToIndex: () => void;
|
navigateToIndex: (options?: NavigateToIndexOptions) => void;
|
||||||
navigateToCreate: () => void;
|
navigateToCreate: () => void;
|
||||||
navigateToEdit: (id: string) => void;
|
navigateToEdit: (id: string) => void;
|
||||||
navigateToDetail: (id: string) => void;
|
navigateToDetail: (id: string) => void;
|
||||||
navigateToDuplicate: (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 {
|
export interface ModalSlice {
|
||||||
|
|||||||
@@ -244,7 +244,9 @@ export function EnterpriseFormPageProvider<E extends BaseEntity = BaseEntity>(pr
|
|||||||
color: 'green',
|
color: 'green',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
navigation.navigateToIndex();
|
navigation.navigateToIndex({
|
||||||
|
resetPage: formPageType === 'CREATE' || formPageType === 'DUPLICATE',
|
||||||
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error?.response?.data?.message;
|
const message = error?.response?.data?.message;
|
||||||
notifications.show({
|
notifications.show({
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
SinglePageModalState,
|
SinglePageModalState,
|
||||||
EnterpriseModuleState,
|
EnterpriseModuleState,
|
||||||
PrivilegeEntity,
|
PrivilegeEntity,
|
||||||
|
NavigateToIndexOptions,
|
||||||
} from '../entities/entity';
|
} from '../entities/entity';
|
||||||
|
import { paginationMetaAfterCreate } from '../components/data-table/server-side-index.utils';
|
||||||
import {
|
import {
|
||||||
EnterpriseConfigContext,
|
EnterpriseConfigContext,
|
||||||
EnterpriseDataServiceContext,
|
EnterpriseDataServiceContext,
|
||||||
@@ -130,11 +132,18 @@ export function EnterpriseModuleProvider<
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Navigation Slice
|
// Navigation Slice
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
const [indexRefreshKey, setIndexRefreshKey] = useState(0);
|
||||||
|
|
||||||
const navigationSlice = useMemo(() => {
|
const navigationSlice = useMemo(() => {
|
||||||
const isSingle = config.moduleCategory === 'SINGLE_PAGE';
|
const isSingle = config.moduleCategory === 'SINGLE_PAGE';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navigateToIndex: () => {
|
indexRefreshKey,
|
||||||
|
navigateToIndex: (options?: NavigateToIndexOptions) => {
|
||||||
|
setIndexRefreshKey((key) => key + 1);
|
||||||
|
if (options?.resetPage) {
|
||||||
|
store.getState().setMetaData(paginationMetaAfterCreate(store.getState().metaData));
|
||||||
|
}
|
||||||
if (isSingle) {
|
if (isSingle) {
|
||||||
setFormState({ open: false, formType: 'CREATE' });
|
setFormState({ open: false, formType: 'CREATE' });
|
||||||
setDetailState({ open: false });
|
setDetailState({ open: false });
|
||||||
@@ -171,7 +180,7 @@ export function EnterpriseModuleProvider<
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}, [config.moduleCategory, config.webUrl, navigate]);
|
}, [config.moduleCategory, config.webUrl, navigate, store, indexRefreshKey]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Render
|
// Render
|
||||||
|
|||||||
Reference in New Issue
Block a user