From 8cc3097f538b3d87968be9561624942faa228266 Mon Sep 17 00:00:00 2001 From: "Omar H." Date: Fri, 10 Jul 2026 11:10:12 -0400 Subject: [PATCH 1/4] feat(iModelGrid): add sort state persistence to IModelTable and IModelGrid components --- .../mui/IModelGridMUI.stories.tsx | 16 +++++ .../containers/iModelGrid/IModelGridMUI.tsx | 14 +++++ .../containers/iModelGrid/IModelTableMUI.tsx | 58 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx b/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx index ff857376..6f151f2e 100644 --- a/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx @@ -158,6 +158,22 @@ TableViewWithOverrides.args = { }, }; +export const TableWithPersistedSort = Template.bind({}); +TableWithPersistedSort.args = { + ...baseArgs, + viewMode: "cells", + preserveSortState: true, + sortStateStorageKey: "storybook-imodel-table-sort", +}; +TableWithPersistedSort.parameters = { + docs: { + description: { + story: + "Sort a column, then reload the page. The sort state is restored from `localStorage` using `sortStateStorageKey`.", + }, + }, +}; + export const OverrideApiDataWithLoadMore: Story = withITwinIdOverride( withAccessTokenOverride((args) => { diff --git a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx index b813b43c..2bd488c4 100644 --- a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx @@ -103,6 +103,16 @@ export interface IModelGridMUIProps tileOverrides?: Partial; tableOverrides?: IModelTableOverridesMUI; stringsOverrides?: Partial; + /** + * When true, the table sort state is persisted to `localStorage` so it is + * restored on subsequent mounts. + */ + preserveSortState?: boolean; + /** + * `localStorage` key used to persist the table sort state when + * `preserveSortState` is true. + */ + sortStateStorageKey?: string; } /** @@ -149,6 +159,8 @@ const IModelGridInternal = ({ onRefetch, dataMode = "internal", disableAddToRecents = false, + preserveSortState, + sortStateStorageKey, }: IModelGridMUIProps) => { const [sort, setSort] = React.useState(sortOptions); @@ -408,6 +420,8 @@ const IModelGridInternal = ({ tableOverrides={tableOverrides} isLoading={fetchStatus === DataStatus.Fetching} fetchMore={fetchMore} + preserveSortState={preserveSortState} + sortStateStorageKey={sortStateStorageKey} data-testid="imodel-table" /> )} diff --git a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx index 82d86087..c33750c0 100644 --- a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx @@ -6,6 +6,7 @@ import { DataGrid, GRID_DEFAULT_LOCALE_TEXT, GridColDef, + GridSortModel, } from "@mui/x-data-grid"; import svgMore from "@stratakit/icons/more-vertical.svg"; import { Icon } from "@stratakit/mui"; @@ -62,8 +63,47 @@ export interface IModelTableMUIProps { isLoading?: boolean; /** Called when more data should be loaded. */ fetchMore?: (() => void) | false; + /** + * When true, the sort state is persisted to `localStorage` so it is restored + * on subsequent mounts. + */ + preserveSortState?: boolean; + /** + * `localStorage` key used to persist the sort state when `preserveSortState` + * is true. + * @default "imodel-table-sort-model" + */ + sortStateStorageKey?: string; } +const DEFAULT_SORT_STATE_STORAGE_KEY = "imodel-table-sort-model"; + +const readPersistedSortModel = (storageKey: string): GridSortModel => { + try { + const raw = window.localStorage.getItem(storageKey); + if (!raw) { + return []; + } + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? (parsed as GridSortModel) : []; + } catch { + return []; + } +}; + +const writePersistedSortModel = ( + storageKey: string, + sortModel: GridSortModel +) => { + try { + window.localStorage.setItem(storageKey, JSON.stringify(sortModel)); + } catch { + console.warn( + `Failed to persist sort model to localStorage under key "${storageKey}".` + ); + } +}; + /** * Table view for iModels using MUI X DataGrid (Community edition). */ @@ -79,6 +119,8 @@ export const IModelTableMUI = ({ } = {}, isLoading, fetchMore, + preserveSortState, + sortStateStorageKey = DEFAULT_SORT_STATE_STORAGE_KEY, }: IModelTableMUIProps) => { // Eagerly load all available data so the table has the full dataset // for client-side pagination and sorting. @@ -89,6 +131,20 @@ export const IModelTableMUI = ({ }, [fetchMore]); const favoritesContext = useIModelFavoritesContext(); + // Internal sort state, seeded from localStorage when persistence is enabled. + const [internalSortModel, setInternalSortModel] = + React.useState(() => + preserveSortState ? readPersistedSortModel(sortStateStorageKey) : [] + ); + + const handleSortModelChange = React.useCallback( + (newSortModel: GridSortModel) => { + setInternalSortModel(newSortModel); + writePersistedSortModel(sortStateStorageKey, newSortModel); + }, + [sortStateStorageKey] + ); + const columns = React.useMemo[]>(() => { const cols: (GridColDef | false)[] = [ !hideColumns.includes(IModelCellColumn.Favorite) && { @@ -188,6 +244,8 @@ export const IModelTableMUI = ({ rows={iModels} columns={columns} loading={isLoading} + sortModel={preserveSortState ? internalSortModel : undefined} + onSortModelChange={preserveSortState ? handleSortModelChange : undefined} onRowClick={ actions ? (params) => { From 137e2f67c5c64197792f49537bb15de437bc2513 Mon Sep 17 00:00:00 2001 From: "Omar H." Date: Fri, 10 Jul 2026 11:10:40 -0400 Subject: [PATCH 2/4] feat(iTwinTable): add sort state persistence to ITwinTable and ITwinGrid components --- .../mui/ITwinGridMUI.stories.tsx | 16 +++++ .../mui/containers/ITwinGrid/ITwinGridMUI.tsx | 14 +++++ .../containers/ITwinGrid/ITwinTableMUI.tsx | 58 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx b/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx index 6c1006f7..d5a66a83 100644 --- a/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx @@ -79,6 +79,22 @@ TableView.args = { ], }; +export const TableWithPersistedSort = Template.bind({}); +TableWithPersistedSort.args = { + ...baseArgs, + viewMode: "cells", + preserveSortState: true, + sortStateStorageKey: "storybook-itwin-table-sort", +}; +TableWithPersistedSort.parameters = { + docs: { + description: { + story: + "Sort a column, then reload the page. The sort state is restored from `localStorage` using `sortStateStorageKey`.", + }, + }, +}; + export const TableViewWithOverrides = Template.bind({}); TableViewWithOverrides.args = { ...baseArgs, diff --git a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx index bce76405..8abf35c4 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx @@ -77,6 +77,16 @@ export interface ITwinGridPropsMUI tableOverrides?: ITwinTableOverridesMUI; /** Localized string overrides - falls back to default English strings if not provided */ stringsOverrides?: Partial; + /** + * When true, the table sort state is persisted to `localStorage` so it is + * restored on subsequent mounts. + */ + preserveSortState?: boolean; + /** + * `localStorage` key used to persist the table sort state when + * `preserveSortState` is true. + */ + sortStateStorageKey?: string; } /** @@ -99,6 +109,8 @@ export const ITwinGridMUI = ({ viewMode, tableOverrides, className, + preserveSortState, + sortStateStorageKey, }: ITwinGridPropsMUI) => { const { iTwinFavorites, @@ -287,6 +299,8 @@ export const ITwinGridMUI = ({ tableOverrides={tableOverrides} isLoading={fetchStatus === DataStatus.Fetching} fetchMore={fetchMore} + preserveSortState={preserveSortState} + sortStateStorageKey={sortStateStorageKey} /> ); }; diff --git a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx index a5c93f7e..257f866e 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx @@ -6,6 +6,7 @@ import { DataGrid, GRID_DEFAULT_LOCALE_TEXT, GridColDef, + GridSortModel, } from "@mui/x-data-grid"; import svgMore from "@stratakit/icons/more-vertical.svg"; import { Icon } from "@stratakit/mui"; @@ -65,8 +66,47 @@ export interface ITwinTableMUIProps { isLoading?: boolean; /** Called when more data should be loaded. */ fetchMore?: (() => void) | false; + /** + * When true, the sort state is persisted to `localStorage` so it is restored + * on subsequent mounts. + */ + preserveSortState?: boolean; + /** + * `localStorage` key used to persist the sort state when `preserveSortState` + * is true. + * @default "itwin-table-sort-model" + */ + sortStateStorageKey?: string; } +const DEFAULT_SORT_STATE_STORAGE_KEY = "itwin-table-sort-model"; + +const readPersistedSortModel = (storageKey: string): GridSortModel => { + try { + const raw = window.localStorage.getItem(storageKey); + if (!raw) { + return []; + } + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? (parsed as GridSortModel) : []; + } catch { + return []; + } +}; + +const writePersistedSortModel = ( + storageKey: string, + sortModel: GridSortModel +) => { + try { + window.localStorage.setItem(storageKey, JSON.stringify(sortModel)); + } catch { + console.warn( + `Failed to persist sort model to localStorage under key "${storageKey}".` + ); + } +}; + /** * Table view for iTwins using MUI X DataGrid (Community edition). */ @@ -85,6 +125,8 @@ export const ITwinTableMUI = ({ } = {}, isLoading, fetchMore, + preserveSortState, + sortStateStorageKey = DEFAULT_SORT_STATE_STORAGE_KEY, }: ITwinTableMUIProps) => { // Eagerly load all available data so the table has the full dataset // for client-side pagination and sorting. @@ -93,6 +135,20 @@ export const ITwinTableMUI = ({ fetchMore(); } }, [fetchMore]); + + // Internal sort state, seeded from localStorage when persistence is enabled. + const [internalSortModel, setInternalSortModel] = + React.useState(() => + preserveSortState ? readPersistedSortModel(sortStateStorageKey) : [] + ); + + const handleSortModelChange = React.useCallback( + (newSortModel: GridSortModel) => { + setInternalSortModel(newSortModel); + writePersistedSortModel(sortStateStorageKey, newSortModel); + }, + [sortStateStorageKey] + ); const columns = React.useMemo[]>(() => { const cols: (GridColDef | false)[] = [ !hideColumns.includes(ITwinCellColumn.Favorite) && { @@ -188,6 +244,8 @@ export const ITwinTableMUI = ({ rows={iTwins} columns={columns} loading={isLoading} + sortModel={preserveSortState ? internalSortModel : undefined} + onSortModelChange={preserveSortState ? handleSortModelChange : undefined} onRowClick={ actions ? (params) => { From 5eb72dbf9f3b51a8564a7c5b46df255db48d43ef Mon Sep 17 00:00:00 2001 From: "Omar H." Date: Fri, 10 Jul 2026 11:53:51 -0400 Subject: [PATCH 3/4] feat: implement controlled sort model for IModelGrid and ITwinGrid components --- .../mui/IModelGridMUI.stories.tsx | 36 +++++++++-- .../mui/ITwinGridMUI.stories.tsx | 34 ++++++++-- .../mui/containers/ITwinGrid/ITwinGridMUI.tsx | 23 ++++--- .../containers/ITwinGrid/ITwinTableMUI.tsx | 64 +++---------------- .../containers/iModelGrid/IModelGridMUI.tsx | 23 ++++--- .../containers/iModelGrid/IModelTableMUI.tsx | 64 +++---------------- 6 files changed, 96 insertions(+), 148 deletions(-) diff --git a/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx b/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx index 6f151f2e..f2a29931 100644 --- a/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx @@ -13,6 +13,7 @@ import Avatar from "@mui/material/Avatar"; import AvatarGroup from "@mui/material/AvatarGroup"; import Chip from "@mui/material/Chip"; import Typography from "@mui/material/Typography"; +import { GridSortModel } from "@mui/x-data-grid"; import { action } from "@storybook/addon-actions"; import { Meta, Story } from "@storybook/react/types-6-0"; import SvgDelete from "@stratakit/icons/delete.svg"; @@ -158,18 +159,39 @@ TableViewWithOverrides.args = { }, }; -export const TableWithPersistedSort = Template.bind({}); -TableWithPersistedSort.args = { +export const TableWithControlledSort: Story = + withITwinIdOverride( + withAccessTokenOverride((args) => { + const [sortModel, setSortModel] = React.useState([ + { field: "name", sort: "desc" }, + ]); + + return ( +
+ + Controlled sort model: {JSON.stringify(sortModel)} + + { + action("sort model changed")(newSortModel); + setSortModel(newSortModel); + }} + /> +
+ ); + }) + ); +TableWithControlledSort.args = { ...baseArgs, - viewMode: "cells", - preserveSortState: true, - sortStateStorageKey: "storybook-imodel-table-sort", }; -TableWithPersistedSort.parameters = { +TableWithControlledSort.parameters = { docs: { description: { story: - "Sort a column, then reload the page. The sort state is restored from `localStorage` using `sortStateStorageKey`.", + "The sort state is fully controlled by the parent via `sortModel` and `onSortModelChange`, so it can be saved anywhere the consumer wants.", }, }, }; diff --git a/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx b/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx index d5a66a83..f68394a0 100644 --- a/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx @@ -18,6 +18,7 @@ import AvatarGroup from "@mui/material/AvatarGroup"; import Box from "@mui/material/Box"; import Chip from "@mui/material/Chip"; import Typography from "@mui/material/Typography"; +import { GridSortModel } from "@mui/x-data-grid"; import { action } from "@storybook/addon-actions"; import { Meta, Story } from "@storybook/react/types-6-0"; import React from "react"; @@ -79,18 +80,37 @@ TableView.args = { ], }; -export const TableWithPersistedSort = Template.bind({}); -TableWithPersistedSort.args = { +export const TableWithControlledSort: Story = + withAccessTokenOverride((args) => { + const [sortModel, setSortModel] = React.useState([ + { field: "number", sort: "desc" }, + ]); + + return ( +
+ + Controlled sort model: {JSON.stringify(sortModel)} + + { + action("sort model changed")(newSortModel); + setSortModel(newSortModel); + }} + /> +
+ ); + }); +TableWithControlledSort.args = { ...baseArgs, - viewMode: "cells", - preserveSortState: true, - sortStateStorageKey: "storybook-itwin-table-sort", }; -TableWithPersistedSort.parameters = { +TableWithControlledSort.parameters = { docs: { description: { story: - "Sort a column, then reload the page. The sort state is restored from `localStorage` using `sortStateStorageKey`.", + "The sort state is fully controlled by the parent via `sortModel` and `onSortModelChange`, so it can be saved anywhere the consumer wants.", }, }, }; diff --git a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx index 8abf35c4..a635137d 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinGridMUI.tsx @@ -3,6 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import Box from "@mui/material/Box"; +import { GridSortModel } from "@mui/x-data-grid"; import React from "react"; import { InView } from "react-intersection-observer"; @@ -78,15 +79,13 @@ export interface ITwinGridPropsMUI /** Localized string overrides - falls back to default English strings if not provided */ stringsOverrides?: Partial; /** - * When true, the table sort state is persisted to `localStorage` so it is - * restored on subsequent mounts. + * Controlled sort model for the table view. When provided, the table's sort + * state is fully controlled by the parent and must be kept in sync via + * `onSortModelChange`. */ - preserveSortState?: boolean; - /** - * `localStorage` key used to persist the table sort state when - * `preserveSortState` is true. - */ - sortStateStorageKey?: string; + sortModel?: GridSortModel; + /** Called whenever the table sort model changes. */ + onSortModelChange?: (sortModel: GridSortModel) => void; } /** @@ -109,8 +108,8 @@ export const ITwinGridMUI = ({ viewMode, tableOverrides, className, - preserveSortState, - sortStateStorageKey, + sortModel, + onSortModelChange, }: ITwinGridPropsMUI) => { const { iTwinFavorites, @@ -299,8 +298,8 @@ export const ITwinGridMUI = ({ tableOverrides={tableOverrides} isLoading={fetchStatus === DataStatus.Fetching} fetchMore={fetchMore} - preserveSortState={preserveSortState} - sortStateStorageKey={sortStateStorageKey} + sortModel={sortModel} + onSortModelChange={onSortModelChange} /> ); }; diff --git a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx index 257f866e..aca2c599 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx @@ -67,46 +67,14 @@ export interface ITwinTableMUIProps { /** Called when more data should be loaded. */ fetchMore?: (() => void) | false; /** - * When true, the sort state is persisted to `localStorage` so it is restored - * on subsequent mounts. + * Controlled sort model. When provided, the table's sort state is fully + * controlled by the parent and must be kept in sync via `onSortModelChange`. */ - preserveSortState?: boolean; - /** - * `localStorage` key used to persist the sort state when `preserveSortState` - * is true. - * @default "itwin-table-sort-model" - */ - sortStateStorageKey?: string; + sortModel?: GridSortModel; + /** Called whenever the sort model changes. */ + onSortModelChange?: (sortModel: GridSortModel) => void; } -const DEFAULT_SORT_STATE_STORAGE_KEY = "itwin-table-sort-model"; - -const readPersistedSortModel = (storageKey: string): GridSortModel => { - try { - const raw = window.localStorage.getItem(storageKey); - if (!raw) { - return []; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? (parsed as GridSortModel) : []; - } catch { - return []; - } -}; - -const writePersistedSortModel = ( - storageKey: string, - sortModel: GridSortModel -) => { - try { - window.localStorage.setItem(storageKey, JSON.stringify(sortModel)); - } catch { - console.warn( - `Failed to persist sort model to localStorage under key "${storageKey}".` - ); - } -}; - /** * Table view for iTwins using MUI X DataGrid (Community edition). */ @@ -125,8 +93,8 @@ export const ITwinTableMUI = ({ } = {}, isLoading, fetchMore, - preserveSortState, - sortStateStorageKey = DEFAULT_SORT_STATE_STORAGE_KEY, + sortModel, + onSortModelChange, }: ITwinTableMUIProps) => { // Eagerly load all available data so the table has the full dataset // for client-side pagination and sorting. @@ -135,20 +103,6 @@ export const ITwinTableMUI = ({ fetchMore(); } }, [fetchMore]); - - // Internal sort state, seeded from localStorage when persistence is enabled. - const [internalSortModel, setInternalSortModel] = - React.useState(() => - preserveSortState ? readPersistedSortModel(sortStateStorageKey) : [] - ); - - const handleSortModelChange = React.useCallback( - (newSortModel: GridSortModel) => { - setInternalSortModel(newSortModel); - writePersistedSortModel(sortStateStorageKey, newSortModel); - }, - [sortStateStorageKey] - ); const columns = React.useMemo[]>(() => { const cols: (GridColDef | false)[] = [ !hideColumns.includes(ITwinCellColumn.Favorite) && { @@ -244,8 +198,8 @@ export const ITwinTableMUI = ({ rows={iTwins} columns={columns} loading={isLoading} - sortModel={preserveSortState ? internalSortModel : undefined} - onSortModelChange={preserveSortState ? handleSortModelChange : undefined} + sortModel={sortModel} + onSortModelChange={onSortModelChange} onRowClick={ actions ? (params) => { diff --git a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx index 2bd488c4..b64eb741 100644 --- a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelGridMUI.tsx @@ -3,6 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import Box from "@mui/material/Box"; +import { GridSortModel } from "@mui/x-data-grid"; import React from "react"; import { InView } from "react-intersection-observer"; @@ -104,15 +105,13 @@ export interface IModelGridMUIProps tableOverrides?: IModelTableOverridesMUI; stringsOverrides?: Partial; /** - * When true, the table sort state is persisted to `localStorage` so it is - * restored on subsequent mounts. + * Controlled sort model for the table view. When provided, the table's sort + * state is fully controlled by the parent and must be kept in sync via + * `onSortModelChange`. */ - preserveSortState?: boolean; - /** - * `localStorage` key used to persist the table sort state when - * `preserveSortState` is true. - */ - sortStateStorageKey?: string; + sortModel?: GridSortModel; + /** Called whenever the table sort model changes. */ + onSortModelChange?: (sortModel: GridSortModel) => void; } /** @@ -159,8 +158,8 @@ const IModelGridInternal = ({ onRefetch, dataMode = "internal", disableAddToRecents = false, - preserveSortState, - sortStateStorageKey, + sortModel, + onSortModelChange, }: IModelGridMUIProps) => { const [sort, setSort] = React.useState(sortOptions); @@ -420,8 +419,8 @@ const IModelGridInternal = ({ tableOverrides={tableOverrides} isLoading={fetchStatus === DataStatus.Fetching} fetchMore={fetchMore} - preserveSortState={preserveSortState} - sortStateStorageKey={sortStateStorageKey} + sortModel={sortModel} + onSortModelChange={onSortModelChange} data-testid="imodel-table" /> )} diff --git a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx index c33750c0..5dadd834 100644 --- a/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/iModelGrid/IModelTableMUI.tsx @@ -64,46 +64,14 @@ export interface IModelTableMUIProps { /** Called when more data should be loaded. */ fetchMore?: (() => void) | false; /** - * When true, the sort state is persisted to `localStorage` so it is restored - * on subsequent mounts. + * Controlled sort model. When provided, the table's sort state is fully + * controlled by the parent and must be kept in sync via `onSortModelChange`. */ - preserveSortState?: boolean; - /** - * `localStorage` key used to persist the sort state when `preserveSortState` - * is true. - * @default "imodel-table-sort-model" - */ - sortStateStorageKey?: string; + sortModel?: GridSortModel; + /** Called whenever the sort model changes. */ + onSortModelChange?: (sortModel: GridSortModel) => void; } -const DEFAULT_SORT_STATE_STORAGE_KEY = "imodel-table-sort-model"; - -const readPersistedSortModel = (storageKey: string): GridSortModel => { - try { - const raw = window.localStorage.getItem(storageKey); - if (!raw) { - return []; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? (parsed as GridSortModel) : []; - } catch { - return []; - } -}; - -const writePersistedSortModel = ( - storageKey: string, - sortModel: GridSortModel -) => { - try { - window.localStorage.setItem(storageKey, JSON.stringify(sortModel)); - } catch { - console.warn( - `Failed to persist sort model to localStorage under key "${storageKey}".` - ); - } -}; - /** * Table view for iModels using MUI X DataGrid (Community edition). */ @@ -119,8 +87,8 @@ export const IModelTableMUI = ({ } = {}, isLoading, fetchMore, - preserveSortState, - sortStateStorageKey = DEFAULT_SORT_STATE_STORAGE_KEY, + sortModel, + onSortModelChange, }: IModelTableMUIProps) => { // Eagerly load all available data so the table has the full dataset // for client-side pagination and sorting. @@ -131,20 +99,6 @@ export const IModelTableMUI = ({ }, [fetchMore]); const favoritesContext = useIModelFavoritesContext(); - // Internal sort state, seeded from localStorage when persistence is enabled. - const [internalSortModel, setInternalSortModel] = - React.useState(() => - preserveSortState ? readPersistedSortModel(sortStateStorageKey) : [] - ); - - const handleSortModelChange = React.useCallback( - (newSortModel: GridSortModel) => { - setInternalSortModel(newSortModel); - writePersistedSortModel(sortStateStorageKey, newSortModel); - }, - [sortStateStorageKey] - ); - const columns = React.useMemo[]>(() => { const cols: (GridColDef | false)[] = [ !hideColumns.includes(IModelCellColumn.Favorite) && { @@ -244,8 +198,8 @@ export const IModelTableMUI = ({ rows={iModels} columns={columns} loading={isLoading} - sortModel={preserveSortState ? internalSortModel : undefined} - onSortModelChange={preserveSortState ? handleSortModelChange : undefined} + sortModel={sortModel} + onSortModelChange={onSortModelChange} onRowClick={ actions ? (params) => { From 553f764e74260903a211d8ae7668542651e9949f Mon Sep 17 00:00:00 2001 From: "Omar H." Date: Fri, 10 Jul 2026 14:01:06 -0400 Subject: [PATCH 4/4] feat(iModelBrowser): add controlled table sorting for iTwin/iModel tables --- .../omar-save-sort-imodel-table_2026-07-10-18-00.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@itwin/imodel-browser-react/omar-save-sort-imodel-table_2026-07-10-18-00.json diff --git a/common/changes/@itwin/imodel-browser-react/omar-save-sort-imodel-table_2026-07-10-18-00.json b/common/changes/@itwin/imodel-browser-react/omar-save-sort-imodel-table_2026-07-10-18-00.json new file mode 100644 index 00000000..2fe0b56f --- /dev/null +++ b/common/changes/@itwin/imodel-browser-react/omar-save-sort-imodel-table_2026-07-10-18-00.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/imodel-browser-react", + "comment": "Controlled table sorting for iTwin/iModel tables", + "type": "minor" + } + ], + "packageName": "@itwin/imodel-browser-react" +} \ No newline at end of file