Skip to content

Commit

Permalink
Add useSavedState flag to Widget config (#1210)
Browse files Browse the repository at this point in the history
* Add useSavedState flag.

* Impl useSavedState

* NextVersion

* rush change

* Extract API

* Add note to docs
  • Loading branch information
GerardasB authored Feb 7, 2025
1 parent 8a548c0 commit 7dfc5b9
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import * as React from "react";
import {
Frontstage,
StagePanelState,
Widget,
WidgetState,
} from "@itwin/appui-react";
import { createTestFrontstage } from "./createTestFrontstage";

/** Used in e2e tests to test different widget configurations. */
export const createTestWidgetFrontstage = () => {
{
const urlParams = new URLSearchParams(window.location.search);
const frontstageParams = getFrontstageParams(urlParams);

const defaultState = getDefaultState(urlParams);
const useSavedState = urlParams.has("useSavedState");

const frontstage = createTestFrontstage({
id: "test-widget",
version: frontstageParams.version,
});

return {
...frontstage,
rightPanel: {
defaultState: StagePanelState.Open,
sections: {
start: [
{
id: "widget-1",
label: "Widget 1",
content: <>Widget 1 content</>,
defaultState,
useSavedState,
},
],
},
},
} satisfies Frontstage;
}
};

function getFrontstageParams(params: URLSearchParams) {
const versionParam = params.get("frontstageVersion");
const version = versionParam ? Number(versionParam) : undefined;
return { version } satisfies Partial<Frontstage>;
}

function getDefaultState(params: URLSearchParams): Widget["defaultState"] {
const param = params.get("defaultState");
if (param === "hidden") return WidgetState.Hidden;
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
StandardContentLayouts,
} from "@itwin/appui-react";

interface CreateTestFrontstageArgs {
interface CreateTestFrontstageArgs extends Partial<Frontstage> {
id: string;
}

export const createTestFrontstage = ({ id }: CreateTestFrontstageArgs) => {
export const createTestFrontstage = (
frontstageArgs: CreateTestFrontstageArgs
) => {
{
const contentGroup = new ContentGroup({
id: "test-group",
Expand All @@ -39,9 +41,9 @@ export const createTestFrontstage = ({ id }: CreateTestFrontstageArgs) => {
});

return {
id,
version: Math.random(),
contentGroup,
...frontstageArgs,
} satisfies Frontstage;
}
};
2 changes: 2 additions & 0 deletions apps/test-app/src/frontend/registerFrontstages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
createITwinUIV2Frontstage,
createITwinUIV2FrontstageProvider,
} from "./appui/frontstages/iTwinUIV2Frontstage";
import { createTestWidgetFrontstage } from "./appui/frontstages/TestWidgetFrontstage";

interface RegisterFrontstagesArgs {
iModelConnection?: IModelConnection;
Expand All @@ -74,6 +75,7 @@ export function registerFrontstages({
}),
createElementStackingFrontstage(),
createTestPanelFrontstage(),
createTestWidgetFrontstage(),
createTestPopoutFrontstage(),
createWidgetApiFrontstage(),
createCustomContentFrontstage(),
Expand Down
1 change: 1 addition & 0 deletions common/api/appui-react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5546,6 +5546,7 @@ export interface Widget {
readonly priority?: number;
// (undocumented)
readonly tooltip?: string | ConditionalStringValue_2;
readonly useSavedState?: boolean;
}

// @public
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/appui-react",
"comment": "Add `useSavedState` to `Widget` interface.",
"type": "none"
}
],
"packageName": "@itwin/appui-react"
}
6 changes: 6 additions & 0 deletions docs/changehistory/NextVersion.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# NextVersion <!-- omit from toc -->

## @itwin/appui-react

### Additions

- Added `useSavedState` property to `Widget` interface. By default widgets with `defaultState=Hidden` are always hidden when the layout is restored (i.e. page is reloaded). When `useSavedState` is set to `true` it will override the default behavior and force the widget to use its saved layout state instead. This is useful for widgets that are hidden by default but should be shown when the layout is restored. [#1210](https://github.com/iTwin/appui/pull/1210)
63 changes: 63 additions & 0 deletions e2e-tests/tests/widget-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,66 @@ test.describe("widget lifecycle", () => {
expect(logs).toContain("Widget WL-B unmount");
});
});

test.describe("defaultState", () => {
test.describe("Hidden", () => {
test("should hide after reload", async ({ context, page }) => {
await page.goto(
"./blank?frontstageId=test-widget&frontstageVersion=1&defaultState=hidden"
);

// Hidden initially
const tab = tabLocator(page, "Widget 1");
await expect(tab).not.toBeVisible();

// Open widget
await setWidgetState(page, "widget-1", WidgetState.Open);
await expect(tab).toBeVisible();
await expectSavedFrontstageState(context, (state) => {
const widgets = Object.values(state.nineZone.widgets);
return widgets.length > 0;
});

// Should hide after reload
await page.reload();
await expectSavedFrontstageState(context, (state) => {
const widgets = Object.values(state.nineZone.widgets);
return widgets.length > 0;
});
await expect(tab).not.toBeVisible();
await expectSavedFrontstageState(context, (state) => {
const widgets = Object.values(state.nineZone.widgets);
return widgets.length === 0;
});
});

test("should persist after reload when useSavedState is enabled", async ({
context,
page,
}) => {
await page.goto(
"./blank?frontstageId=test-widget&frontstageVersion=1&defaultState=hidden&useSavedState"
);

// Hidden initially
const tab = tabLocator(page, "Widget 1");
await expect(tab).not.toBeVisible();

// Open widget
await setWidgetState(page, "widget-1", WidgetState.Open);
await expect(tab).toBeVisible();
await expectSavedFrontstageState(context, (state) => {
const widgets = Object.values(state.nineZone.widgets);
return widgets.length > 0;
});

// Should stay open after reload
await page.reload();
await expectSavedFrontstageState(context, (state) => {
const widgets = Object.values(state.nineZone.widgets);
return widgets.length > 0;
});
await expect(tab).toBeVisible();
});
});
});
2 changes: 2 additions & 0 deletions ui/appui-react/src/appui-react/widget-panels/Frontstage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ function hideWidgetDefs(frontstageDef: FrontstageDef) {

/** Hide widget with `Hidden` or `Unloaded` defaultState. */
function hideWidgetDef(frontstageDef: FrontstageDef, widgetDef: WidgetDef) {
if (widgetDef.initialConfig?.useSavedState === true) return;

if (widgetDef.defaultState === WidgetState.Hidden) {
frontstageDef.dispatch({
type: "WIDGET_TAB_HIDE",
Expand Down
13 changes: 11 additions & 2 deletions ui/appui-react/src/appui-react/widgets/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,19 @@ export interface Widget {
* It is not possible to disable the floating of a widget if `allowedPanels` is an empty array.
*/
readonly canFloat?: boolean | CanFloatWidgetOptions;
/** Defaults to `Floating` if widget is not allowed to dock to any panels. Otherwise defaults to `Closed`.
* @note If `Hidden` the widget will be hidden after the layout is restored independently of saved layout state.
/** The default state of the widget.
* - Defaults to `Floating` if the widget is not allowed to dock to any panels.
* - Otherwise, defaults to `Closed`.
*
* @note If set to `Hidden`, the widget will be hidden after the layout is restored, independently of the saved layout state. Set `useSavedState` to `true` to disable this behavior.
* @note If set to `Unloaded`, the widget will be unloaded after the layout is restored, independently of the saved layout state. Set `useSavedState` to `true` to disable this behavior.
*/
readonly defaultState?: WidgetState;
/** When enabled, the widget will always use the saved layout state. `defaultState` will only be used for the initial layout setup.
* This is useful for scenarios where the widget should only be hidden/unloaded initially, but is always restored to the last state.
* @note This property is only useful to disable the special handling of `defaultState` when it is set to `Hidden` or `Unloaded`.
*/
readonly useSavedState?: boolean;
/** Content of the Widget. */
readonly content?: React.ReactNode;
/** @deprecated in 4.16.0. Use {@link Widget.iconNode} instead. */
Expand Down

0 comments on commit 7dfc5b9

Please sign in to comment.