Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/imodel-browser-react",
"comment": "Add option to pass $orderby API query param",
"type": "minor"
}
],
"packageName": "@itwin/imodel-browser-react"
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ export interface ITwinGridProps {
* A function allow filtering on anything, is used in a normal array.filter.
*/
filterOptions?: ITwinFilterOptions;
/**
* Set the `$orderby` parameter when fetching iTwins from the iTwin API, e.g. `displayName ASC`.
*
* This only has an effect when the `requestType` is empty - it does not apply to "favorites" or "recents".
*
* See https://developer.bentley.com/apis/itwins/operations/get-my-itwins/#odata-queries for details.
Comment thread
aruniverse marked this conversation as resolved.
*/
orderbyOptions?: string;
/** List of actions to build for each iTwin context menu. */
iTwinActions?: ContextMenuBuilderItem<ITwinFull>[];
/** Function (can be a react hook) that returns state for an iTwin, returned values will be applied as props to the iTwinTile, overrides ITwinGrid provided values */
Expand Down Expand Up @@ -109,6 +117,7 @@ export const ITwinGrid = ({
accessToken,
apiOverrides,
filterOptions,
orderbyOptions,
onThumbnailClick,
iTwinActions,
requestType,
Expand Down Expand Up @@ -156,6 +165,7 @@ export const ITwinGrid = ({
accessToken,
apiOverrides,
filterOptions,
orderbyOptions,
shouldRefetchFavorites,
resetShouldRefetchFavorites,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,35 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { renderHook } from "@testing-library/react-hooks";
import { rest } from "msw";
import {
type ResponseComposition,
type RestContext,
type RestRequest,
rest,
} from "msw";

import { server } from "../../tests/mocks/server";
import { DataStatus } from "../../types";
import { useITwinData } from "./useITwinData";

describe("useITwinData hook", () => {
const accessToken = "accessToken";
const urlWatcher = jest.fn();

// Establish API mocking before all tests.
beforeAll(() => server.listen());
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers());
afterEach(() => {
server.resetHandlers();
jest.clearAllMocks();
});
// Clean up after the tests are finished.
afterAll(() => server.close());

it("returns all iTwins and proper status on successful call", async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken: "accessToken" })
useITwinData({ accessToken })
);

await waitForNextUpdate();
Expand All @@ -32,7 +43,7 @@ describe("useITwinData hook", () => {
});
it("returns favorite iTwins and proper status on successful call", async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken: "accessToken", requestType: "favorites" })
useITwinData({ accessToken, requestType: "favorites" })
);

await waitForNextUpdate();
Expand All @@ -44,7 +55,7 @@ describe("useITwinData hook", () => {
});
it("returns recent iTwins and proper status on successful call", async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken: "accessToken", requestType: "recents" })
useITwinData({ accessToken, requestType: "recents" })
);

await waitForNextUpdate();
Expand All @@ -56,7 +67,7 @@ describe("useITwinData hook", () => {
});
it("returns searched iTwins and proper status on successful call", async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken: "accessToken", filterOptions: "searched" })
useITwinData({ accessToken, filterOptions: "searched" })
);

await waitForNextUpdate();
Expand All @@ -75,7 +86,7 @@ describe("useITwinData hook", () => {
);

const { result, waitForValueToChange } = renderHook(() =>
useITwinData({ accessToken: "accessToken" })
useITwinData({ accessToken })
);

await waitForValueToChange(() => result.current.status);
Expand All @@ -86,10 +97,10 @@ describe("useITwinData hook", () => {
it("returns apiOverrides.data without fetching when it is provided", async () => {
const data = [{ id: "rerenderedId", displayName: "rerenderedDisplayName" }];
const fetchData = [{ id: "fetchedId", displayName: "fetchedDisplayName" }];
const watcher = jest.fn();

server.use(
rest.get("https://api.bentley.com/itwins/", (req, res, ctx) => {
watcher();
urlWatcher(req.url.toString());
return res(ctx.status(200), ctx.json({ iTwins: fetchData }));
})
);
Expand All @@ -98,12 +109,12 @@ describe("useITwinData hook", () => {
Parameters<typeof useITwinData>,
ReturnType<typeof useITwinData>
>((initialValue) => useITwinData(...initialValue), {
initialProps: [{ accessToken: "accessToken" }],
initialProps: [{ accessToken }],
});

await waitForNextUpdate();

expect(watcher).toHaveBeenCalledTimes(1);
expect(urlWatcher).toHaveBeenCalledTimes(1);
expect(result.current.status).toEqual(DataStatus.Complete);
expect(result.current.iTwins).toEqual(fetchData);

Expand All @@ -119,14 +130,14 @@ describe("useITwinData hook", () => {
{ id: "rerenderedId", displayName: "rerenderedDisplayName" },
]);
expect(result.current.status).toEqual(DataStatus.Complete);
expect(watcher).toHaveBeenCalledTimes(1);
expect(urlWatcher).toHaveBeenCalledTimes(1);

rerender([{ accessToken: "accessToken" }]);
rerender([{ accessToken }]);
await waitForNextUpdate();

expect(result.current.iTwins).toEqual(fetchData);
expect(result.current.status).toEqual(DataStatus.Complete);
expect(watcher).toHaveBeenCalledTimes(2);
expect(urlWatcher).toHaveBeenCalledTimes(2);
});

it("returns proper error if no accessToken is provided without data override", async () => {
Expand Down Expand Up @@ -174,4 +185,99 @@ describe("useITwinData hook", () => {

expect(result.current.iTwins.map((iTwin) => iTwin.id)).toEqual(expected);
});

describe("orderByOptions", () => {
const orderbyOptions = "displayName DESC";
const fetchedITwins = [
{ id: "fetchedId", displayName: "fetchedDisplayName" },
];

const handleRequest = (
req: RestRequest,
res: ResponseComposition,
ctx: RestContext
) => {
urlWatcher(req.url.toString());
return res(ctx.status(200), ctx.json({ iTwins: fetchedITwins }));
};

it("returns ordered iTwins and proper status on successful call", async () => {
server.use(rest.get("https://api.bentley.com/itwins/", handleRequest));
const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken, orderbyOptions: "displayName ASC" })
);

await waitForNextUpdate();

expect(result.current.iTwins).toEqual(fetchedITwins);
expect(urlWatcher).toHaveBeenCalledWith(
expect.stringContaining("$orderby=displayName%20ASC")
);
expect(result.current.status).toEqual(DataStatus.Complete);
});

it("ignores orderBy options for favorites request", async () => {
server.use(
rest.get("https://api.bentley.com/itwins/favorites", handleRequest)
);

const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken, requestType: "favorites", orderbyOptions })
);

await waitForNextUpdate();
expect(urlWatcher).toHaveBeenCalledWith(
expect.not.stringContaining("$orderby")
);
expect(result.current.status).toEqual(DataStatus.Complete);
});

it("ignores orderBy options for recents request", async () => {
server.use(
rest.get("https://api.bentley.com/itwins/recents", handleRequest)
);

const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken, requestType: "recents", orderbyOptions })
);

await waitForNextUpdate();
expect(urlWatcher).toHaveBeenCalledWith(
expect.not.stringContaining("$orderby")
);
expect(result.current.status).toEqual(DataStatus.Complete);
});

it("properly encodes orderBy options with special characters", async () => {
server.use(rest.get("https://api.bentley.com/itwins/", handleRequest));

const { result, waitForNextUpdate } = renderHook(() =>
useITwinData({ accessToken, orderbyOptions })
);

await waitForNextUpdate();
expect(urlWatcher).toHaveBeenCalledWith(
expect.stringContaining(encodeURIComponent(orderbyOptions))
);
expect(result.current.status).toEqual(DataStatus.Complete);
});

it("refetches data when orderBy options change", async () => {
server.use(rest.get("https://api.bentley.com/itwins/", handleRequest));

const { rerender, waitForNextUpdate } = renderHook<
Parameters<typeof useITwinData>,
ReturnType<typeof useITwinData>
>((initialValue) => useITwinData(...initialValue), {
initialProps: [{ accessToken, orderbyOptions }],
});

await waitForNextUpdate();
expect(urlWatcher).toHaveBeenCalledTimes(1);

rerender([{ accessToken, orderbyOptions: "somethingDifferent" }]);
await waitForNextUpdate();
expect(urlWatcher).toHaveBeenCalledTimes(2);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ProjectDataHookOptions {
accessToken?: string | (() => Promise<string>) | undefined;
apiOverrides?: ApiOverrides<ITwinFull[]>;
filterOptions?: ITwinFilterOptions;
orderbyOptions?: string;
shouldRefetchFavorites?: boolean;
resetShouldRefetchFavorites?: () => void;
}
Expand All @@ -32,6 +33,7 @@ export const useITwinData = ({
accessToken,
apiOverrides,
filterOptions,
orderbyOptions,
shouldRefetchFavorites,
resetShouldRefetchFavorites,
}: ProjectDataHookOptions) => {
Expand Down Expand Up @@ -76,6 +78,7 @@ export const useITwinData = ({
accessToken,
requestType,
iTwinSubClass,
orderbyOptions,
data,
serverEnvironmentPrefix,
refetchData,
Expand Down Expand Up @@ -110,9 +113,14 @@ export const useITwinData = ({
["favorites", "recents"].includes(requestType) || !filterOptions
? ""
: `&$search=${encodeURIComponent(String(filterOptions).trim())}`;
const orderby =
["favorites", "recents"].includes(requestType) || !orderbyOptions
? ""
: `&$orderby=${encodeURIComponent(String(orderbyOptions).trim())}`;

const url = `${_getAPIServer(
serverEnvironmentPrefix
)}/itwins/${endpoint}${subClass}${paging}${search}`;
)}/itwins/${endpoint}${subClass}${paging}${search}${orderby}`;

const makeFetchRequest = async () => {
const options: RequestInit = {
Expand Down Expand Up @@ -165,6 +173,7 @@ export const useITwinData = ({
data,
serverEnvironmentPrefix,
filterOptions,
orderbyOptions,
page,
morePages,
iTwinSubClass,
Expand Down