diff --git a/common/changes/@itwin/imodel-browser-react/vp-fix-filtering-issue_2025-09-17-09-16.json b/common/changes/@itwin/imodel-browser-react/vp-fix-filtering-issue_2025-09-17-09-16.json new file mode 100644 index 00000000..c5be9596 --- /dev/null +++ b/common/changes/@itwin/imodel-browser-react/vp-fix-filtering-issue_2025-09-17-09-16.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/imodel-browser-react", + "comment": "Fix iModelGrid Filtering via postProcessCallback to Show Complete Results Across All Data", + "type": "patch" + } + ], + "packageName": "@itwin/imodel-browser-react" +} \ No newline at end of file diff --git a/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.test.tsx b/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.test.tsx index fb205f61..0576b836 100644 --- a/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.test.tsx +++ b/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.test.tsx @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import "@testing-library/jest-dom"; -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import React from "react"; -import { DataStatus, IModelCellOverrides, IModelGrid } from "../.."; +import { DataStatus, IModelCellOverrides, IModelFull, IModelGrid } from "../.."; import * as useIModelData from "./useIModelData"; describe("IModelGrid", () => { @@ -185,4 +185,120 @@ describe("IModelGrid", () => { menuItems = queryAllByRole("menuitem"); expect(menuItems.length).toBe(2); // Action 1 and Action 2 }); + + const generateMockIModels = (count: number, prefix = ""): IModelFull[] => { + return Array.from({ length: count }).map((_, index) => ({ + id: `${prefix}${index}`, + displayName: `${prefix}${index}`, + name: `${prefix}${index}`, + description: `${prefix}-description`, + })); + }; + + it("should show all data with postProcessCallback filtering", async () => { + const pageSize = 3; + const firstPageIModels = generateMockIModels(3, "iModel"); + firstPageIModels[1].description = "unmatched"; + firstPageIModels[2].description = "unmatched"; + + const secondPageIModels = generateMockIModels(3, "iModel"); + secondPageIModels[1].description = "unmatched"; + secondPageIModels[2].description = "unmatched"; + + const thirdPageIModels = generateMockIModels(3, "iModel"); + + const mockFetchMore = jest.fn(); + + const useIModelDataMock = jest.spyOn(useIModelData, "useIModelData"); + + useIModelDataMock.mockReturnValue({ + iModels: firstPageIModels, + status: DataStatus.Complete, + fetchMore: mockFetchMore, + refetchIModels: jest.fn(), + }); + + const postProcessCallback = jest + .fn() + .mockImplementation((iModels: IModelFull[]) => { + return iModels.filter( + (iModel) => iModel.description === "iModel-description" + ); + }); + + const { rerender } = render( + + ); + + await waitFor(() => { + expect(mockFetchMore).toHaveBeenCalled(); + }); + + useIModelDataMock.mockReturnValue({ + iModels: [...firstPageIModels, ...secondPageIModels], + status: DataStatus.Complete, + fetchMore: mockFetchMore, + refetchIModels: jest.fn(), + }); + + mockFetchMore.mockClear(); + postProcessCallback.mockImplementation((iModels: IModelFull[]) => { + return iModels.filter( + (iModel) => iModel.description === "iModel-description" + ); + }); + + rerender( + + ); + + expect(postProcessCallback).toHaveBeenCalledWith( + [...firstPageIModels, ...secondPageIModels], + DataStatus.Complete, + undefined + ); + + await waitFor(() => { + expect(mockFetchMore).toHaveBeenCalled(); + }); + + useIModelDataMock.mockReturnValue({ + iModels: [...firstPageIModels, ...secondPageIModels, ...thirdPageIModels], + status: DataStatus.Complete, + fetchMore: mockFetchMore, + refetchIModels: jest.fn(), + }); + + mockFetchMore.mockClear(); + + postProcessCallback.mockImplementation((iModels: IModelFull[]) => { + return iModels.filter( + (iModel) => iModel.description === "iModel-description" + ); + }); + + rerender( + + ); + + expect(postProcessCallback).toHaveBeenCalledWith( + [...firstPageIModels, ...secondPageIModels, ...thirdPageIModels], + DataStatus.Complete, + undefined + ); + + expect(mockFetchMore).not.toHaveBeenCalled(); + }); }); diff --git a/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx b/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx index 657c6951..b7832ad4 100644 --- a/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx +++ b/packages/modules/imodel-browser/src/containers/iModelGrid/IModelGrid.tsx @@ -20,7 +20,7 @@ import { _mergeStrings } from "../../utils/_apiOverrides"; import { ContextMenuBuilderItem } from "../../utils/_buildMenuOptions"; import { IModelGhostTile } from "../iModelTiles/IModelGhostTile"; import { IModelTile, IModelTileProps } from "../iModelTiles/IModelTile"; -import { useIModelData } from "./useIModelData"; +import { DEFAULT_PAGE_SIZE, useIModelData } from "./useIModelData"; import { useIModelTableConfig } from "./useIModelTableConfig"; export interface IModelGridProps { /** @@ -173,6 +173,16 @@ export const IModelGrid = ({ [postProcessCallback, fetchediModels, fetchStatus, searchText] ); + React.useEffect(() => { + if ( + iModels.length < (pageSize ?? DEFAULT_PAGE_SIZE) && + fetchMore && + fetchStatus !== DataStatus.Fetching + ) { + fetchMore(); + } + }, [iModels.length, pageSize, fetchMore, fetchStatus]); + const { columns, onRowClick } = useIModelTableConfig({ iModelActions, onThumbnailClick, diff --git a/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts b/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts index d2610255..82cbc23d 100644 --- a/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts +++ b/packages/modules/imodel-browser/src/containers/iModelGrid/useIModelData.ts @@ -25,7 +25,7 @@ export interface IModelDataHookOptions { /** @deprecated in 2.1 It is no longer used as it has no effect on the data fetching. */ viewMode?: ViewType; } -const DEFAULT_PAGE_SIZE = 100; +export const DEFAULT_PAGE_SIZE = 100; export const useIModelData = ({ iTwinId,