From 75e0424e36ff7946a63b31859354e0db79d3d910 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:02:58 +0700 Subject: [PATCH] feat: implement persistent AG Grid column state --- .../full-page/presentation/store/index.ts | 3 + .../components/data-table/index.tsx | 77 ++++++++++++++++++- .../enterprise-module/entities/entity.ts | 5 ++ .../providers/module.provider.tsx | 6 +- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts b/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts index d347059..f55dce7 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts +++ b/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts @@ -16,4 +16,7 @@ export const fullPageStore = create((set) => ({ privileges: [], setPrivileges: (privileges) => set({ privileges }), + + tableConfig: null, + setTableConfig: (config) => set({ tableConfig: config }), })); 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 f98ba8f..c19676b 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 @@ -10,6 +10,10 @@ import { MenuItemDef, DefaultMenuItem, StatusBar, + ColumnMovedEvent, + ColumnResizedEvent, + ColumnVisibleEvent, + SortChangedEvent, } from 'ag-grid-community'; import { Box, Group, TextInput, Indicator, CloseButton } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; @@ -181,6 +185,12 @@ export function EnterpriseDataTable(props: EnterpriseDataT searchKey = 'search', filterConfig, + // Grid config props + onColumnMoved, + onColumnResized, + onColumnVisible, + onSortChanged, + // Bulk action props customBulkActions, batchSize, @@ -200,8 +210,16 @@ export function EnterpriseDataTable(props: EnterpriseDataT // --------------------------------------------------------------------------- const { t } = useEnterpriseModuleTranslationContext(); const { dataServices } = useEnterpriseModuleDataServiceContext(); - const { selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData } = - useEnterpriseModuleSelectionContext(); + const { + selectedRows, + setSelectedRows, + metaData, + setMetaData, + filterData, + setFilterData, + tableConfig, + setTableConfig, + } = useEnterpriseModuleSelectionContext(); const navigation = useEnterpriseModuleNavigationContext(); const { config } = useEnterpriseModuleConfigContext(); @@ -791,6 +809,11 @@ export function EnterpriseDataTable(props: EnterpriseDataT setSelectedRows([]); if (params.api) { + // Restore grid configuration on mount + if (tableConfig) { + params.api.applyColumnState({ state: tableConfig, applyOrder: true }); + } + // Attach the server-side datasource to the grid API params.api.setGridOption('serverSideDatasource', datasource); @@ -803,7 +826,7 @@ export function EnterpriseDataTable(props: EnterpriseDataT } } }, - [datasource, setSelectedRows, isPaginated, metaData], + [datasource, setSelectedRows, isPaginated, metaData, tableConfig], ); // Triggered whenever the row selection in the grid changes @@ -817,6 +840,50 @@ export function EnterpriseDataTable(props: EnterpriseDataT [setSelectedRows], ); + // --------------------------------------------------------------------------- + // Save Grid Configuration State Handlers + // --------------------------------------------------------------------------- + const saveGridState = useCallback(() => { + if (!gridApiRef.current || !setTableConfig) return; + const state = gridApiRef.current.getColumnState(); + setTableConfig(state); + }, [setTableConfig]); + + const handleColumnMoved = useCallback( + (e: ColumnMovedEvent) => { + saveGridState(); + if (onColumnMoved) onColumnMoved(e); + }, + [saveGridState, onColumnMoved], + ); + + const handleColumnResized = useCallback( + (e: ColumnResizedEvent) => { + // Only save when resizing is finished to prevent excessive re-renders + if (e.finished) { + saveGridState(); + } + if (onColumnResized) onColumnResized(e); + }, + [saveGridState, onColumnResized], + ); + + const handleColumnVisible = useCallback( + (e: ColumnVisibleEvent) => { + saveGridState(); + if (onColumnVisible) onColumnVisible(e); + }, + [saveGridState, onColumnVisible], + ); + + const handleSortChanged = useCallback( + (e: SortChangedEvent) => { + saveGridState(); + if (onSortChanged) onSortChanged(e); + }, + [saveGridState, onSortChanged], + ); + // Configures the context menu items available when right-clicking a cell const getContextMenuItems = useCallback((): | (DefaultMenuItem | MenuItemDef)[] @@ -923,6 +990,10 @@ export function EnterpriseDataTable(props: EnterpriseDataT enableCellTextSelection={true} onSelectionChanged={handleSelectionChanged} onGridReady={onGridReady} + onColumnMoved={handleColumnMoved} + onColumnResized={handleColumnResized} + onColumnVisible={handleColumnVisible} + onSortChanged={handleSortChanged} getContextMenuItems={getContextMenuItems} statusBar={statusBar} domLayout={domLayout} diff --git a/packages/ui/src/foundations/enterprise-module/entities/entity.ts b/packages/ui/src/foundations/enterprise-module/entities/entity.ts index 94f4a0b..711b43a 100644 --- a/packages/ui/src/foundations/enterprise-module/entities/entity.ts +++ b/packages/ui/src/foundations/enterprise-module/entities/entity.ts @@ -4,6 +4,7 @@ import type { ZodType } from 'zod'; import type { BaseEntity, BaseRemoteDataServices, StandardPaginationMeta } from '@repo/core-api/data-services'; import type { ModulePageHeaderProps } from '../components/module-page-header'; import { PageActionsProps } from '../../../components'; +import { ColumnState } from 'ag-grid-community'; // --------------------------------------------------------------------------- // Module Constants & Base Types @@ -140,6 +141,8 @@ export interface SelectionSlice< setMetaData: (data: TMeta | null) => void; filterData: TFilter | null; setFilterData: (data: TFilter | null) => void; + tableConfig: ColumnState[] | null; + setTableConfig: (config: ColumnState[] | null) => void; } /** @@ -156,6 +159,8 @@ export interface EnterpriseModuleState< setFilterData: (data: TFilter | null) => void; selectedRows: E[]; setSelectedRows: (rows: E[]) => void; + tableConfig: ColumnState[] | null; + setTableConfig: (config: ColumnState[] | null) => void; privileges: string[]; setPrivileges: (privileges: string[]) => void; } 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 3d405bb..e424703 100644 --- a/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx +++ b/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx @@ -82,6 +82,8 @@ export function EnterpriseModuleProvider< const setMetaData = store((state: S) => state.setMetaData); const filterData = store((state: S) => state.filterData); const setFilterData = store((state: S) => state.setFilterData); + const tableConfig = store((state: S) => state.tableConfig); + const setTableConfig = store((state: S) => state.setTableConfig); const selectionSlice = useMemo( () => ({ @@ -91,8 +93,10 @@ export function EnterpriseModuleProvider< setMetaData, filterData, setFilterData, + tableConfig, + setTableConfig, }), - [selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData], + [selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData, tableConfig, setTableConfig], ); // ---------------------------------------------------------------------------