From bc5a48831456731c54111ae5d63fa4c932704c69 Mon Sep 17 00:00:00 2001 From: Alex Dunae Date: Tue, 16 Jun 2026 10:49:43 -0700 Subject: [PATCH 1/5] Prevent concurrent iTwin API fetches The Stratakit data table requires pre-fetching all records in order to support client-side search. Previously in-progress HTTP requests would get aborted by next-page requests, resulting in `net::ERR_ABORTED`. This PR adds proper guards. We also add Storybook controls for toggling between different requestType and viewMode options for easier testing. --- ...twin-tableview-fixes_2026-06-16-17-50.json | 10 +++ .../src/imodel-browser/IModelGrid.stories.tsx | 24 +++++- .../src/imodel-browser/ITwinGrid.stories.tsx | 20 +++++ .../mui/IModelGridMUI.stories.tsx | 24 +++++- .../mui/ITwinGridMUI.stories.tsx | 20 +++++ .../containers/ITwinGrid/useITwinData.test.ts | 77 ++++++++++++++++++- .../src/containers/ITwinGrid/useITwinData.ts | 5 +- .../containers/ITwinGrid/ITwinTableMUI.tsx | 2 +- 8 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 common/changes/@itwin/imodel-browser-react/alex-itwin-tableview-fixes_2026-06-16-17-50.json diff --git a/common/changes/@itwin/imodel-browser-react/alex-itwin-tableview-fixes_2026-06-16-17-50.json b/common/changes/@itwin/imodel-browser-react/alex-itwin-tableview-fixes_2026-06-16-17-50.json new file mode 100644 index 00000000..e0016f44 --- /dev/null +++ b/common/changes/@itwin/imodel-browser-react/alex-itwin-tableview-fixes_2026-06-16-17-50.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/imodel-browser-react", + "comment": "Prevent concurrent iTwin API fetches", + "type": "patch" + } + ], + "packageName": "@itwin/imodel-browser-react" +} \ No newline at end of file diff --git a/packages/apps/storybook/src/imodel-browser/IModelGrid.stories.tsx b/packages/apps/storybook/src/imodel-browser/IModelGrid.stories.tsx index 900f83a1..6f8ad605 100644 --- a/packages/apps/storybook/src/imodel-browser/IModelGrid.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/IModelGrid.stories.tsx @@ -40,7 +40,29 @@ export const IModelGrid = (props: IModelGridProps) => ( export default { title: "imodel-browser/IModelGrid", component: IModelGrid, - argTypes: accessTokenArgTypes, + argTypes: { + ...accessTokenArgTypes, + requestType: { + options: ["all", "recents", "favorites"], + mapping: { + all: "", + recents: "recents", + favorites: "favorites", + }, + control: { + type: "radio", + }, + }, + viewMode: { + options: ["tile", "cells"], + control: { + type: "radio", + }, + }, + }, + args: { + requestType: "all", + }, excludeStories: ["IModelGrid"], } as Meta; diff --git a/packages/apps/storybook/src/imodel-browser/ITwinGrid.stories.tsx b/packages/apps/storybook/src/imodel-browser/ITwinGrid.stories.tsx index 3cf45a61..89ec6864 100644 --- a/packages/apps/storybook/src/imodel-browser/ITwinGrid.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/ITwinGrid.stories.tsx @@ -41,6 +41,26 @@ export default { component: ITwinGrid, argTypes: { accessToken, + requestType: { + options: ["all", "recents", "favorites"], + mapping: { + all: "", + recents: "recents", + favorites: "favorites", + }, + control: { + type: "radio", + }, + }, + viewMode: { + options: ["tile", "cells"], + control: { + type: "radio", + }, + }, + }, + args: { + requestType: "all", }, excludeStories: ["ITwinGrid"], } as Meta; 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 c5584528..d56dcb33 100644 --- a/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/mui/IModelGridMUI.stories.tsx @@ -38,7 +38,29 @@ export const IModelGridMUI = (props: IModelGridMUIProps) => ( export default { title: "imodel-browser/IModelGridMUI", component: IModelGridMUI, - argTypes: accessTokenArgTypes, + argTypes: { + ...accessTokenArgTypes, + requestType: { + options: ["all", "recents", "favorites"], + mapping: { + all: "", + recents: "recents", + favorites: "favorites", + }, + control: { + type: "radio", + }, + }, + viewMode: { + options: ["tile", "cells"], + control: { + type: "radio", + }, + }, + }, + args: { + requestType: "all", + }, excludeStories: ["IModelGridMUI"], } as Meta; 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 8a9acff1..6c1006f7 100644 --- a/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx +++ b/packages/apps/storybook/src/imodel-browser/mui/ITwinGridMUI.stories.tsx @@ -405,6 +405,26 @@ export default { component: ITwinGrid, argTypes: { accessToken, + viewMode: { + options: ["tile", "cells"], + control: { + type: "radio", + }, + }, + requestType: { + options: ["all", "recents", "favorites"], + mapping: { + all: "", + recents: "recents", + favorites: "favorites", + }, + control: { + type: "radio", + }, + }, + }, + args: { + requestType: "all", }, excludeStories: ["ITwinGrid"], } as Meta; diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts index 8c0f4f3d..c9487b52 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { renderHook } from "@testing-library/react-hooks"; +import { act, renderHook } from "@testing-library/react-hooks"; import { type ResponseComposition, type RestContext, @@ -280,4 +280,79 @@ describe("useITwinData hook", () => { expect(urlWatcher).toHaveBeenCalledTimes(2); }); }); + + describe("fetchMore", () => { + it("ignores fetchMore until the initial request settles", async () => { + const requestedPages: (string | null)[] = []; + server.use( + rest.get("https://api.bentley.com/itwins/", (req, res, ctx) => { + const skip = req.url.searchParams.get("$skip"); + requestedPages.push(skip); + return res( + ctx.status(200), + ctx.json({ + iTwins: [{ id: `id-${skip}`, displayName: `name-${skip}` }], + }) + ); + }) + ); + + const { result, waitForNextUpdate } = renderHook(() => + useITwinData({ accessToken }) + ); + + // fetchMore is available immediately but must be ignored until the first request completes. + act(() => { + result.current.fetchMore?.(); + result.current.fetchMore?.(); + }); + + await waitForNextUpdate(); + + expect(result.current.status).toEqual(DataStatus.Complete); + // Only the first page should have been requested + expect(requestedPages).toEqual(["0"]); + expect(result.current.iTwins).toEqual([ + { id: "id-0", displayName: "name-0" }, + ]); + }); + + it("fetches the next page once the previous request has settled", async () => { + const firstPage = Array.from({ length: 5 }, (_, i) => ({ + id: `first-${i}`, + displayName: `first-${i}`, + })); + const secondPage = [{ id: "second-0", displayName: "second-0" }]; + server.use( + rest.get("https://api.bentley.com/itwins/", (req, res, ctx) => { + const skip = req.url.searchParams.get("$skip"); + return res( + ctx.status(200), + ctx.json({ iTwins: skip === "0" ? firstPage : secondPage }) + ); + }) + ); + + const { result, waitForNextUpdate } = renderHook(() => + useITwinData({ accessToken }) + ); + + await waitForNextUpdate(); + // A full page keeps morePages true, so fetchMore stays available. + expect(result.current.status).toEqual(DataStatus.Complete); + expect(result.current.iTwins).toHaveLength(5); + expect(result.current.fetchMore).toBeDefined(); + + act(() => { + result.current.fetchMore?.(); + }); + await waitForNextUpdate(); + + expect(result.current.iTwins).toHaveLength(6); + expect(result.current.iTwins).toContainEqual({ + id: "second-0", + displayName: "second-0", + }); + }); + }); }); diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts index e5a5285a..7e4c503a 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.ts @@ -51,10 +51,11 @@ export const useITwinData = ({ setProjects([]); setPage(0); setMorePages(true); - fetchingMoreRef.current = false; + fetchingMoreRef.current = true; }, []); - const fetchingMoreRef = React.useRef(false); + // We start in a fetching state + const fetchingMoreRef = React.useRef(true); const fetchMore = React.useCallback(() => { if (fetchingMoreRef.current) { return; 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..e4d79bd1 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx @@ -92,7 +92,7 @@ export const ITwinTableMUI = ({ if (fetchMore) { fetchMore(); } - }, [fetchMore]); + }, [fetchMore, iTwins.length]); const columns = React.useMemo[]>(() => { const cols: (GridColDef | false)[] = [ !hideColumns.includes(ITwinCellColumn.Favorite) && { From 4bf9e1de33f7d80c1e396d77739be7adb2c75632 Mon Sep 17 00:00:00 2001 From: Alex Dunae Date: Tue, 16 Jun 2026 11:04:07 -0700 Subject: [PATCH 2/5] test fixes --- .../src/containers/ITwinGrid/useITwinData.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts index c9487b52..e1f77cfe 100644 --- a/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts +++ b/packages/modules/imodel-browser/src/containers/ITwinGrid/useITwinData.test.ts @@ -318,7 +318,7 @@ describe("useITwinData hook", () => { }); it("fetches the next page once the previous request has settled", async () => { - const firstPage = Array.from({ length: 5 }, (_, i) => ({ + const firstPage = Array.from({ length: 100 }, (_, i) => ({ id: `first-${i}`, displayName: `first-${i}`, })); @@ -338,9 +338,8 @@ describe("useITwinData hook", () => { ); await waitForNextUpdate(); - // A full page keeps morePages true, so fetchMore stays available. expect(result.current.status).toEqual(DataStatus.Complete); - expect(result.current.iTwins).toHaveLength(5); + expect(result.current.iTwins).toHaveLength(100); expect(result.current.fetchMore).toBeDefined(); act(() => { @@ -348,7 +347,7 @@ describe("useITwinData hook", () => { }); await waitForNextUpdate(); - expect(result.current.iTwins).toHaveLength(6); + expect(result.current.iTwins).toHaveLength(101); expect(result.current.iTwins).toContainEqual({ id: "second-0", displayName: "second-0", From 0bcf47b391911ae2ae12aaa9a843bd00cc044a57 Mon Sep 17 00:00:00 2001 From: imodeljs-admin Date: Tue, 16 Jun 2026 18:38:57 +0000 Subject: [PATCH 3/5] Update changelogs [skip ci] --- .../alex-mui-codeowners_2026-06-12-19-03.json | 10 ---------- .../alex-mui-tweaks_2026-06-15-22-00.json | 10 ---------- packages/modules/imodel-browser/CHANGELOG.json | 17 +++++++++++++++++ packages/modules/imodel-browser/CHANGELOG.md | 9 ++++++++- 4 files changed, 25 insertions(+), 21 deletions(-) delete mode 100644 common/changes/@itwin/imodel-browser-react/alex-mui-codeowners_2026-06-12-19-03.json delete mode 100644 common/changes/@itwin/imodel-browser-react/alex-mui-tweaks_2026-06-15-22-00.json diff --git a/common/changes/@itwin/imodel-browser-react/alex-mui-codeowners_2026-06-12-19-03.json b/common/changes/@itwin/imodel-browser-react/alex-mui-codeowners_2026-06-12-19-03.json deleted file mode 100644 index 29c877ba..00000000 --- a/common/changes/@itwin/imodel-browser-react/alex-mui-codeowners_2026-06-12-19-03.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@itwin/imodel-browser-react", - "comment": "Reorganize internal paths of Stratakit MUI components", - "type": "none" - } - ], - "packageName": "@itwin/imodel-browser-react" -} diff --git a/common/changes/@itwin/imodel-browser-react/alex-mui-tweaks_2026-06-15-22-00.json b/common/changes/@itwin/imodel-browser-react/alex-mui-tweaks_2026-06-15-22-00.json deleted file mode 100644 index cdf81e20..00000000 --- a/common/changes/@itwin/imodel-browser-react/alex-mui-tweaks_2026-06-15-22-00.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "changes": [ - { - "packageName": "@itwin/imodel-browser-react", - "comment": "Update Stratakit card aspect ratio and title style", - "type": "patch" - } - ], - "packageName": "@itwin/imodel-browser-react" -} \ No newline at end of file diff --git a/packages/modules/imodel-browser/CHANGELOG.json b/packages/modules/imodel-browser/CHANGELOG.json index d806723a..5d07be2b 100644 --- a/packages/modules/imodel-browser/CHANGELOG.json +++ b/packages/modules/imodel-browser/CHANGELOG.json @@ -1,6 +1,23 @@ { "name": "@itwin/imodel-browser-react", "entries": [ + { + "version": "4.3.1", + "tag": "@itwin/imodel-browser-react_v4.3.1", + "date": "Tue, 16 Jun 2026 18:38:57 GMT", + "comments": { + "none": [ + { + "comment": "Reorganize internal paths of Stratakit MUI components" + } + ], + "patch": [ + { + "comment": "Update Stratakit card aspect ratio and title style" + } + ] + } + }, { "version": "4.3.0", "tag": "@itwin/imodel-browser-react_v4.3.0", diff --git a/packages/modules/imodel-browser/CHANGELOG.md b/packages/modules/imodel-browser/CHANGELOG.md index 6a33fac0..421ab894 100644 --- a/packages/modules/imodel-browser/CHANGELOG.md +++ b/packages/modules/imodel-browser/CHANGELOG.md @@ -1,6 +1,13 @@ # Change Log - @itwin/imodel-browser-react -This log was last generated on Fri, 12 Jun 2026 14:00:59 GMT and should not be manually modified. +This log was last generated on Tue, 16 Jun 2026 18:38:57 GMT and should not be manually modified. + +## 4.3.1 +Tue, 16 Jun 2026 18:38:57 GMT + +### Patches + +- Update Stratakit card aspect ratio and title style ## 4.3.0 Fri, 12 Jun 2026 14:00:59 GMT From 6662c16e19785a3c3696626d1c08530b7955cbff Mon Sep 17 00:00:00 2001 From: imodeljs-admin Date: Tue, 16 Jun 2026 18:38:58 +0000 Subject: [PATCH 4/5] Bump versions [skip ci] --- packages/modules/imodel-browser/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/imodel-browser/package.json b/packages/modules/imodel-browser/package.json index ba300bea..3589f481 100644 --- a/packages/modules/imodel-browser/package.json +++ b/packages/modules/imodel-browser/package.json @@ -2,7 +2,7 @@ "name": "@itwin/imodel-browser-react", "description": "Components that let the user browse the iModels of a context and select one.", "repository": "https://github.com/iTwin/admin-components-react/tree/main/packages/modules/imodel-browser", - "version": "4.3.0", + "version": "4.3.1", "main": "cjs/index.js", "module": "esm/index.js", "types": "cjs/index.d.ts", From 38f409c534410acbd99a81b314c0e481cdab5f78 Mon Sep 17 00:00:00 2001 From: Alex Dunae Date: Wed, 17 Jun 2026 10:48:59 -0700 Subject: [PATCH 5/5] revert eager load --- .../src/mui/containers/ITwinGrid/ITwinTableMUI.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e4d79bd1..a5c93f7e 100644 --- a/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx +++ b/packages/modules/imodel-browser/src/mui/containers/ITwinGrid/ITwinTableMUI.tsx @@ -92,7 +92,7 @@ export const ITwinTableMUI = ({ if (fetchMore) { fetchMore(); } - }, [fetchMore, iTwins.length]); + }, [fetchMore]); const columns = React.useMemo[]>(() => { const cols: (GridColDef | false)[] = [ !hideColumns.includes(ITwinCellColumn.Favorite) && {