Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to open collapsed containing panel when open widget request is made #3289

Merged
merged 7 commits into from
Mar 2, 2022
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
2 changes: 2 additions & 0 deletions common/api/appui-abstract.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2099,6 +2099,8 @@ export enum UiItemsApplicationAction {

// @public
export class UiItemsManager {
// @internal
static clearAllProviders(): void;
static getBackstageItems(): BackstageItem[];
static getStatusBarItems(stageId: string, stageUsage: string, stageAppData?: any): CommonStatusBarItem[];
static getToolbarButtonItems(stageId: string, stageUsage: string, toolbarUsage: ToolbarUsage, toolbarOrientation: ToolbarOrientation, stageAppData?: any): CommonToolbarItem[];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/appui-abstract",
"comment": "Provide internal method to clear out all registered item providers for use in unit testing.",
"type": "none"
}
],
"packageName": "@itwin/appui-abstract"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/appui-react",
"comment": "Add the ability to Open a collapsed Panel when a request to Open a widget in a collapsed panel is made.",
"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
Expand Up @@ -84,3 +84,9 @@ It is now possible to retrieve `Units` from schemas stored in IModels. The new [

UiFramework.setIModelConnection(iModelConnection, true);
```

## AppUI Updates

### WidgetState changes

The property [WidgetDef.state]($appui-react) will now return `WidgetState.Closed` if the widget is in a panel that is collapsed, or the panel size is 0 or undefined. When `WidgetState.Open` is passed to the method [WidgetDef.setWidgetState]($appui-react) the containing panel will also open if it is in a collapsed state.
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ export class NetworkTracingUiProvider implements UiItemsProvider {
public provideWidgets(stageId: string, _stageUsage: string, location: StagePanelLocation,
section?: StagePanelSection): ReadonlyArray<AbstractWidgetProps> {
const widgets: AbstractWidgetProps[] = [];
if (stageId === NetworkTracingFrontstage.stageId && location === StagePanelLocation.Right && section === StagePanelSection.Start) {
if ((stageId === NetworkTracingFrontstage.stageId || stageId === "ui-test-app:no-widget-frontstage") &&
location === StagePanelLocation.Right && section === StagePanelSection.Start) {
/** This widget when only be displayed when there is an element selected. */
const widget: AbstractWidgetProps = {
id: "ui-item-provider-test:elementDataListWidget",
Expand Down
6 changes: 6 additions & 0 deletions ui/appui-abstract/src/appui-abstract/UiItemsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ export interface UiItemProviderRegisteredEventArgs {
export class UiItemsManager {
private static _registeredUiItemsProviders: Map<string, UiItemsProvider> = new Map<string, UiItemsProvider>();

/** For use in unit testing
* @internal */
public static clearAllProviders() {
UiItemsManager._registeredUiItemsProviders.clear();
}

/** Event raised any time a UiProvider is registered or unregistered. */
public static readonly onUiProviderRegisteredEvent = new BeEvent<(ev: UiItemProviderRegisteredEventArgs) => void>();

Expand Down
9 changes: 9 additions & 0 deletions ui/appui-abstract/src/test/UiItemsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ describe("UiItemsManager", () => {
expect(UiItemsManager.hasRegisteredProviders).to.be.false;
});

it("can clear all providers", () => {
const testUiProvider = new TestUiItemsProvider("TestUiItemsProvider");
expect(UiItemsManager.hasRegisteredProviders).to.be.false;
UiItemsManager.register(testUiProvider);
expect(UiItemsManager.hasRegisteredProviders).to.be.true;
UiItemsManager.clearAllProviders();
expect(UiItemsManager.hasRegisteredProviders).to.be.false;
});

it("if no registered providers no tools are available", () => {
const toolSpecs = UiItemsManager.getToolbarButtonItems("stage", testStageUsage, ToolbarUsage.ContentManipulation, ToolbarOrientation.Horizontal);
expect(toolSpecs.length).to.be.eq(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,8 +708,13 @@ export class FrontstageDef {
if (isFloatingLocation(location))
return WidgetState.Floating;

let collapsedPanel = false;
if ("side" in location) {
const panel = this.nineZoneState.panels[location.side];
collapsedPanel = panel.collapsed || undefined === panel.size || 0 === panel.size;
}
const widgetContainer = this.nineZoneState.widgets[location.widgetId];
if (widgetDef.id === widgetContainer.activeTabId)
if (widgetDef.id === widgetContainer.activeTabId && !collapsedPanel)
return WidgetState.Open;
else
return WidgetState.Closed;
Expand Down
12 changes: 12 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 @@ -278,6 +278,7 @@ export function addWidgets(state: NineZoneState, widgets: ReadonlyArray<WidgetDe

const activeWidget = visibleWidgets.find((widget) => widget.isActive);
const minimized = !activeWidget;
// istanbul ignore else
if (activeWidget?.defaultState !== WidgetState.Floating) {
state = addPanelWidget(state, side, widgetId, tabs, {
activeTabId: activeWidget ? activeWidget.id : tabs[0],
Expand Down Expand Up @@ -638,6 +639,7 @@ const stateVersion = 11; // this needs to be bumped when NineZoneState is change
export function initializeNineZoneState(frontstageDef: FrontstageDef): NineZoneState {
let nineZone = defaultNineZone;
nineZone = produce(nineZone, (stateDraft) => {
// istanbul ignore next
if (!FrontstageManager.nineZoneSize)
return;
stateDraft.size = {
Expand Down Expand Up @@ -841,6 +843,16 @@ export const setWidgetState = produce((
const widget = nineZone.widgets[location.widgetId];
widget.minimized = false;
widget.activeTabId = id;
// ensure panel containing widget is not collapsed
// istanbul ignore else
if ("side" in location) {
const panel = nineZone.panels[location.side];
panel.collapsed && (panel.collapsed = false);
// istanbul ignore next
if (undefined === panel.size || 0 === panel.size) {
panel.size = panel.minSize ?? 200;
}
}
} else if (state === WidgetState.Closed) {
const id = widgetDef.id;
let location = findTab(nineZone, id);
Expand Down
7 changes: 6 additions & 1 deletion ui/appui-react/src/test/frontstage/FrontstageDef.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { expect } from "chai";
import * as React from "react";
import * as sinon from "sinon";
import produce from "immer";
import { MockRender } from "@itwin/core-frontend";
import { CoreTools, Frontstage, FRONTSTAGE_SETTINGS_NAMESPACE, FrontstageDef, FrontstageManager, FrontstageProps, FrontstageProvider, getFrontstageStateSettingName, StagePanelDef, UiFramework, WidgetDef } from "../../appui-react";
import TestUtils, { storageMock } from "../TestUtils";
Expand Down Expand Up @@ -323,12 +324,15 @@ describe("float and dock widget", () => {

it("panel widget should popout", async () => {
let state = createNineZoneState({ size: { height: 1000, width: 1600 } });
state = addPanelWidget(state, "right", "rightStart", ["t1"], { minimized: true });
state = addPanelWidget(state, "left", "leftStart", ["t1"], { minimized: true });
state = addPanelWidget(state, "right", "rightMiddle", ["t2", "t4"], { activeTabId: "t2" });
state = addPanelWidget(state, "right", "rightEnd", ["t3"]);
state = addTab(state, "t1", { preferredPopoutWidgetSize: { width: 99, height: 99, x: 99, y: 99 } });
state = addTab(state, "t2");
state = addTab(state, "t3");
state = produce(state, (draft) => {
draft.panels.right.size = 300;
});

const frontstageDef = new FrontstageDef();
const nineZoneStateSetter = sinon.spy();
Expand All @@ -354,6 +358,7 @@ describe("float and dock widget", () => {
.onFirstCall().returns(t1);
findWidgetDefGetter.returns(t2);

expect(frontstageDef.getWidgetCurrentState(t1)).to.eql(WidgetState.Closed);
expect(frontstageDef.getWidgetCurrentState(t2)).to.eql(WidgetState.Open);
expect(frontstageDef.getWidgetCurrentState(t4)).to.eql(WidgetState.Closed);

Expand Down
183 changes: 179 additions & 4 deletions ui/appui-react/src/test/widget-panels/Frontstage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ export class TestFrontstageUi2 extends FrontstageProvider {
}
}

/** @internal */
export class TestFrontstageWithHiddenWidget extends FrontstageProvider {
public static stageId = "TestFrontstageWithHiddenWidget";
public get id(): string {
return TestFrontstageWithHiddenWidget.stageId;
}

public get frontstage() {
return (
<Frontstage
id={this.id}
defaultTool={CoreTools.selectElementCommand}
contentGroup={TestUtils.TestContentGroup1}
/>
);
}
}

/** @internal */
export class TestFrontstageUi1 extends FrontstageProvider {
public static stageId = "TestFrontstageUi1";
Expand Down Expand Up @@ -377,6 +395,25 @@ class TestDuplicateWidgetProvider implements UiItemsProvider {
}
}

class TestHiddenWidgetProvider implements UiItemsProvider {
public static stageId = "TestFrontstageWithHiddenWidget";
public get id(): string {
return TestHiddenWidgetProvider.stageId;
}

public provideWidgets(_stageId: string, _stageUsage: string, location: StagePanelLocation, section?: StagePanelSection) {
const widgets: Array<AbstractWidgetProps> = [];
if (location === StagePanelLocation.Left && section === StagePanelSection.Middle)
widgets.push({
id: "TestHiddenWidgetProviderLM1",
label: "TestHiddenWidgetProvider Hidden LM1",
getWidgetContent: () => "TestHiddenWidgetProvider LM1 widget",
defaultState: WidgetState.Hidden,
});
return widgets;
}
}

describe("Frontstage local storage wrapper", () => {
const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!;
const localStorageMock = storageMock();
Expand Down Expand Up @@ -683,6 +720,91 @@ describe("Frontstage local storage wrapper", () => {
spy.called.should.true;
});

it("should show WidgetState as closed in panel size is undefined", () => {
const frontstageDef = new FrontstageDef();
sinon.stub(frontstageDef, "isReady").get(() => true);

let nineZoneState = createNineZoneState();
nineZoneState = addPanelWidget(nineZoneState, "left", "start", ["t1", "t2"], { activeTabId: "t1" });
nineZoneState = addTab(nineZoneState, "t1");
frontstageDef.nineZoneState = nineZoneState;
const widgetDef = new WidgetDef({
id: "t1",
defaultState: WidgetState.Hidden,
});

const leftPanel = new StagePanelDef();
leftPanel.initializeFromProps({
resizable: true,
widgets: [
<Widget
key="start"
id="start"
/>,
],
}, StagePanelLocation.Left);
sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel);

sinon.stub(frontstageDef, "getStagePanelDef").withArgs(StagePanelLocation.Left).returns(leftPanel);
sinon.stub(frontstageDef, "findWidgetDef").withArgs("t1").returns(widgetDef);

// const panel = frontstageDef.nineZoneState.panels.left;
expect(frontstageDef.getWidgetCurrentState(widgetDef)).to.be.eql(WidgetState.Closed);
});

it("should show WidgetState as closed in panel size is 0", () => {
const frontstageDef = new FrontstageDef();
sinon.stub(frontstageDef, "isReady").get(() => true);

let nineZoneState = createNineZoneState();
nineZoneState = addPanelWidget(nineZoneState, "left", "start", ["t1", "t2"], { activeTabId: "t1" });
nineZoneState = addTab(nineZoneState, "t1");
frontstageDef.nineZoneState = nineZoneState;
const widgetDef = new WidgetDef({
id: "t1",
defaultState: WidgetState.Hidden,
});

const leftPanel = new StagePanelDef();
leftPanel.initializeFromProps({
resizable: true,
size: 0,
widgets: [
<Widget
key="start"
id="start"
/>,
],
}, StagePanelLocation.Left);
sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel);

sinon.stub(frontstageDef, "getStagePanelDef").withArgs(StagePanelLocation.Left).returns(leftPanel);
sinon.stub(frontstageDef, "findWidgetDef").withArgs("t1").returns(widgetDef);

// const panel = frontstageDef.nineZoneState.panels.left;
expect(frontstageDef.getWidgetCurrentState(widgetDef)).to.be.eql(WidgetState.Closed);
});

it("should show WidgetState as closed in panel is collapsed", () => {
const frontstageDef = new FrontstageDef();
sinon.stub(frontstageDef, "isReady").get(() => true);

let nineZoneState = createNineZoneState();
nineZoneState = addPanelWidget(nineZoneState, "left", "start", ["t1", "t2"], { activeTabId: "t1" });
nineZoneState = addTab(nineZoneState, "t1");
nineZoneState = produce(nineZoneState, (draft) => {
draft.panels.left.collapsed = true;
});
frontstageDef.nineZoneState = nineZoneState;
const widgetDef = new WidgetDef({
id: "t1",
defaultState: WidgetState.Open,
});

sinon.stub(frontstageDef, "findWidgetDef").withArgs("t1").returns(widgetDef);
expect(frontstageDef.getWidgetCurrentState(widgetDef)).to.be.eql(WidgetState.Closed);
});

it("should ignore nineZoneState changes of other frontstages", () => {
const frontstageDef = new FrontstageDef();
const nineZoneState = createNineZoneState();
Expand Down Expand Up @@ -1837,13 +1959,10 @@ describe("Frontstage local storage wrapper", () => {
});

afterEach(() => {
UiItemsManager.unregister("TestUi2Provider");
UiItemsManager.clearAllProviders();
FrontstageManager.clearFrontstageProviders();
FrontstageManager.setActiveFrontstageDef(undefined);
FrontstageManager.nineZoneSize = undefined;
});

afterEach(() => {
TestUtils.terminateUiFramework();
IModelApp.shutdown();
});
Expand Down Expand Up @@ -1875,6 +1994,62 @@ describe("Frontstage local storage wrapper", () => {
await wrapper.findByText("TestUi2Provider W1");
expect(wrapper.queryAllByText("Left Start 1").length).to.equal(1);
expect(wrapper.queryAllByText("TestUi2Provider RM1").length).to.equal(1);

});

it("should support widgets with default state of hidden", async () => {
UiItemsManager.register(new TestHiddenWidgetProvider());

const frontstageProvider = new TestFrontstageWithHiddenWidget();
FrontstageManager.addFrontstageProvider(frontstageProvider);
const frontstageDef = await FrontstageManager.getFrontstageDef(frontstageProvider.frontstage.props.id);
if (frontstageDef)
frontstageDef.nineZoneState = createNineZoneState();

await FrontstageManager.setActiveFrontstageDef(frontstageDef);
const widgetDef = frontstageDef?.findWidgetDef("TestHiddenWidgetProviderLM1");
expect (widgetDef).to.not.be.undefined;

const wrapper = render(<Provider store={TestUtils.store}><WidgetPanelsFrontstage /></Provider>);
// should be hidden initially
expect(wrapper.queryAllByText("TestHiddenWidgetProvider LM1 widget").length).to.equal(0);

act(() => {
widgetDef?.setWidgetState(WidgetState.Open);
});

// should be present after setting state to Open
expect(wrapper.queryAllByText("TestHiddenWidgetProvider LM1 widget").length).to.equal(1);
});

it("should open collapsed panel when widget is opened", async () => {
UiItemsManager.register(new TestHiddenWidgetProvider());

const frontstageProvider = new TestFrontstageWithHiddenWidget();
FrontstageManager.addFrontstageProvider(frontstageProvider);
const frontstageDef = await FrontstageManager.getFrontstageDef(frontstageProvider.frontstage.props.id);
if (frontstageDef) {
let state = createNineZoneState();
state = produce(state, (draft) => {
draft.panels.left.collapsed = true;
});
frontstageDef.nineZoneState = state;
}

await FrontstageManager.setActiveFrontstageDef(frontstageDef);
const widgetDef = frontstageDef?.findWidgetDef("TestHiddenWidgetProviderLM1");
expect (widgetDef).to.not.be.undefined;

const wrapper = render(<Provider store={TestUtils.store}><WidgetPanelsFrontstage /></Provider>);
// should be hidden initially
expect(wrapper.queryAllByText("TestHiddenWidgetProvider LM1 widget").length).to.equal(0);

act(() => {
widgetDef?.setWidgetState(WidgetState.Open);
});

// should be present after setting state to Open
expect(wrapper.queryAllByText("TestHiddenWidgetProvider LM1 widget").length).to.equal(1);
});

it("should listen for window close event", async () => {
Expand Down