diff --git a/common/api/summary/ui-core.exports.csv b/common/api/summary/ui-core.exports.csv index b44df36f757f..5f93596d1bad 100644 --- a/common/api/summary/ui-core.exports.csv +++ b/common/api/summary/ui-core.exports.csv @@ -155,6 +155,7 @@ public;LoadingSpinner public;LoadingSpinnerProps public;LoadingStatus public;LoadingStatusProps +public;LocalSettingsStorage beta;LocalUiSettings public;MainTabsProps internal;mergeRefs @@ -225,14 +226,15 @@ public;SearchBoxProps public;Select: (props: SelectProps) => JSX.Element | null public;SelectOption public;SelectProps +public;SessionSettingsStorage beta;SessionUiSettings beta;SettingsContainer: ({ tabs, onSettingsTabSelected, currentSettingsTab, settingsManager, showHeader }: SettingsContainerProps) => JSX.Element beta;SettingsContainerProps beta;SettingsManager -beta;SettingsProvider beta;SettingsProvidersChangedEvent beta;SettingsProvidersChangedEventArgs beta;SettingsTabEntry +beta;SettingsTabsProvider internal;shallowDiffers: (a: internal;Size public;SizeProps @@ -289,10 +291,11 @@ public;TreeNodeProps public;TreeProps public;UiCore public;UiEvent -beta;UiSetting -public;UiSettings +public;UiSetting +public;UiSettings = UiSettingsStorage public;UiSettingsResult public;UiSettingsStatus +public;UiSettingsStorage public;UnderlinedButton(props: UnderlinedButtonProps): JSX.Element public;UnderlinedButtonProps public;useDisposable diff --git a/common/api/summary/ui-framework.exports.csv b/common/api/summary/ui-framework.exports.csv index bf61000a9da0..0c183f3c0eeb 100644 --- a/common/api/summary/ui-framework.exports.csv +++ b/common/api/summary/ui-framework.exports.csv @@ -37,6 +37,7 @@ public;AnyItemDef = GroupItemDef | CommandItemDef | ToolItemDef | ActionButtonIt public;AnyWidgetProps = WidgetProps | ToolWidgetProps | NavigationWidgetProps internal;appendWidgets(state: NineZoneState, widgetDefs: ReadonlyArray public;AppNotificationManager +beta;AppUiSettings beta;areNoFeatureOverridesActive(): boolean public;Backstage beta;BackstageActionItem @@ -110,7 +111,7 @@ public;ConfigurableUiControlConstructor = new (info: ConfigurableCreateInfo, opt public;ConfigurableUiControlType public;ConfigurableUiElement public;ConfigurableUiManager -public;ConfigurableUiReducer(state: ConfigurableUiState | undefined, _action: ConfigurableUiActionsUnion): ConfigurableUiState +public;ConfigurableUiReducer(state: ConfigurableUiState | undefined, action: ConfigurableUiActionsUnion): ConfigurableUiState public;ConfigurableUiState beta;connectIModelConnection: (mapStateToProps?: any, mapDispatchToProps?: any) => import("react-redux").InferableComponentEnhancerWithProps beta;connectIModelConnectionAndViewState: (mapStateToProps?: any, mapDispatchToProps?: any) => import("react-redux").InferableComponentEnhancerWithProps @@ -245,6 +246,7 @@ beta;getQuantityFormatsSettingsManagerEntry(itemPriority: number, opts?: Partial beta;getSelectionContextSyncEventIds(): string[] internal;getStableWidgetProps(widgetProps: WidgetProps, stableId: string): WidgetProps internal;getStagePanelType: (location: StagePanelLocation_2) => StagePanelType +beta;getUiSettingsManagerEntry(itemPriority: number, allowSettingUiFrameworkVersion?: boolean): SettingsTabEntry internal;getWidgetId(side: PanelSide, key: StagePanelZoneDefKeys): WidgetIdTypes public;GroupButton(props: GroupButtonProps): JSX.Element internal;GroupButtonItem(props: GroupButtonProps_2): JSX.Element @@ -274,6 +276,7 @@ beta;IModelViewportControl beta;IModelViewportControlOptions internal;INACTIVITY_TIME_DEFAULT = 3500 beta;Indicator +beta;InitialAppUiSettings internal;initializeNineZoneState(frontstageDef: FrontstageDef): NineZoneState internal;initializePanel(nineZone: NineZoneState, frontstageDef: FrontstageDef, panelSide: PanelSide): NineZoneState alpha;InputEditorCommitHandler @@ -416,7 +419,7 @@ internal;ProjectScope internal;ProjectServices public;PromptField: import("react-redux").ConnectedComponent public;PropsHelper -beta;QuantityFormatSettingsPanel({ initialQuantityType, availableUnitSystems }: QuantityFormatterSettingsOptions): JSX.Element +beta;QuantityFormatSettingsPage({ initialQuantityType, availableUnitSystems }: QuantityFormatterSettingsOptions): JSX.Element beta;QuantityFormatterSettingsOptions alpha;ReactContent public;ReactMessage = ReactMessage_2 @@ -600,9 +603,11 @@ public;UiFramework internal;UiIntervalEvent internal;UiIntervalEventArgs internal;UiSettingsContext: React.Context -alpha;UiSettingsProvider(props: UiSettingsProviderProps): JSX.Element -alpha;UiSettingsProviderProps +beta;UiSettingsPage({ allowSettingUiFrameworkVersion }: +beta;UiSettingsProvider(props: UiSettingsProviderProps): JSX.Element +beta;UiSettingsProviderProps public;UiShowHideManager +internal;UiShowHideSettingsProvider public;UiVisibilityChangedEvent public;UiVisibilityEventArgs internal;useActiveFrontstageDef(): FrontstageDef | undefined @@ -626,6 +631,8 @@ internal;useNineZoneDispatch(frontstageDef: FrontstageDef): NineZoneDispatch internal;useNineZoneState(frontstageDef: FrontstageDef): NineZoneState | undefined public;UserProfileBackstageItem public;UserProfileBackstageItemProps +beta;UserSettingsProvider +public;UserSettingsStorage internal;useSavedFrontstageState(frontstageDef: FrontstageDef): void internal;useSaveFrontstageSettings(frontstageDef: FrontstageDef): void internal;useStatusBarEntry(): DockedStatusBarEntryContextArg @@ -633,7 +640,7 @@ internal;useSyncDefinitions(frontstageDef: FrontstageDef): void internal;useToolSettingsNode(): React.ReactNode beta;useUiItemsProviderStatusBarItems: (manager: StatusBarItemsManager_2) => readonly CommonStatusBarItem[] beta;useUiItemsProviderToolbarItems: (manager: ToolbarItemsManager, toolbarUsage: ToolbarUsage, toolbarOrientation: ToolbarOrientation) => readonly CommonToolbarItem[] -internal;useUiSettingsContext(): UiSettings +beta;useUiSettingsStorageContext(): UiSettingsStorage internal;useUiVisibility(): boolean internal;useUpdateNineZoneSize(frontstageDef: FrontstageDef): void alpha;useVisibilityTreeFiltering: (nodeLoader: AbstractTreeNodeLoaderWithProvider diff --git a/common/api/ui-components.api.md b/common/api/ui-components.api.md index c69e4342ecdc..a07d25fa7bf1 100644 --- a/common/api/ui-components.api.md +++ b/common/api/ui-components.api.md @@ -73,6 +73,7 @@ import { TimeDisplay } from '@bentley/ui-abstract'; import { TimeFormat } from '@bentley/ui-core'; import { UiEvent } from '@bentley/ui-core'; import { UiSettings } from '@bentley/ui-core'; +import { UiSettingsStorage } from '@bentley/ui-core'; import { UnitProps } from '@bentley/imodeljs-quantity'; import { UnitsProvider } from '@bentley/imodeljs-quantity'; import { Vector3d } from '@bentley/geometry-core'; @@ -4527,9 +4528,11 @@ export interface TableProps extends CommonProps { scrollToRow?: number; selectionMode?: SelectionMode; settingsIdentifier?: string; + settingsStorage?: UiSettingsStorage; showHideColumns?: boolean; stripedRows?: boolean; tableSelectionTarget?: TableSelectionTarget; + // @deprecated uiSettings?: UiSettings; } diff --git a/common/api/ui-core.api.md b/common/api/ui-core.api.md index 7cfad92565fc..6420e016e9f0 100644 --- a/common/api/ui-core.api.md +++ b/common/api/ui-core.api.md @@ -1247,8 +1247,8 @@ export interface LoadingStatusProps extends CommonProps { percent: number; } -// @beta -export class LocalUiSettings implements UiSettings { +// @public +export class LocalSettingsStorage implements UiSettingsStorage { constructor(w?: Window); // (undocumented) deleteSetting(settingNamespace: string, settingName: string): Promise; @@ -1260,6 +1260,11 @@ export class LocalUiSettings implements UiSettings { w: Window; } +// @beta @deprecated +export class LocalUiSettings extends LocalSettingsStorage { + constructor(w?: Window); +} + // @public export interface MainTabsProps extends TabsProps { mainClassName: string; @@ -1886,8 +1891,8 @@ export interface SelectProps extends React.SelectHTMLAttributes; @@ -1899,6 +1904,11 @@ export class SessionUiSettings implements UiSettings { w: Window; } +// @beta @deprecated +export class SessionUiSettings extends SessionSettingsStorage { + constructor(w?: Window); +} + // @beta export const SettingsContainer: ({ tabs, onSettingsTabSelected, currentSettingsTab, settingsManager, showHeader }: SettingsContainerProps) => JSX.Element; @@ -1920,7 +1930,7 @@ export interface SettingsContainerProps { export class SettingsManager { activateSettingsTab(settingsTabId: string): void; // (undocumented) - addSettingsProvider(settingsProvider: SettingsProvider): void; + addSettingsProvider(settingsProvider: SettingsTabsProvider): void; closeSettingsContainer(closeFunc: (args: any) => void, closeFuncArgs?: any): void; getSettingEntries(stageId: string, stageUsage: string): Array; // @internal @@ -1931,19 +1941,12 @@ export class SettingsManager { readonly onProcessSettingsTabActivation: ProcessSettingsTabActivationEvent; readonly onSettingsProvidersChanged: SettingsProvidersChangedEvent; // (undocumented) - get providers(): ReadonlyArray; - set providers(p: ReadonlyArray); + get providers(): ReadonlyArray; + set providers(p: ReadonlyArray); // (undocumented) removeSettingsProvider(providerId: string): boolean; } -// @beta -export interface SettingsProvider { - // (undocumented) - getSettingEntries(stageId: string, stageUsage: string): ReadonlyArray | undefined; - readonly id: string; -} - // @beta export class SettingsProvidersChangedEvent extends BeUiEvent { } @@ -1951,7 +1954,7 @@ export class SettingsProvidersChangedEvent extends BeUiEvent; + readonly providers: ReadonlyArray; } // @beta @@ -1967,6 +1970,13 @@ export interface SettingsTabEntry { readonly tooltip?: string | JSX.Element; } +// @beta +export interface SettingsTabsProvider { + // (undocumented) + getSettingEntries(stageId: string, stageUsage: string): ReadonlyArray | undefined; + readonly id: string; +} + // @internal export const shallowDiffers: (a: { [key: string]: any; @@ -2477,17 +2487,19 @@ export class UiCore { export class UiEvent extends BeUiEvent { } -// @beta +// @public export class UiSetting { - constructor(settingNamespace: string, settingName: string, getValue: () => T, applyValue?: ((v: T) => void) | undefined); + constructor(settingNamespace: string, settingName: string, getValue: () => T, applyValue?: ((v: T) => void) | undefined, defaultValue?: T | undefined); // (undocumented) applyValue?: ((v: T) => void) | undefined; - deleteSetting(uiSettings: UiSettings): Promise; - getSetting(uiSettings: UiSettings): Promise; - getSettingAndApplyValue(uiSettings: UiSettings): Promise; + // (undocumented) + defaultValue?: T | undefined; + deleteSetting(storage: UiSettingsStorage): Promise; + getSetting(storage: UiSettingsStorage): Promise; + getSettingAndApplyValue(storage: UiSettingsStorage): Promise; // (undocumented) getValue: () => T; - saveSetting(uiSettings: UiSettings): Promise; + saveSetting(storage: UiSettingsStorage): Promise; // (undocumented) settingName: string; // (undocumented) @@ -2495,14 +2507,7 @@ export class UiSetting { } // @public -export interface UiSettings { - // (undocumented) - deleteSetting(settingNamespace: string, settingName: string): Promise; - // (undocumented) - getSetting(settingNamespace: string, settingName: string): Promise; - // (undocumented) - saveSetting(settingNamespace: string, settingName: string, setting: any): Promise; -} +export type UiSettings = UiSettingsStorage; // @public export interface UiSettingsResult { @@ -2526,6 +2531,16 @@ export enum UiSettingsStatus { UnknownError = 2 } +// @public +export interface UiSettingsStorage { + // (undocumented) + deleteSetting(settingNamespace: string, settingName: string): Promise; + // (undocumented) + getSetting(settingNamespace: string, settingName: string): Promise; + // (undocumented) + saveSetting(settingNamespace: string, settingName: string, setting: any): Promise; +} + // @public export function UnderlinedButton(props: UnderlinedButtonProps): JSX.Element; diff --git a/common/api/ui-framework.api.md b/common/api/ui-framework.api.md index 51ecde7a6414..9bdfd1551404 100644 --- a/common/api/ui-framework.api.md +++ b/common/api/ui-framework.api.md @@ -164,9 +164,11 @@ import { UiAdmin } from '@bentley/ui-abstract'; import { UiDataProvider } from '@bentley/ui-abstract'; import { UiEvent } from '@bentley/ui-core'; import { UiLayoutDataProvider } from '@bentley/ui-abstract'; +import { UiSetting } from '@bentley/ui-core'; import { UiSettings } from '@bentley/ui-core'; import { UiSettingsResult } from '@bentley/ui-core'; import { UiSettingsStatus } from '@bentley/ui-core'; +import { UiSettingsStorage } from '@bentley/ui-core'; import { UnifiedSelectionTreeEventHandler } from '@bentley/presentation-components'; import { UnifiedSelectionTreeEventHandlerParams } from '@bentley/presentation-components'; import { UnitSystemKey } from '@bentley/imodeljs-frontend'; @@ -486,6 +488,25 @@ export class AppNotificationManager extends NotificationManager { updatePointerMessage(displayPoint: XAndY, relativePosition: RelativePosition): void; } +// @beta +export class AppUiSettings implements UserSettingsProvider { + constructor(defaults: Partial); + // (undocumented) + apply(storage: UiSettingsStorage): Promise; + // (undocumented) + colorTheme: UiSetting; + // (undocumented) + dragInteraction: UiSetting; + // (undocumented) + frameworkVersion: UiSetting; + // (undocumented) + loadUserSettings(storage: UiSettingsStorage): Promise; + // (undocumented) + readonly providerId = "AppUiSettingsProvider"; + // (undocumented) + widgetOpacity: UiSetting; +} + // @beta export function areNoFeatureOverridesActive(): boolean; @@ -1014,6 +1035,10 @@ export class ConfigurableCreateInfo { // @public export enum ConfigurableUiActionId { + // (undocumented) + SetDragInteraction = "configurableui:set-drag-interaction", + // (undocumented) + SetFrameworkVersion = "configurableui:set-framework-version", // (undocumented) SetSnapMode = "configurableui:set_snapmode", // (undocumented) @@ -1030,6 +1055,8 @@ export const ConfigurableUiActions: { setTheme: (theme: string) => import("../redux/redux-ts").ActionWithPayload; setToolPrompt: (toolPrompt: string) => import("../redux/redux-ts").ActionWithPayload; setWidgetOpacity: (opacity: number) => import("../redux/redux-ts").ActionWithPayload; + setDragInteraction: (dragInteraction: boolean) => import("../redux/redux-ts").ActionWithPayload; + setFrameworkVersion: (frameworkVersion: string) => import("../redux/redux-ts").ActionWithPayload; }; // @public @@ -1120,10 +1147,12 @@ export class ConfigurableUiManager { } // @public -export function ConfigurableUiReducer(state: ConfigurableUiState | undefined, _action: ConfigurableUiActionsUnion): ConfigurableUiState; +export function ConfigurableUiReducer(state: ConfigurableUiState | undefined, action: ConfigurableUiActionsUnion): ConfigurableUiState; // @public export interface ConfigurableUiState { + // (undocumented) + frameworkVersion: string; // (undocumented) snapMode: number; // (undocumented) @@ -1131,6 +1160,8 @@ export interface ConfigurableUiState { // (undocumented) toolPrompt: string; // (undocumented) + useDragInteraction: boolean; + // (undocumented) widgetOpacity: number; } @@ -2016,7 +2047,7 @@ export interface FooterModeFieldProps extends StatusFieldProps { } // @internal (undocumented) -export class FrameworkAccuDraw extends AccuDraw { +export class FrameworkAccuDraw extends AccuDraw implements UserSettingsProvider { constructor(); static get displayNotifications(): boolean; static set displayNotifications(v: boolean); @@ -2032,6 +2063,8 @@ export class FrameworkAccuDraw extends AccuDraw { static readonly isSideRotationConditional: ConditionalBooleanValue; static readonly isTopRotationConditional: ConditionalBooleanValue; static readonly isViewRotationConditional: ConditionalBooleanValue; + // (undocumented) + loadUserSettings(storage: UiSettings): Promise; static readonly onAccuDrawUiSettingsChangedEvent: AccuDrawUiSettingsChangedEvent; // (undocumented) onCompassModeChange(): void; @@ -2043,6 +2076,8 @@ export class FrameworkAccuDraw extends AccuDraw { // (undocumented) onRotationModeChange(): void; // (undocumented) + readonly providerId = "FrameworkAccuDraw"; + // (undocumented) setFocusItem(index: ItemField): void; // (undocumented) static translateFromItemField(item: ItemField): AccuDrawField; @@ -2056,7 +2091,7 @@ export class FrameworkAccuDraw extends AccuDraw { export const FrameworkReducer: (state: import("./redux-ts").CombinedReducerState<{ configurableUiState: typeof ConfigurableUiReducer; sessionState: typeof SessionStateReducer; -}>, action: import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject>> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject>> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject>>) => import("./redux-ts").CombinedReducerState<{ +}>, action: import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject>> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject>> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject> | import("./redux-ts").DeepReadonlyObject>>) => import("./redux-ts").CombinedReducerState<{ configurableUiState: typeof ConfigurableUiReducer; sessionState: typeof SessionStateReducer; }>; @@ -2723,6 +2758,9 @@ export function getStableWidgetProps(widgetProps: WidgetProps, stableId: string) // @internal (undocumented) export const getStagePanelType: (location: StagePanelLocation_2) => StagePanelType; +// @beta +export function getUiSettingsManagerEntry(itemPriority: number, allowSettingUiFrameworkVersion?: boolean): SettingsTabEntry; + // @internal (undocumented) export function getWidgetId(side: PanelSide, key: StagePanelZoneDefKeys): WidgetIdTypes; @@ -2908,14 +2946,8 @@ export interface HTMLElementPopupProps extends PopupPropsBase { relativePosition: RelativePosition; } -// @beta -export class IModelAppUiSettings implements UiSettings { - // (undocumented) - deleteSetting(namespace: string, name: string): Promise; - // (undocumented) - getSetting(namespace: string, name: string): Promise; - // (undocumented) - saveSetting(namespace: string, name: string, setting: any): Promise; +// @beta @deprecated +export class IModelAppUiSettings extends UserSettingsStorage { } // @beta @@ -3038,6 +3070,18 @@ export class Indicator extends React.Component { render(): JSX.Element; } +// @beta +export interface InitialAppUiSettings { + // (undocumented) + colorTheme: string; + // (undocumented) + dragInteraction: boolean; + // (undocumented) + frameworkVersion: string; + // (undocumented) + widgetOpacity: number; +} + // @internal (undocumented) export function initializeNineZoneState(frontstageDef: FrontstageDef): NineZoneState; @@ -4328,7 +4372,7 @@ export class PropsHelper { } // @beta -export function QuantityFormatSettingsPanel({ initialQuantityType, availableUnitSystems }: QuantityFormatterSettingsOptions): JSX.Element; +export function QuantityFormatSettingsPage({ initialQuantityType, availableUnitSystems }: QuantityFormatterSettingsOptions): JSX.Element; // @beta export interface QuantityFormatterSettingsOptions { @@ -5762,6 +5806,8 @@ export enum SyncUiEventId { NavigationAidActivated = "navigationaidactivated", SelectionSetChanged = "selectionsetchanged", SettingsProvidersChanged = "settingsproviderschanged", + // (undocumented) + ShowHideManagerSettingChange = "show-hide-setting-change", TaskActivated = "taskactivated", ToolActivated = "toolactivated", UiSettingsChanged = "uisettingschanged", @@ -5939,7 +5985,7 @@ export class ToolAssistanceField extends React.Component; // @internal (undocumented) - static contextType: React.Context; + static contextType: React.Context; // @internal (undocumented) static readonly defaultProps: ToolAssistanceFieldDefaultProps; // @internal (undocumented) @@ -5957,7 +6003,7 @@ export interface ToolAssistanceFieldProps extends StatusFieldProps { defaultPromptAtCursor: boolean; fadeOutCursorPrompt: boolean; includePromptAtCursor: boolean; - uiSettings?: UiSettings; + uiSettings?: UiSettingsStorage; } // @internal @@ -6375,7 +6421,7 @@ export class UiFramework { // (undocumented) static getIsUiVisible(): boolean; // @beta (undocumented) - static getUiSettings(): UiSettings; + static getUiSettingsStorage(): UiSettingsStorage; // @beta (undocumented) static getUserInfo(): UserInfo | undefined; // (undocumented) @@ -6409,6 +6455,8 @@ export class UiFramework { }): Promise; // @internal (undocumented) static get projectServices(): ProjectServices; + // @alpha + static registerUserSettingsProvider(entry: UserSettingsProvider): boolean; // (undocumented) static setAccudrawSnapMode(snapMode: SnapMode): void; // (undocumented) @@ -6432,7 +6480,11 @@ export class UiFramework { // @beta static get settingsManager(): SettingsManager; // @beta (undocumented) - static setUiSettings(uiSettings: UiSettings, immediateSync?: boolean): void; + static setUiSettingsStorage(storage: UiSettingsStorage, immediateSync?: boolean): Promise; + // (undocumented) + static setUiVersion(version: string): void; + // (undocumented) + static setUseDragInteraction(useDragInteraction: boolean): void; // @beta (undocumented) static setUserInfo(userInfo: UserInfo | undefined, immediateSync?: boolean): void; // (undocumented) @@ -6443,6 +6495,8 @@ export class UiFramework { static translate(key: string | string[]): string; // @beta static get uiVersion(): string; + // (undocumented) + static get useDragInteraction(): boolean; // @alpha (undocumented) static get widgetManager(): WidgetManager; } @@ -6458,17 +6512,22 @@ export interface UiIntervalEventArgs { } // @internal (undocumented) -export const UiSettingsContext: React.Context; +export const UiSettingsContext: React.Context; -// @alpha +// @beta +export function UiSettingsPage({ allowSettingUiFrameworkVersion }: { + allowSettingUiFrameworkVersion: boolean; +}): JSX.Element; + +// @beta export function UiSettingsProvider(props: UiSettingsProviderProps): JSX.Element; -// @alpha +// @beta export interface UiSettingsProviderProps { // (undocumented) children?: React.ReactNode; // (undocumented) - uiSettings: UiSettings; + settingsStorage: UiSettingsStorage; } // @public @@ -6482,6 +6541,12 @@ export class UiShowHideManager { static set inactivityTime(time: number); static get isUiVisible(): boolean; static set isUiVisible(visible: boolean); + // @internal (undocumented) + static setAutoHideUi(value: boolean): void; + // @internal (undocumented) + static setSnapWidgetOpacity(value: boolean): void; + // @internal (undocumented) + static setUseProximityOpacity(value: boolean): void; static get showHideFooter(): boolean; static set showHideFooter(showHide: boolean); static get showHidePanels(): boolean; @@ -6494,6 +6559,22 @@ export class UiShowHideManager { static set useProximityOpacity(value: boolean); } +// @internal +export class UiShowHideSettingsProvider implements UserSettingsProvider { + // (undocumented) + static initialize(): void; + // (undocumented) + loadUserSettings(storage: UiSettings): Promise; + // (undocumented) + readonly providerId = "UiShowHideSettingsProvider"; + // (undocumented) + static storeAutoHideUi(v: boolean, storage?: UiSettings): Promise; + // (undocumented) + static storeSnapWidgetOpacity(v: boolean, storage?: UiSettings): Promise; + // (undocumented) + static storeUseProximityOpacity(v: boolean, storage?: UiSettings): Promise; + } + // @public export class UiVisibilityChangedEvent extends UiEvent { } @@ -6575,6 +6656,22 @@ export interface UserProfileBackstageItemProps extends CommonProps { userInfo: UserInfo; } +// @beta +export interface UserSettingsProvider { + loadUserSettings(storage: UiSettingsStorage): Promise; + providerId: string; +} + +// @public +export class UserSettingsStorage implements UiSettingsStorage { + // (undocumented) + deleteSetting(namespace: string, name: string): Promise; + // (undocumented) + getSetting(namespace: string, name: string): Promise; + // (undocumented) + saveSetting(namespace: string, name: string, setting: any): Promise; +} + // @internal (undocumented) export function useSavedFrontstageState(frontstageDef: FrontstageDef): void; @@ -6596,8 +6693,8 @@ export const useUiItemsProviderStatusBarItems: (manager: StatusBarItemsManager_2 // @beta export const useUiItemsProviderToolbarItems: (manager: ToolbarItemsManager, toolbarUsage: ToolbarUsage, toolbarOrientation: ToolbarOrientation) => readonly CommonToolbarItem[]; -// @internal (undocumented) -export function useUiSettingsContext(): UiSettings; +// @beta (undocumented) +export function useUiSettingsStorageContext(): UiSettingsStorage; // @internal (undocumented) export function useUiVisibility(): boolean; diff --git a/common/changes/@bentley/ui-components/addUiSettingsPage_2021-03-24-14-51.json b/common/changes/@bentley/ui-components/addUiSettingsPage_2021-03-24-14-51.json new file mode 100644 index 000000000000..d38006110ce6 --- /dev/null +++ b/common/changes/@bentley/ui-components/addUiSettingsPage_2021-03-24-14-51.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@bentley/ui-components", + "comment": "Update to use UiSettingsStorage.", + "type": "none" + } + ], + "packageName": "@bentley/ui-components", + "email": "65047615+bsteinbk@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@bentley/ui-core/addUiSettingsPage_2021-03-24-14-51.json b/common/changes/@bentley/ui-core/addUiSettingsPage_2021-03-24-14-51.json new file mode 100644 index 000000000000..d39f2cf1051c --- /dev/null +++ b/common/changes/@bentley/ui-core/addUiSettingsPage_2021-03-24-14-51.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@bentley/ui-core", + "comment": "Add more descriptive UiSettingsStorage, LocalSettingsStorage, and SessionSettingsStorage and deprecate badly name beta classes.", + "type": "none" + } + ], + "packageName": "@bentley/ui-core", + "email": "65047615+bsteinbk@users.noreply.github.com" +} \ No newline at end of file diff --git a/common/changes/@bentley/ui-framework/addUiSettingsPage_2021-03-24-14-51.json b/common/changes/@bentley/ui-framework/addUiSettingsPage_2021-03-24-14-51.json new file mode 100644 index 000000000000..1d417cc54339 --- /dev/null +++ b/common/changes/@bentley/ui-framework/addUiSettingsPage_2021-03-24-14-51.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@bentley/ui-framework", + "comment": "Add UiSettingsPage, AppUiSettings, and ability to register UserSettingsProvider to provide default settings from UsSettingsStorage.", + "type": "none" + } + ], + "packageName": "@bentley/ui-framework", + "email": "65047615+bsteinbk@users.noreply.github.com" +} \ No newline at end of file diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index c9d1525de2e6..2f297d742cca 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -13,29 +13,39 @@ publish: false ## New settings UI features -### Add Settings Page to set Quantity Formatting Overrides +### Add Settings Tabs and Pages to UI -The [QuantityFormatSettingsPanel]($ui-framework) component has been added to the @bentley/ui-framework package to provide the UI to set both the [PresentationUnitSystem]($presentation-common) and formatting overrides in the [QuantityFormatter]($frontend). This panel can be used in the new [SettingsContainer]($ui-core) UI component. The function `getQuantityFormatsSettingsManagerEntry` will return a [SettingsTabEntry]($ui-core) for use by the [SettingsManager]($ui-core). Below is an example of registering the `QuantityFormatSettingsPanel` with the `SettingsManager`. +#### Quantity Formatting Settings + +The [QuantityFormatSettingsPage]($ui-framework) component has been added to provide the UI to set both the [PresentationUnitSystem]($presentation-common) and formatting overrides in the [QuantityFormatter]($frontend). This component can be used in the new [SettingsContainer]($ui-core) UI component. The function `getQuantityFormatsSettingsManagerEntry` will return a [SettingsTabEntry]($ui-core) for use by the [SettingsManager]($ui-core). + +#### User Interface Settings + +The [UiSettingsPage]($ui-framework) component has been to provide the UI to set general UI settings that effect the look and feel of the App UI user interface. This component can be used in the new [SettingsContainer]($ui-core) UI component. The function `getUiSettingsManagerEntry` will return a [SettingsTabEntry]($ui-core) for use by the [SettingsManager]($ui-core). + +#### Registering Settings + +Below is an example of registering the `QuantityFormatSettingsPage` with the `SettingsManager`. ```ts // Sample settings provider that dynamically adds settings into the setting stage -export class AppSettingsProvider implements SettingsProvider { - public readonly id = "AppSettingsProvider"; +export class AppSettingsTabsProvider implements SettingsTabsProvider { + public readonly id = "AppSettingsTabsProvider"; public getSettingEntries(_stageId: string, _stageUsage: string): ReadonlyArray | undefined { return [ getQuantityFormatsSettingsManagerEntry(10, {availableUnitSystems:new Set(["metric","imperial","usSurvey"])}), + getUiSettingsManagerEntry(30, true), ]; } public static initializeAppSettingProvider() { - UiFramework.settingsManager.addSettingsProvider(new AppSettingsProvider()); + UiFramework.settingsManager.addSettingsProvider(new AppSettingsTabsProvider()); } } - ``` -The `QuantityFormatSettingsPanel` is marked as alpha in this release and is subject to minor modifications in future releases. +The `QuantityFormatSettingsPage` is marked as alpha in this release and is subject to minor modifications in future releases. ## @bentley/imodeljs-quantity package @@ -59,6 +69,14 @@ The class [NativeApp]($frontend) has been promoted from @alpha to @beta. `Native ## Breaking Api Changes +### @bentley/ui-core package + +The beta class `SettingsProvider` was renamed to `SettingsTabsProvider`. + +### @bentley/ui-framework package + +The beta class `QuantityFormatSettingsPanel` was renamed to `QuantityFormatSettingsPage`. + ### @bentley/imodeljs-quantity package #### UnitProps property name change diff --git a/docs/learning/ui/core/Settings.md b/docs/learning/ui/core/Settings.md index 684aa2bf204a..cf08ec5466c2 100644 --- a/docs/learning/ui/core/Settings.md +++ b/docs/learning/ui/core/Settings.md @@ -1,20 +1,20 @@ # Settings -The [SettingsManager]($ui-core) allows the registration of [SettingsProvider]($ui-core) classes to provide the settings to display with the [SettingsContainer]($ui-core) component. In an application that employs App UI, the [SettingsModalFrontstage]($ui-framework) frontstage will display the SettingsContainer and its entries. +The [SettingsManager]($ui-core) allows the registration of [SettingsTabsProvider]($ui-core) classes to provide the settings to display with the [SettingsContainer]($ui-core) component. In an application that employs App UI, the [SettingsModalFrontstage]($ui-framework) frontstage will display the SettingsContainer and its entries. ## SettingsTabEntry -Registered [SettingsProvider]($ui-core) instance will implement the method `getSettingEntries` to return an array of [SettingsProvider]($ui-core) items. Each SettingsTabEntry will populate a Tab entry in the [SettingsContainer]($ui-core) component. The `tabId` property is a string and must be unique across all registered SettingsProviders. A common practice is to prefix the tabId with the package name to ensure uniqueness. The `page` property holds the React.Element that will be used to construct the component to edit settings for this entry. It is the `page's` responsibility to persist and retrieve persisted settings. If the `page` contains multiple properties that must be saved together the SettingsTabEntry can set the `pageWillHandleCloseRequest` property to `true`. The `page's` control should then register to be notified when the setting container is closing or when the active SettingsEnty is changing so that any unsaved data can be saved. Two React hooks are provided to assist: `settingsManager.onProcessSettingsTabActivation` and `settingsManager.onProcessSettingsContainerClose`. +Registered [SettingsTabsProvider]($ui-core) instance will implement the method `getSettingEntries` to return an array of [SettingsTabsProvider]($ui-core) items. Each SettingsTabEntry will populate a Tab entry in the [SettingsContainer]($ui-core) component. The `tabId` property is a string and must be unique across all registered SettingsProviders. A common practice is to prefix the tabId with the package name to ensure uniqueness. The `page` property holds the React.Element that will be used to construct the component to edit settings for this entry. It is the `page's` responsibility to persist and retrieve persisted settings. If the `page` contains multiple properties that must be saved together the SettingsTabEntry can set the `pageWillHandleCloseRequest` property to `true`. The `page's` control should then register to be notified when the setting container is closing or when the active SettingsEnty is changing so that any unsaved data can be saved. Two React hooks are provided to assist: `settingsManager.onProcessSettingsTabActivation` and `settingsManager.onProcessSettingsContainerClose`. ## Example -### Example SettingsProvider +### Example SettingsTabsProvider Example below shows a settings provide that provides two settings pages. The first one depicts a page that has properties that cannot be saved immediately as individual properties are changed. It must be treated as a modal where once all values are define a save button is used the save the changes. The second settings page handles settings that can be immediately saved when changed and does not require any special processing when the page is closed. ```tsx // Sample UI items provider that dynamically adds ui items -export class ExampleSettingsProvider implements SettingsProvider { +export class ExampleSettingsProvider implements SettingsTabsProvider { public readonly id = "myApp:ExampleSettingsProvider"; public getSettingEntries(stageId: string, stageUsage: string): ReadonlyArray | undefined { @@ -145,6 +145,6 @@ function SaveFormatModalDialog({ persistChangesFunc, persistChangesFuncArg, onDi ## API Reference - [SettingsManager]($ui-core) -- [SettingsProvider]($ui-core) +- [SettingsTabsProvider]($ui-core) - [SettingsContainer]($ui-core) - [SettingsTabEntry]($ui-core) diff --git a/docs/learning/ui/framework/Backstage.md b/docs/learning/ui/framework/Backstage.md index a39d300915a8..72865192c151 100644 --- a/docs/learning/ui/framework/Backstage.md +++ b/docs/learning/ui/framework/Backstage.md @@ -24,7 +24,7 @@ export function AppBackstageComposer() { } ``` -Note: the static method `SettingsModalFrontstage.getBackstageActionItem` used above, will create an entry for a `Settings` stage. This stage will display [SettingsTabEntry]($ui-core) items from [SettingsProvider]($ui-core) classes registered with the [SettingsManager]($ui-core). The `SettingsManager` instance is referenced by property `UiFramework.settingsManager`. +Note: the static method `SettingsModalFrontstage.getBackstageActionItem` used above, will create an entry for a `Settings` stage. This stage will display [SettingsTabEntry]($ui-core) items from [SettingsTabsProvider]($ui-core) classes registered with the [SettingsManager]($ui-core). The `SettingsManager` instance is referenced by property `UiFramework.settingsManager`. See additional info in [Backstage](../../../learning/ui/abstract/Backstage.md). diff --git a/docs/learning/ui/framework/UiSettings.md b/docs/learning/ui/framework/UiSettings.md new file mode 100644 index 000000000000..cea021f318a7 --- /dev/null +++ b/docs/learning/ui/framework/UiSettings.md @@ -0,0 +1,131 @@ +# UI Settings + +'UI Settings' refers to settings that define the state of different parts of the UI. Settings like if the UI is displayed in "dark" or "light" theme, size and location of column sizes or panel sizes. + +## Settings Storage + +Settings that are set up to be stored between "session" need to be stored and retrieved from some storage location. There are two provided storage locations that will serve that purpose. [LocalSettingsStorage]($ui-core) will use browser localStorage. [UserSettingsStorage]($ui-framework) will use the Product Settings Service available through IModelApp.settings. Please note that UserSettingsStorage requires the user to be logged-in to have access to this storage. + +If an application wants to store settings only for the current session [SessionSettingsStorage]($ui-core) is available. + +An application can choose to create and register their own class that implements the [UiSettingsStorage](ui-core) interface, if a custom storage location is desired for UI Settings. + +### Defining which storage to use + +Typically in the index.tsx file of an IModelApp that uses `App UI` user interface the code will set the storage location once the user has signed in. + +```ts + public static getUiSettingsStorage(): UiSettingsStorage { + const authorized = !!IModelApp.authorizationClient && IModelApp.authorizationClient.isAuthorized; + if (!authorized) { + return MyIModelApp._localUiSettingsStorage; // instance of LocalSettingsStorage + } + return MyIModelApp._UserUiSettingsStorage; // instance of UserSettingsStorage + } +``` + +The component [UiSettingsProvider]($ui-framework) can be added into the component tree to provide the storage object via React context. See hook [useUiSettingsStorageContext]($ui-framework). Below is an example of how to wrap the [ConfigurableUiContent]($ui-framework) element so that the context is available to all App UI components. + +```tsx + + } + /> + +``` + +## UserSettingsProvider + +The [UserSettingsProvider]($ui-framework) interface can be implemented by classes that want to restore their saved settings when the method [UiFramework.setUiSettingsStorage]($ui-framework) is called to set the UiSettingsStorage. A `UserSettingsProvider` class must be registered by calling [UiFramework.registerUserSettingsProvider] and supplying the implementing class instance. The `UserSettingsProvider` interface only requires that the provider define a unique `providerId` that is used to ensure the provider is only registered once. It must also implement the method `loadUserSettings(storage: UiSettingsStorage)` to asynchronously load its settings from [UiSettingsStorage](ui-core). + +### AppUiSettings + +The [AppUiSettings]($ui-framework) class, which implements the UserSettingsProvider interface, can be instantiated by the IModelApp and registered as an UserSettingsProvider. This is left up to the application so each one can provide default values for the settings maintained by the `AppUiSettings` class. Below is an excerpt from application startup code that shows the registration of `AppUiSettings`. + +```ts + public static async initialize() { + await UiFramework.initialize(undefined); + + // initialize Presentation + await Presentation.initialize({ + activeLocale: IModelApp.i18n.languageList()[0], + }); + Presentation.selection.scopes.activeScope = "top-assembly"; + + // other initialization calls not shown in this example excerpt + + // app specific call to register setting pages to display + AppSettingsTabsProvider.initializeAppSettingProvider(); + + // Create and register the AppUiSettings instance to provide default for ui settings in Redux store + const lastTheme = (window.localStorage&&window.localStorage.getItem("uifw:defaultTheme"))??SYSTEM_PREFERRED_COLOR_THEME; + const defaults = { + colorTheme: lastTheme ?? SYSTEM_PREFERRED_COLOR_THEME, + dragInteraction: false, + frameworkVersion: "2", + widgetOpacity: 0.8, + }; + + // initialize any settings providers that may need to have defaults set by iModelApp + UiFramework.registerUserSettingsProvider(new AppUiSettings(defaults)); + + // go ahead and initialize settings before login or in case login is by-passed + await UiFramework.setUiSettingsStorage(SampleAppIModelApp.getUiSettingsStorage()); + } +``` + +## Settings Components + +### Quantity Formatting Settings + + The [QuantityFormatSettingsPage]($ui-framework) component provides the UI to set both the [PresentationUnitSystem]($presentation-common) and formatting overrides in the [QuantityFormatter]($frontend). This component can be used in the new [SettingsContainer]($ui-core) UI component. The function `getQuantityFormatsSettingsManagerEntry` will return a [SettingsTabEntry]($ui-core) for use by the [SettingsManager]($ui-core). + +### User Interface Settings + + The [UiSettingsPage]($ui-framework) component provides the UI to set general UI settings that effect the look and feel of the App UI user interface. This component can be used in the new [SettingsContainer]($ui-core) UI component. The function `getUiSettingsManagerEntry` will return a [SettingsTabEntry]($ui-core) for use by the [SettingsManager]($ui-core). + +### Settings stage + +UI and Quantity Settings as well as other settings can be present to the user for editing using the stage [SettingsModalFrontstage]($ui-framework). This stage will display all [SettingsTabEntry]($ui-core) entries that are provided via [SettingsTabsProvider]($ui-core) classes. `SettingsTabsProvider` classes can be registered with the [SettingsManager]($ui-core) by the host application, package, or extension loaded into an IModelApp using the App UI user interface. The steps to add a settings stage include. + +#### Adding a backstage item + +The [SettingsModalFrontstage.getBackstageActionItem] method can be used to get a [BackstageActionItem]($ui-abstract) that is used to construct the backstage. Below is an example of how to set up a backstage menu component that will display the 'Settings' entry if `SettingsTabEntry` items are provided. + +```tsx +export function AppBackstageComposerComponent({ userInfo }: AppBackstageComposerProps) { + const [backstageItems] = React.useState(() => { + return [ + BackstageItemUtilities.createStageLauncher(ViewsFrontstage.stageId, 100, 10, IModelApp.i18n.translate("SampleApp:backstage.viewIModel"), + IModelApp.i18n.translate("SampleApp:backstage.iModelStage"), `svg:${stageIconSvg}`), + SettingsModalFrontstage.getBackstageActionItem (100, 20), + ]; + }); + + return ( + } + /> + ); +} +``` + +#### Defining a SettingsTabsProvider + +Below is an example [SettingsTabsProvider]($ui-core) class that adds two settings pages, one for Units Formatting and the other for UI Settings. In the `AppUiSettings` example above the call to the static method [AppSettingsTabsProvider.initializeAppSettingProvider] is called to add this provider with the SettingsManager instance held by UiFramework. + +```tsx +export class AppSettingsTabsProvider implements SettingsTabsProvider { + public readonly id = "AppSettingsTabsProvider"; + + public getSettingEntries(_stageId: string, _stageUsage: string): ReadonlyArray | undefined { + return [ + getQuantityFormatsSettingsManagerEntry(10, {availableUnitSystems:new Set(["metric","imperial","usSurvey"])}), + getUiSettingsManagerEntry(20), + ]; + } + + public static initializeAppSettingProvider() { + UiFramework.settingsManager.addSettingsProvider(new AppSettingsTabsProvider()); + } +``` diff --git a/docs/learning/ui/framework/index.md b/docs/learning/ui/framework/index.md index 40901b377524..a09344455b8f 100644 --- a/docs/learning/ui/framework/index.md +++ b/docs/learning/ui/framework/index.md @@ -20,6 +20,7 @@ There are numerous React components and TypeScript classes in the `@bentley/ui-f - [Dialogs](./Dialogs.md) - [Workflows and Tasks](./TasksWorkflows.md) - [KeyboardShortcut]($ui-framework:KeyboardShortcut) - A keystroke or combination of keystrokes used to launch a command or tool. +- [UI Settings](./UiSettings.md) ## Other Topics diff --git a/test-apps/ui-test-app/public/index.html b/test-apps/ui-test-app/public/index.html index 5f41f5642664..5f41375c29ec 100644 --- a/test-apps/ui-test-app/public/index.html +++ b/test-apps/ui-test-app/public/index.html @@ -1,49 +1,50 @@ - - - - - - - - iModel.js UI Test App - - - + - - + + - - -
- - + \ No newline at end of file diff --git a/test-apps/ui-test-app/public/locales/en/SampleApp.json b/test-apps/ui-test-app/public/locales/en/SampleApp.json index 8a9b3ddb7bf6..5a5a7a5b4cf3 100644 --- a/test-apps/ui-test-app/public/locales/en/SampleApp.json +++ b/test-apps/ui-test-app/public/locales/en/SampleApp.json @@ -28,26 +28,8 @@ "loading": "Loading..." }, "settingsStage": { - "settings": "Settings", - "light": "Light", - "dark": "Dark", - "systemPreferred": "System preferred", - "themeTitle": "Theme", - "themeDescription": "Toggle the theme between light and dark", - "autoHideTitle": "Auto-Hide UI", - "autoHideDescription": "Toggle the auto-hide of UI after inactivity", - "dragInteractionTitle": "Drag Interaction", - "dragInteractionDescription": "Toggle the toolbar interaction between click and drag", - "newUiTitle": "Use 2.0 Ui", - "newUiDescription": "Use new 2.0 UI layout", - "useProximityOpacityTitle": "Change Toolbar Opacity", - "useProximityOpacityDescription": "Change the toolbar opacity as the mouse gets closer or farther away", - "snapWidgetOpacityTitle": "Snap Widget Opacity", - "snapWidgetOpacityDescription": "Immediately change the toolbar opacity when the mouse gets close", "accuDrawNotificationsTitle": "Display AccuDraw Notifications", - "accuDrawNotificationsDescription": "Display toast notifications when AccuDraw status changes", - "widgetOpacityTitle": "Widget Opacity", - "widgetOpacityDescription": "Opacity when mouse is not hovering for 2.0 floating widgets and all 1.0 widgets" + "accuDrawNotificationsDescription": "Display toast notifications when AccuDraw status changes" }, "QuantityFormatModalFrontstage": { "QuantityFormatStage": "Quantity Format Overrides" diff --git a/test-apps/ui-test-app/src/frontend/AppUiSettings.ts b/test-apps/ui-test-app/src/frontend/AppUiSettings.ts deleted file mode 100644 index b74d712ac4b0..000000000000 --- a/test-apps/ui-test-app/src/frontend/AppUiSettings.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Copyright (c) Bentley Systems, Incorporated. All rights reserved. -* See LICENSE.md in the project root for license terms and full copyright notice. -*--------------------------------------------------------------------------------------------*/ -import { UiSetting, UiSettings } from "@bentley/ui-core"; -import { FrameworkAccuDraw, UiFramework, UiShowHideManager } from "@bentley/ui-framework"; -import { SampleAppIModelApp, SampleAppUiActionId } from "."; - -export class AppUiSettings { - private static _settingNamespace = "AppUiSettings"; - private _settings: Array> = []; - - public colorTheme: UiSetting; - public autoHideUi: UiSetting; - public useProximityOpacity: UiSetting; - public snapWidgetOpacity: UiSetting; - public dragInteraction: UiSetting; - public frameworkVersion: UiSetting; - public accuDrawNotifications: UiSetting; - public widgetOpacity: UiSetting; - - constructor() { - this._settings = []; - - this.colorTheme = new UiSetting(AppUiSettings._settingNamespace, "ColorTheme", UiFramework.getColorTheme, UiFramework.setColorTheme); - this._settings.push(this.colorTheme); - - this.autoHideUi = new UiSetting(AppUiSettings._settingNamespace, "AutoHideUi", - () => UiShowHideManager.autoHideUi, (value: boolean) => UiShowHideManager.autoHideUi = value); - this._settings.push(this.autoHideUi); - - this.useProximityOpacity = new UiSetting(AppUiSettings._settingNamespace, "UseProximityOpacity", - () => UiShowHideManager.useProximityOpacity, (value: boolean) => UiShowHideManager.useProximityOpacity = value); - this._settings.push(this.useProximityOpacity); - - this.snapWidgetOpacity = new UiSetting(AppUiSettings._settingNamespace, "SnapWidgetOpacity", - () => UiShowHideManager.snapWidgetOpacity, (value: boolean) => UiShowHideManager.snapWidgetOpacity = value); - this._settings.push(this.snapWidgetOpacity); - - this.dragInteraction = new UiSetting(AppUiSettings._settingNamespace, "DragInteraction", - () => SampleAppIModelApp.store.getState().sampleAppState.dragInteraction, - (value: boolean) => UiFramework.dispatchActionToStore(SampleAppUiActionId.setDragInteraction, value, true)); - this._settings.push(this.dragInteraction); - - this.frameworkVersion = new UiSetting(AppUiSettings._settingNamespace, "FrameworkVersion", - () => SampleAppIModelApp.store.getState().sampleAppState.frameworkVersion, - (value: string) => UiFramework.dispatchActionToStore(SampleAppUiActionId.setFrameworkVersion, value, true)); - this._settings.push(this.frameworkVersion); - - this.accuDrawNotifications = new UiSetting(AppUiSettings._settingNamespace, "AccuDrawNotifications", - () => FrameworkAccuDraw.displayNotifications, (value: boolean) => FrameworkAccuDraw.displayNotifications = value); - this._settings.push(this.accuDrawNotifications); - - this.widgetOpacity = new UiSetting(AppUiSettings._settingNamespace, "WidgetOpacity", - () => UiFramework.getWidgetOpacity(), (value: number) => UiFramework.setWidgetOpacity(value)); - this._settings.push(this.widgetOpacity); - } - - public async apply(uiSettings: UiSettings): Promise { - for (const setting of this._settings) { - await setting.getSettingAndApplyValue(uiSettings); - } - } -} diff --git a/test-apps/ui-test-app/src/frontend/appui/backstage/AppBackstageComposer.tsx b/test-apps/ui-test-app/src/frontend/appui/backstage/AppBackstageComposer.tsx index eb777f3c98cd..53d3f7eec341 100644 --- a/test-apps/ui-test-app/src/frontend/appui/backstage/AppBackstageComposer.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/backstage/AppBackstageComposer.tsx @@ -7,7 +7,7 @@ import { connect } from "react-redux"; import { UserInfo } from "@bentley/itwin-client"; import { IModelApp } from "@bentley/imodeljs-frontend"; import { BackstageItemUtilities, BadgeType, ConditionalBooleanValue } from "@bentley/ui-abstract"; -import { BackstageComposer, FrontstageManager, SettingsModalFrontstage, UserProfileBackstageItem } from "@bentley/ui-framework"; +import { BackstageComposer, FrontstageManager, SettingsModalFrontstage, UiFramework, UserProfileBackstageItem } from "@bentley/ui-framework"; import { ComponentExamplesModalFrontstage } from "../frontstages/component-examples/ComponentExamples"; import { LocalFileOpenFrontstage } from "../frontstages/LocalFileStage"; import { RootState, SampleAppIModelApp, SampleAppUiActionId } from "../.."; @@ -33,7 +33,7 @@ interface AppBackstageComposerProps { export function AppBackstageComposerComponent({ userInfo }: AppBackstageComposerProps) { const hiddenCondition3 = new ConditionalBooleanValue(() => SampleAppIModelApp.getTestProperty() === "HIDE", [SampleAppUiActionId.setTestProperty]); const enableCondition = new ConditionalBooleanValue(() => SampleAppIModelApp.getTestProperty() === "HIDE", [SampleAppUiActionId.setTestProperty]); - const notUi2Condition = new ConditionalBooleanValue(() => SampleAppIModelApp.getUiFrameworkProperty() === "1", [SampleAppUiActionId.toggleFrameworkVersion, SampleAppUiActionId.setFrameworkVersion]); + const notUi2Condition = new ConditionalBooleanValue(() => UiFramework.uiVersion === "1", ["configurableui:set-framework-version"]); const imodelIndexHidden = new ConditionalBooleanValue(() => SampleAppIModelApp.isIModelLocal, [SampleAppUiActionId.setIsIModelLocal]); const openLocalFileHidden = new ConditionalBooleanValue(() => SampleAppIModelApp.testAppConfiguration?.snapshotPath === undefined, [SampleAppUiActionId.setIsIModelLocal]); @@ -44,7 +44,7 @@ export function AppBackstageComposerComponent({ userInfo }: AppBackstageComposer BackstageItemUtilities.createStageLauncher("IModelOpen", 300, 10, IModelApp.i18n.translate("SampleApp:backstage.imodelopen"), undefined, "icon-folder-opened"), BackstageItemUtilities.createStageLauncher("IModelIndex", 300, 20, IModelApp.i18n.translate("SampleApp:backstage.imodelindex"), undefined, "icon-placeholder", { isHidden: imodelIndexHidden }), BackstageItemUtilities.createActionItem("SampleApp.open-local-file", 300, 30, async () => LocalFileOpenFrontstage.open(), IModelApp.i18n.translate("SampleApp:backstage:fileSelect"), undefined, "icon-placeholder", { isHidden: openLocalFileHidden }), - SettingsModalFrontstage.getBackstageActionItem (400, 10), + SettingsModalFrontstage.getBackstageActionItem(400, 10), ]; } @@ -58,7 +58,7 @@ export function AppBackstageComposerComponent({ userInfo }: AppBackstageComposer BackstageItemUtilities.createStageLauncher("IModelOpen", 300, 10, IModelApp.i18n.translate("SampleApp:backstage.imodelopen"), undefined, "icon-folder-opened"), BackstageItemUtilities.createStageLauncher("IModelIndex", 300, 20, IModelApp.i18n.translate("SampleApp:backstage.imodelindex"), undefined, "icon-placeholder", { isHidden: imodelIndexHidden }), BackstageItemUtilities.createActionItem("SampleApp.open-local-file", 300, 30, async () => LocalFileOpenFrontstage.open(), IModelApp.i18n.translate("SampleApp:backstage:fileSelect"), undefined, "icon-placeholder", { isHidden: openLocalFileHidden }), - SettingsModalFrontstage.getBackstageActionItem (400, 10), + SettingsModalFrontstage.getBackstageActionItem(400, 10), BackstageItemUtilities.createActionItem("SampleApp.componentExamples", 400, 20, () => FrontstageManager.openModalFrontstage(new ComponentExamplesModalFrontstage()), IModelApp.i18n.translate("SampleApp:backstage.componentExamples"), undefined, "icon-details", { badgeType: BadgeType.New }), ]; }); diff --git a/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.scss b/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.scss index a2a6217ae19d..d0784c6cd5c8 100644 --- a/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.scss +++ b/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.scss @@ -52,7 +52,6 @@ $settings-rightpanel-width: 220px; > .right-panel { display:flex; - justify-content: center; align-items: center; min-width: $settings-rightpanel-width; padding: 30px 50px 30px 30px; diff --git a/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.tsx b/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.tsx index f1370da3ca6c..59a7f0dddc31 100644 --- a/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/frontstages/Settings.tsx @@ -8,171 +8,29 @@ import "./Settings.scss"; import * as React from "react"; -import { connect } from "react-redux"; -import { Dispatch } from "redux"; -import { OptionType, Slider, ThemedSelect, ThemedSelectProps, Toggle } from "@bentley/ui-core"; -import { ColorTheme, FrameworkAccuDraw, SyncUiEventDispatcher, SYSTEM_PREFERRED_COLOR_THEME, UiFramework, UiShowHideManager } from "@bentley/ui-framework"; -import { RootState, SampleAppActions, SampleAppIModelApp, SampleAppUiActionId } from "../.."; -interface UiSettingsPageProps { - dragInteraction: boolean; - onToggleDragInteraction: () => void; - frameworkVersion: string; - onToggleFrameworkVersion: () => void; -} - -function isOptionType(value: OptionType | ReadonlyArray): value is OptionType { - if (Array.isArray(value)) - return false; - return true; -} +import { Toggle } from "@bentley/ui-core"; +import { FrameworkAccuDraw, UiFramework } from "@bentley/ui-framework"; /** UiSettingsPage displaying the active settings. */ -class UiSettingsPageComponent extends React.Component { - private _themeTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.themeTitle"); - private _themeDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.themeDescription"); - private _autoHideTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.autoHideTitle"); - private _autoHideDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.autoHideDescription"); - private _dragInteractionTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.dragInteractionTitle"); - private _dragInteractionDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.dragInteractionDescription"); - private _useNewUiTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.newUiTitle"); - private _useNewUiDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.newUiDescription"); - private _useProximityOpacityTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.useProximityOpacityTitle"); - private _useProximityOpacityDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.useProximityOpacityDescription"); - private _snapWidgetOpacityTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.snapWidgetOpacityTitle"); - private _snapWidgetOpacityDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.snapWidgetOpacityDescription"); - private _darkLabel = UiFramework.i18n.translate("SampleApp:settingsStage.dark"); - private _lightLabel = UiFramework.i18n.translate("SampleApp:settingsStage.light"); - private _systemPreferredLabel = UiFramework.i18n.translate("SampleApp:settingsStage.systemPreferred"); +export class AccudrawSettingsPageComponent extends React.Component { private _accuDrawNotificationsTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.accuDrawNotificationsTitle"); private _accuDrawNotificationsDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.accuDrawNotificationsDescription"); - private _widgetOpacityTitle: string = UiFramework.i18n.translate("SampleApp:settingsStage.widgetOpacityTitle"); - private _widgetOpacityDescription: string = UiFramework.i18n.translate("SampleApp:settingsStage.widgetOpacityDescription"); - - private _defaultThemeOption = { label: this._systemPreferredLabel, value: SYSTEM_PREFERRED_COLOR_THEME }; - private _themeOptions: Array = [ - this._defaultThemeOption, - { label: this._lightLabel, value: ColorTheme.Light }, - { label: this._darkLabel, value: ColorTheme.Dark }, - ]; - - private _getDefaultThemeOption() { - const theme = UiFramework.getColorTheme(); - for (const option of this._themeOptions) { - if (option.value === theme) - return option; - } - return this._defaultThemeOption; - } - - private _onThemeChange: ThemedSelectProps["onChange"] = async (value) => { - if (!value) - return; - if (!isOptionType(value)) - return; - - UiFramework.setColorTheme(value.value); - - await SampleAppIModelApp.appUiSettings.colorTheme.saveSetting(SampleAppIModelApp.uiSettings); - }; - - private _onAutoHideChange = async () => { - UiShowHideManager.autoHideUi = !UiShowHideManager.autoHideUi; - - await SampleAppIModelApp.appUiSettings.autoHideUi.saveSetting(SampleAppIModelApp.uiSettings); - }; - - private _onUseProximityOpacityChange = async () => { - UiShowHideManager.useProximityOpacity = !UiShowHideManager.useProximityOpacity; - - await SampleAppIModelApp.appUiSettings.useProximityOpacity.saveSetting(SampleAppIModelApp.uiSettings); - }; - - private _onSnapWidgetOpacityChange = async () => { - UiShowHideManager.snapWidgetOpacity = !UiShowHideManager.snapWidgetOpacity; - - await SampleAppIModelApp.appUiSettings.snapWidgetOpacity.saveSetting(SampleAppIModelApp.uiSettings); - }; private _onAccuDrawNotificationsChange = async () => { FrameworkAccuDraw.displayNotifications = !FrameworkAccuDraw.displayNotifications; - - await SampleAppIModelApp.appUiSettings.accuDrawNotifications.saveSetting(SampleAppIModelApp.uiSettings); - }; - - private _onWidgetOpacityChange = async (values: readonly number[]) => { - if (values.length > 0) { - UiFramework.setWidgetOpacity(values[0]); - await SampleAppIModelApp.appUiSettings.widgetOpacity.saveSetting(SampleAppIModelApp.uiSettings); - } }; public render(): React.ReactNode { return (
- - -
- } - /> - } - /> - } - /> - } - /> - } - /> - } - /> } - /> - v.toFixed(1)} - showTicks showTickLabels getTickCount={() => 10} formatTick={(v: number) => v.toFixed(1)} /> - } + settingUi={} /> ); } } -function mapStateToProps(state: RootState) { - return { dragInteraction: state.sampleAppState.dragInteraction, frameworkVersion: state.sampleAppState.frameworkVersion }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - onToggleDragInteraction: async () => { - dispatch(SampleAppActions.toggleDragInteraction()); - await SampleAppIModelApp.appUiSettings.dragInteraction.saveSetting(SampleAppIModelApp.uiSettings); - }, - onToggleFrameworkVersion: async () => { - dispatch(SampleAppActions.toggleFrameworkVersion()); - SyncUiEventDispatcher.dispatchSyncUiEvent(SampleAppUiActionId.toggleFrameworkVersion); - await SampleAppIModelApp.appUiSettings.frameworkVersion.saveSetting(SampleAppIModelApp.uiSettings); - }, - dispatch, - }; -} - -// eslint-disable-next-line @typescript-eslint/naming-convention -export const ConnectedUiSettingsPage = connect(mapStateToProps, mapDispatchToProps)(UiSettingsPageComponent); - interface SettingsItemProps { title: string; description: string; diff --git a/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx b/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx index 05967d765a47..c7ba50b27507 100644 --- a/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/frontstages/component-examples/ComponentExamplesProvider.tsx @@ -24,7 +24,7 @@ import { SearchBox, Select, SettingsContainer, SettingsTabEntry, Slider, SmallText, Spinner, SpinnerSize, SplitButton, Subheading, Textarea, ThemedSelect, Tile, Title, Toggle, ToggleButtonType, UnderlinedButton, VerticalTabs, } from "@bentley/ui-core"; -import { MessageManager, ModalDialogManager, QuantityFormatSettingsPanel, ReactNotifyMessageDetails, UiFramework } from "@bentley/ui-framework"; +import { MessageManager, ModalDialogManager, QuantityFormatSettingsPage, ReactNotifyMessageDetails, UiFramework } from "@bentley/ui-framework"; import { SampleAppIModelApp } from "../../.."; import { ComponentExampleCategory, ComponentExampleProps } from "./ComponentExamples"; import { SampleContextMenu } from "./SampleContextMenu"; @@ -32,17 +32,17 @@ import { SampleExpandableBlock } from "./SampleExpandableBlock"; import { SampleImageCheckBox } from "./SampleImageCheckBox"; import { SamplePopupContextMenu } from "./SamplePopupContextMenu"; import { FormatPopupButton } from "./FormatPopupButton"; -import { ConnectedUiSettingsPage } from "../Settings"; +import { AccudrawSettingsPageComponent } from "../Settings"; function MySettingsPage() { const tabs: SettingsTabEntry[] = [ { itemPriority: 10, tabId: "Quantity", pageWillHandleCloseRequest: true, label: "Quantity", tooltip: "Quantity Format Settings", icon: "icon-measure", - page: , + page: , }, { - itemPriority: 20, tabId: "UI", label: "UI", subLabel: "UI and Accudraw", tooltip: "UI Settings", icon: "icon-paintbrush", - page: , + itemPriority: 20, tabId: "Accudraw", label: "Accudraw", tooltip: "Accudraw Settings", icon: "icon-paintbrush", + page: , }, { itemPriority: 30, tabId: "page3", label: "page3", page:
Page 3
}, { itemPriority: 40, tabId: "page4", label: "page4", subLabel: "disabled page4", isDisabled: true, page:
Page 4
}, diff --git a/test-apps/ui-test-app/src/frontend/appui/uiproviders/AppSettingsProvider.tsx b/test-apps/ui-test-app/src/frontend/appui/uiproviders/AppSettingsTabsProvider.tsx similarity index 58% rename from test-apps/ui-test-app/src/frontend/appui/uiproviders/AppSettingsProvider.tsx rename to test-apps/ui-test-app/src/frontend/appui/uiproviders/AppSettingsTabsProvider.tsx index ed6a44be1417..cd3a42bcb52c 100644 --- a/test-apps/ui-test-app/src/frontend/appui/uiproviders/AppSettingsProvider.tsx +++ b/test-apps/ui-test-app/src/frontend/appui/uiproviders/AppSettingsTabsProvider.tsx @@ -4,29 +4,29 @@ *--------------------------------------------------------------------------------------------*/ import * as React from "react"; -import { getQuantityFormatsSettingsManagerEntry, UiFramework } from "@bentley/ui-framework"; -import { SettingsProvider, SettingsTabEntry } from "@bentley/ui-core"; -import { ConnectedUiSettingsPage } from "../frontstages/Settings"; +import { getQuantityFormatsSettingsManagerEntry, getUiSettingsManagerEntry, UiFramework } from "@bentley/ui-framework"; +import { SettingsTabEntry, SettingsTabsProvider } from "@bentley/ui-core"; +import { AccudrawSettingsPageComponent } from "../frontstages/Settings"; // Sample settings provider that dynamically adds settings into the setting stage -export class AppSettingsProvider implements SettingsProvider { - public readonly id = "AppSettingsProvider"; +export class AppSettingsTabsProvider implements SettingsTabsProvider { + public readonly id = "AppSettingsTabsProvider"; public getSettingEntries(_stageId: string, _stageUsage: string): ReadonlyArray | undefined { return [ getQuantityFormatsSettingsManagerEntry(10, {availableUnitSystems:new Set(["metric","imperial","usSurvey"])}), { - itemPriority: 20, tabId: "ui-test-app:UI", label: "UI", - page: , + itemPriority: 20, tabId: "ui-test-app:Accudraw", label: "Accudraw", + page: , isDisabled: false, icon: "icon-paintbrush", - subLabel: "UI and Accudraw", - tooltip: "UI and Accudraw Settings", + tooltip: "Accudraw Settings", }, + getUiSettingsManagerEntry(30, true), ]; } public static initializeAppSettingProvider() { - UiFramework.settingsManager.addSettingsProvider(new AppSettingsProvider()); + UiFramework.settingsManager.addSettingsProvider(new AppSettingsTabsProvider()); } } diff --git a/test-apps/ui-test-app/src/frontend/index.tsx b/test-apps/ui-test-app/src/frontend/index.tsx index 011033477738..792b865bd901 100644 --- a/test-apps/ui-test-app/src/frontend/index.tsx +++ b/test-apps/ui-test-app/src/frontend/index.tsx @@ -36,12 +36,12 @@ import { PresentationUnitSystem } from "@bentley/presentation-common"; import { Presentation } from "@bentley/presentation-frontend"; import { getClassName } from "@bentley/ui-abstract"; import { BeDragDropContext } from "@bentley/ui-components"; -import { LocalUiSettings, UiSettings } from "@bentley/ui-core"; +import { LocalSettingsStorage, UiSettings } from "@bentley/ui-core"; import { - ActionsUnion, AppNotificationManager, ConfigurableUiContent, createAction, DeepReadonly, DragDropLayerRenderer, FrameworkAccuDraw, FrameworkReducer, + ActionsUnion, AppNotificationManager, AppUiSettings, ConfigurableUiContent, createAction, DeepReadonly, DragDropLayerRenderer, FrameworkAccuDraw, FrameworkReducer, FrameworkRootState, FrameworkToolAdmin, FrameworkUiAdmin, FrameworkVersion, FrontstageDeactivatedEventArgs, FrontstageDef, FrontstageManager, - IModelAppUiSettings, IModelInfo, ModalFrontstageClosedEventArgs, SafeAreaContext, StateManager, SyncUiEventDispatcher, ThemeManager, - ToolbarDragInteractionContext, UiFramework, UiSettingsProvider, + IModelInfo, ModalFrontstageClosedEventArgs, SafeAreaContext, StateManager, SyncUiEventDispatcher, SYSTEM_PREFERRED_COLOR_THEME, ThemeManager, ToolbarDragInteractionContext, + UiFramework, UiSettingsProvider, UserSettingsStorage, } from "@bentley/ui-framework"; import { SafeAreaInsets } from "@bentley/ui-ninezone"; import { getSupportedRpcs } from "../common/rpcs"; @@ -55,8 +55,7 @@ import { IModelViewportControl } from "./appui/contentviews/IModelViewport"; import { EditFrontstage } from "./appui/frontstages/editing/EditFrontstage"; import { LocalFileOpenFrontstage } from "./appui/frontstages/LocalFileStage"; import { ViewsFrontstage } from "./appui/frontstages/ViewsFrontstage"; -import { AppSettingsProvider } from "./appui/uiproviders/AppSettingsProvider"; -import { AppUiSettings } from "./AppUiSettings"; +import { AppSettingsTabsProvider } from "./appui/uiproviders/AppSettingsTabsProvider"; import { AppViewManager } from "./favorites/AppViewManager"; // Favorite Properties Support import { ElementSelectionListener } from "./favorites/ElementSelectionListener"; // Favorite Properties Support import { AnalysisAnimationTool } from "./tools/AnalysisAnimation"; @@ -73,7 +72,7 @@ import { EditingSessionTool } from "./tools/editing/PrimitiveToolEx"; RpcConfiguration.developmentMode = true; // cSpell:ignore setTestProperty sampleapp uitestapp setisimodellocal projectwise mobx hypermodeling testapp urlps -// cSpell:ignore toggledraginteraction toggleframeworkversion setdraginteraction setframeworkversion +// cSpell:ignore toggledraginteraction toggleframeworkversion set-drag-interaction set-framework-version /** Action Ids used by redux and to send sync UI components. Typically used to refresh visibility or enable state of control. * Use lower case strings to be compatible with SyncUi processing. @@ -82,25 +81,17 @@ export enum SampleAppUiActionId { setTestProperty = "sampleapp:settestproperty", setAnimationViewId = "sampleapp:setAnimationViewId", setIsIModelLocal = "sampleapp:setisimodellocal", - toggleDragInteraction = "sampleapp:toggledraginteraction", - toggleFrameworkVersion = "sampleapp:toggleframeworkversion", - setDragInteraction = "sampleapp:setdraginteraction", - setFrameworkVersion = "sampleapp:setframeworkversion", } export interface SampleAppState { testProperty: string; animationViewId: string; - dragInteraction: boolean; - frameworkVersion: string; isIModelLocal: boolean; } const initialState: SampleAppState = { testProperty: "", animationViewId: "", - dragInteraction: true, - frameworkVersion: "1", isIModelLocal: false, }; @@ -109,10 +100,6 @@ export const SampleAppActions = { setTestProperty: (testProperty: string) => createAction(SampleAppUiActionId.setTestProperty, testProperty), setAnimationViewId: (viewId: string) => createAction(SampleAppUiActionId.setAnimationViewId, viewId), setIsIModelLocal: (isIModelLocal: boolean) => createAction(SampleAppUiActionId.setIsIModelLocal, isIModelLocal), - toggleDragInteraction: () => createAction(SampleAppUiActionId.toggleDragInteraction), - toggleFrameworkVersion: () => createAction(SampleAppUiActionId.toggleFrameworkVersion), - setDragInteraction: (dragInteraction: boolean) => createAction(SampleAppUiActionId.setDragInteraction, dragInteraction), - setFrameworkVersion: (frameworkVersion: string) => createAction(SampleAppUiActionId.setFrameworkVersion, frameworkVersion), }; class SampleAppAccuSnap extends AccuSnap { @@ -147,18 +134,6 @@ function SampleAppReducer(state: SampleAppState = initialState, action: SampleAp case SampleAppUiActionId.setIsIModelLocal: { return { ...state, isIModelLocal: action.payload }; } - case SampleAppUiActionId.toggleDragInteraction: { - return { ...state, dragInteraction: !state.dragInteraction }; - } - case SampleAppUiActionId.toggleFrameworkVersion: { - return { ...state, frameworkVersion: state.frameworkVersion === "1" ? "2" : "1" }; - } - case SampleAppUiActionId.setDragInteraction: { - return { ...state, dragInteraction: action.payload }; - } - case SampleAppUiActionId.setFrameworkVersion: { - return { ...state, frameworkVersion: action.payload }; - } } return state; } @@ -180,7 +155,8 @@ export class SampleAppIModelApp { public static iModelParams: SampleIModelParams | undefined; public static testAppConfiguration: TestAppConfiguration | undefined; private static _appStateManager: StateManager | undefined; - private static _appUiSettings = new AppUiSettings(); + private static _localUiSettings = new LocalSettingsStorage(); + private static _UserUiSettingsStorage = new UserSettingsStorage(); // Favorite Properties Support private static _selectionSetListener = new ElementSelectionListener(true); @@ -189,17 +165,14 @@ export class SampleAppIModelApp { return StateManager.store as Store; } - public static get uiSettings(): UiSettings { - return UiFramework.getUiSettings(); - } - - public static set uiSettings(v: UiSettings) { - UiFramework.setUiSettings(v); - SampleAppIModelApp._appUiSettings.apply(v); // eslint-disable-line @typescript-eslint/no-floating-promises + public static getUiSettingsStorage(): UiSettings { + const authorized = !!IModelApp.authorizationClient && IModelApp.authorizationClient.isAuthorized; + if (SampleAppIModelApp.testAppConfiguration?.useLocalSettings || !authorized) { + return SampleAppIModelApp._localUiSettings; + } + return SampleAppIModelApp._UserUiSettingsStorage; } - public static get appUiSettings(): AppUiSettings { return SampleAppIModelApp._appUiSettings; } - public static async startup(opts: WebViewerAppOpts & NativeAppOpts): Promise { if (ProcessDetector.isElectronAppFrontend) { await ElectronApp.startup(opts); @@ -285,7 +258,23 @@ export class SampleAppIModelApp { // To test map-layer extension comment out the following and ensure ui-test-app\build\imjs_extensions contains map-layers, if not see Readme.md in map-layers package. await MapLayersUI.initialize(false); // if false then add widget in FrontstageDef - AppSettingsProvider.initializeAppSettingProvider(); + AppSettingsTabsProvider.initializeAppSettingProvider(); + + // Create and register the AppUiSettings instance to provide default for ui settings in Redux store + const lastTheme = (window.localStorage&&window.localStorage.getItem("uifw:defaultTheme"))??SYSTEM_PREFERRED_COLOR_THEME; + const defaults = { + colorTheme: lastTheme ?? SYSTEM_PREFERRED_COLOR_THEME, + dragInteraction: false, + frameworkVersion: "2", + widgetOpacity: 0.8, + }; + + // initialize any settings providers that may need to have defaults set by iModelApp + UiFramework.registerUserSettingsProvider(new AppUiSettings(defaults)); + + // go ahead and initialize settings before login or in case login is by-passed + await UiFramework.setUiSettingsStorage(SampleAppIModelApp.getUiSettingsStorage()); + // try starting up event loop if not yet started so key-in palette can be opened IModelApp.startEventLoop(); } @@ -521,7 +510,7 @@ export class SampleAppIModelApp { } public static getUiFrameworkProperty(): string { - return SampleAppIModelApp.store.getState().sampleAppState.frameworkVersion; + return SampleAppIModelApp.store.getState().frameworkState.configurableUiState.frameworkVersion; } public static saveAnimationViewId(value: string, immediateSync = false) { @@ -565,17 +554,17 @@ function AppFrameworkVersionComponent(props: { frameworkVersion: string, childre } function mapDragInteractionStateToProps(state: RootState) { - return { dragInteraction: state.sampleAppState.dragInteraction }; + return { dragInteraction: state.frameworkState.configurableUiState.useDragInteraction }; } function mapFrameworkVersionStateToProps(state: RootState) { - return { frameworkVersion: state.sampleAppState.frameworkVersion }; + return { frameworkVersion: state.frameworkState.configurableUiState.frameworkVersion }; } const AppDragInteraction = connect(mapDragInteractionStateToProps)(AppDragInteractionComponent); const AppFrameworkVersion = connect(mapFrameworkVersionStateToProps)(AppFrameworkVersionComponent); -class SampleAppViewer extends React.Component { +class SampleAppViewer extends React.Component { constructor(props: any) { super(props); @@ -586,7 +575,7 @@ class SampleAppViewer extends React.Component { + private _onUserStateChanged = async (_accessToken: AccessToken | undefined) => { const authorized = !!IModelApp.authorizationClient && IModelApp.authorizationClient.isAuthorized; - this.setState({ authorized, uiSettings: this.getUiSettings(authorized) }); + const uiSettingsStorage = SampleAppIModelApp.getUiSettingsStorage(); + await UiFramework.setUiSettingsStorage(uiSettingsStorage); + this.setState({ authorized, uiSettingsStorage}); this._initializeSignin(authorized); // eslint-disable-line @typescript-eslint/no-floating-promises }; - private getUiSettings(authorized: boolean): UiSettings { - if (SampleAppIModelApp.testAppConfiguration?.useLocalSettings || !authorized) { - SampleAppIModelApp.uiSettings = new LocalUiSettings(); - } else { - SampleAppIModelApp.uiSettings = new IModelAppUiSettings(); - } - - return SampleAppIModelApp.uiSettings; - } - private _handleFrontstageDeactivatedEvent = (args: FrontstageDeactivatedEventArgs): void => { Logger.logInfo(SampleAppIModelApp.loggerCategory(this), `Frontstage exit: id=${args.deactivatedFrontstageDef.id} totalTime=${args.totalTime} engagementTime=${args.engagementTime} idleTime=${args.idleTime}`); }; @@ -643,7 +624,7 @@ class SampleAppViewer extends React.Component {/** UiSettingsProvider is optional. By default LocalUiSettings is used to store UI settings. */} - + } /> @@ -760,7 +741,6 @@ async function main() { IModelApp.telemetry.addClient(applicationInsightsClient); } - // wait for both our i18n namespaces to be read. await SampleAppIModelApp.initialize(); // register new QuantityType diff --git a/ui/components/src/test/color/ColorPickerButton.test.tsx b/ui/components/src/test/color/ColorPickerButton.test.tsx index 713d797ac31b..23e67f490a3e 100644 --- a/ui/components/src/test/color/ColorPickerButton.test.tsx +++ b/ui/components/src/test/color/ColorPickerButton.test.tsx @@ -80,6 +80,11 @@ describe("", () => { expect(spyOnColorPick).to.be.calledOnce; expect(button.getAttribute("data-value")).to.eq("rgb(255,0,0)"); // red } + + // ensure update prop is handled + const newColorDef = ColorDef.create(ColorByName.green); // green = 0x008000, + renderedComponent.rerender(); + expect(button.getAttribute("data-value")).to.eq("rgb(0,128,0)"); // green }); it("readonly - button press should not open popup", async () => { diff --git a/ui/components/src/test/table/component/Table.test.tsx b/ui/components/src/test/table/component/Table.test.tsx index b438dfe69fce..b58150eebbb0 100644 --- a/ui/components/src/test/table/component/Table.test.tsx +++ b/ui/components/src/test/table/component/Table.test.tsx @@ -13,7 +13,7 @@ import * as moq from "typemoq"; import { BeDuration } from "@bentley/bentleyjs-core"; import { PrimitiveValue, PropertyConverterInfo, PropertyDescription, PropertyRecord, PropertyValue, PropertyValueFormat, SpecialKey } from "@bentley/ui-abstract"; -import { HorizontalAlignment, LocalUiSettings } from "@bentley/ui-core"; +import { HorizontalAlignment, LocalSettingsStorage } from "@bentley/ui-core"; import { CellItem, ColumnDescription, PropertyUpdatedArgs, PropertyValueRendererManager, RowItem, SelectionMode, Table, TableDataChangeEvent, @@ -1355,7 +1355,7 @@ describe("Table", () => { reorderableColumns={true} ref={ref} settingsIdentifier="test" - uiSettings={new LocalUiSettings({ localStorage: storageMock() } as Window)} + settingsStorage={new LocalSettingsStorage({ localStorage: storageMock() } as Window)} />); await waitForSpy(onRowsLoaded); table.update(); @@ -1378,7 +1378,7 @@ describe("Table", () => { onRowsLoaded={onRowsLoaded} settingsIdentifier="test" showHideColumns={true} - uiSettings={new LocalUiSettings({ localStorage: storageMock() } as Window)} + settingsStorage={new LocalSettingsStorage({ localStorage: storageMock() } as Window)} />); await waitForSpy(onRowsLoaded); table.update(); diff --git a/ui/components/src/ui-components/color/ColorPickerButton.tsx b/ui/components/src/ui-components/color/ColorPickerButton.tsx index 9ffc1065008f..5246671c3610 100644 --- a/ui/components/src/ui-components/color/ColorPickerButton.tsx +++ b/ui/components/src/ui-components/color/ColorPickerButton.tsx @@ -63,14 +63,9 @@ const ForwardRefColorPickerButton = React.forwardRef { - // istanbul ignore else - if (initialColor !== initialColorRef.current) { - initialColorRef.current = initialColor; - } setColorDef(initialColor); }, [initialColor]); diff --git a/ui/components/src/ui-components/datepicker/DatePickerPopupButton.tsx b/ui/components/src/ui-components/datepicker/DatePickerPopupButton.tsx index d21bc91716e9..64c7417e57fe 100644 --- a/ui/components/src/ui-components/datepicker/DatePickerPopupButton.tsx +++ b/ui/components/src/ui-components/datepicker/DatePickerPopupButton.tsx @@ -47,20 +47,12 @@ export function DatePickerPopupButton({ displayEditField, timeDisplay, selected, const timeLabelRef = React.useRef(UiComponents.translate("datepicker.time")); const toolTipLabelRef = React.useRef(UiComponents.translate("datepicker.selectDate")); const toolTipLabel = React.useMemo(() => buttonToolTip ? buttonToolTip : toolTipLabelRef.current, [buttonToolTip]); - const initialDateRef = React.useRef(selected); - // See if new initialDate props have changed since component mounted + // See if props have changed since component mounted React.useEffect(() => { - // istanbul ignore else - if (selected.getTime() !== initialDateRef.current.getTime()) { - const newWorkingDate = new Date(selected.getTime()); - // istanbul ignore else - if (workingDate.getTime() !== newWorkingDate.getTime()) { - setWorkingDate(newWorkingDate); - } - initialDateRef.current = selected; - } - }, [selected, workingDate]); + const newWorkingDate = new Date(selected.getTime()); + setWorkingDate(newWorkingDate); + }, [selected]); const buttonRef = React.useRef(null); const togglePopupDisplay = React.useCallback((event: React.MouseEvent) => { diff --git a/ui/components/src/ui-components/oidc/SignIn.scss b/ui/components/src/ui-components/oidc/SignIn.scss index dd06191c4646..b91b7552759b 100644 --- a/ui/components/src/ui-components/oidc/SignIn.scss +++ b/ui/components/src/ui-components/oidc/SignIn.scss @@ -54,18 +54,12 @@ $signin-hyperlink-hover-color: $buic-foreground-primary-tone; .components-signin-prompt { margin-top: 3em; text-align: center; + height: 7em; } /* sign in button */ .components-signin-button { @include uicore-buttons-primary-large; - margin-top: auto; - } - - /* signing-in message */ - .components-signingin-message { - margin-top: 3em; - text-align: center; } /* "Register" container */ diff --git a/ui/components/src/ui-components/oidc/SignIn.tsx b/ui/components/src/ui-components/oidc/SignIn.tsx index a71af4641645..7e1284710fe0 100644 --- a/ui/components/src/ui-components/oidc/SignIn.tsx +++ b/ui/components/src/ui-components/oidc/SignIn.tsx @@ -91,7 +91,10 @@ export class SignIn extends React.PureComponent {
- {this.state.prompt} + {(this.state.isSigningIn && this.props.signingInMessage !== undefined) ? + {this.props.signingInMessage} : + {this.state.prompt} + }
); diff --git a/ui/components/src/ui-components/quantityformat/FormatPanel.tsx b/ui/components/src/ui-components/quantityformat/FormatPanel.tsx index 025d3a1a1963..8525aaba3a21 100644 --- a/ui/components/src/ui-components/quantityformat/FormatPanel.tsx +++ b/ui/components/src/ui-components/quantityformat/FormatPanel.tsx @@ -46,15 +46,11 @@ export function FormatPanel(props: FormatPanelProps) { const [formatSpec, setFormatSpec] = React.useState(); const { initialFormat, showSample, initialMagnitude, unitsProvider, persistenceUnit, onFormatChange, provideFormatSpec, enableMinimumProperties } = props; const [formatProps, setFormatProps] = React.useState(initialFormat); - const initialFormatRef = React.useRef(initialFormat); const [showOptions, setShowOptions] = React.useState(false); React.useEffect(() => { - if (initialFormatRef.current !== initialFormat) { - initialFormatRef.current = initialFormat; - setFormatProps(initialFormat); - setFormatSpec(undefined); // this will trigger the new spec to be created in the useEffect hook - } + setFormatProps(initialFormat); + setFormatSpec(undefined); // this will trigger the new spec to be created in the useEffect hook }, [initialFormat]); const handleUserFormatChanges = React.useCallback((newProps: FormatProps) => { diff --git a/ui/components/src/ui-components/quantityformat/FormatSample.tsx b/ui/components/src/ui-components/quantityformat/FormatSample.tsx index e5dc796c616d..22ff36de509f 100644 --- a/ui/components/src/ui-components/quantityformat/FormatSample.tsx +++ b/ui/components/src/ui-components/quantityformat/FormatSample.tsx @@ -26,16 +26,14 @@ export interface FormatSampleProps extends CommonProps { */ export function FormatSample(props: FormatSampleProps) { const { initialMagnitude, formatSpec, hideLabels } = props; - const initialMagnitudeRef = React.useRef(initialMagnitude ?? 0); - const [magnitude, setMagnitude] = React.useState(initialMagnitudeRef.current); - const [sampleValue, setSampleValue] = React.useState(magnitude.toString()); + const initialValue = initialMagnitude??0; + const [magnitude, setMagnitude] = React.useState(initialValue); + const [sampleValue, setSampleValue] = React.useState(initialValue.toString()); React.useEffect(() => { - if (initialMagnitudeRef.current !== initialMagnitude) { - initialMagnitudeRef.current = initialMagnitude ?? 0; - setMagnitude(initialMagnitudeRef.current); - setSampleValue(initialMagnitudeRef.current.toString()); - } + const value = initialMagnitude ?? 0; + setMagnitude(value); + setSampleValue(value.toString()); }, [initialMagnitude]); const handleOnValueBlur = React.useCallback(() => { diff --git a/ui/components/src/ui-components/table/component/Table.tsx b/ui/components/src/ui-components/table/component/Table.tsx index 6c550fa784d6..a6204e7c30d0 100644 --- a/ui/components/src/ui-components/table/component/Table.tsx +++ b/ui/components/src/ui-components/table/component/Table.tsx @@ -15,7 +15,8 @@ import ReactResizeDetector from "react-resize-detector"; import { DisposableList, Guid, GuidString } from "@bentley/bentleyjs-core"; import { PropertyValueFormat } from "@bentley/ui-abstract"; import { - CommonProps, Dialog, isNavigationKey, ItemKeyboardNavigator, LocalUiSettings, Orientation, SortDirection, UiSettings, UiSettingsStatus, + CommonProps, Dialog, isNavigationKey, ItemKeyboardNavigator, LocalSettingsStorage, + Orientation, SortDirection, UiSettings, UiSettingsStatus, UiSettingsStorage, } from "@bentley/ui-core"; import { MultiSelectionHandler, OnItemsDeselectedCallback, OnItemsSelectedCallback, SelectionHandler, SingleSelectionHandler, @@ -125,6 +126,9 @@ export interface TableProps extends CommonProps { /** Indicates whether the Table columns are reorderable */ reorderableColumns?: boolean; /** Optional parameter for persistent UI settings. Used for column reordering and show persistency. */ + settingsStorage?: UiSettingsStorage; + /** Optional parameter for persistent UI settings. Used for column reordering and show persistency. + * @deprecated use settingsStorage property */ uiSettings?: UiSettings; /** Identifying string used for persistent state. */ settingsIdentifier?: string; @@ -495,8 +499,9 @@ export class Table extends React.Component { let dataGridColumns = columnDescriptions.map(this._columnDescriptionToReactDataGridColumn); if (this.props.settingsIdentifier) { - const uiSettings: UiSettings = this.props.uiSettings || /* istanbul ignore next */ new LocalUiSettings(); - const reorderResult = await uiSettings.getSetting(this.props.settingsIdentifier, "ColumnReorder"); + // eslint-disable-next-line deprecation/deprecation + const settingsStorage: UiSettingsStorage = this.props.settingsStorage || /* istanbul ignore next */ this.props.uiSettings || /* istanbul ignore next */ new LocalSettingsStorage(); + const reorderResult = await settingsStorage.getSetting(this.props.settingsIdentifier, "ColumnReorder"); // istanbul ignore next if (reorderResult.status === UiSettingsStatus.Success) { const setting = reorderResult.setting as string[]; @@ -504,9 +509,9 @@ export class Table extends React.Component { dataGridColumns = setting.map((key) => dataGridColumns.filter((col) => col.key === key)[0]); } else if (reorderResult.status === UiSettingsStatus.NotFound) { const keys = columnDescriptions.map((col) => col.key); - await uiSettings.saveSetting(this.props.settingsIdentifier, "ColumnReorder", keys); + await settingsStorage.saveSetting(this.props.settingsIdentifier, "ColumnReorder", keys); } - const showhideResult = await uiSettings.getSetting(this.props.settingsIdentifier, "ColumnShowHideHiddenColumns"); + const showhideResult = await settingsStorage.getSetting(this.props.settingsIdentifier, "ColumnShowHideHiddenColumns"); // istanbul ignore next if (showhideResult.status === UiSettingsStatus.Success) { const hiddenColumns = showhideResult.setting as string[]; @@ -1216,9 +1221,9 @@ export class Table extends React.Component { cols.splice(columnTargetIndex, 0, cols.splice(columnSourceIndex, 1)[0]); // istanbul ignore else if (this.props.settingsIdentifier) { - const uiSettings: UiSettings = this.props.uiSettings || /* istanbul ignore next */ new LocalUiSettings(); + const settingsStorage: UiSettingsStorage = this.props.settingsStorage || /* istanbul ignore next */ new LocalSettingsStorage(); const keys = cols.map((col) => col.key); - uiSettings.saveSetting(this.props.settingsIdentifier, "ColumnReorder", keys); // eslint-disable-line @typescript-eslint/no-floating-promises + settingsStorage.saveSetting(this.props.settingsIdentifier, "ColumnReorder", keys); // eslint-disable-line @typescript-eslint/no-floating-promises } this.setState({ columns: [] }, () => { // fix react-data-grid update issues this.setState({ columns: cols }); @@ -1347,8 +1352,8 @@ export class Table extends React.Component { private _handleShowHideChange = (cols: string[]) => { this.setState({ hiddenColumns: cols }); if (this.props.settingsIdentifier) { - const uiSettings: UiSettings = this.props.uiSettings || new LocalUiSettings(); - uiSettings.saveSetting(this.props.settingsIdentifier, "ColumnShowHideHiddenColumns", cols); // eslint-disable-line @typescript-eslint/no-floating-promises + const settingsStorage: UiSettingsStorage = this.props.settingsStorage || new LocalSettingsStorage(); + settingsStorage.saveSetting(this.props.settingsIdentifier, "ColumnShowHideHiddenColumns", cols); // eslint-disable-line @typescript-eslint/no-floating-promises } return true; }; diff --git a/ui/core/src/test/settings/SettingsManager.test.tsx b/ui/core/src/test/settings/SettingsManager.test.tsx index 5529b58e2ad9..f61ddbd39d8d 100644 --- a/ui/core/src/test/settings/SettingsManager.test.tsx +++ b/ui/core/src/test/settings/SettingsManager.test.tsx @@ -8,7 +8,7 @@ import { render } from "@testing-library/react"; import { expect } from "chai"; import * as sinon from "sinon"; import { SettingsContainer, useSaveBeforeActivatingNewSettingsTab, useSaveBeforeClosingSettingsContainer } from "../../ui-core/settings/SettingsContainer"; -import { SettingsManager, SettingsProvider, SettingsTabEntry } from "../../ui-core/settings/SettingsManager"; +import { SettingsManager, SettingsTabEntry, SettingsTabsProvider } from "../../ui-core/settings/SettingsManager"; function TestModalSettingsPage({ settingsManager, title }: { settingsManager: SettingsManager, title: string }) { @@ -25,7 +25,7 @@ function TestModalSettingsPage({ settingsManager, title }: { settingsManager: Se describe("", () => { const settingsManager = new SettingsManager(); - class TestSettingsProvider implements SettingsProvider { + class TestSettingsProvider implements SettingsTabsProvider { public readonly id = "AppSettingsProvider"; public getSettingEntries(_stageId: string, _stageUsage: string): ReadonlyArray | undefined { diff --git a/ui/core/src/test/uisettings/LocalUiSettings.test.ts b/ui/core/src/test/uisettings/LocalUiSettings.test.ts index cfcaa0c5aa04..5aa792d1ecf4 100644 --- a/ui/core/src/test/uisettings/LocalUiSettings.test.ts +++ b/ui/core/src/test/uisettings/LocalUiSettings.test.ts @@ -2,25 +2,30 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ + import { expect } from "chai"; -import { LocalUiSettings, UiSettingsStatus } from "../../ui-core"; +import { LocalSettingsStorage, LocalUiSettings, UiSettingsStatus } from "../../ui-core"; import { storageMock } from "../TestUtils"; describe("LocalUiSettings", () => { it("default constructor executes successfully", () => { - const initialLocalUiSettings = new LocalUiSettings(); + const initialLocalUiSettings = new LocalUiSettings(); // eslint-disable-line deprecation/deprecation + expect(initialLocalUiSettings).to.not.be.undefined; + }); + it("default LocalSettingsStorage constructor executes successfully", () => { + const initialLocalUiSettings = new LocalSettingsStorage(); // eslint-disable-line deprecation/deprecation expect(initialLocalUiSettings).to.not.be.undefined; }); describe("saveSetting", () => { - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const localUiSettings = new LocalSettingsStorage({ localStorage: storageMock() } as Window); it("Should save setting correctly", async () => { const result = await localUiSettings.saveSetting("Testing", "TestData", { test123: "4567" }); expect(result.status).to.equal(UiSettingsStatus.Success); }); }); describe("getSetting", async () => { - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const localUiSettings = new LocalSettingsStorage({ localStorage: storageMock() } as Window); await localUiSettings.saveSetting("Testing", "TestData", { test123: "4567" }); it("Should load setting correctly", async () => { const result = await localUiSettings.getSetting("Testing", "TestData"); @@ -34,7 +39,7 @@ describe("LocalUiSettings", () => { }); }); describe("deleteSetting", async () => { - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const localUiSettings = new LocalSettingsStorage({ localStorage: storageMock() } as Window); await localUiSettings.saveSetting("Testing", "TestData", { test123: "4567" }); it("Should remove setting correctly", async () => { const result = await localUiSettings.deleteSetting("Testing", "TestData"); diff --git a/ui/core/src/test/uisettings/SessionUiSettings.test.ts b/ui/core/src/test/uisettings/SessionUiSettings.test.ts index df060b1d79e0..994bcfc0636b 100644 --- a/ui/core/src/test/uisettings/SessionUiSettings.test.ts +++ b/ui/core/src/test/uisettings/SessionUiSettings.test.ts @@ -3,24 +3,28 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; -import { SessionUiSettings, UiSettingsStatus } from "../../ui-core"; +import { SessionSettingsStorage, SessionUiSettings, UiSettingsStatus } from "../../ui-core"; import { storageMock } from "../TestUtils"; describe("SessionUiSettings", () => { it("default constructor executes successfully", () => { - const initialSessionUiSettings = new SessionUiSettings(); + const initialSessionUiSettings = new SessionUiSettings(); // eslint-disable-line deprecation/deprecation + expect(initialSessionUiSettings).to.not.be.undefined; + }); + it("default SessionSettingsStorage constructor executes successfully", () => { + const initialSessionUiSettings = new SessionSettingsStorage(); // eslint-disable-line deprecation/deprecation expect(initialSessionUiSettings).to.not.be.undefined; }); describe("saveSetting", () => { - const sessionSettings = new SessionUiSettings({ sessionStorage: storageMock() } as Window); + const sessionSettings = new SessionSettingsStorage({ sessionStorage: storageMock() } as Window); it("Should save setting correctly", async () => { const result = await sessionSettings.saveSetting("Testing", "TestData", { test123: "4567" }); expect(result.status).to.equal(UiSettingsStatus.Success); }); }); describe("getSetting", async () => { - const sessionSettings = new SessionUiSettings({ sessionStorage: storageMock() } as Window); + const sessionSettings = new SessionSettingsStorage({ sessionStorage: storageMock() } as Window); await sessionSettings.saveSetting("Testing", "TestData", { test123: "4567" }); it("Should load setting correctly", async () => { @@ -35,7 +39,7 @@ describe("SessionUiSettings", () => { }); }); describe("deleteSetting", async () => { - const sessionSettings = new SessionUiSettings({ sessionStorage: storageMock() } as Window); + const sessionSettings = new SessionSettingsStorage({ sessionStorage: storageMock() } as Window); await sessionSettings.saveSetting("Testing", "TestData", { test123: "4567" }); it("Should remove setting correctly", async () => { diff --git a/ui/core/src/test/uisettings/UiSetting.test.ts b/ui/core/src/test/uisettings/UiSetting.test.ts index 4c75de1dedb9..8b3f3f57e76d 100644 --- a/ui/core/src/test/uisettings/UiSetting.test.ts +++ b/ui/core/src/test/uisettings/UiSetting.test.ts @@ -3,7 +3,7 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { expect } from "chai"; -import { LocalUiSettings, UiSetting, UiSettingsStatus } from "../../ui-core"; +import { LocalSettingsStorage, LocalUiSettings, UiSetting, UiSettingsStatus } from "../../ui-core"; import { storageMock } from "../TestUtils"; function getBoolean(): boolean { return true; } @@ -20,7 +20,7 @@ describe("UiSetting", () => { }); describe("saveSetting", () => { - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); // eslint-disable-line deprecation/deprecation it("Should save setting correctly", async () => { const uiSetting = new UiSetting("Namespace", "Setting", getBoolean); @@ -29,21 +29,8 @@ describe("UiSetting", () => { }); }); - describe("getSetting", async () => { - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); - const uiSetting = new UiSetting("Namespace", "Setting", getString); - await uiSetting.saveSetting(localUiSettings); - - it("Should load setting correctly", async () => { - const result = await uiSetting.getSetting(localUiSettings); - expect(result.status).to.equal(UiSettingsStatus.Success); - expect(result.setting).to.not.be.null; - expect(result.setting).to.equal(getString()); - }); - }); - describe("deleteSetting", async () => { - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const localUiSettings = new LocalSettingsStorage({ localStorage: storageMock() } as Window); const uiSetting = new UiSetting("Namespace", "Setting", getString); await uiSetting.saveSetting(localUiSettings); @@ -62,7 +49,7 @@ describe("UiSetting", () => { let value = 100; function applyNumber(v: number) { value = v; } - const localUiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const localUiSettings = new LocalSettingsStorage({ localStorage: storageMock() } as Window); const uiSetting = new UiSetting("Namespace", "Setting", getNumber, applyNumber); await uiSetting.saveSetting(localUiSettings); @@ -79,6 +66,16 @@ describe("UiSetting", () => { expect(result.status).to.eq(UiSettingsStatus.Uninitialized); }); + it("Should use default value if no applyValue", async () => { + const defaultValue = 999; + // make sure testing with a new key not yet in mock storage + const uiSetting2 = new UiSetting("Namespace", "TEST-XYZ", getNumber, applyNumber, defaultValue); + const result = await uiSetting2.getSettingAndApplyValue(localUiSettings); + expect(result.status).to.eq(UiSettingsStatus.Success); + expect(result.setting).to.eq(defaultValue); + expect(value).to.eq(defaultValue); + }); + it("Should return NotFound if not saved", async () => { const uiSetting3 = new UiSetting("Namespace", "XYZ", getNumber, applyNumber); const result = await uiSetting3.getSettingAndApplyValue(localUiSettings); diff --git a/ui/core/src/ui-core.ts b/ui/core/src/ui-core.ts index ac65bc3ac519..0e165c9bbf44 100644 --- a/ui/core/src/ui-core.ts +++ b/ui/core/src/ui-core.ts @@ -153,9 +153,9 @@ export { Tree, TreeProps } from "./ui-core/tree/Tree"; export { TreeNodePlaceholder, TreeNodePlaceholderProps } from "./ui-core/tree/Placeholder"; export * from "./ui-core/uisettings/UiSetting"; -export * from "./ui-core/uisettings/UiSettings"; -export * from "./ui-core/uisettings/LocalUiSettings"; -export * from "./ui-core/uisettings/SessionUiSettings"; +export * from "./ui-core/uisettings/UiSettingsStorage"; +export * from "./ui-core/uisettings/LocalSettingsStorage"; +export * from "./ui-core/uisettings/SessionSettingsStorage"; export * from "./ui-core/utils/IconHelper"; export * from "./ui-core/utils/Point"; diff --git a/ui/core/src/ui-core/listbox/Listbox.tsx b/ui/core/src/ui-core/listbox/Listbox.tsx index 9f7d87fa26e5..3d584d2e1e41 100644 --- a/ui/core/src/ui-core/listbox/Listbox.tsx +++ b/ui/core/src/ui-core/listbox/Listbox.tsx @@ -115,21 +115,16 @@ function processKeyboardNavigation(optionValues: ListboxItemProps[], itemIndex: export function Listbox(props: ListboxProps) { const { ariaLabel, ariaLabelledBy, id, children, selectedValue, className, onListboxValueChange, onKeyPress, ...otherProps } = props; const listRef = React.useRef(null); - const [listId] = React.useState(id ?? Guid.createValue()); + const [listId] = React.useState(()=>{return id ?? Guid.createValue();}); const optionValues = React.useMemo(() => getOptionValueArray(children), [children]); const classes = React.useMemo(() => classnames("core-listbox", className), [className]); const [currentValue, setCurrentValue] = React.useState(selectedValue); const [focusValue, setFocusValue] = React.useState(currentValue); - const initialSelectedValueRef = React.useRef(selectedValue); React.useEffect(() => { - // istanbul ignore else - if (initialSelectedValueRef.current !== selectedValue) { - initialSelectedValueRef.current = selectedValue; - setCurrentValue(selectedValue); - setFocusValue(selectedValue); - } - }, [currentValue, selectedValue]); + setCurrentValue(selectedValue); + setFocusValue(selectedValue); + }, [selectedValue]); const scrollTopRef = React.useRef(0); const handleValueChange = React.useCallback((newValue: ListboxValue, isControlOrCommandPressed?: boolean) => { diff --git a/ui/core/src/ui-core/settings/SettingsManager.tsx b/ui/core/src/ui-core/settings/SettingsManager.tsx index 62e199993cb4..704bf1fdefdd 100644 --- a/ui/core/src/ui-core/settings/SettingsManager.tsx +++ b/ui/core/src/ui-core/settings/SettingsManager.tsx @@ -33,7 +33,7 @@ export interface SettingsTabEntry { readonly isDisabled?: boolean | ConditionalBooleanValue; } -/** Event class for [[this.onSettingsProvidersChanged]] which is emitted when a new SettingsProvider is added or removed. +/** Event class for [[this.onSettingsProvidersChanged]] which is emitted when a new SettingsTabsProvider is added or removed. * @beta */ export class SettingsProvidersChangedEvent extends BeUiEvent { } @@ -42,7 +42,7 @@ export class SettingsProvidersChangedEvent extends BeUiEvent; + readonly providers: ReadonlyArray; } /** Event class for [[this.onProcessSettingsTabActivation]] which is emitted when a new Tab needs to be activated. This allows the current @@ -94,7 +94,7 @@ export interface ActivateSettingsTabEventArgs { * classes that implement this interface need to be registered with the [[SettingsManager]]. * @beta */ -export interface SettingsProvider { +export interface SettingsTabsProvider { /** Id of provider, used to remove registration. */ readonly id: string; getSettingEntries(stageId: string, stageUsage: string): ReadonlyArray | undefined; @@ -104,7 +104,7 @@ export interface SettingsProvider { * @beta */ export class SettingsManager { - private _providers: ReadonlyArray = []; + private _providers: ReadonlyArray = []; /** Event raised when SettingsProviders are changed. */ @@ -131,8 +131,8 @@ export class SettingsManager { public readonly onCloseSettingsContainer = new CloseSettingsContainerEvent(); /** @beta */ - public get providers(): ReadonlyArray { return this._providers; } - public set providers(p: ReadonlyArray) { + public get providers(): ReadonlyArray { return this._providers; } + public set providers(p: ReadonlyArray) { this._providers = p; this.onSettingsProvidersChanged.emit({ providers: p }); } @@ -151,7 +151,7 @@ export class SettingsManager { this.onCloseSettingsContainer.emit({ closeFunc, closeFuncArgs }); } - public addSettingsProvider(settingsProvider: SettingsProvider): void { + public addSettingsProvider(settingsProvider: SettingsTabsProvider): void { const foundProvider = this._providers.find((p) => p.id === settingsProvider.id); if (!foundProvider) { const updatedProviders = [ diff --git a/ui/core/src/ui-core/tabs/tabs.scss b/ui/core/src/ui-core/tabs/tabs.scss index fd2f9e2b937e..a5b5abc2d533 100644 --- a/ui/core/src/ui-core/tabs/tabs.scss +++ b/ui/core/src/ui-core/tabs/tabs.scss @@ -46,7 +46,10 @@ } .uicore-tabs-icon { - width: ( $uicore-icons-small + 10px ); + width: ( $uicore-icons-small + 12px ); + height: ( $uicore-icons-small + 12px ); + display: flex; + align-items: center; } } diff --git a/ui/core/src/ui-core/uisettings/LocalUiSettings.ts b/ui/core/src/ui-core/uisettings/LocalSettingsStorage.ts similarity index 76% rename from ui/core/src/ui-core/uisettings/LocalUiSettings.ts rename to ui/core/src/ui-core/uisettings/LocalSettingsStorage.ts index 584599f1af70..e82a8212358e 100644 --- a/ui/core/src/ui-core/uisettings/LocalUiSettings.ts +++ b/ui/core/src/ui-core/uisettings/LocalSettingsStorage.ts @@ -6,13 +6,13 @@ * @module UiSettings */ -import { UiSettings, UiSettingsResult, UiSettingsStatus } from "./UiSettings"; +import { UiSettingsResult, UiSettingsStatus, UiSettingsStorage } from "./UiSettingsStorage"; /** - * Implementation of [[UiSettings]] using Window.localStorage. - * @beta + * Implementation of [[UiSettingsStorage]] using Window.localStorage. + * @public */ -export class LocalUiSettings implements UiSettings { +export class LocalSettingsStorage implements UiSettingsStorage { constructor(public w: Window = window) { } @@ -38,3 +38,11 @@ export class LocalUiSettings implements UiSettings { return { status: UiSettingsStatus.Success }; } } + +/** Alias for [[LocalSettingsStorage]] + * @beta @deprecated use LocalSettingsStorage + */ +export class LocalUiSettings extends LocalSettingsStorage { + constructor(w: Window = window) { super (w);} +} + diff --git a/ui/core/src/ui-core/uisettings/SessionUiSettings.ts b/ui/core/src/ui-core/uisettings/SessionSettingsStorage.ts similarity index 80% rename from ui/core/src/ui-core/uisettings/SessionUiSettings.ts rename to ui/core/src/ui-core/uisettings/SessionSettingsStorage.ts index 018ba446b2ab..c98dbc387579 100644 --- a/ui/core/src/ui-core/uisettings/SessionUiSettings.ts +++ b/ui/core/src/ui-core/uisettings/SessionSettingsStorage.ts @@ -6,13 +6,13 @@ * @module UiSettings */ -import { UiSettings, UiSettingsResult, UiSettingsStatus } from "./UiSettings"; +import { UiSettingsResult, UiSettingsStatus, UiSettingsStorage } from "./UiSettingsStorage"; /** * Implementation of [[UiSettings]] using Window.sessionStorage. - * @beta + * @public */ -export class SessionUiSettings implements UiSettings { +export class SessionSettingsStorage implements UiSettingsStorage { constructor(public w: Window = window) { } @@ -38,3 +38,10 @@ export class SessionUiSettings implements UiSettings { return { status: UiSettingsStatus.Success }; } } + +/** Alias for [[SessionSettingsStorage]] + * @beta @deprecated use SessionSettingsStorage + */ +export class SessionUiSettings extends SessionSettingsStorage { + constructor(w: Window = window) { super (w);} +} diff --git a/ui/core/src/ui-core/uisettings/UiSetting.ts b/ui/core/src/ui-core/uisettings/UiSetting.ts index 0d73740f490b..c699da52f4e7 100644 --- a/ui/core/src/ui-core/uisettings/UiSetting.ts +++ b/ui/core/src/ui-core/uisettings/UiSetting.ts @@ -6,10 +6,10 @@ * @module UiSettings */ -import { UiSettings, UiSettingsResult, UiSettingsStatus } from "./UiSettings"; +import { UiSettingsResult, UiSettingsStatus, UiSettingsStorage } from "./UiSettingsStorage"; /** A Ui Setting with namespace and setting name. - * @beta + * @public */ export class UiSetting { /** Constructor @@ -17,31 +17,36 @@ export class UiSetting { * @param settingName Name for the setting, passed to UiSettings. * @param getValue Function for getting the value from the application. * @param applyValue Function for applying the setting value to the application. + * @param defaultValue Optional default value if not already stored. */ - public constructor(public settingNamespace: string, public settingName: string, public getValue: () => T, public applyValue?: (v: T) => void) { + public constructor(public settingNamespace: string, public settingName: string, public getValue: () => T, public applyValue?: (v: T) => void, public defaultValue?: T) { } - /** Gets the setting from UiSettings */ - public async getSetting(uiSettings: UiSettings): Promise { - return uiSettings.getSetting(this.settingNamespace, this.settingName); + /** Gets the setting from [[UiSettingsStorage]] */ + public async getSetting(storage: UiSettingsStorage): Promise { + return storage.getSetting(this.settingNamespace, this.settingName); } /** Saves the setting value from the `getValue` function to UiSettings */ - public async saveSetting(uiSettings: UiSettings): Promise { - return uiSettings.saveSetting(this.settingNamespace, this.settingName, this.getValue()); + public async saveSetting(storage: UiSettingsStorage): Promise { + return storage.saveSetting(this.settingNamespace, this.settingName, this.getValue()); } /** Deletes the setting from UiSettings */ - public async deleteSetting(uiSettings: UiSettings): Promise { - return uiSettings.deleteSetting(this.settingNamespace, this.settingName); + public async deleteSetting(storage: UiSettingsStorage): Promise { + return storage.deleteSetting(this.settingNamespace, this.settingName); } /** Gets the setting from UiSettings and applies the value using the `applyValue` function */ - public async getSettingAndApplyValue(uiSettings: UiSettings): Promise { + public async getSettingAndApplyValue(storage: UiSettingsStorage): Promise { if (this.applyValue) { - const result = await this.getSetting(uiSettings); - if (result !== undefined && result.status === UiSettingsStatus.Success) { + const result = await this.getSetting(storage); + if (result.status === UiSettingsStatus.Success) { this.applyValue(result.setting); + } else if (undefined !== this.defaultValue) { + this.applyValue(this.defaultValue); + result.setting = this.defaultValue; + result.status = UiSettingsStatus.Success; } return result; } diff --git a/ui/core/src/ui-core/uisettings/UiSettings.ts b/ui/core/src/ui-core/uisettings/UiSettingsStorage.ts similarity index 80% rename from ui/core/src/ui-core/uisettings/UiSettings.ts rename to ui/core/src/ui-core/uisettings/UiSettingsStorage.ts index 76d912005c62..bddb86cae00d 100644 --- a/ui/core/src/ui-core/uisettings/UiSettings.ts +++ b/ui/core/src/ui-core/uisettings/UiSettingsStorage.ts @@ -9,13 +9,18 @@ /** Interface for getting, saving and deleting settings. * @public */ -export interface UiSettings { +export interface UiSettingsStorage { getSetting(settingNamespace: string, settingName: string): Promise; saveSetting(settingNamespace: string, settingName: string, setting: any): Promise; deleteSetting(settingNamespace: string, settingName: string): Promise; } -/** Enum for [[UiSettings]] status. +/** Alias for [[UiSettingsStorage]] + * @public + */ +export type UiSettings = UiSettingsStorage; + +/** Enum for [[UiSettingsStorage]] status. * @public */ export enum UiSettingsStatus { @@ -26,7 +31,7 @@ export enum UiSettingsStatus { AuthorizationError = 4, } -/** Interface for [[UiSettings]] result. +/** Interface for result of accessing setting in [[UiSettingsStorage]]. * @public */ export interface UiSettingsResult { diff --git a/ui/framework/public/locales/en/UiFramework.json b/ui/framework/public/locales/en/UiFramework.json index 75262a3e64e3..949a4a3bdca2 100644 --- a/ui/framework/public/locales/en/UiFramework.json +++ b/ui/framework/public/locales/en/UiFramework.json @@ -143,6 +143,29 @@ "formatSectionLabel": "Quantity Formats", "setButtonLabel": "Set", "clearButtonLabel": "Clear" + }, + "uiSettingsPage": { + "label": "UI Settings", + "tooltip": "UI Settings", + "light": "Light", + "dark": "Dark", + "systemPreferred": "System preferred", + "themeTitle": "Theme", + "themeDescription": "Toggle the theme between light and dark", + "autoHideTitle": "Auto-Hide UI", + "autoHideDescription": "Toggle the auto-hide of UI after inactivity", + "dragInteractionTitle": "Toolbar Group Buttons show child action item", + "dragInteractionDescription": "Requires long press to open pop-up panel. Single click executes displayed action.", + "newUiTitle": "Use 2.0 Ui", + "newUiDescription": "Use new 2.0 UI layout", + "useProximityOpacityTitle": "Change Toolbar Opacity", + "useProximityOpacityDescription": "Change the toolbar opacity as the mouse gets closer or farther away", + "snapWidgetOpacityTitle": "Snap Toolbar Opacity", + "snapWidgetOpacityDescription": "Immediately change the toolbar opacity when the mouse gets close", + "accuDrawNotificationsTitle": "Display AccuDraw Notifications", + "accuDrawNotificationsDescription": "Display toast notifications when AccuDraw status changes", + "widgetOpacityTitle": "Widget Opacity", + "widgetOpacityDescription": "Opacity when mouse is not hovering for 2.0 floating widgets and all 1.0 widgets" } }, "tools": { diff --git a/ui/framework/src/test/UiFramework.test.ts b/ui/framework/src/test/UiFramework.test.ts index 03e655f0fbdb..898697e992d6 100644 --- a/ui/framework/src/test/UiFramework.test.ts +++ b/ui/framework/src/test/UiFramework.test.ts @@ -2,6 +2,8 @@ * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ +// cSpell:ignore typemoq, tabid + import { expect } from "chai"; import * as moq from "typemoq"; import * as sinon from "sinon"; @@ -9,231 +11,284 @@ import { Id64String, Logger } from "@bentley/bentleyjs-core"; import { IModelApp, IModelConnection, MockRender, ViewState } from "@bentley/imodeljs-frontend"; import { Presentation } from "@bentley/presentation-frontend"; import { initialize as initializePresentationTesting, terminate as terminatePresentationTesting } from "@bentley/presentation-testing"; -import { ColorTheme, CursorMenuData, SettingsModalFrontstage, UiFramework } from "../ui-framework"; +import { ColorTheme, CursorMenuData, SettingsModalFrontstage, UiFramework, UserSettingsProvider } from "../ui-framework"; import { DefaultIModelServices } from "../ui-framework/clientservices/DefaultIModelServices"; import { DefaultProjectServices } from "../ui-framework/clientservices/DefaultProjectServices"; -import TestUtils, { mockUserInfo } from "./TestUtils"; -import { UiSettings } from "@bentley/ui-core"; +import TestUtils, { mockUserInfo, storageMock } from "./TestUtils"; +import { LocalSettingsStorage, UiSettingsStorage } from "@bentley/ui-core"; import { OpenSettingsTool } from "../ui-framework/tools/OpenSettingsTool"; -describe("UiFramework", () => { +describe("UiFramework localStorage Wrapper", () => { - beforeEach(() => { - TestUtils.terminateUiFramework(); - }); + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + const localStorageMock = storageMock(); - afterEach(() => { - TestUtils.terminateUiFramework(); + before(async () => { + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); }); - it("store should throw Error without initialize", () => { - expect(() => UiFramework.store).to.throw(Error); + after(() => { + Object.defineProperty(window, "localStorage", localStorageToRestore); }); - it("i18n should throw Error without initialize", () => { - expect(() => UiFramework.i18n).to.throw(Error); - }); + describe("UiFramework", () => { - it("i18nNamespace should return UiFramework", () => { - expect(UiFramework.i18nNamespace).to.eq("UiFramework"); - }); + beforeEach(() => { + TestUtils.terminateUiFramework(); + }); - it("packageName should return ui-framework", () => { - expect(UiFramework.packageName).to.eq("ui-framework"); - }); + afterEach(() => { + TestUtils.terminateUiFramework(); + }); - it("translate should return the key (in test environment)", async () => { - await TestUtils.initializeUiFramework(true); - expect(UiFramework.translate("test1.test2")).to.eq("test1.test2"); - }); + it("store should throw Error without initialize", () => { + expect(() => UiFramework.store).to.throw(Error); + }); - it("test OpenSettingsTool", async () => { - await TestUtils.initializeUiFramework(true); + it("i18n should throw Error without initialize", () => { + expect(() => UiFramework.i18n).to.throw(Error); + }); - const spy = sinon.spy(); - const tabName = "page1"; - const handleOpenSetting = (settingsCategory: string)=> { - expect (settingsCategory).to.eql(tabName); - spy(); - }; + it("i18nNamespace should return UiFramework", () => { + expect(UiFramework.i18nNamespace).to.eq("UiFramework"); + }); - const handleOpenSetting2 = (_settingsCategory: string)=> { - spy(); - }; + it("packageName should return ui-framework", () => { + expect(UiFramework.packageName).to.eq("ui-framework"); + }); - const showSettingsStageToRestore = Object.getOwnPropertyDescriptor(SettingsModalFrontstage, "showSettingsStage")!; - Object.defineProperty(SettingsModalFrontstage, "showSettingsStage", { - get: () => handleOpenSetting, + it("translate should return the key (in test environment)", async () => { + await TestUtils.initializeUiFramework(true); + expect(UiFramework.translate("test1.test2")).to.eq("test1.test2"); }); - const tool = new OpenSettingsTool(); - // tabid arg - tool.parseAndRun(tabName); - spy.calledOnce.should.true; - spy.resetHistory(); - // No tabid arg - Object.defineProperty(SettingsModalFrontstage, "showSettingsStage", { - get: () => handleOpenSetting2, + it("test OpenSettingsTool", async () => { + await TestUtils.initializeUiFramework(true); + + const spy = sinon.spy(); + const tabName = "page1"; + const handleOpenSetting = (settingsCategory: string)=> { + expect (settingsCategory).to.eql(tabName); + spy(); + }; + + const handleOpenSetting2 = (_settingsCategory: string)=> { + spy(); + }; + + const showSettingsStageToRestore = Object.getOwnPropertyDescriptor(SettingsModalFrontstage, "showSettingsStage")!; + Object.defineProperty(SettingsModalFrontstage, "showSettingsStage", { + get: () => handleOpenSetting, + }); + const tool = new OpenSettingsTool(); + // tabid arg + tool.parseAndRun(tabName); + spy.calledOnce.should.true; + spy.resetHistory(); + + // No tabid arg + Object.defineProperty(SettingsModalFrontstage, "showSettingsStage", { + get: () => handleOpenSetting2, + }); + tool.parseAndRun(); + spy.calledOnce.should.true; + spy.resetHistory(); + + Object.defineProperty(SettingsModalFrontstage, "showSettingsStage", showSettingsStageToRestore); }); - tool.parseAndRun(); - spy.calledOnce.should.true; - spy.resetHistory(); - Object.defineProperty(SettingsModalFrontstage, "showSettingsStage", showSettingsStageToRestore); - }); + it("loggerCategory should correctly handle null or undefined object", () => { + expect(UiFramework.loggerCategory(null)).to.eq(UiFramework.packageName); + expect(UiFramework.loggerCategory(undefined)).to.eq(UiFramework.packageName); + }); - it("loggerCategory should correctly handle null or undefined object", () => { - expect(UiFramework.loggerCategory(null)).to.eq(UiFramework.packageName); - expect(UiFramework.loggerCategory(undefined)).to.eq(UiFramework.packageName); - }); + it("calling initialize twice should log", async () => { + const spyLogger = sinon.spy(Logger, "logInfo"); + expect(UiFramework.initialized).to.be.false; + await UiFramework.initialize(TestUtils.store, TestUtils.i18n); + expect(UiFramework.initialized).to.be.true; + await UiFramework.initialize(TestUtils.store, TestUtils.i18n); + spyLogger.calledOnce.should.true; + }); - it("calling initialize twice should log", async () => { - const spyLogger = sinon.spy(Logger, "logInfo"); - expect(UiFramework.initialized).to.be.false; - await UiFramework.initialize(TestUtils.store, TestUtils.i18n); - expect(UiFramework.initialized).to.be.true; - await UiFramework.initialize(TestUtils.store, TestUtils.i18n); - spyLogger.calledOnce.should.true; - }); + it("calling initialize without I18N will use IModelApp.i18n", async () => { + await MockRender.App.startup(); - it("calling initialize without I18N will use IModelApp.i18n", async () => { - await MockRender.App.startup(); + await UiFramework.initialize(TestUtils.store); + expect(UiFramework.i18n).to.eq(IModelApp.i18n); - await UiFramework.initialize(TestUtils.store); - expect(UiFramework.i18n).to.eq(IModelApp.i18n); + await MockRender.App.shutdown(); + }); - await MockRender.App.shutdown(); - }); + it("projectServices should throw Error without initialize", () => { + expect(() => UiFramework.projectServices).to.throw(Error); + }); - it("projectServices should throw Error without initialize", () => { - expect(() => UiFramework.projectServices).to.throw(Error); - }); + it("iModelServices should throw Error without initialize", () => { + expect(() => UiFramework.iModelServices).to.throw(Error); + }); - it("iModelServices should throw Error without initialize", () => { - expect(() => UiFramework.iModelServices).to.throw(Error); - }); + it("projectServices & iModelServices should return defaults", async () => { + await TestUtils.initializeUiFramework(true); + expect(UiFramework.projectServices).to.be.instanceOf(DefaultProjectServices); + expect(UiFramework.iModelServices).to.be.instanceOf(DefaultIModelServices); + expect(UiFramework.frameworkStateKey).to.equal("testDifferentFrameworkKey"); + }); - it("projectServices & iModelServices should return defaults", async () => { - await TestUtils.initializeUiFramework(true); - expect(UiFramework.projectServices).to.be.instanceOf(DefaultProjectServices); - expect(UiFramework.iModelServices).to.be.instanceOf(DefaultIModelServices); - expect(UiFramework.frameworkStateKey).to.equal("testDifferentFrameworkKey"); - }); + it("test default frameworkState key", async () => { + await TestUtils.initializeUiFramework(); + expect(UiFramework.projectServices).to.be.instanceOf(DefaultProjectServices); + expect(UiFramework.iModelServices).to.be.instanceOf(DefaultIModelServices); + expect(UiFramework.frameworkStateKey).to.equal("frameworkState"); + TestUtils.terminateUiFramework(); + }); - it("test default frameworkState key", async () => { - await TestUtils.initializeUiFramework(); - expect(UiFramework.projectServices).to.be.instanceOf(DefaultProjectServices); - expect(UiFramework.iModelServices).to.be.instanceOf(DefaultIModelServices); - expect(UiFramework.frameworkStateKey).to.equal("frameworkState"); - TestUtils.terminateUiFramework(); - }); + it("IsUiVisible", async () => { + await TestUtils.initializeUiFramework(); + UiFramework.setIsUiVisible(false); + expect(UiFramework.getIsUiVisible()).to.be.false; + TestUtils.terminateUiFramework(); + }); - it("IsUiVisible", async () => { - await TestUtils.initializeUiFramework(); - UiFramework.setIsUiVisible(false); - expect(UiFramework.getIsUiVisible()).to.be.false; - TestUtils.terminateUiFramework(); - }); + it("ColorTheme", async () => { + await TestUtils.initializeUiFramework(); + UiFramework.setColorTheme(ColorTheme.Dark); + expect(UiFramework.getColorTheme()).to.eq(ColorTheme.Dark); + TestUtils.terminateUiFramework(); + }); - it("ColorTheme", async () => { - await TestUtils.initializeUiFramework(); - UiFramework.setColorTheme(ColorTheme.Dark); - expect(UiFramework.getColorTheme()).to.eq(ColorTheme.Dark); - TestUtils.terminateUiFramework(); - }); + it("test selection scope state data", async () => { + await TestUtils.initializeUiFramework(); + expect(UiFramework.getActiveSelectionScope()).to.equal("element"); + const scopes = UiFramework.getAvailableSelectionScopes(); + expect(scopes.length).to.be.greaterThan(0); - it("test selection scope state data", async () => { - await TestUtils.initializeUiFramework(); - expect(UiFramework.getActiveSelectionScope()).to.equal("element"); - const scopes = UiFramework.getAvailableSelectionScopes(); - expect(scopes.length).to.be.greaterThan(0); + // since "file" is not a valid scope the active scope should still be element + UiFramework.setActiveSelectionScope("file"); + expect(UiFramework.getActiveSelectionScope()).to.equal("element"); + TestUtils.terminateUiFramework(); + }); - // since "file" is not a valid scope the active scope should still be element - UiFramework.setActiveSelectionScope("file"); - expect(UiFramework.getActiveSelectionScope()).to.equal("element"); - TestUtils.terminateUiFramework(); - }); + it("WidgetOpacity", async () => { + await TestUtils.initializeUiFramework(); + const testValue = 0.50; + UiFramework.setWidgetOpacity(testValue); + expect(UiFramework.getWidgetOpacity()).to.eq(testValue); + TestUtils.terminateUiFramework(); + }); - it("WidgetOpacity", async () => { - await TestUtils.initializeUiFramework(); - const testValue = 0.50; - UiFramework.setWidgetOpacity(testValue); - expect(UiFramework.getWidgetOpacity()).to.eq(testValue); - TestUtils.terminateUiFramework(); - }); + it("ActiveIModelId", async () => { + await TestUtils.initializeUiFramework(); + const testValue = "Test"; + UiFramework.setActiveIModelId(testValue); + expect(UiFramework.getActiveIModelId()).to.eq(testValue); + TestUtils.terminateUiFramework(); + }); - it("ActiveIModelId", async () => { - await TestUtils.initializeUiFramework(); - const testValue = "Test"; - UiFramework.setActiveIModelId(testValue); - expect(UiFramework.getActiveIModelId()).to.eq(testValue); - TestUtils.terminateUiFramework(); - }); + class testSettingsProvider implements UserSettingsProvider { + public readonly providerId = "testSettingsProvider"; + public settingsLoaded = false; + public async loadUserSettings(storage: UiSettingsStorage) { + if (storage) + this.settingsLoaded = true; + } + } - it("SessionState setters/getters", async () => { - await TestUtils.initializeUiFramework(); + it("SessionState setters/getters", async () => { + await TestUtils.initializeUiFramework(); + const settingsProvider = new testSettingsProvider(); + UiFramework.registerUserSettingsProvider(settingsProvider); - const userInfo = mockUserInfo(); + const userInfo = mockUserInfo(); - UiFramework.setUserInfo(userInfo); - expect(UiFramework.getUserInfo()!.id).to.eq(userInfo.id); + UiFramework.setUserInfo(userInfo); + expect(UiFramework.getUserInfo()!.id).to.eq(userInfo.id); - UiFramework.setDefaultIModelViewportControlId("DefaultIModelViewportControlId"); - expect(UiFramework.getDefaultIModelViewportControlId()).to.eq("DefaultIModelViewportControlId"); + UiFramework.setDefaultIModelViewportControlId("DefaultIModelViewportControlId"); + expect(UiFramework.getDefaultIModelViewportControlId()).to.eq("DefaultIModelViewportControlId"); - const testViewId: Id64String = "0x12345678"; - UiFramework.setDefaultViewId(testViewId); - expect(UiFramework.getDefaultViewId()).to.eq(testViewId); + const testViewId: Id64String = "0x12345678"; + UiFramework.setDefaultViewId(testViewId); + expect(UiFramework.getDefaultViewId()).to.eq(testViewId); - const imodelMock = moq.Mock.ofType(); - UiFramework.setIModelConnection(imodelMock.object); - expect(UiFramework.getIModelConnection()).to.eq(imodelMock.object); + const imodelMock = moq.Mock.ofType(); + UiFramework.setIModelConnection(imodelMock.object); + expect(UiFramework.getIModelConnection()).to.eq(imodelMock.object); - const uisettingsMock = moq.Mock.ofType(); - UiFramework.setUiSettings(uisettingsMock.object); - expect(UiFramework.getUiSettings()).to.eq(uisettingsMock.object); + expect(settingsProvider.settingsLoaded).to.be.false; - UiFramework.closeCursorMenu(); - expect(UiFramework.getCursorMenuData()).to.be.undefined; + const uisettings = new LocalSettingsStorage(); + await UiFramework.setUiSettingsStorage(uisettings); + expect(UiFramework.getUiSettingsStorage()).to.eq(uisettings); + expect(settingsProvider.settingsLoaded).to.be.true; + settingsProvider.settingsLoaded = false; + // if we try to set storage to same object this should be a noop and the settingsLoaded property should remain false; + await UiFramework.setUiSettingsStorage(uisettings); + expect(settingsProvider.settingsLoaded).to.be.false; - const menuData: CursorMenuData = { items: [], position: { x: 100, y: 100 } }; - UiFramework.openCursorMenu(menuData); - expect(UiFramework.getCursorMenuData()).not.to.be.undefined; + const uiVersion1 = "1"; + UiFramework.setUiVersion (uiVersion1); + expect(UiFramework.uiVersion).to.eql(uiVersion1); - const viewState = moq.Mock.ofType(); - UiFramework.setDefaultViewState(viewState.object); - expect(UiFramework.getDefaultViewState()).not.to.be.undefined; - }); + const uiVersion = "2"; + UiFramework.setUiVersion (uiVersion); + expect(UiFramework.uiVersion).to.eql(uiVersion); + UiFramework.setUiVersion (""); + expect(UiFramework.uiVersion).to.eql(uiVersion); -}); + const useDragInteraction = true; + UiFramework.setUseDragInteraction (useDragInteraction); + expect(UiFramework.useDragInteraction).to.eql(useDragInteraction); -// before we can test setting scope to a valid scope id we must make sure Presentation Manager is initialized. -describe("Requires Presentation", () => { - const shutdownIModelApp = async () => { - if (IModelApp.initialized) - await IModelApp.shutdown(); - }; - - beforeEach(async () => { - await shutdownIModelApp(); - Presentation.terminate(); - await initializePresentationTesting(); - }); + UiFramework.closeCursorMenu(); + expect(UiFramework.getCursorMenuData()).to.be.undefined; - afterEach(async () => { - await terminatePresentationTesting(); - }); + const menuData: CursorMenuData = { items: [], position: { x: 100, y: 100 } }; + UiFramework.openCursorMenu(menuData); + expect(UiFramework.getCursorMenuData()).not.to.be.undefined; - describe("initialize and setActiveSelectionScope", () => { + const viewState = moq.Mock.ofType(); + UiFramework.setDefaultViewState(viewState.object); + expect(UiFramework.getDefaultViewState()).not.to.be.undefined; - it("creates manager instances", async () => { - await TestUtils.initializeUiFramework(); - UiFramework.setActiveSelectionScope("element"); TestUtils.terminateUiFramework(); - Presentation.terminate(); - await shutdownIModelApp(); + // try again when store is not defined + expect(UiFramework.useDragInteraction).to.eql(false); }); + }); + // before we can test setting scope to a valid scope id we must make sure Presentation Manager is initialized. + describe("Requires Presentation", () => { + const shutdownIModelApp = async () => { + if (IModelApp.initialized) + await IModelApp.shutdown(); + }; + + beforeEach(async () => { + await shutdownIModelApp(); + Presentation.terminate(); + await initializePresentationTesting(); + }); + + afterEach(async () => { + await terminatePresentationTesting(); + }); + + describe("initialize and setActiveSelectionScope", () => { + + it("creates manager instances", async () => { + await TestUtils.initializeUiFramework(); + UiFramework.setActiveSelectionScope("element"); + TestUtils.terminateUiFramework(); + + Presentation.terminate(); + await shutdownIModelApp(); + }); + }); + + }); }); diff --git a/ui/framework/src/test/accudraw/FrameworkAccuDraw.test.ts b/ui/framework/src/test/accudraw/FrameworkAccuDraw.test.ts index a730cc199f33..cf75225fa5af 100644 --- a/ui/framework/src/test/accudraw/FrameworkAccuDraw.test.ts +++ b/ui/framework/src/test/accudraw/FrameworkAccuDraw.test.ts @@ -5,179 +5,209 @@ import * as sinon from "sinon"; import { expect } from "chai"; import { BeButtonEvent, CompassMode, CurrentState, IModelApp, IModelAppOptions, ItemField, MockRender, RotationMode } from "@bentley/imodeljs-frontend"; -import TestUtils from "../TestUtils"; +import TestUtils, { storageMock } from "../TestUtils"; import { FrameworkAccuDraw } from "../../ui-framework/accudraw/FrameworkAccuDraw"; import { AccuDrawUiAdmin, ConditionalBooleanValue } from "@bentley/ui-abstract"; import { FrameworkUiAdmin } from "../../ui-framework/uiadmin/FrameworkUiAdmin"; +import { UiFramework } from "../../ui-framework/UiFramework"; // cspell:ignore dont uiadmin -describe("FrameworkAccuDraw", () => { - before(async () => { - await TestUtils.initializeUiFramework(); - - const opts: IModelAppOptions = {}; - opts.accuDraw = new FrameworkAccuDraw(); - opts.uiAdmin = new FrameworkUiAdmin(); - await MockRender.App.startup(opts); - }); - - after(async () => { - await MockRender.App.shutdown(); - TestUtils.terminateUiFramework(); - }); - - it("FrameworkAccuDraw.displayNotifications should set & return correctly", () => { - FrameworkAccuDraw.displayNotifications = false; - expect(FrameworkAccuDraw.displayNotifications).to.be.false; - FrameworkAccuDraw.displayNotifications = true; - expect(FrameworkAccuDraw.displayNotifications).to.be.true; - }); - - it("should call onCompassModeChange & emit onAccuDrawSetModeEvent & set conditionals", () => { - FrameworkAccuDraw.displayNotifications = true; - const spy = sinon.spy(); - const spyMessage = sinon.spy(IModelApp.notifications, "outputMessage"); - const remove = AccuDrawUiAdmin.onAccuDrawSetModeEvent.addListener(spy); - - IModelApp.accuDraw.setCompassMode(CompassMode.Polar); - FrameworkAccuDraw.isPolarModeConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isPolarModeConditional)).to.be.true; - spy.calledOnce.should.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - - IModelApp.accuDraw.setCompassMode(CompassMode.Rectangular); - FrameworkAccuDraw.isRectangularModeConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isRectangularModeConditional)).to.be.true; - spy.calledTwice.should.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - - FrameworkAccuDraw.displayNotifications = false; - IModelApp.accuDraw.setCompassMode(CompassMode.Polar); - spyMessage.called.should.false; - spyMessage.resetHistory(); - - remove(); - }); +describe("FrameworkAccuDraw localStorage Wrapper", () => { - it("should call onFieldLockChange & emit onAccuDrawSetFieldLockEvent", () => { - const spy = sinon.spy(); - const remove = AccuDrawUiAdmin.onAccuDrawSetFieldLockEvent.addListener(spy); - IModelApp.accuDraw.setFieldLock(ItemField.X_Item, true); - spy.calledOnce.should.true; - spy.resetHistory(); - IModelApp.accuDraw.setFieldLock(ItemField.Y_Item, true); - spy.calledOnce.should.true; - spy.resetHistory(); - IModelApp.accuDraw.setFieldLock(ItemField.Z_Item, true); - spy.calledOnce.should.true; - spy.resetHistory(); - IModelApp.accuDraw.setFieldLock(ItemField.ANGLE_Item, true); - spy.calledOnce.should.true; - spy.resetHistory(); - IModelApp.accuDraw.setFieldLock(ItemField.DIST_Item, true); - spy.calledOnce.should.true; - spy.resetHistory(); - remove(); - }); - - it("should set rotation & conditionals correctly & notify", () => { - FrameworkAccuDraw.displayNotifications = true; - const spyMessage = sinon.spy(IModelApp.notifications, "outputMessage"); - - IModelApp.accuDraw.setRotationMode(RotationMode.Top); - FrameworkAccuDraw.isTopRotationConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isTopRotationConditional)).to.be.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - IModelApp.accuDraw.setRotationMode(RotationMode.Front); - FrameworkAccuDraw.isFrontRotationConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isFrontRotationConditional)).to.be.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - IModelApp.accuDraw.setRotationMode(RotationMode.Side); - FrameworkAccuDraw.isSideRotationConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isSideRotationConditional)).to.be.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - IModelApp.accuDraw.setRotationMode(RotationMode.View); - FrameworkAccuDraw.isViewRotationConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isViewRotationConditional)).to.be.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - IModelApp.accuDraw.setRotationMode(RotationMode.ACS); - FrameworkAccuDraw.isACSRotationConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isACSRotationConditional)).to.be.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - IModelApp.accuDraw.setRotationMode(RotationMode.Context); - FrameworkAccuDraw.isContextRotationConditional.refresh(); - expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isContextRotationConditional)).to.be.true; - spyMessage.calledOnce.should.true; - spyMessage.resetHistory(); - - FrameworkAccuDraw.displayNotifications = false; - IModelApp.accuDraw.setRotationMode(RotationMode.Top); - spyMessage.calledOnce.should.false; - spyMessage.resetHistory(); - }); - - it("should call onFieldValueChange & emit onAccuDrawSetFieldValueToUiEvent", () => { - const spy = sinon.spy(); - const remove = AccuDrawUiAdmin.onAccuDrawSetFieldValueToUiEvent.addListener(spy); - IModelApp.accuDraw.setValueByIndex(ItemField.X_Item, 1.0); - IModelApp.accuDraw.onFieldValueChange(ItemField.X_Item); - spy.calledOnce.should.true; - remove(); - }); + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + const localStorageMock = storageMock(); - it("should emit onAccuDrawSetFieldFocusEvent", () => { - const spy = sinon.spy(); - const remove = AccuDrawUiAdmin.onAccuDrawSetFieldFocusEvent.addListener(spy); - IModelApp.accuDraw.setFocusItem(ItemField.X_Item); - spy.calledOnce.should.true; - remove(); - }); - - it("should emit onAccuDrawGrabInputFocusEvent", () => { - const spy = sinon.spy(); - const remove = AccuDrawUiAdmin.onAccuDrawGrabInputFocusEvent.addListener(spy); - IModelApp.accuDraw.grabInputFocus(); - spy.calledOnce.should.true; - remove(); + before(async () => { + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); }); - it("hasInputFocus should return false", () => { - expect(IModelApp.accuDraw.hasInputFocus).to.be.false; + after(() => { + Object.defineProperty(window, "localStorage", localStorageToRestore); }); - it("should emit onAccuDrawSetFieldValueToUiEvent & onAccuDrawSetFieldFocusEvent", () => { - const spyValue = sinon.spy(); - const remove = AccuDrawUiAdmin.onAccuDrawSetFieldValueToUiEvent.addListener(spyValue); - const spyFocus = sinon.spy(); - const removeFocusSpy = AccuDrawUiAdmin.onAccuDrawSetFieldFocusEvent.addListener(spyFocus); - - IModelApp.accuDraw.currentState = CurrentState.Deactivated; - IModelApp.accuDraw.onMotion(new BeButtonEvent()); - spyValue.called.should.false; - spyValue.resetHistory(); - - IModelApp.accuDraw.currentState = CurrentState.Active; - IModelApp.accuDraw.onMotion(new BeButtonEvent()); - spyValue.called.should.true; - spyFocus.called.should.true; - spyValue.resetHistory(); - spyFocus.resetHistory(); - - IModelApp.accuDraw.dontMoveFocus = true; - IModelApp.accuDraw.onMotion(new BeButtonEvent()); - spyValue.called.should.true; - spyFocus.called.should.false; - - remove(); - removeFocusSpy(); + describe("FrameworkAccuDraw", () => { + before(async () => { + await TestUtils.initializeUiFramework(); + + const opts: IModelAppOptions = {}; + opts.accuDraw = new FrameworkAccuDraw(); + opts.uiAdmin = new FrameworkUiAdmin(); + await MockRender.App.startup(opts); + }); + + after(async () => { + await MockRender.App.shutdown(); + TestUtils.terminateUiFramework(); + }); + + it("FrameworkAccuDraw.displayNotifications should set & return correctly", () => { + FrameworkAccuDraw.displayNotifications = false; + expect(FrameworkAccuDraw.displayNotifications).to.be.false; + FrameworkAccuDraw.displayNotifications = true; + expect(FrameworkAccuDraw.displayNotifications).to.be.true; + }); + + it("should call onCompassModeChange & emit onAccuDrawSetModeEvent & set conditionals", () => { + FrameworkAccuDraw.displayNotifications = true; + const spy = sinon.spy(); + const spyMessage = sinon.spy(IModelApp.notifications, "outputMessage"); + const remove = AccuDrawUiAdmin.onAccuDrawSetModeEvent.addListener(spy); + + IModelApp.accuDraw.setCompassMode(CompassMode.Polar); + FrameworkAccuDraw.isPolarModeConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isPolarModeConditional)).to.be.true; + spy.calledOnce.should.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + + IModelApp.accuDraw.setCompassMode(CompassMode.Rectangular); + FrameworkAccuDraw.isRectangularModeConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isRectangularModeConditional)).to.be.true; + spy.calledTwice.should.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + + FrameworkAccuDraw.displayNotifications = false; + IModelApp.accuDraw.setCompassMode(CompassMode.Polar); + spyMessage.called.should.false; + spyMessage.resetHistory(); + + remove(); + }); + + it("should call onFieldLockChange & emit onAccuDrawSetFieldLockEvent", () => { + const spy = sinon.spy(); + const remove = AccuDrawUiAdmin.onAccuDrawSetFieldLockEvent.addListener(spy); + IModelApp.accuDraw.setFieldLock(ItemField.X_Item, true); + spy.calledOnce.should.true; + spy.resetHistory(); + IModelApp.accuDraw.setFieldLock(ItemField.Y_Item, true); + spy.calledOnce.should.true; + spy.resetHistory(); + IModelApp.accuDraw.setFieldLock(ItemField.Z_Item, true); + spy.calledOnce.should.true; + spy.resetHistory(); + IModelApp.accuDraw.setFieldLock(ItemField.ANGLE_Item, true); + spy.calledOnce.should.true; + spy.resetHistory(); + IModelApp.accuDraw.setFieldLock(ItemField.DIST_Item, true); + spy.calledOnce.should.true; + spy.resetHistory(); + remove(); + }); + + it("should set rotation & conditionals correctly & notify", () => { + FrameworkAccuDraw.displayNotifications = true; + const spyMessage = sinon.spy(IModelApp.notifications, "outputMessage"); + + IModelApp.accuDraw.setRotationMode(RotationMode.Top); + FrameworkAccuDraw.isTopRotationConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isTopRotationConditional)).to.be.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + IModelApp.accuDraw.setRotationMode(RotationMode.Front); + FrameworkAccuDraw.isFrontRotationConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isFrontRotationConditional)).to.be.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + IModelApp.accuDraw.setRotationMode(RotationMode.Side); + FrameworkAccuDraw.isSideRotationConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isSideRotationConditional)).to.be.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + IModelApp.accuDraw.setRotationMode(RotationMode.View); + FrameworkAccuDraw.isViewRotationConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isViewRotationConditional)).to.be.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + IModelApp.accuDraw.setRotationMode(RotationMode.ACS); + FrameworkAccuDraw.isACSRotationConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isACSRotationConditional)).to.be.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + IModelApp.accuDraw.setRotationMode(RotationMode.Context); + FrameworkAccuDraw.isContextRotationConditional.refresh(); + expect(ConditionalBooleanValue.getValue(FrameworkAccuDraw.isContextRotationConditional)).to.be.true; + spyMessage.calledOnce.should.true; + spyMessage.resetHistory(); + + FrameworkAccuDraw.displayNotifications = false; + IModelApp.accuDraw.setRotationMode(RotationMode.Top); + spyMessage.calledOnce.should.false; + spyMessage.resetHistory(); + }); + + it("should call onFieldValueChange & emit onAccuDrawSetFieldValueToUiEvent", () => { + const spy = sinon.spy(); + const remove = AccuDrawUiAdmin.onAccuDrawSetFieldValueToUiEvent.addListener(spy); + IModelApp.accuDraw.setValueByIndex(ItemField.X_Item, 1.0); + IModelApp.accuDraw.onFieldValueChange(ItemField.X_Item); + spy.calledOnce.should.true; + remove(); + }); + + it("should emit onAccuDrawSetFieldFocusEvent", () => { + const spy = sinon.spy(); + const remove = AccuDrawUiAdmin.onAccuDrawSetFieldFocusEvent.addListener(spy); + IModelApp.accuDraw.setFocusItem(ItemField.X_Item); + spy.calledOnce.should.true; + remove(); + }); + + it("should emit onAccuDrawGrabInputFocusEvent", () => { + const spy = sinon.spy(); + const remove = AccuDrawUiAdmin.onAccuDrawGrabInputFocusEvent.addListener(spy); + IModelApp.accuDraw.grabInputFocus(); + spy.calledOnce.should.true; + remove(); + }); + + it("hasInputFocus should return false", () => { + expect(IModelApp.accuDraw.hasInputFocus).to.be.false; + }); + + it("should emit onAccuDrawSetFieldValueToUiEvent & onAccuDrawSetFieldFocusEvent", () => { + const spyValue = sinon.spy(); + const remove = AccuDrawUiAdmin.onAccuDrawSetFieldValueToUiEvent.addListener(spyValue); + const spyFocus = sinon.spy(); + const removeFocusSpy = AccuDrawUiAdmin.onAccuDrawSetFieldFocusEvent.addListener(spyFocus); + + IModelApp.accuDraw.currentState = CurrentState.Deactivated; + IModelApp.accuDraw.onMotion(new BeButtonEvent()); + spyValue.called.should.false; + spyValue.resetHistory(); + + IModelApp.accuDraw.currentState = CurrentState.Active; + IModelApp.accuDraw.onMotion(new BeButtonEvent()); + spyValue.called.should.true; + spyFocus.called.should.true; + spyValue.resetHistory(); + spyFocus.resetHistory(); + + IModelApp.accuDraw.dontMoveFocus = true; + IModelApp.accuDraw.onMotion(new BeButtonEvent()); + spyValue.called.should.true; + spyFocus.called.should.false; + + remove(); + removeFocusSpy(); + }); + + it("should save/retrieve displayNotifications to/from user storage", async () => { + FrameworkAccuDraw.displayNotifications = true; + await TestUtils.flushAsyncOperations(); + expect(FrameworkAccuDraw.displayNotifications).to.be.true; + FrameworkAccuDraw.displayNotifications = false; + await TestUtils.flushAsyncOperations(); + expect(FrameworkAccuDraw.displayNotifications).to.be.false; + + const instance = new FrameworkAccuDraw(); + await instance.loadUserSettings (UiFramework.getUiSettingsStorage()); + await TestUtils.flushAsyncOperations(); + expect(FrameworkAccuDraw.displayNotifications).to.be.false; + }); }); - }); diff --git a/ui/framework/src/test/frontstage/ModalSettingsStage.test.tsx b/ui/framework/src/test/frontstage/ModalSettingsStage.test.tsx index 21629aceb64f..f9bdb0267d3f 100644 --- a/ui/framework/src/test/frontstage/ModalSettingsStage.test.tsx +++ b/ui/framework/src/test/frontstage/ModalSettingsStage.test.tsx @@ -9,7 +9,7 @@ import { render } from "@testing-library/react"; import { CoreTools, FrontstageDef, FrontstageManager, FrontstageProps, ModalFrontstage, ModalFrontstageInfo, SettingsModalFrontstage } from "../../ui-framework"; import TestUtils from "../TestUtils"; import { UiFramework } from "../../ui-framework/UiFramework"; -import { SettingsManager, SettingsProvider, SettingsTabEntry, useSaveBeforeActivatingNewSettingsTab, useSaveBeforeClosingSettingsContainer } from "@bentley/ui-core"; +import { SettingsManager, SettingsTabEntry, SettingsTabsProvider, useSaveBeforeActivatingNewSettingsTab, useSaveBeforeClosingSettingsContainer } from "@bentley/ui-core"; import { IModelApp, MockRender } from "@bentley/imodeljs-frontend"; import { ConditionalBooleanValue } from "@bentley/ui-abstract"; @@ -88,7 +88,7 @@ describe("ModalSettingsStage", () => { expect (ConditionalBooleanValue.getValue(backstageActionItem.isHidden)).to.be.true; }); - class TestSettingsProvider implements SettingsProvider { + class TestSettingsProvider implements SettingsTabsProvider { public readonly id = "AppSettingsProvider"; public getSettingEntries(_stageId: string, _stageUsage: string): ReadonlyArray | undefined { diff --git a/ui/framework/src/test/popup/KeyinPalettePanel.test.tsx b/ui/framework/src/test/popup/KeyinPalettePanel.test.tsx index 2318a6179c4a..8116defafa7b 100644 --- a/ui/framework/src/test/popup/KeyinPalettePanel.test.tsx +++ b/ui/framework/src/test/popup/KeyinPalettePanel.test.tsx @@ -51,13 +51,13 @@ describe("", () => { }); it("test clearKeyinPaletteHistory", async () => { - const uiSettings = UiFramework.getUiSettings(); - if (uiSettings) { - await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["keyin1", "keyin2"]); - let settingsResult = await uiSettings.getSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY); + const uiSettingsStorage = UiFramework.getUiSettingsStorage(); + if (uiSettingsStorage) { + await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["keyin1", "keyin2"]); + let settingsResult = await uiSettingsStorage.getSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY); expect(UiSettingsStatus.Success === settingsResult.status); clearKeyinPaletteHistory(); - settingsResult = await uiSettings.getSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY); + settingsResult = await uiSettingsStorage.getSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY); expect(UiSettingsStatus.NotFound === settingsResult.status); } }); @@ -197,9 +197,9 @@ describe("", () => { }); it("Renders and filters out bogus history entry", async () => { - const uiSettings = UiFramework.getUiSettings(); - if (uiSettings) { - await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); + const uiSettingsStorage = UiFramework.getUiSettingsStorage(); + if (uiSettingsStorage) { + await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); } const keyins: KeyinEntry[] = [{ value: "keyin one" }, { value: "keyin two" }]; const renderedComponent = render(); @@ -212,9 +212,9 @@ describe("", () => { }); it("handles key presses in select input ", async () => { - const uiSettings = UiFramework.getUiSettings(); - if (uiSettings) { - await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); + const uiSettingsStorage = UiFramework.getUiSettingsStorage(); + if (uiSettingsStorage) { + await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); } const keyins: KeyinEntry[] = [{ value: "keyin one" }, { value: "keyin two" }]; const renderedComponent = render(); @@ -233,9 +233,9 @@ describe("", () => { }); it("handles ctrl+key presses in select input ", async () => { - const uiSettings = UiFramework.getUiSettings(); - if (uiSettings) { - await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); + const uiSettingsStorage = UiFramework.getUiSettingsStorage(); + if (uiSettingsStorage) { + await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); } const keyins: KeyinEntry[] = [{ value: "keyin one" }, { value: "keyin two" }]; const renderedComponent = render(); @@ -272,9 +272,9 @@ describe("", () => { }); it("Handles listbox click processing", async () => { - const uiSettings = UiFramework.getUiSettings(); - if (uiSettings) { - await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); + const uiSettingsStorage = UiFramework.getUiSettingsStorage(); + if (uiSettingsStorage) { + await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); } const keyins: KeyinEntry[] = [{ value: "keyin one" }, { value: "keyin two" }]; const renderedComponent = render(); @@ -292,9 +292,9 @@ describe("", () => { }); it("Handles listbox CTRL+click processing", async () => { - const uiSettings = UiFramework.getUiSettings(); - if (uiSettings) { - await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); + const uiSettingsStorage = UiFramework.getUiSettingsStorage(); + if (uiSettingsStorage) { + await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, ["history1", "history2", "bogus"]); } const keyins: KeyinEntry[] = [{ value: "keyin one" }, { value: "keyin two" }]; const renderedComponent = render(); diff --git a/ui/framework/src/test/redux/StateManager.test.ts b/ui/framework/src/test/redux/StateManager.test.ts index a72958a66f18..80f236db4efb 100644 --- a/ui/framework/src/test/redux/StateManager.test.ts +++ b/ui/framework/src/test/redux/StateManager.test.ts @@ -5,8 +5,10 @@ import { expect } from "chai"; import { UiError } from "@bentley/ui-abstract"; -import { ActionCreatorsObject, ActionsUnion, createAction, FrameworkReducer, ReducerRegistryInstance } from "../../ui-framework"; +import { ActionCreatorsObject, ActionsUnion, createAction, FrameworkReducer, ReducerRegistryInstance, SYSTEM_PREFERRED_COLOR_THEME, WIDGET_OPACITY_DEFAULT } from "../../ui-framework"; import { StateManager } from "../../ui-framework/redux/StateManager"; +import { ConfigurableUiActions, ConfigurableUiReducer, ConfigurableUiState } from "../../ui-framework/configurableui/state"; +import { SnapMode } from "@bentley/imodeljs-frontend"; // Fake state for the host app interface IAppState { @@ -179,3 +181,35 @@ describe("StateManager", () => { }); }); + +describe("ConfigurableUiReducer", () => { + it("should process actions", () => { + // exercise the ConfigurableUiActions + const initialState: ConfigurableUiState = { + snapMode: SnapMode.NearestKeypoint as number, + toolPrompt: "", + theme: SYSTEM_PREFERRED_COLOR_THEME, + widgetOpacity: WIDGET_OPACITY_DEFAULT, + useDragInteraction: false, + frameworkVersion: "2", + }; + + let outState = ConfigurableUiReducer(initialState, ConfigurableUiActions.setDragInteraction(true)); + expect(outState.useDragInteraction).to.be.true; + + outState = ConfigurableUiReducer(initialState, ConfigurableUiActions.setToolPrompt("Hello-From-Tool")); + expect(outState.toolPrompt).to.be.eql("Hello-From-Tool"); + + outState = ConfigurableUiReducer(initialState, ConfigurableUiActions.setTheme("dark")); + expect(outState.theme).to.be.eql("dark"); + + outState = ConfigurableUiReducer(initialState, ConfigurableUiActions.setWidgetOpacity(.75)); + expect(outState.widgetOpacity).to.be.eql(.75); + + outState = ConfigurableUiReducer(initialState, ConfigurableUiActions.setSnapMode(SnapMode.Center)); + expect(outState.snapMode).to.be.eql(SnapMode.Center); + + outState = ConfigurableUiReducer(initialState, ConfigurableUiActions.setFrameworkVersion("1")); + expect(outState.frameworkVersion).to.be.eql("1"); + }); +}); diff --git a/ui/framework/src/test/settings/QuantityFormat.test.tsx b/ui/framework/src/test/settings/QuantityFormat.test.tsx index 38f57cfb2c01..c8397d318630 100644 --- a/ui/framework/src/test/settings/QuantityFormat.test.tsx +++ b/ui/framework/src/test/settings/QuantityFormat.test.tsx @@ -19,7 +19,7 @@ import { ModalDialogRenderer } from "../../ui-framework/dialog/ModalDialogManage import { FormatProps } from "@bentley/imodeljs-quantity"; import { UiFramework } from "../../ui-framework/UiFramework"; -describe("QuantityFormatSettingsPanel", () => { +describe("QuantityFormatSettingsPage", () => { let presentationManagerMock: moq.IMock; const sandbox = sinon.createSandbox(); diff --git a/ui/framework/src/test/settings/UiSettingsPage.test.tsx b/ui/framework/src/test/settings/UiSettingsPage.test.tsx new file mode 100644 index 000000000000..3eccdeafa985 --- /dev/null +++ b/ui/framework/src/test/settings/UiSettingsPage.test.tsx @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +import { expect } from "chai"; +import * as React from "react"; +import { fireEvent, render } from "@testing-library/react"; +import { getUiSettingsManagerEntry, UiSettingsPage } from "../../ui-framework/settings/ui/UiSettingsPage"; +import TestUtils, { storageMock } from "../TestUtils"; +import { UiFramework } from "../../ui-framework/UiFramework"; + +describe("UiSettingsPage", () => { + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + let localStorageMock = storageMock(); + + beforeEach(async () => { + // create a new mock each run so there are no "stored values" + localStorageMock = storageMock(); + await TestUtils.initializeUiFramework(); + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); + }); + + afterEach(() => { + TestUtils.terminateUiFramework(); + Object.defineProperty(window, "localStorage", localStorageToRestore); + }); + + function getInputBySpanTitle(titleSpan: HTMLElement) { + const settingsItemDiv = titleSpan.parentElement?.parentElement; + expect(settingsItemDiv).not.to.be.undefined; + return settingsItemDiv!.querySelector("input"); + } + + it("renders using getUiSettingsManagerEntry (V1)", async () => { + const tabEntry = getUiSettingsManagerEntry (10, false); + const wrapper = render(tabEntry.page); + expect(wrapper).not.to.be.undefined; + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(3); + wrapper.unmount(); + }); + + function getSelectBySpanTitle(titleSpan: HTMLElement) { + const settingsItemDiv = titleSpan.parentElement?.parentElement; + expect(settingsItemDiv).not.to.be.undefined; + return settingsItemDiv!.querySelector("select"); + } + + it("renders without version option (V1) set theme", async () => { + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + + const themeSpan = wrapper.getByText("settings.uiSettingsPage.themeTitle"); + const themeSelect = getSelectBySpanTitle (themeSpan); + expect(themeSelect).not.to.be.null; + await TestUtils.flushAsyncOperations(); + fireEvent.change(themeSelect!, { target: {value: "dark"}}); + await TestUtils.flushAsyncOperations(); + expect(themeSelect!.value).to.be.eq("dark"); + fireEvent.change(themeSelect!, { target: {value: "light"}}); + await TestUtils.flushAsyncOperations(); + expect(themeSelect!.value).to.be.eq("light"); + wrapper.unmount(); + }); + + it("renders without version option (V1) set widget opacity", async () => { + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + const handles = wrapper.container.querySelectorAll(".core-slider-handle"); + expect(handles.length).to.eq(1); + fireEvent.mouseDown(handles[0]); + fireEvent.mouseUp(handles[0]); + await TestUtils.flushAsyncOperations(); + // trigger sync event processing + UiFramework.setWidgetOpacity(.5); + await TestUtils.flushAsyncOperations(); + wrapper.unmount(); + }); + + it("renders without version option (V1) toggle auto-hide", async () => { + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + const autoHideSpan = wrapper.getByText("settings.uiSettingsPage.autoHideTitle"); + const checkbox = getInputBySpanTitle (autoHideSpan); + expect(checkbox).not.to.be.null; + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.true; + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.false; + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(3); + wrapper.unmount(); + }); + + it("renders with version option (V1)", async () => { + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(4); + + const titleSpan = wrapper.getByText("settings.uiSettingsPage.newUiTitle"); + const checkbox = getInputBySpanTitle (titleSpan); + expect(checkbox).not.to.be.null; + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(7); + + wrapper.unmount(); + }); + + it("renders without version option (V2) toggle drag interaction", async () => { + UiFramework.setUiVersion("2"); + await TestUtils.flushAsyncOperations(); + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + + const titleSpan = wrapper.getByText("settings.uiSettingsPage.dragInteractionTitle"); + const checkbox = getInputBySpanTitle (titleSpan); + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.true; + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.false; + wrapper.unmount(); + }); + + it("renders without version option (V2) toggle useProximityOpacity", async () => { + UiFramework.setUiVersion("2"); + await TestUtils.flushAsyncOperations(); + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + + const titleSpan = wrapper.getByText("settings.uiSettingsPage.useProximityOpacityTitle"); + const checkbox = getInputBySpanTitle (titleSpan); + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.false; + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.true; + wrapper.unmount(); + }); + + it("renders without version option (V2) toggle snapWidgetOpacity", async () => { + UiFramework.setUiVersion("2"); + await TestUtils.flushAsyncOperations(); + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + + const titleSpan = wrapper.getByText("settings.uiSettingsPage.snapWidgetOpacityTitle"); + const checkbox = getInputBySpanTitle (titleSpan); + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.true; + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(checkbox?.checked).to.be.false; + wrapper.unmount(); + }); + + it("renders with version option (V2) toggle ui-version", async () => { + UiFramework.setUiVersion("2"); + await TestUtils.flushAsyncOperations(); + const wrapper = render(); + expect(wrapper).not.to.be.undefined; + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(7); + const uiVersionSpan = wrapper.getByText("settings.uiSettingsPage.newUiTitle"); + const checkbox = getInputBySpanTitle (uiVersionSpan); + + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(4); + + fireEvent.click(checkbox!); + await TestUtils.flushAsyncOperations(); + expect(wrapper.container.querySelectorAll("span.title").length).to.eq(7); + + wrapper.unmount(); + }); + +}); diff --git a/ui/framework/src/test/statusfields/toolassistance/ToolAssistanceField.test.tsx b/ui/framework/src/test/statusfields/toolassistance/ToolAssistanceField.test.tsx index eb3caf2cf44b..611869c45ac9 100644 --- a/ui/framework/src/test/statusfields/toolassistance/ToolAssistanceField.test.tsx +++ b/ui/framework/src/test/statusfields/toolassistance/ToolAssistanceField.test.tsx @@ -9,7 +9,7 @@ import * as sinon from "sinon"; import { Logger } from "@bentley/bentleyjs-core"; import { MockRender, ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod } from "@bentley/imodeljs-frontend"; import { WidgetState } from "@bentley/ui-abstract"; -import { LocalUiSettings, Toggle } from "@bentley/ui-core"; +import { LocalSettingsStorage, Toggle } from "@bentley/ui-core"; import { FooterPopup, TitleBarButton } from "@bentley/ui-ninezone"; import { AppNotificationManager, ConfigurableCreateInfo, ConfigurableUiControlType, CursorPopupManager, FrontstageManager, StatusBar, StatusBarWidgetControl, @@ -18,11 +18,11 @@ import { import TestUtils, { mount, storageMock } from "../../TestUtils"; describe("ToolAssistanceField", () => { - const uiSettings = new LocalUiSettings({ localStorage: storageMock() } as Window); + const uiSettingsStorage = new LocalSettingsStorage({ localStorage: storageMock() } as Window); before(async () => { - await uiSettings.saveSetting("ToolAssistance", "showPromptAtCursor", true); - await uiSettings.saveSetting("ToolAssistance", "mouseTouchTabIndex", 0); + await uiSettingsStorage.saveSetting("ToolAssistance", "showPromptAtCursor", true); + await uiSettingsStorage.saveSetting("ToolAssistance", "mouseTouchTabIndex", 0); }); class AppStatusBarWidgetControl extends StatusBarWidgetControl { @@ -35,7 +35,7 @@ describe("ToolAssistanceField", () => { <> + uiSettings={uiSettingsStorage} /> ); } diff --git a/ui/framework/src/test/uisettings/AppUiSettings.test.ts b/ui/framework/src/test/uisettings/AppUiSettings.test.ts new file mode 100644 index 000000000000..9bc1e23c9324 --- /dev/null +++ b/ui/framework/src/test/uisettings/AppUiSettings.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +import { expect } from "chai"; +import { storageMock, TestUtils } from "../TestUtils"; +import { UiFramework } from "../../ui-framework/UiFramework"; +import { AppUiSettings } from "../../ui-framework/uisettings/AppUiSettings"; +import { SYSTEM_PREFERRED_COLOR_THEME } from "../../ui-framework/theme/ThemeManager"; + +describe("AppUiSettings", () => { + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + let localStorageMock = storageMock(); + + beforeEach(async () => { + // create a new mock each run so there are no "stored values" + localStorageMock = storageMock(); + await TestUtils.initializeUiFramework(); + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); + }); + + afterEach(() => { + TestUtils.terminateUiFramework(); + Object.defineProperty(window, "localStorage", localStorageToRestore); + }); + + it("should get/set settings", async () => { + + const uiSetting = new AppUiSettings({}); + await uiSetting.loadUserSettings(UiFramework.getUiSettingsStorage()); + const uiVersion = "2"; + const opacity = 0.5; + const colorTheme = "dark"; + const useDragInteraction = true; + UiFramework.setUiVersion (uiVersion); + UiFramework.setWidgetOpacity(opacity); + UiFramework.setUseDragInteraction(true); + UiFramework.setColorTheme(colorTheme); + UiFramework.setUseDragInteraction(useDragInteraction); + await TestUtils.flushAsyncOperations(); + expect(UiFramework.uiVersion).to.eql(uiVersion); + expect(UiFramework.getWidgetOpacity()).to.eql(opacity); + expect(UiFramework.getColorTheme()).to.eql(colorTheme); + expect(UiFramework.useDragInteraction).to.eql(useDragInteraction); + }); + + it("should used default settings", async () => { + const defaults = { + colorTheme: SYSTEM_PREFERRED_COLOR_THEME, + dragInteraction: false, + frameworkVersion: "2", + widgetOpacity: 0.8, + }; + + const uiSetting = new AppUiSettings(defaults); + await uiSetting.loadUserSettings(UiFramework.getUiSettingsStorage()); + await TestUtils.flushAsyncOperations(); + expect(UiFramework.uiVersion).to.eql(defaults.frameworkVersion); + expect(UiFramework.getWidgetOpacity()).to.eql(defaults.widgetOpacity); + expect(UiFramework.getColorTheme()).to.eql(defaults.colorTheme); + expect(UiFramework.useDragInteraction).to.eql(defaults.dragInteraction); + }); + +}); diff --git a/ui/framework/src/test/uisettings/IModelAppUiSettings.test.ts b/ui/framework/src/test/uisettings/UserSettingsStorage.test.ts similarity index 92% rename from ui/framework/src/test/uisettings/IModelAppUiSettings.test.ts rename to ui/framework/src/test/uisettings/UserSettingsStorage.test.ts index f75c609a6e09..1c6eb00937a9 100644 --- a/ui/framework/src/test/uisettings/IModelAppUiSettings.test.ts +++ b/ui/framework/src/test/uisettings/UserSettingsStorage.test.ts @@ -7,10 +7,10 @@ import { expect } from "chai"; import { AuthorizedFrontendRequestContext, IModelApp, MockRender } from "@bentley/imodeljs-frontend"; import { SettingsAdmin, SettingsResult, SettingsStatus } from "@bentley/product-settings-client"; import { UiSettingsStatus } from "@bentley/ui-core"; -import { IModelAppUiSettings, settingsStatusToUiSettingsStatus } from "../../ui-framework"; +import { settingsStatusToUiSettingsStatus, UserSettingsStorage } from "../../ui-framework"; import { TestUtils } from "../TestUtils"; -describe("IModelAppUiSettings", () => { +describe("UserSettingsStorage", () => { before(async () => { await TestUtils.initializeUiFramework(); await MockRender.App.startup(); @@ -30,7 +30,7 @@ describe("IModelAppUiSettings", () => { saveUserSetting, })); sinon.stub(IModelApp, "authorizationClient").get(() => ({ hasSignedIn: true })); - const sut = new IModelAppUiSettings(); + const sut = new UserSettingsStorage(); await sut.saveSetting("TESTNAMESPACE", "TESTNAME", "testvalue"); saveUserSetting.calledOnceWithExactly(sinon.match.any, "testvalue", "TESTNAMESPACE", "TESTNAME", true).should.true; }); @@ -41,7 +41,7 @@ describe("IModelAppUiSettings", () => { deleteUserSetting, })); sinon.stub(IModelApp, "authorizationClient").get(() => ({ hasSignedIn: true })); - const sut = new IModelAppUiSettings(); + const sut = new UserSettingsStorage(); await sut.deleteSetting("TESTNAMESPACE", "TESTNAME"); deleteUserSetting.calledOnceWithExactly(sinon.match.any, "TESTNAMESPACE", "TESTNAME", true).should.true; }); @@ -52,7 +52,7 @@ describe("IModelAppUiSettings", () => { getUserSetting, })); sinon.stub(IModelApp, "authorizationClient").get(() => ({ hasSignedIn: true })); - const sut = new IModelAppUiSettings(); + const sut = new UserSettingsStorage(); const settingResult = await sut.getSetting("TESTNAMESPACE", "TESTNAME"); getUserSetting.calledOnceWithExactly(sinon.match.any, "TESTNAMESPACE", "TESTNAME", true).should.true; settingResult.setting.should.eq("testvalue"); @@ -60,21 +60,21 @@ describe("IModelAppUiSettings", () => { it("should fail to save setting", async () => { sinon.stub(IModelApp, "authorizationClient").get(() => ({ hasSignedIn: false })); - const sut = new IModelAppUiSettings(); + const sut = new UserSettingsStorage(); const result = await sut.saveSetting("TESTNAMESPACE", "TESTNAME", "testvalue"); expect(result.status).to.eq(UiSettingsStatus.AuthorizationError); }); it("should fail to delete setting", async () => { sinon.stub(IModelApp, "authorizationClient").get(() => ({ hasSignedIn: false })); - const sut = new IModelAppUiSettings(); + const sut = new UserSettingsStorage(); const result = await sut.deleteSetting("TESTNAMESPACE", "TESTNAME"); expect(result.status).to.eq(UiSettingsStatus.AuthorizationError); }); it("should fail to get setting", async () => { sinon.stub(IModelApp, "authorizationClient").get(() => ({ hasSignedIn: false })); - const sut = new IModelAppUiSettings(); + const sut = new UserSettingsStorage(); const result = await sut.getSetting("TESTNAMESPACE", "TESTNAME"); expect(result.status).to.eq(UiSettingsStatus.AuthorizationError); }); diff --git a/ui/framework/src/test/utils/UiShowHideManager.test.tsx b/ui/framework/src/test/utils/UiShowHideManager.test.tsx index 65918d996819..0e29cab8d11e 100644 --- a/ui/framework/src/test/utils/UiShowHideManager.test.tsx +++ b/ui/framework/src/test/utils/UiShowHideManager.test.tsx @@ -9,221 +9,268 @@ import { render } from "@testing-library/react"; import { ConfigurableCreateInfo, ContentControl, ContentGroup, ContentLayout, ContentLayoutDef, FrontstageManager, INACTIVITY_TIME_DEFAULT, UiFramework, UiShowHideManager, + UiShowHideSettingsProvider, } from "../../ui-framework"; import { TestFrontstage } from "../frontstage/FrontstageTestUtils"; -import TestUtils from "../TestUtils"; +import TestUtils, { storageMock } from "../TestUtils"; +import { LocalSettingsStorage } from "@bentley/ui-core"; -describe("UiShowHideManager", () => { +describe("UiShowHideManager localStorage Wrapper", () => { + + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + const localStorageMock = storageMock(); before(async () => { - await TestUtils.initializeUiFramework(); + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); }); after(() => { - TestUtils.terminateUiFramework(); + Object.defineProperty(window, "localStorage", localStorageToRestore); }); - describe("getters and setters", () => { + describe("UiShowHideManager", () => { - it("autoHideUi should return default of false", () => { - expect(UiShowHideManager.autoHideUi).to.be.false; + before(async () => { + await TestUtils.initializeUiFramework(); }); - it("autoHideUi should set & return correct value", () => { - UiShowHideManager.autoHideUi = true; - expect(UiShowHideManager.autoHideUi).to.be.true; - UiShowHideManager.autoHideUi = false; - expect(UiShowHideManager.autoHideUi).to.be.false; + after(() => { + TestUtils.terminateUiFramework(); }); - it("showHidePanels should return default of false", () => { - expect(UiShowHideManager.showHidePanels).to.be.false; - }); + describe("getters and setters", () => { - it("showHidePanels should set & return correct value", () => { - const spyMethod = sinon.spy(); - const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); + it("autoHideUi should return default of false", () => { + expect(UiShowHideManager.autoHideUi).to.be.false; + }); - UiShowHideManager.showHidePanels = true; - expect(UiShowHideManager.showHidePanels).to.be.true; - spyMethod.calledOnce.should.true; + it("autoHideUi should set & return correct value", () => { + UiShowHideManager.autoHideUi = true; + expect(UiShowHideManager.autoHideUi).to.be.true; + UiShowHideManager.autoHideUi = false; + expect(UiShowHideManager.autoHideUi).to.be.false; + }); - UiShowHideManager.showHidePanels = false; - expect(UiShowHideManager.showHidePanels).to.be.false; - spyMethod.calledTwice.should.true; + it("showHidePanels should return default of false", () => { + expect(UiShowHideManager.showHidePanels).to.be.false; + }); - remove(); - }); + it("showHidePanels should set & return correct value", () => { + const spyMethod = sinon.spy(); + const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); - it("showHideFooter should return default of false", () => { - expect(UiShowHideManager.showHideFooter).to.be.false; - }); + UiShowHideManager.showHidePanels = true; + expect(UiShowHideManager.showHidePanels).to.be.true; + spyMethod.calledOnce.should.true; - it("showHideFooter should set & return correct value", () => { - const spyMethod = sinon.spy(); - const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); + UiShowHideManager.showHidePanels = false; + expect(UiShowHideManager.showHidePanels).to.be.false; + spyMethod.calledTwice.should.true; - UiShowHideManager.showHideFooter = true; - expect(UiShowHideManager.showHideFooter).to.be.true; - spyMethod.calledOnce.should.true; + remove(); + }); - UiShowHideManager.showHideFooter = false; - expect(UiShowHideManager.showHideFooter).to.be.false; - spyMethod.calledTwice.should.true; + it("showHideFooter should return default of false", () => { + expect(UiShowHideManager.showHideFooter).to.be.false; + }); - remove(); - }); + it("showHideFooter should set & return correct value", () => { + const spyMethod = sinon.spy(); + const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); - it("useProximityOpacity should return default of true", () => { - expect(UiShowHideManager.useProximityOpacity).to.be.true; - }); + UiShowHideManager.showHideFooter = true; + expect(UiShowHideManager.showHideFooter).to.be.true; + spyMethod.calledOnce.should.true; - it("useProximityOpacity should set & return correct value", () => { - const spyMethod = sinon.spy(); - const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); + UiShowHideManager.showHideFooter = false; + expect(UiShowHideManager.showHideFooter).to.be.false; + spyMethod.calledTwice.should.true; - UiShowHideManager.useProximityOpacity = false; - expect(UiShowHideManager.useProximityOpacity).to.be.false; - spyMethod.calledOnce.should.true; + remove(); + }); - UiShowHideManager.useProximityOpacity = true; - expect(UiShowHideManager.useProximityOpacity).to.be.true; - spyMethod.calledTwice.should.true; + it("useProximityOpacity should return default of true", () => { + expect(UiShowHideManager.useProximityOpacity).to.be.true; + }); - remove(); - }); + it("useProximityOpacity should set & return correct value", () => { + const spyMethod = sinon.spy(); + const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); - it("snapWidgetOpacity should return default of false", () => { - expect(UiShowHideManager.snapWidgetOpacity).to.be.false; - }); + UiShowHideManager.useProximityOpacity = false; + expect(UiShowHideManager.useProximityOpacity).to.be.false; + spyMethod.calledOnce.should.true; - it("snapWidgetOpacity should set & return correct value", () => { - const spyMethod = sinon.spy(); - const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); + UiShowHideManager.useProximityOpacity = true; + expect(UiShowHideManager.useProximityOpacity).to.be.true; + spyMethod.calledTwice.should.true; - UiShowHideManager.snapWidgetOpacity = true; - expect(UiShowHideManager.snapWidgetOpacity).to.be.true; - spyMethod.calledOnce.should.true; + remove(); + }); - UiShowHideManager.snapWidgetOpacity = false; - expect(UiShowHideManager.snapWidgetOpacity).to.be.false; - spyMethod.calledTwice.should.true; + it("snapWidgetOpacity should return default of false", () => { + expect(UiShowHideManager.snapWidgetOpacity).to.be.false; + }); - remove(); - }); + it("snapWidgetOpacity should set & return correct value", () => { + const spyMethod = sinon.spy(); + const remove = UiFramework.onUiVisibilityChanged.addListener(spyMethod); - it("inactivityTime should return default", () => { - expect(UiShowHideManager.inactivityTime).to.eq(INACTIVITY_TIME_DEFAULT); - }); + UiShowHideManager.snapWidgetOpacity = true; + expect(UiShowHideManager.snapWidgetOpacity).to.be.true; + spyMethod.calledOnce.should.true; - it("inactivityTime should set & return correct value", () => { - const testValue = 10000; - UiShowHideManager.inactivityTime = testValue; - expect(UiShowHideManager.inactivityTime).to.eq(testValue); + UiShowHideManager.snapWidgetOpacity = false; + expect(UiShowHideManager.snapWidgetOpacity).to.be.false; + spyMethod.calledTwice.should.true; + + remove(); + }); + + it("inactivityTime should return default", () => { + expect(UiShowHideManager.inactivityTime).to.eq(INACTIVITY_TIME_DEFAULT); + }); + + it("inactivityTime should set & return correct value", () => { + const testValue = 10000; + UiShowHideManager.inactivityTime = testValue; + expect(UiShowHideManager.inactivityTime).to.eq(testValue); + }); }); - }); - describe("Frontstage Activate", () => { + describe("Frontstage Activate", () => { - it("activating Frontstage should show UI", async () => { - UiFramework.setIsUiVisible(false); - expect(UiShowHideManager.isUiVisible).to.eq(false); - UiShowHideManager.autoHideUi = true; + it("activating Frontstage should show UI", async () => { + UiFramework.setIsUiVisible(false); + expect(UiShowHideManager.isUiVisible).to.eq(false); + UiShowHideManager.autoHideUi = true; - const frontstageProvider = new TestFrontstage(); - FrontstageManager.addFrontstageProvider(frontstageProvider); - await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); + const frontstageProvider = new TestFrontstage(); + FrontstageManager.addFrontstageProvider(frontstageProvider); + await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); - await TestUtils.flushAsyncOperations(); - expect(UiShowHideManager.isUiVisible).to.eq(true); + await TestUtils.flushAsyncOperations(); + expect(UiShowHideManager.isUiVisible).to.eq(true); + }); }); - }); - describe("Content Mouse Events", () => { + describe("Content Mouse Events", () => { - class TestContentControl extends ContentControl { - constructor(info: ConfigurableCreateInfo, options: any) { - super(info, options); + class TestContentControl extends ContentControl { + constructor(info: ConfigurableCreateInfo, options: any) { + super(info, options); - this.reactNode =
Test
; + this.reactNode =
Test
; + } } - } - const myContentGroup: ContentGroup = new ContentGroup({ - contents: [{ id: "myContent", classId: TestContentControl }], + const myContentGroup: ContentGroup = new ContentGroup({ + contents: [{ id: "myContent", classId: TestContentControl }], + }); + + const myContentLayout: ContentLayoutDef = new ContentLayoutDef({ + id: "SingleContent", + descriptionKey: "UiFramework:tests.singleContent", + priority: 100, + }); + + it("Mouse move in content view should show the UI then hide after inactivity", () => { + const fakeTimers = sinon.useFakeTimers(); + UiFramework.setIsUiVisible(false); + UiShowHideManager.autoHideUi = true; + UiShowHideManager.inactivityTime = 20; + expect(UiShowHideManager.isUiVisible).to.eq(false); + + const component = render(); + const container = component.getByTestId("single-content-container"); + container.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, view: window })); + + fakeTimers.tick(0); + expect(UiShowHideManager.isUiVisible).to.eq(true); + + fakeTimers.tick(1000); + fakeTimers.restore(); + expect(UiShowHideManager.isUiVisible).to.eq(false); + }); + + it("Mouse move in content view should do nothing if autoHideUi is off", async () => { + UiFramework.setIsUiVisible(false); + UiShowHideManager.autoHideUi = false; + expect(UiShowHideManager.isUiVisible).to.eq(false); + + const component = render(); + const container = component.getByTestId("single-content-container"); + container.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, view: window })); + + await TestUtils.flushAsyncOperations(); + expect(UiShowHideManager.isUiVisible).to.eq(false); + }); }); - const myContentLayout: ContentLayoutDef = new ContentLayoutDef({ - id: "SingleContent", - descriptionKey: "UiFramework:tests.singleContent", - priority: 100, - }); + describe("Widget Mouse Events", () => { + it("Mouse enter in widget should show the UI", async () => { + UiFramework.setIsUiVisible(false); + UiShowHideManager.autoHideUi = true; + expect(UiShowHideManager.isUiVisible).to.eq(false); - it("Mouse move in content view should show the UI then hide after inactivity", () => { - const fakeTimers = sinon.useFakeTimers(); - UiFramework.setIsUiVisible(false); - UiShowHideManager.autoHideUi = true; - UiShowHideManager.inactivityTime = 20; - expect(UiShowHideManager.isUiVisible).to.eq(false); + // const component = render(); + // const container = component.getByTestId("single-content-container"); + // container.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window })); - const component = render(); - const container = component.getByTestId("single-content-container"); - container.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, view: window })); + // TEMP + UiShowHideManager.handleWidgetMouseEnter(); - fakeTimers.tick(0); - expect(UiShowHideManager.isUiVisible).to.eq(true); + await TestUtils.flushAsyncOperations(); + expect(UiShowHideManager.isUiVisible).to.eq(true); + }); - fakeTimers.tick(1000); - fakeTimers.restore(); - expect(UiShowHideManager.isUiVisible).to.eq(false); - }); + it("Mouse enter in widget should do nothing if autoHideUi is off", async () => { + UiFramework.setIsUiVisible(false); + UiShowHideManager.autoHideUi = false; + expect(UiShowHideManager.isUiVisible).to.eq(false); - it("Mouse move in content view should do nothing if autoHideUi is off", async () => { - UiFramework.setIsUiVisible(false); - UiShowHideManager.autoHideUi = false; - expect(UiShowHideManager.isUiVisible).to.eq(false); + // const component = render(); + // const container = component.getByTestId("single-content-container"); + // container.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window })); - const component = render(); - const container = component.getByTestId("single-content-container"); - container.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, cancelable: true, view: window })); + // TEMP + UiShowHideManager.handleWidgetMouseEnter(); - await TestUtils.flushAsyncOperations(); - expect(UiShowHideManager.isUiVisible).to.eq(false); + await TestUtils.flushAsyncOperations(); + expect(UiShowHideManager.isUiVisible).to.eq(false); + }); }); }); - describe("Widget Mouse Events", () => { - it("Mouse enter in widget should show the UI", async () => { - UiFramework.setIsUiVisible(false); - UiShowHideManager.autoHideUi = true; - expect(UiShowHideManager.isUiVisible).to.eq(false); + describe("UiShowHideSettingsProvider ", () => { - // const component = render(); - // const container = component.getByTestId("single-content-container"); - // container.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window })); + it("should get and set defaults", async () => { + const settingsStorage = new LocalSettingsStorage (); + await UiShowHideSettingsProvider.storeAutoHideUi(false, settingsStorage); + await UiShowHideSettingsProvider.storeUseProximityOpacity (false, settingsStorage); + await UiShowHideSettingsProvider.storeSnapWidgetOpacity(false, settingsStorage); + await TestUtils.initializeUiFramework(); - // TEMP - UiShowHideManager.handleWidgetMouseEnter(); + const uiShowHideSettingsProvider = new UiShowHideSettingsProvider (); + await uiShowHideSettingsProvider.loadUserSettings (UiFramework.getUiSettingsStorage()); - await TestUtils.flushAsyncOperations(); - expect(UiShowHideManager.isUiVisible).to.eq(true); - }); + expect(UiShowHideManager.autoHideUi).to.eq(false); + expect(UiShowHideManager.useProximityOpacity).to.eq(false); + expect(UiShowHideManager.snapWidgetOpacity).to.eq(false); - it("Mouse enter in widget should do nothing if autoHideUi is off", async () => { - UiFramework.setIsUiVisible(false); - UiShowHideManager.autoHideUi = false; - expect(UiShowHideManager.isUiVisible).to.eq(false); + UiShowHideManager.setAutoHideUi(true); + UiShowHideManager.setUseProximityOpacity(true); + UiShowHideManager.setSnapWidgetOpacity(true); + expect(UiShowHideManager.autoHideUi).to.eq(true); + expect(UiShowHideManager.useProximityOpacity).to.eq(true); + expect(UiShowHideManager.snapWidgetOpacity).to.eq(true); - // const component = render(); - // const container = component.getByTestId("single-content-container"); - // container.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window })); + TestUtils.terminateUiFramework(); - // TEMP - UiShowHideManager.handleWidgetMouseEnter(); - - await TestUtils.flushAsyncOperations(); - expect(UiShowHideManager.isUiVisible).to.eq(false); }); + }); }); diff --git a/ui/framework/src/test/widget-panels/Frontstage.test.snap b/ui/framework/src/test/widget-panels/Frontstage.test.snap index 4f927623ad08..5739ff675a7b 100644 --- a/ui/framework/src/test/widget-panels/Frontstage.test.snap +++ b/ui/framework/src/test/widget-panels/Frontstage.test.snap @@ -109,6 +109,528 @@ exports[`ActiveFrontstageDefProvider should render 1`] = ` `; +exports[`Frontstage local storage wrapper ActiveFrontstageDefProvider should render 1`] = ` +
+ } + toolSettingsContent={} + widgetContent={} + > + + +
+`; + +exports[`Frontstage local storage wrapper WidgetPanelsFrontstage should not render w/o frontstage 1`] = `""`; + +exports[`Frontstage local storage wrapper WidgetPanelsFrontstage should render 1`] = ` + +`; + +exports[`Frontstage local storage wrapper WidgetPanelsFrontstage should render modal stage content 1`] = ` + +`; + +exports[`Frontstage local storage wrapper initializeNineZoneState should initialize widgets 1`] = ` +Object { + "draggedTab": undefined, + "floatingWidgets": Object { + "allIds": Array [], + "byId": Object {}, + }, + "panels": Object { + "bottom": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "bottom", + "size": undefined, + "span": true, + "widgets": Array [], + }, + "left": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "left", + "size": undefined, + "widgets": Array [], + }, + "right": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "right", + "size": undefined, + "widgets": Array [], + }, + "top": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "top", + "size": undefined, + "span": true, + "widgets": Array [], + }, + }, + "size": Object { + "height": 0, + "width": 0, + }, + "tabs": Object { + "nz-tool-settings-tab": Object { + "allowedPanelTargets": Array [ + "bottom", + "left", + "right", + ], + "id": "nz-tool-settings-tab", + "label": "Tool Settings", + }, + }, + "toolSettings": Object { + "type": "docked", + }, + "widgets": Object {}, +} +`; + +exports[`Frontstage local storage wrapper packNineZoneState should remove labels 1`] = ` +Object { + "draggedTab": undefined, + "floatingWidgets": Object { + "allIds": Array [ + "w1", + ], + "byId": Object { + "w1": Object { + "bounds": Object { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "home": Object { + "side": "left", + "widgetId": undefined, + "widgetIndex": 0, + }, + "id": "w1", + }, + }, + }, + "panels": Object { + "bottom": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "bottom", + "size": undefined, + "span": true, + "widgets": Array [], + }, + "left": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "left", + "size": undefined, + "widgets": Array [], + }, + "right": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "right", + "size": undefined, + "widgets": Array [], + }, + "top": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "top", + "size": undefined, + "span": true, + "widgets": Array [], + }, + }, + "size": Object { + "height": 0, + "width": 0, + }, + "tabs": Object { + "t1": Object { + "allowedPanelTargets": undefined, + "id": "t1", + "preferredFloatingWidgetSize": undefined, + }, + }, + "toolSettings": Object { + "type": "docked", + }, + "widgets": Object { + "w1": Object { + "activeTabId": "t1", + "id": "w1", + "minimized": false, + "tabs": Array [ + "t1", + ], + }, + }, +} +`; + +exports[`Frontstage local storage wrapper restoreNineZoneState should log error if widgetDef is not found 1`] = ` +Object { + "frontstageId": "", + "tabId": "t1", +} +`; + +exports[`Frontstage local storage wrapper restoreNineZoneState should restore tabs 1`] = ` +Object { + "draggedTab": undefined, + "floatingWidgets": Object { + "allIds": Array [], + "byId": Object {}, + }, + "panels": Object { + "bottom": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "bottom", + "size": undefined, + "span": true, + "widgets": Array [], + }, + "left": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "left", + "size": undefined, + "widgets": Array [], + }, + "right": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "right", + "size": undefined, + "widgets": Array [], + }, + "top": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "top", + "size": undefined, + "span": true, + "widgets": Array [], + }, + }, + "size": Object { + "height": 0, + "width": 0, + }, + "tabs": Object { + "nz-tool-settings-tab": Object { + "allowedPanelTargets": Array [ + "bottom", + "left", + "right", + ], + "id": "nz-tool-settings-tab", + "label": "Tool Settings", + }, + "t1": Object { + "id": "t1", + "label": "Widget", + }, + }, + "toolSettings": Object { + "type": "docked", + }, + "widgets": Object {}, +} +`; + +exports[`Frontstage local storage wrapper useSavedFrontstageState should load saved nineZoneState 1`] = ` +Object { + "draggedTab": undefined, + "floatingWidgets": Object { + "allIds": Array [], + "byId": Object {}, + }, + "panels": Object { + "bottom": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "bottom", + "size": undefined, + "span": true, + "widgets": Array [], + }, + "left": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "left", + "size": undefined, + "widgets": Array [], + }, + "right": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 3, + "minSize": 200, + "pinned": true, + "resizable": true, + "side": "right", + "size": undefined, + "widgets": Array [], + }, + "top": Object { + "collapseOffset": 100, + "collapsed": false, + "maxSize": 600, + "maxWidgetCount": 2, + "minSize": 100, + "pinned": true, + "resizable": true, + "side": "top", + "size": undefined, + "span": true, + "widgets": Array [], + }, + }, + "size": Object { + "height": 0, + "width": 0, + }, + "tabs": Object { + "nz-tool-settings-tab": Object { + "allowedPanelTargets": Array [ + "bottom", + "left", + "right", + ], + "id": "nz-tool-settings-tab", + "label": "Tool Settings", + }, + }, + "toolSettings": Object { + "type": "docked", + }, + "widgets": Object {}, +} +`; + exports[`WidgetPanelsFrontstage should not render w/o frontstage 1`] = `""`; exports[`WidgetPanelsFrontstage should render 1`] = ` diff --git a/ui/framework/src/test/widget-panels/Frontstage.test.tsx b/ui/framework/src/test/widget-panels/Frontstage.test.tsx index ba166367966b..9185d6481dc3 100644 --- a/ui/framework/src/test/widget-panels/Frontstage.test.tsx +++ b/ui/framework/src/test/widget-panels/Frontstage.test.tsx @@ -337,84 +337,7 @@ export class TestUi2Provider implements UiItemsProvider { } } -describe("WidgetPanelsFrontstage", () => { - it("should render", () => { - const frontstageDef = new FrontstageDef(); - sinon.stub(FrontstageManager, "activeFrontstageDef").get(() => frontstageDef); - const wrapper = shallow(); - wrapper.should.matchSnapshot(); - }); - - it("should render modal stage content", () => { - const modalStageInfo = { - title: "TestModalStage", - content:
Hello World!
, - }; - sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => modalStageInfo); - const frontstageDef = new FrontstageDef(); - const contentGroup = moq.Mock.ofType(); - sinon.stub(FrontstageManager, "activeFrontstageDef").get(() => frontstageDef); - sinon.stub(frontstageDef, "contentGroup").get(() => contentGroup.object); - const wrapper = shallow(); - wrapper.should.matchSnapshot(); - }); - - it("should not render w/o frontstage", () => { - sinon.stub(FrontstageManager, "activeFrontstageDef").get(() => undefined); - const wrapper = shallow(); - wrapper.should.matchSnapshot(); - }); -}); - -describe("ModalFrontstageComposer", () => { - before(async () => { - await TestUtils.initializeUiFramework(); - }); - - after(() => { - TestUtils.terminateUiFramework(); - }); - - it("should render modal stage content when mounted", () => { - const modalStageInfo = { - title: "TestModalStage", - content:
Hello World!
, - }; - mount(); - }); - - it("should add tool activated event listener", () => { - const addListenerSpy = sinon.spy(FrontstageManager.onModalFrontstageChangedEvent, "addListener"); - const removeListenerSpy = sinon.spy(FrontstageManager.onModalFrontstageChangedEvent, "removeListener"); - const sut = renderHook(() => useActiveModalFrontstageInfo()); - sut.unmount(); - addListenerSpy.calledOnce.should.true; - removeListenerSpy.calledOnce.should.true; - }); - - it("should update active modal info", () => { - const modalStageInfo = { - title: "TestModalStage", - content:
Hello World!
, - }; - - sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => undefined); - renderHook(() => useActiveModalFrontstageInfo()); - act(() => { - sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => undefined); - FrontstageManager.onModalFrontstageChangedEvent.emit({ - modalFrontstageCount: 0, - }); - - sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => modalStageInfo); - FrontstageManager.onModalFrontstageChangedEvent.emit({ - modalFrontstageCount: 1, - }); - }); - }); -}); - -describe("ActiveFrontstageDefProvider", () => { +describe("Frontstage local storage wrapper", () => { const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; const localStorageMock = storageMock(); @@ -422,1539 +345,1615 @@ describe("ActiveFrontstageDefProvider", () => { Object.defineProperty(window, "localStorage", { get: () => localStorageMock, }); - - await TestUtils.initializeUiFramework(); }); after(() => { Object.defineProperty(window, "localStorage", localStorageToRestore); - TestUtils.terminateUiFramework(); }); - beforeEach(() => { - sinon.stub(FrontstageManager, "nineZoneSize").set(() => { }); - }); + describe("WidgetPanelsFrontstage", () => { + it("should render", () => { + const frontstageDef = new FrontstageDef(); + sinon.stub(FrontstageManager, "activeFrontstageDef").get(() => frontstageDef); + const wrapper = shallow(); + wrapper.should.matchSnapshot(); + }); + + it("should render modal stage content", () => { + const modalStageInfo = { + title: "TestModalStage", + content:
Hello World!
, + }; + sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => modalStageInfo); + const frontstageDef = new FrontstageDef(); + const contentGroup = moq.Mock.ofType(); + sinon.stub(FrontstageManager, "activeFrontstageDef").get(() => frontstageDef); + sinon.stub(frontstageDef, "contentGroup").get(() => contentGroup.object); + const wrapper = shallow(); + wrapper.should.matchSnapshot(); + }); - it("should render", () => { - const frontstageDef = new FrontstageDef(); - const wrapper = shallow(); - wrapper.should.matchSnapshot(); + it("should not render w/o frontstage", () => { + sinon.stub(FrontstageManager, "activeFrontstageDef").get(() => undefined); + const wrapper = shallow(); + wrapper.should.matchSnapshot(); + }); }); - it("should fall back to cached NineZoneState", () => { - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = createNineZoneState(); + describe("ModalFrontstageComposer", () => { + before(async () => { + await TestUtils.initializeUiFramework(); + }); - const newFrontstageDef = new FrontstageDef(); - newFrontstageDef.nineZoneState = undefined; + after(() => { + TestUtils.terminateUiFramework(); + }); - const wrapper = mount<{ frontstageDef: FrontstageDef }>(); - wrapper.setProps({ frontstageDef: newFrontstageDef }); + it("should render modal stage content when mounted", () => { + const modalStageInfo = { + title: "TestModalStage", + content:
Hello World!
, + }; + mount(); + }); - const nineZone = wrapper.find(NineZone); - nineZone.prop("state").should.eq(frontstageDef.nineZoneState); - }); -}); + it("should add tool activated event listener", () => { + const addListenerSpy = sinon.spy(FrontstageManager.onModalFrontstageChangedEvent, "addListener"); + const removeListenerSpy = sinon.spy(FrontstageManager.onModalFrontstageChangedEvent, "removeListener"); + const sut = renderHook(() => useActiveModalFrontstageInfo()); + sut.unmount(); + addListenerSpy.calledOnce.should.true; + removeListenerSpy.calledOnce.should.true; + }); -describe("useNineZoneDispatch", () => { - beforeEach(() => { - sinon.stub(FrontstageManager, "nineZoneSize").set(() => { }); - }); + it("should update active modal info", () => { + const modalStageInfo = { + title: "TestModalStage", + content:
Hello World!
, + }; - it("should modify nineZoneState with default NineZoneReducer", () => { - const frontstageDef = new FrontstageDef(); - const nineZoneState = createNineZoneState(); - frontstageDef.nineZoneState = nineZoneState; - const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); - result.current({ - type: "PANEL_INITIALIZE", - side: "left", - size: 200, - }); - frontstageDef.nineZoneState.should.not.eq(nineZoneState); - (frontstageDef.nineZoneState.panels.left.size === 200).should.true; + sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => undefined); + renderHook(() => useActiveModalFrontstageInfo()); + act(() => { + sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => undefined); + FrontstageManager.onModalFrontstageChangedEvent.emit({ + modalFrontstageCount: 0, + }); + + sinon.stub(FrontstageManager, "activeModalFrontstage").get(() => modalStageInfo); + FrontstageManager.onModalFrontstageChangedEvent.emit({ + modalFrontstageCount: 1, + }); + }); + }); }); - it("should not modify when nineZoneState is not defined", () => { - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = undefined; - const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); - result.current({ - type: "PANEL_INITIALIZE", - side: "left", - size: 200, + describe("ActiveFrontstageDefProvider", () => { + before(async () => { + await TestUtils.initializeUiFramework(); }); - (frontstageDef.nineZoneState === undefined).should.true; - }); - it("should set nineZoneSize when RESIZE is received", () => { - const spy = sinon.stub(FrontstageManager, "nineZoneSize").set(() => { }); - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = createNineZoneState(); - const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); - result.current({ - type: "RESIZE", - size: { - width: 5, - height: 10, - }, - }); - spy.calledOnceWithExactly(sinon.match({ width: 5, height: 10 })); - }); + after(() => { + TestUtils.terminateUiFramework(); + }); - it("should set vertical (left/right) panel max size from percentage spec", () => { - const frontstageDef = new FrontstageDef(); - const panel = new StagePanelDef(); - sinon.stub(panel, "maxSizeSpec").get(() => ({ percentage: 50 })); - sinon.stub(frontstageDef, "leftPanel").get(() => panel); - frontstageDef.nineZoneState = createNineZoneState(); - const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); - result.current({ - type: "RESIZE", - size: { - height: 200, - width: 500, - }, - }); - frontstageDef.nineZoneState.panels.left.maxSize.should.eq(250); - }); + beforeEach(() => { + sinon.stub(FrontstageManager, "nineZoneSize").set(() => { }); + }); - it("should set horizontal (top/bottom) panel max size from percentage spec", () => { - const frontstageDef = new FrontstageDef(); - const panel = new StagePanelDef(); - sinon.stub(panel, "maxSizeSpec").get(() => ({ percentage: 50 })); - sinon.stub(frontstageDef, "topPanel").get(() => panel); - frontstageDef.nineZoneState = createNineZoneState(); - const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); - result.current({ - type: "RESIZE", - size: { - height: 200, - width: 500, - }, - }); - frontstageDef.nineZoneState.panels.top.maxSize.should.eq(100); - }); + it("should render", () => { + const frontstageDef = new FrontstageDef(); + const wrapper = shallow(); + wrapper.should.matchSnapshot(); + }); - it("should update panel size", () => { - const frontstageDef = new FrontstageDef(); - const panel = new StagePanelDef(); - sinon.stub(panel, "maxSizeSpec").get(() => 250); - sinon.stub(frontstageDef, "leftPanel").get(() => panel); - - let state = createNineZoneState(); - state = produce(state, (draft) => { - draft.panels.left.size = 300; - }); - frontstageDef.nineZoneState = state; - const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); - result.current({ - type: "RESIZE", - size: { - height: 200, - width: 500, - }, - }); - frontstageDef.nineZoneState.panels.left.size!.should.eq(250); - }); -}); + it("should fall back to cached NineZoneState", () => { + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = createNineZoneState(); -describe("useNineZoneState", () => { - it("should return initial nineZoneState", () => { - const frontstageDef = new FrontstageDef(); - const nineZoneState = createNineZoneState(); - frontstageDef.nineZoneState = nineZoneState; - const { result } = renderHook(() => useNineZoneState(frontstageDef)); - nineZoneState.should.eq(result.current); - }); + const newFrontstageDef = new FrontstageDef(); + newFrontstageDef.nineZoneState = undefined; - it("should return nineZoneState of provided frontstageDef", () => { - const frontstageDef = new FrontstageDef(); - const nineZoneState = createNineZoneState(); - frontstageDef.nineZoneState = nineZoneState; - const newFrontstageDef = new FrontstageDef(); - const newNineZoneState = createNineZoneState(); - newFrontstageDef.nineZoneState = newNineZoneState; - const { result, rerender } = renderHook((def: FrontstageDef) => useNineZoneState(def), { - initialProps: frontstageDef, - }); - rerender(newFrontstageDef); - newNineZoneState.should.eq(result.current); - }); + const wrapper = mount<{ frontstageDef: FrontstageDef }>(); + wrapper.setProps({ frontstageDef: newFrontstageDef }); - it("should return updated nineZoneState", () => { - const frontstageDef = new FrontstageDef(); - const nineZoneState = createNineZoneState(); - const newNineZoneState = createNineZoneState(); - frontstageDef.nineZoneState = nineZoneState; - const { result } = renderHook(() => useNineZoneState(frontstageDef)); - act(() => { - frontstageDef.nineZoneState = newNineZoneState; + const nineZone = wrapper.find(NineZone); + nineZone.prop("state").should.eq(frontstageDef.nineZoneState); }); - newNineZoneState.should.eq(result.current); }); - it("should ignore nineZoneState changes of other frontstages", () => { - const frontstageDef = new FrontstageDef(); - const nineZoneState = createNineZoneState(); - const newNineZoneState = createNineZoneState(); - frontstageDef.nineZoneState = nineZoneState; - const { result } = renderHook(() => useNineZoneState(frontstageDef)); - act(() => { - (new FrontstageDef()).nineZoneState = newNineZoneState; + describe("useNineZoneDispatch", () => { + beforeEach(() => { + sinon.stub(FrontstageManager, "nineZoneSize").set(() => { }); }); - nineZoneState.should.eq(result.current); - }); -}); -describe("useSavedFrontstageState", () => { - it("should load saved nineZoneState", async () => { - const setting = createFrontstageState(); - const uiSettings = new UiSettingsStub(); - sinon.stub(uiSettings, "getSetting").resolves({ - status: UiSettingsStatus.Success, - setting, - }); - const frontstageDef = new FrontstageDef(); - renderHook(() => useSavedFrontstageState(frontstageDef), { - wrapper: (props) => , + it("should modify nineZoneState with default NineZoneReducer", () => { + const frontstageDef = new FrontstageDef(); + const nineZoneState = createNineZoneState(); + frontstageDef.nineZoneState = nineZoneState; + const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); + result.current({ + type: "PANEL_INITIALIZE", + side: "left", + size: 200, + }); + frontstageDef.nineZoneState.should.not.eq(nineZoneState); + (frontstageDef.nineZoneState.panels.left.size === 200).should.true; }); - await TestUtils.flushAsyncOperations(); - frontstageDef.nineZoneState?.should.matchSnapshot(); - }); - it("should not load nineZoneState when nineZoneState is already initialized", async () => { - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = createNineZoneState(); - const uiSettings = new UiSettingsStub(); - const spy = sinon.spy(uiSettings, "getSetting"); - renderHook(() => useSavedFrontstageState(frontstageDef), { - wrapper: (props) => , + it("should not modify when nineZoneState is not defined", () => { + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = undefined; + const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); + result.current({ + type: "PANEL_INITIALIZE", + side: "left", + size: 200, + }); + (frontstageDef.nineZoneState === undefined).should.true; }); - spy.notCalled.should.true; - }); - it("should initialize nineZoneState", async () => { - const setting = createFrontstageState(); - const uiSettings = new UiSettingsStub(); - sinon.stub(uiSettings, "getSetting").returns(Promise.resolve({ - status: UiSettingsStatus.Success, - setting, - })); - const frontstageDef = new FrontstageDef(); - sinon.stub(frontstageDef, "version").get(() => setting.version + 1); - renderHook(() => useSavedFrontstageState(frontstageDef), { - wrapper: (props) => , - }); - await TestUtils.flushAsyncOperations(); - (frontstageDef.nineZoneState !== undefined).should.true; - frontstageDef.nineZoneState!.should.not.eq(setting.nineZone); - }); - - it("should add missing widgets", async () => { - const setting = createFrontstageState(); - const uiSettings = new UiSettingsStub(); - sinon.stub(uiSettings, "getSetting").resolves({ - status: UiSettingsStatus.Success, - setting, - }); - const frontstageDef = new FrontstageDef(); - const leftPanel = new StagePanelDef(); - leftPanel.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - }, StagePanelLocation.Left); - sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); - - renderHook(() => useSavedFrontstageState(frontstageDef), { - wrapper: (props) => , - }); - await TestUtils.flushAsyncOperations(); - - should().exist(frontstageDef.nineZoneState?.tabs.w1); - }); -}); - -describe("useSaveFrontstageSettings", () => { - it("should save frontstage settings", () => { - const fakeTimers = sinon.useFakeTimers(); - const uiSettings = new UiSettingsStub(); - const spy = sinon.stub(uiSettings, "saveSetting").resolves({ - status: UiSettingsStatus.Success, - }); - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = createNineZoneState(); - renderHook(() => useSaveFrontstageSettings(frontstageDef), { - wrapper: (props) => , + it("should set nineZoneSize when RESIZE is received", () => { + const spy = sinon.stub(FrontstageManager, "nineZoneSize").set(() => { }); + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = createNineZoneState(); + const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); + result.current({ + type: "RESIZE", + size: { + width: 5, + height: 10, + }, + }); + spy.calledOnceWithExactly(sinon.match({ width: 5, height: 10 })); }); - fakeTimers.tick(1000); - fakeTimers.restore(); - - spy.calledOnce.should.true; - }); - it("should not save if tab is dragged", () => { - const fakeTimers = sinon.useFakeTimers(); - const uiSettings = new UiSettingsStub(); - const spy = sinon.stub(uiSettings, "saveSetting").resolves({ - status: UiSettingsStatus.Success, - }); - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = produce(createNineZoneState(), (draft) => { - draft.draggedTab = createDraggedTabState("t1"); + it("should set vertical (left/right) panel max size from percentage spec", () => { + const frontstageDef = new FrontstageDef(); + const panel = new StagePanelDef(); + sinon.stub(panel, "maxSizeSpec").get(() => ({ percentage: 50 })); + sinon.stub(frontstageDef, "leftPanel").get(() => panel); + frontstageDef.nineZoneState = createNineZoneState(); + const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); + result.current({ + type: "RESIZE", + size: { + height: 200, + width: 500, + }, + }); + frontstageDef.nineZoneState.panels.left.maxSize.should.eq(250); }); - renderHook(() => useSaveFrontstageSettings(frontstageDef), { - wrapper: (props) => , + + it("should set horizontal (top/bottom) panel max size from percentage spec", () => { + const frontstageDef = new FrontstageDef(); + const panel = new StagePanelDef(); + sinon.stub(panel, "maxSizeSpec").get(() => ({ percentage: 50 })); + sinon.stub(frontstageDef, "topPanel").get(() => panel); + frontstageDef.nineZoneState = createNineZoneState(); + const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); + result.current({ + type: "RESIZE", + size: { + height: 200, + width: 500, + }, + }); + frontstageDef.nineZoneState.panels.top.maxSize.should.eq(100); }); - fakeTimers.tick(1000); - fakeTimers.restore(); - spy.notCalled.should.true; - }); -}); + it("should update panel size", () => { + const frontstageDef = new FrontstageDef(); + const panel = new StagePanelDef(); + sinon.stub(panel, "maxSizeSpec").get(() => 250); + sinon.stub(frontstageDef, "leftPanel").get(() => panel); -describe("useFrontstageManager", () => { - it("should not handle onWidgetStateChangedEvent when nineZoneState is unset", () => { - const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = undefined; - renderHook(() => useFrontstageManager(frontstageDef)); - const widgetDef = new WidgetDef({}); - FrontstageManager.onWidgetStateChangedEvent.emit({ - widgetDef, - widgetState: WidgetState.Open, - }); - (frontstageDef.nineZoneState === undefined).should.true; + let state = createNineZoneState(); + state = produce(state, (draft) => { + draft.panels.left.size = 300; + }); + frontstageDef.nineZoneState = state; + const { result } = renderHook(() => useNineZoneDispatch(frontstageDef)); + result.current({ + type: "RESIZE", + size: { + height: 200, + width: 500, + }, + }); + frontstageDef.nineZoneState.panels.left.size!.should.eq(250); + }); }); - it("should handle onWidgetStateChangedEvent", () => { - const frontstageDef = new FrontstageDef(); - let nineZoneState = createNineZoneState(); - nineZoneState = addPanelWidget(nineZoneState, "left", "w1", ["t1"]); - nineZoneState = addPanelWidget(nineZoneState, "left", "w2", ["t2"]); - nineZoneState = addTab(nineZoneState, "t1"); - frontstageDef.nineZoneState = nineZoneState; - renderHook(() => useFrontstageManager(frontstageDef)); - const widgetDef = new WidgetDef({ - id: "t1", - }); - FrontstageManager.onWidgetStateChangedEvent.emit({ - widgetDef, - widgetState: WidgetState.Closed, - }); - frontstageDef.nineZoneState.widgets.w1.minimized.should.true; - }); + describe("useNineZoneState", () => { + it("should return initial nineZoneState", () => { + const frontstageDef = new FrontstageDef(); + const nineZoneState = createNineZoneState(); + frontstageDef.nineZoneState = nineZoneState; + const { result } = renderHook(() => useNineZoneState(frontstageDef)); + nineZoneState.should.eq(result.current); + }); - it("should handle onWidgetShowEvent", () => { - const frontstageDef = new FrontstageDef(); - let nineZoneState = createNineZoneState(); - nineZoneState = addPanelWidget(nineZoneState, "left", "w1", ["t1"]); - nineZoneState = addTab(nineZoneState, "t1"); - nineZoneState = produce(nineZoneState, (draft) => { - draft.panels.left.collapsed = true; + it("should return nineZoneState of provided frontstageDef", () => { + const frontstageDef = new FrontstageDef(); + const nineZoneState = createNineZoneState(); + frontstageDef.nineZoneState = nineZoneState; + const newFrontstageDef = new FrontstageDef(); + const newNineZoneState = createNineZoneState(); + newFrontstageDef.nineZoneState = newNineZoneState; + const { result, rerender } = renderHook((def: FrontstageDef) => useNineZoneState(def), { + initialProps: frontstageDef, + }); + rerender(newFrontstageDef); + newNineZoneState.should.eq(result.current); }); - frontstageDef.nineZoneState = nineZoneState; - renderHook(() => useFrontstageManager(frontstageDef)); - const widgetDef = new WidgetDef({ - id: "t1", + + it("should return updated nineZoneState", () => { + const frontstageDef = new FrontstageDef(); + const nineZoneState = createNineZoneState(); + const newNineZoneState = createNineZoneState(); + frontstageDef.nineZoneState = nineZoneState; + const { result } = renderHook(() => useNineZoneState(frontstageDef)); + act(() => { + frontstageDef.nineZoneState = newNineZoneState; + }); + newNineZoneState.should.eq(result.current); }); - FrontstageManager.onWidgetShowEvent.emit({ - widgetDef, + + it("should ignore nineZoneState changes of other frontstages", () => { + const frontstageDef = new FrontstageDef(); + const nineZoneState = createNineZoneState(); + const newNineZoneState = createNineZoneState(); + frontstageDef.nineZoneState = nineZoneState; + const { result } = renderHook(() => useNineZoneState(frontstageDef)); + act(() => { + (new FrontstageDef()).nineZoneState = newNineZoneState; + }); + nineZoneState.should.eq(result.current); }); - frontstageDef.nineZoneState.panels.left.collapsed.should.false; }); - it("should handle onWidgetExpandEvent", () => { - const frontstageDef = new FrontstageDef(); - let nineZoneState = createNineZoneState(); - nineZoneState = addPanelWidget(nineZoneState, "left", "w1", ["t1"], { minimized: true }); - nineZoneState = addPanelWidget(nineZoneState, "left", "w2", ["t2"]); - nineZoneState = addTab(nineZoneState, "t1"); - frontstageDef.nineZoneState = nineZoneState; - renderHook(() => useFrontstageManager(frontstageDef)); - const widgetDef = new WidgetDef({ - id: "t1", - }); - FrontstageManager.onWidgetExpandEvent.emit({ - widgetDef, - }); - frontstageDef.nineZoneState.widgets.w1.minimized.should.false; - }); + describe("useSavedFrontstageState", () => { + it("should load saved nineZoneState", async () => { + const setting = createFrontstageState(); + const uiSettings = new UiSettingsStub(); + sinon.stub(uiSettings, "getSetting").resolves({ + status: UiSettingsStatus.Success, + setting, + }); + const frontstageDef = new FrontstageDef(); + renderHook(() => useSavedFrontstageState(frontstageDef), { + wrapper: (props) => , + }); + await TestUtils.flushAsyncOperations(); + frontstageDef.nineZoneState?.should.matchSnapshot(); + }); - describe("onFrontstageRestoreLayoutEvent", () => { - it("should delete saved setting", () => { + it("should not load nineZoneState when nineZoneState is already initialized", async () => { const frontstageDef = new FrontstageDef(); frontstageDef.nineZoneState = createNineZoneState(); const uiSettings = new UiSettingsStub(); - const spy = sinon.spy(uiSettings, "deleteSetting"); - renderHook(() => useFrontstageManager(frontstageDef), { - wrapper: (props) => , + const spy = sinon.spy(uiSettings, "getSetting"); + renderHook(() => useSavedFrontstageState(frontstageDef), { + wrapper: (props) => , + }); + spy.notCalled.should.true; + }); + + it("should initialize nineZoneState", async () => { + const setting = createFrontstageState(); + const uiSettings = new UiSettingsStub(); + sinon.stub(uiSettings, "getSetting").returns(Promise.resolve({ + status: UiSettingsStatus.Success, + setting, + })); + const frontstageDef = new FrontstageDef(); + sinon.stub(frontstageDef, "version").get(() => setting.version + 1); + renderHook(() => useSavedFrontstageState(frontstageDef), { + wrapper: (props) => , + }); + await TestUtils.flushAsyncOperations(); + (frontstageDef.nineZoneState !== undefined).should.true; + frontstageDef.nineZoneState!.should.not.eq(setting.nineZone); + }); + + it("should add missing widgets", async () => { + const setting = createFrontstageState(); + const uiSettings = new UiSettingsStub(); + sinon.stub(uiSettings, "getSetting").resolves({ + status: UiSettingsStatus.Success, + setting, }); - FrontstageManager.onFrontstageRestoreLayoutEvent.emit({ - frontstageDef, + const frontstageDef = new FrontstageDef(); + const leftPanel = new StagePanelDef(); + leftPanel.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + }, StagePanelLocation.Left); + sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); + + renderHook(() => useSavedFrontstageState(frontstageDef), { + wrapper: (props) => , }); - spy.calledOnce.should.true; + await TestUtils.flushAsyncOperations(); + + should().exist(frontstageDef.nineZoneState?.tabs.w1); }); + }); - it("should unset nineZoneState", () => { + describe("useSaveFrontstageSettings", () => { + it("should save frontstage settings", () => { + const fakeTimers = sinon.useFakeTimers(); + const uiSettings = new UiSettingsStub(); + const spy = sinon.stub(uiSettings, "saveSetting").resolves({ + status: UiSettingsStatus.Success, + }); const frontstageDef = new FrontstageDef(); frontstageDef.nineZoneState = createNineZoneState(); + renderHook(() => useSaveFrontstageSettings(frontstageDef), { + wrapper: (props) => , + }); + fakeTimers.tick(1000); + fakeTimers.restore(); + + spy.calledOnce.should.true; + }); + + it("should not save if tab is dragged", () => { + const fakeTimers = sinon.useFakeTimers(); const uiSettings = new UiSettingsStub(); - renderHook(() => useFrontstageManager(frontstageDef), { - wrapper: (props) => , + const spy = sinon.stub(uiSettings, "saveSetting").resolves({ + status: UiSettingsStatus.Success, }); - const frontstageDef1 = new FrontstageDef(); - sinon.stub(frontstageDef1, "id").get(() => "f1"); - frontstageDef1.nineZoneState = createNineZoneState(); - FrontstageManager.onFrontstageRestoreLayoutEvent.emit({ - frontstageDef: frontstageDef1, + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = produce(createNineZoneState(), (draft) => { + draft.draggedTab = createDraggedTabState("t1"); + }); + renderHook(() => useSaveFrontstageSettings(frontstageDef), { + wrapper: (props) => , }); - (frontstageDef1.nineZoneState === undefined).should.true; + fakeTimers.tick(1000); + fakeTimers.restore(); + + spy.notCalled.should.true; }); }); - describe("onWidgetLabelChangedEvent", () => { - it("should update tab label", () => { + describe("useFrontstageManager", () => { + it("should not handle onWidgetStateChangedEvent when nineZoneState is unset", () => { const frontstageDef = new FrontstageDef(); - let state = createNineZoneState(); - state = addPanelWidget(state, "left", "w1", ["t1"]); - state = addTab(state, "t1"); - frontstageDef.nineZoneState = state; - const widgetDef = new WidgetDef({ id: "t1" }); + frontstageDef.nineZoneState = undefined; renderHook(() => useFrontstageManager(frontstageDef)); - - sinon.stub(widgetDef, "label").get(() => "test"); - FrontstageManager.onWidgetLabelChangedEvent.emit({ + const widgetDef = new WidgetDef({}); + FrontstageManager.onWidgetStateChangedEvent.emit({ widgetDef, + widgetState: WidgetState.Open, }); + (frontstageDef.nineZoneState === undefined).should.true; + }); - frontstageDef.nineZoneState.tabs.t1.label.should.eq("test"); + it("should handle onWidgetStateChangedEvent", () => { + const frontstageDef = new FrontstageDef(); + let nineZoneState = createNineZoneState(); + nineZoneState = addPanelWidget(nineZoneState, "left", "w1", ["t1"]); + nineZoneState = addPanelWidget(nineZoneState, "left", "w2", ["t2"]); + nineZoneState = addTab(nineZoneState, "t1"); + frontstageDef.nineZoneState = nineZoneState; + renderHook(() => useFrontstageManager(frontstageDef)); + const widgetDef = new WidgetDef({ + id: "t1", + }); + FrontstageManager.onWidgetStateChangedEvent.emit({ + widgetDef, + widgetState: WidgetState.Closed, + }); + frontstageDef.nineZoneState.widgets.w1.minimized.should.true; }); - it("should not fail if tab doesn't exist", () => { + it("should handle onWidgetShowEvent", () => { const frontstageDef = new FrontstageDef(); - frontstageDef.nineZoneState = createNineZoneState(); - const widgetDef = new WidgetDef({ id: "t1" }); + let nineZoneState = createNineZoneState(); + nineZoneState = addPanelWidget(nineZoneState, "left", "w1", ["t1"]); + nineZoneState = addTab(nineZoneState, "t1"); + nineZoneState = produce(nineZoneState, (draft) => { + draft.panels.left.collapsed = true; + }); + frontstageDef.nineZoneState = nineZoneState; renderHook(() => useFrontstageManager(frontstageDef)); + const widgetDef = new WidgetDef({ + id: "t1", + }); + FrontstageManager.onWidgetShowEvent.emit({ + widgetDef, + }); + frontstageDef.nineZoneState.panels.left.collapsed.should.false; + }); - sinon.stub(widgetDef, "label").get(() => "test"); + it("should handle onWidgetExpandEvent", () => { + const frontstageDef = new FrontstageDef(); + let nineZoneState = createNineZoneState(); + nineZoneState = addPanelWidget(nineZoneState, "left", "w1", ["t1"], { minimized: true }); + nineZoneState = addPanelWidget(nineZoneState, "left", "w2", ["t2"]); + nineZoneState = addTab(nineZoneState, "t1"); + frontstageDef.nineZoneState = nineZoneState; + renderHook(() => useFrontstageManager(frontstageDef)); + const widgetDef = new WidgetDef({ + id: "t1", + }); + FrontstageManager.onWidgetExpandEvent.emit({ + widgetDef, + }); + frontstageDef.nineZoneState.widgets.w1.minimized.should.false; + }); + + describe("onFrontstageRestoreLayoutEvent", () => { + it("should delete saved setting", () => { + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = createNineZoneState(); + const uiSettings = new UiSettingsStub(); + const spy = sinon.spy(uiSettings, "deleteSetting"); + renderHook(() => useFrontstageManager(frontstageDef), { + wrapper: (props) => , + }); + FrontstageManager.onFrontstageRestoreLayoutEvent.emit({ + frontstageDef, + }); + spy.calledOnce.should.true; + }); - (() => { - FrontstageManager.onWidgetLabelChangedEvent.emit({ widgetDef }); - }).should.not.throw(); + it("should unset nineZoneState", () => { + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = createNineZoneState(); + const uiSettings = new UiSettingsStub(); + renderHook(() => useFrontstageManager(frontstageDef), { + wrapper: (props) => , + }); + const frontstageDef1 = new FrontstageDef(); + sinon.stub(frontstageDef1, "id").get(() => "f1"); + frontstageDef1.nineZoneState = createNineZoneState(); + FrontstageManager.onFrontstageRestoreLayoutEvent.emit({ + frontstageDef: frontstageDef1, + }); + (frontstageDef1.nineZoneState === undefined).should.true; + }); }); - }); -}); -describe("useSyncDefinitions", () => { - it("should set panel widget state to Open", () => { - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); - const widgetDef = new WidgetDef({}); - sinon.stub(widgetDef, "id").get(() => "t1"); - const spy = sinon.spy(widgetDef, "setWidgetState"); - zoneDef.addWidgetDef(widgetDef); - renderHook(() => useSyncDefinitions(frontstageDef)); - act(() => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - frontstageDef.nineZoneState = nineZone; - }); - spy.calledOnceWithExactly(WidgetState.Open).should.true; - }); + describe("onWidgetLabelChangedEvent", () => { + it("should update tab label", () => { + const frontstageDef = new FrontstageDef(); + let state = createNineZoneState(); + state = addPanelWidget(state, "left", "w1", ["t1"]); + state = addTab(state, "t1"); + frontstageDef.nineZoneState = state; + const widgetDef = new WidgetDef({ id: "t1" }); + renderHook(() => useFrontstageManager(frontstageDef)); - it("should set panel widget state to Closed", () => { - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); - const widgetDef = new WidgetDef({}); - sinon.stub(widgetDef, "id").get(() => "t1"); - const spy = sinon.spy(widgetDef, "setWidgetState"); - zoneDef.addWidgetDef(widgetDef); - renderHook(() => useSyncDefinitions(frontstageDef)); - act(() => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1", "t2"], { activeTabId: "t2" }); - nineZone = addTab(nineZone, "t1"); - nineZone = addTab(nineZone, "t2"); - frontstageDef.nineZoneState = nineZone; - }); - spy.calledOnceWithExactly(WidgetState.Closed).should.true; - }); + sinon.stub(widgetDef, "label").get(() => "test"); + FrontstageManager.onWidgetLabelChangedEvent.emit({ + widgetDef, + }); - it("should set StagePanelDef size", () => { - const frontstageDef = new FrontstageDef(); - const rightPanel = new StagePanelDef(); - sinon.stub(frontstageDef, "rightPanel").get(() => rightPanel); - const spy = sinon.spy(rightPanel, "size", ["set"]); - renderHook(() => useSyncDefinitions(frontstageDef)); - act(() => { - let nineZone = createNineZoneState(); - nineZone = produce(nineZone, (draft) => { - draft.panels.right.size = 234; + frontstageDef.nineZoneState.tabs.t1.label.should.eq("test"); + }); + + it("should not fail if tab doesn't exist", () => { + const frontstageDef = new FrontstageDef(); + frontstageDef.nineZoneState = createNineZoneState(); + const widgetDef = new WidgetDef({ id: "t1" }); + renderHook(() => useFrontstageManager(frontstageDef)); + + sinon.stub(widgetDef, "label").get(() => "test"); + + (() => { + FrontstageManager.onWidgetLabelChangedEvent.emit({ widgetDef }); + }).should.not.throw(); }); - frontstageDef.nineZoneState = nineZone; }); - sinon.assert.calledOnceWithExactly(spy.set, 234); }); - it("should set StagePanelState.Off", () => { - const frontstageDef = new FrontstageDef(); - const rightPanel = new StagePanelDef(); - const spy = sinon.spy(); - sinon.stub(rightPanel, "panelState").get(() => StagePanelState.Off).set(spy); - sinon.stub(frontstageDef, "rightPanel").get(() => rightPanel); - renderHook(() => useSyncDefinitions(frontstageDef)); - act(() => { - let nineZone = createNineZoneState(); - nineZone = produce(nineZone, (draft) => { - draft.panels.right.collapsed = true; + describe("useSyncDefinitions", () => { + it("should set panel widget state to Open", () => { + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); + const widgetDef = new WidgetDef({}); + sinon.stub(widgetDef, "id").get(() => "t1"); + const spy = sinon.spy(widgetDef, "setWidgetState"); + zoneDef.addWidgetDef(widgetDef); + renderHook(() => useSyncDefinitions(frontstageDef)); + act(() => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + frontstageDef.nineZoneState = nineZone; }); - frontstageDef.nineZoneState = nineZone; + spy.calledOnceWithExactly(WidgetState.Open).should.true; }); - sinon.assert.calledOnceWithExactly(spy, StagePanelState.Off); - }); - it("should set floating widget state to Open", () => { - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); - const widgetDef = new WidgetDef({}); - sinon.stub(widgetDef, "id").get(() => "t1"); - const spy = sinon.spy(widgetDef, "setWidgetState"); - zoneDef.addWidgetDef(widgetDef); - renderHook(() => useSyncDefinitions(frontstageDef)); - act(() => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - frontstageDef.nineZoneState = nineZone; + it("should set panel widget state to Closed", () => { + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); + const widgetDef = new WidgetDef({}); + sinon.stub(widgetDef, "id").get(() => "t1"); + const spy = sinon.spy(widgetDef, "setWidgetState"); + zoneDef.addWidgetDef(widgetDef); + renderHook(() => useSyncDefinitions(frontstageDef)); + act(() => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1", "t2"], { activeTabId: "t2" }); + nineZone = addTab(nineZone, "t1"); + nineZone = addTab(nineZone, "t2"); + frontstageDef.nineZoneState = nineZone; + }); + spy.calledOnceWithExactly(WidgetState.Closed).should.true; }); - spy.calledOnceWithExactly(WidgetState.Open).should.true; - }); - it("should set floating widget state to Closed", () => { - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); - const widgetDef = new WidgetDef({}); - sinon.stub(widgetDef, "id").get(() => "t1"); - const spy = sinon.spy(widgetDef, "setWidgetState"); - zoneDef.addWidgetDef(widgetDef); - renderHook(() => useSyncDefinitions(frontstageDef)); - act(() => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1", "t2"], undefined, { activeTabId: "t2" }); - nineZone = addTab(nineZone, "t1"); - frontstageDef.nineZoneState = nineZone; + it("should set StagePanelDef size", () => { + const frontstageDef = new FrontstageDef(); + const rightPanel = new StagePanelDef(); + sinon.stub(frontstageDef, "rightPanel").get(() => rightPanel); + const spy = sinon.spy(rightPanel, "size", ["set"]); + renderHook(() => useSyncDefinitions(frontstageDef)); + act(() => { + let nineZone = createNineZoneState(); + nineZone = produce(nineZone, (draft) => { + draft.panels.right.size = 234; + }); + frontstageDef.nineZoneState = nineZone; + }); + sinon.assert.calledOnceWithExactly(spy.set, 234); }); - spy.calledOnceWithExactly(WidgetState.Closed).should.true; - }); -}); -describe("initializeNineZoneState", () => { - it("should initialize widgets", () => { - const frontstageDef = new FrontstageDef(); - sinon.stub(frontstageDef, "centerLeft").get(() => new ZoneDef()); - sinon.stub(frontstageDef, "bottomLeft").get(() => new ZoneDef()); - sinon.stub(frontstageDef, "leftPanel").get(() => new StagePanelDef()); - sinon.stub(frontstageDef, "centerRight").get(() => new ZoneDef()); - sinon.stub(frontstageDef, "bottomRight").get(() => new ZoneDef()); - sinon.stub(frontstageDef, "rightPanel").get(() => new StagePanelDef()); - sinon.stub(frontstageDef, "topPanel").get(() => new StagePanelDef()); - sinon.stub(frontstageDef, "topMostPanel").get(() => new StagePanelDef()); - sinon.stub(frontstageDef, "bottomPanel").get(() => new StagePanelDef()); - sinon.stub(frontstageDef, "bottomMostPanel").get(() => new StagePanelDef()); - const state = initializeNineZoneState(frontstageDef); - state.should.matchSnapshot(); - }); + it("should set StagePanelState.Off", () => { + const frontstageDef = new FrontstageDef(); + const rightPanel = new StagePanelDef(); + const spy = sinon.spy(); + sinon.stub(rightPanel, "panelState").get(() => StagePanelState.Off).set(spy); + sinon.stub(frontstageDef, "rightPanel").get(() => rightPanel); + renderHook(() => useSyncDefinitions(frontstageDef)); + act(() => { + let nineZone = createNineZoneState(); + nineZone = produce(nineZone, (draft) => { + draft.panels.right.collapsed = true; + }); + frontstageDef.nineZoneState = nineZone; + }); + sinon.assert.calledOnceWithExactly(spy, StagePanelState.Off); + }); - it("should keep one widget open", () => { - const frontstageDef = new FrontstageDef(); - const centerLeft = new ZoneDef(); - const widgetDef = new WidgetDef({ - id: "w1", + it("should set floating widget state to Open", () => { + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); + const widgetDef = new WidgetDef({}); + sinon.stub(widgetDef, "id").get(() => "t1"); + const spy = sinon.spy(widgetDef, "setWidgetState"); + zoneDef.addWidgetDef(widgetDef); + renderHook(() => useSyncDefinitions(frontstageDef)); + act(() => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + frontstageDef.nineZoneState = nineZone; + }); + spy.calledOnceWithExactly(WidgetState.Open).should.true; }); - sinon.stub(frontstageDef, "centerLeft").get(() => centerLeft); - sinon.stub(centerLeft, "widgetDefs").get(() => [widgetDef]); - const state = initializeNineZoneState(frontstageDef); - state.widgets.leftStart.activeTabId.should.eq("w1"); - }); - it("should initialize size", () => { - sinon.stub(FrontstageManager, "nineZoneSize").get(() => new Size(10, 20)); - const frontstageDef = new FrontstageDef(); - const sut = initializeNineZoneState(frontstageDef); - sut.size.should.eql({ width: 10, height: 20 }); + it("should set floating widget state to Closed", () => { + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); + const widgetDef = new WidgetDef({}); + sinon.stub(widgetDef, "id").get(() => "t1"); + const spy = sinon.spy(widgetDef, "setWidgetState"); + zoneDef.addWidgetDef(widgetDef); + renderHook(() => useSyncDefinitions(frontstageDef)); + act(() => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1", "t2"], undefined, { activeTabId: "t2" }); + nineZone = addTab(nineZone, "t1"); + frontstageDef.nineZoneState = nineZone; + }); + spy.calledOnceWithExactly(WidgetState.Closed).should.true; + }); }); - it("should not initialize size", () => { - const frontstageDef = new FrontstageDef(); - const sut = initializeNineZoneState(frontstageDef); - sut.size.should.eql({ width: 0, height: 0 }); - }); + describe("initializeNineZoneState", () => { + it("should initialize widgets", () => { + const frontstageDef = new FrontstageDef(); + sinon.stub(frontstageDef, "centerLeft").get(() => new ZoneDef()); + sinon.stub(frontstageDef, "bottomLeft").get(() => new ZoneDef()); + sinon.stub(frontstageDef, "leftPanel").get(() => new StagePanelDef()); + sinon.stub(frontstageDef, "centerRight").get(() => new ZoneDef()); + sinon.stub(frontstageDef, "bottomRight").get(() => new ZoneDef()); + sinon.stub(frontstageDef, "rightPanel").get(() => new StagePanelDef()); + sinon.stub(frontstageDef, "topPanel").get(() => new StagePanelDef()); + sinon.stub(frontstageDef, "topMostPanel").get(() => new StagePanelDef()); + sinon.stub(frontstageDef, "bottomPanel").get(() => new StagePanelDef()); + sinon.stub(frontstageDef, "bottomMostPanel").get(() => new StagePanelDef()); + const state = initializeNineZoneState(frontstageDef); + state.should.matchSnapshot(); + }); + + it("should keep one widget open", () => { + const frontstageDef = new FrontstageDef(); + const centerLeft = new ZoneDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "centerLeft").get(() => centerLeft); + sinon.stub(centerLeft, "widgetDefs").get(() => [widgetDef]); + const state = initializeNineZoneState(frontstageDef); + state.widgets.leftStart.activeTabId.should.eq("w1"); + }); - it("should initialize preferredPanelWidgetSize of tool settings widget", () => { - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ - id: "w1", - preferredPanelSize: "fit-content", - }); - sinon.stub(frontstageDef, "topCenter").get(() => zoneDef); - sinon.stub(zoneDef, "getSingleWidgetDef").returns(widgetDef); - const sut = initializeNineZoneState(frontstageDef); - sut.tabs[toolSettingsTabId].preferredPanelWidgetSize!.should.eq("fit-content"); - }); + it("should initialize size", () => { + sinon.stub(FrontstageManager, "nineZoneSize").get(() => new Size(10, 20)); + const frontstageDef = new FrontstageDef(); + const sut = initializeNineZoneState(frontstageDef); + sut.size.should.eql({ width: 10, height: 20 }); + }); - it("should add panel zone widgets", () => { - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const start = new StagePanelZoneDef(); - const middle = new StagePanelZoneDef(); - const end = new StagePanelZoneDef(); - const w1 = new WidgetDef({ id: "w1" }); - const w2 = new WidgetDef({ id: "w2" }); - const w3 = new WidgetDef({ id: "w3" }); - sinon.stub(frontstageDef, "leftPanel").get(() => panelDef); - sinon.stub(panelDef.panelZones, "start").get(() => start); - sinon.stub(panelDef.panelZones, "middle").get(() => middle); - sinon.stub(panelDef.panelZones, "end").get(() => end); - sinon.stub(start, "widgetDefs").get(() => [w1]); - sinon.stub(middle, "widgetDefs").get(() => [w2]); - sinon.stub(end, "widgetDefs").get(() => [w3]); - const state = initializeNineZoneState(frontstageDef); - state.panels.left.widgets.should.eql(["leftStart", "leftMiddle", "leftEnd"]); - should().exist("w1"); - should().exist("w2"); - should().exist("w3"); - }); -}); + it("should not initialize size", () => { + const frontstageDef = new FrontstageDef(); + const sut = initializeNineZoneState(frontstageDef); + sut.size.should.eql({ width: 0, height: 0 }); + }); -describe("addPanelWidgets", () => { - it("should add widgets from panel zones", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const leftPanel = new StagePanelDef(); - const panelZones = new StagePanelZonesDef(); - const panelZone = new StagePanelZoneDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); - sinon.stub(leftPanel, "panelZones").get(() => panelZones); - sinon.stub(panelZones, "start").get(() => panelZone); - sinon.stub(panelZone, "widgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "left"); - state.panels.left.widgets[0].should.eq("leftStart"); - }); + it("should initialize preferredPanelWidgetSize of tool settings widget", () => { + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ + id: "w1", + preferredPanelSize: "fit-content", + }); + sinon.stub(frontstageDef, "topCenter").get(() => zoneDef); + sinon.stub(zoneDef, "getSingleWidgetDef").returns(widgetDef); + const sut = initializeNineZoneState(frontstageDef); + sut.tabs[toolSettingsTabId].preferredPanelWidgetSize!.should.eq("fit-content"); + }); - it("should add bottomLeft widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "bottomLeft").get(() => zoneDef); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "left"); - state.panels.left.widgets[0].should.eq("leftMiddle"); - state.widgets.leftMiddle.tabs.should.eql(["w1"]); - }); + it("should add panel zone widgets", () => { + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const start = new StagePanelZoneDef(); + const middle = new StagePanelZoneDef(); + const end = new StagePanelZoneDef(); + const w1 = new WidgetDef({ id: "w1" }); + const w2 = new WidgetDef({ id: "w2" }); + const w3 = new WidgetDef({ id: "w3" }); + sinon.stub(frontstageDef, "leftPanel").get(() => panelDef); + sinon.stub(panelDef.panelZones, "start").get(() => start); + sinon.stub(panelDef.panelZones, "middle").get(() => middle); + sinon.stub(panelDef.panelZones, "end").get(() => end); + sinon.stub(start, "widgetDefs").get(() => [w1]); + sinon.stub(middle, "widgetDefs").get(() => [w2]); + sinon.stub(end, "widgetDefs").get(() => [w3]); + const state = initializeNineZoneState(frontstageDef); + state.panels.left.widgets.should.eql(["leftStart", "leftMiddle", "leftEnd"]); + should().exist("w1"); + should().exist("w2"); + should().exist("w3"); + }); + }); + + describe("addPanelWidgets", () => { + it("should add widgets from panel zones", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const leftPanel = new StagePanelDef(); + const panelZones = new StagePanelZonesDef(); + const panelZone = new StagePanelZoneDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); + sinon.stub(leftPanel, "panelZones").get(() => panelZones); + sinon.stub(panelZones, "start").get(() => panelZone); + sinon.stub(panelZone, "widgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "left"); + state.panels.left.widgets[0].should.eq("leftStart"); + }); - it("should add centerRight widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "right"); - state.panels.right.widgets[0].should.eq("rightStart"); - state.widgets.rightStart.tabs.should.eql(["w1"]); - }); + it("should add bottomLeft widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "bottomLeft").get(() => zoneDef); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "left"); + state.panels.left.widgets[0].should.eq("leftMiddle"); + state.widgets.leftMiddle.tabs.should.eql(["w1"]); + }); - it("should add bottomRight widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "bottomRight").get(() => zoneDef); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "right"); - state.panels.right.widgets[0].should.eq("rightMiddle"); - state.widgets.rightMiddle.tabs.should.eql(["w1"]); - }); + it("should add centerRight widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "right"); + state.panels.right.widgets[0].should.eq("rightStart"); + state.widgets.rightStart.tabs.should.eql(["w1"]); + }); - it("should add leftPanel widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "leftPanel").get(() => panelDef); - sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "left"); - state.panels.left.widgets[0].should.eq("leftEnd"); - state.widgets.leftEnd.tabs.should.eql(["w1"]); - }); + it("should add bottomRight widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "bottomRight").get(() => zoneDef); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "right"); + state.panels.right.widgets[0].should.eq("rightMiddle"); + state.widgets.rightMiddle.tabs.should.eql(["w1"]); + }); - it("should add rightPanel widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "rightPanel").get(() => panelDef); - sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "right"); - state.panels.right.widgets[0].should.eq("rightEnd"); - state.widgets.rightEnd.tabs.should.eql(["w1"]); - }); + it("should add leftPanel widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "leftPanel").get(() => panelDef); + sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "left"); + state.panels.left.widgets[0].should.eq("leftEnd"); + state.widgets.leftEnd.tabs.should.eql(["w1"]); + }); - it("should add topPanel widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "topPanel").get(() => panelDef); - sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "top"); - state.panels.top.widgets[0].should.eq("topStart"); - state.widgets.topStart.tabs.should.eql(["w1"]); - }); + it("should add rightPanel widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "rightPanel").get(() => panelDef); + sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "right"); + state.panels.right.widgets[0].should.eq("rightEnd"); + state.widgets.rightEnd.tabs.should.eql(["w1"]); + }); - it("should add topMostPanel widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "topMostPanel").get(() => panelDef); - sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "top"); - state.panels.top.widgets[0].should.eq("topEnd"); - state.widgets.topEnd.tabs.should.eql(["w1"]); - }); + it("should add topPanel widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "topPanel").get(() => panelDef); + sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "top"); + state.panels.top.widgets[0].should.eq("topStart"); + state.widgets.topStart.tabs.should.eql(["w1"]); + }); - it("should add bottomPanel widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "bottomPanel").get(() => panelDef); - sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "bottom"); - state.panels.bottom.widgets[0].should.eq("bottomStart"); - state.widgets.bottomStart.tabs.should.eql(["w1"]); - }); + it("should add topMostPanel widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "topMostPanel").get(() => panelDef); + sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "top"); + state.panels.top.widgets[0].should.eq("topEnd"); + state.widgets.topEnd.tabs.should.eql(["w1"]); + }); - it("should add bottomMostPanel widgets", () => { - let state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - const widgetDef = new WidgetDef({ - id: "w1", - }); - sinon.stub(frontstageDef, "bottomMostPanel").get(() => panelDef); - sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); - state = addPanelWidgets(state, frontstageDef, "bottom"); - state.panels.bottom.widgets[0].should.eq("bottomEnd"); - state.widgets.bottomEnd.tabs.should.eql(["w1"]); - }); -}); + it("should add bottomPanel widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "bottomPanel").get(() => panelDef); + sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "bottom"); + state.panels.bottom.widgets[0].should.eq("bottomStart"); + state.widgets.bottomStart.tabs.should.eql(["w1"]); + }); -describe("initializePanel", () => { - it("should initialize max size", () => { - const state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const leftPanel = new StagePanelDef(); - sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); - sinon.stub(leftPanel, "maxSizeSpec").get(() => 100); - const sut = initializePanel(state, frontstageDef, "left"); - sut.panels.left.maxSize.should.eq(100); + it("should add bottomMostPanel widgets", () => { + let state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + const widgetDef = new WidgetDef({ + id: "w1", + }); + sinon.stub(frontstageDef, "bottomMostPanel").get(() => panelDef); + sinon.stub(panelDef, "panelWidgetDefs").get(() => [widgetDef]); + state = addPanelWidgets(state, frontstageDef, "bottom"); + state.panels.bottom.widgets[0].should.eq("bottomEnd"); + state.widgets.bottomEnd.tabs.should.eql(["w1"]); + }); }); - it("should initialize min size", () => { - const state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const leftPanel = new StagePanelDef(); - sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); - sinon.stub(leftPanel, "minSize").get(() => 50); - const sut = initializePanel(state, frontstageDef, "left"); - sut.panels.left.minSize.should.eq(50); - }); -}); + describe("initializePanel", () => { + it("should initialize max size", () => { + const state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const leftPanel = new StagePanelDef(); + sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); + sinon.stub(leftPanel, "maxSizeSpec").get(() => 100); + const sut = initializePanel(state, frontstageDef, "left"); + sut.panels.left.maxSize.should.eq(100); + }); -describe("addWidgets", () => { - it("should use widget label", () => { - let state = createNineZoneState(); - const widget = new WidgetDef({ - id: "w1", - label: "Widget 1", + it("should initialize min size", () => { + const state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const leftPanel = new StagePanelDef(); + sinon.stub(frontstageDef, "leftPanel").get(() => leftPanel); + sinon.stub(leftPanel, "minSize").get(() => 50); + const sut = initializePanel(state, frontstageDef, "left"); + sut.panels.left.minSize.should.eq(50); }); - state = addWidgets(state, [widget], "left", "leftStart"); - state.tabs.w1.label.should.eq("Widget 1"); }); - it("should activate tab based on widget state", () => { - let state = createNineZoneState(); - const widget = new WidgetDef({ - id: "w1", - defaultState: WidgetState.Open, + describe("addWidgets", () => { + it("should use widget label", () => { + let state = createNineZoneState(); + const widget = new WidgetDef({ + id: "w1", + label: "Widget 1", + }); + state = addWidgets(state, [widget], "left", "leftStart"); + state.tabs.w1.label.should.eq("Widget 1"); }); - state = addWidgets(state, [widget], "left", "leftStart"); - state.widgets.leftStart.activeTabId.should.eq("w1"); - }); -}); -describe("getWidgetId", () => { - it("should return 'leftStart'", () => { - getWidgetId("left", "start").should.eq("leftStart"); + it("should activate tab based on widget state", () => { + let state = createNineZoneState(); + const widget = new WidgetDef({ + id: "w1", + defaultState: WidgetState.Open, + }); + state = addWidgets(state, [widget], "left", "leftStart"); + state.widgets.leftStart.activeTabId.should.eq("w1"); + }); }); - it("should return 'leftMiddle'", () => { - getWidgetId("left", "middle").should.eq("leftMiddle"); - }); + describe("getWidgetId", () => { + it("should return 'leftStart'", () => { + getWidgetId("left", "start").should.eq("leftStart"); + }); - it("should return 'leftEnd'", () => { - getWidgetId("left", "end").should.eq("leftEnd"); - }); + it("should return 'leftMiddle'", () => { + getWidgetId("left", "middle").should.eq("leftMiddle"); + }); - it("should return 'rightStart'", () => { - getWidgetId("right", "start").should.eq("rightStart"); - }); + it("should return 'leftEnd'", () => { + getWidgetId("left", "end").should.eq("leftEnd"); + }); - it("should return 'rightMiddle'", () => { - getWidgetId("right", "middle").should.eq("rightMiddle"); - }); + it("should return 'rightStart'", () => { + getWidgetId("right", "start").should.eq("rightStart"); + }); - it("should return 'rightEnd'", () => { - getWidgetId("right", "end").should.eq("rightEnd"); - }); + it("should return 'rightMiddle'", () => { + getWidgetId("right", "middle").should.eq("rightMiddle"); + }); - it("should return 'topStart'", () => { - getWidgetId("top", "start").should.eq("topStart"); - }); + it("should return 'rightEnd'", () => { + getWidgetId("right", "end").should.eq("rightEnd"); + }); - it("should return 'topEnd'", () => { - getWidgetId("top", "end").should.eq("topEnd"); - }); + it("should return 'topStart'", () => { + getWidgetId("top", "start").should.eq("topStart"); + }); - it("should return 'bottomStart'", () => { - getWidgetId("bottom", "start").should.eq("bottomStart"); - }); + it("should return 'topEnd'", () => { + getWidgetId("top", "end").should.eq("topEnd"); + }); - it("should return 'bottomEnd'", () => { - getWidgetId("bottom", "end").should.eq("bottomEnd"); - }); -}); + it("should return 'bottomStart'", () => { + getWidgetId("bottom", "start").should.eq("bottomStart"); + }); -describe("isFrontstageStateSettingResult", () => { - it("isFrontstageStateSettingResult", () => { - isFrontstageStateSettingResult({ status: UiSettingsStatus.UnknownError }).should.false; + it("should return 'bottomEnd'", () => { + getWidgetId("bottom", "end").should.eq("bottomEnd"); + }); }); -}); -describe("setWidgetState", () => { - it("should not update for other states", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Floating); - sut.should.eq(nineZone); + describe("isFrontstageStateSettingResult", () => { + it("isFrontstageStateSettingResult", () => { + isFrontstageStateSettingResult({ status: UiSettingsStatus.UnknownError }).should.false; + }); }); - describe("WidgetState.Open", () => { - it("should open widget", () => { + describe("setWidgetState", () => { + it("should not update for other states", () => { let nineZone = createNineZoneState(); nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Open); - sut.widgets.w1.activeTabId.should.eq("t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Floating); + sut.should.eq(nineZone); }); - it("should add removed tab", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t2" }), WidgetState.Open); - sut.panels.left.widgets.length.should.eq(2); - }); + describe("WidgetState.Open", () => { + it("should open widget", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Open); + sut.widgets.w1.activeTabId.should.eq("t1"); + }); - it("should add removed tab by existing widget", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addPanelWidget(nineZone, "left", "w2", ["t2"]); - nineZone = addTab(nineZone, "t1"); - nineZone = addTab(nineZone, "t2"); - const widgetDef = new WidgetDef({ id: "t3" }); - widgetDef.tabLocation = { - ...widgetDef.tabLocation, - widgetId: "w2", - }; - const sut = setWidgetState(nineZone, widgetDef, WidgetState.Open); - sut.widgets.w2.tabs.should.eql(["t3", "t2"]); - }); + it("should add removed tab", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t2" }), WidgetState.Open); + sut.panels.left.widgets.length.should.eq(2); + }); - it("should add removed tab to existing panel widget", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addPanelWidget(nineZone, "left", "w2", ["t2_1", "t2_2", "t2_3"]); - nineZone = addPanelWidget(nineZone, "left", "w3", ["t3"]); - nineZone = addTab(nineZone, "t1"); - nineZone = addTab(nineZone, "t2_1"); - nineZone = addTab(nineZone, "t2_2"); - nineZone = addTab(nineZone, "t2_3"); - nineZone = addTab(nineZone, "t3"); - const widgetDef = new WidgetDef({ id: "t4" }); - widgetDef.tabLocation = { - ...widgetDef.tabLocation, - widgetIndex: 1, - tabIndex: 2, - }; - const sut = setWidgetState(nineZone, widgetDef, WidgetState.Open); - sut.widgets.w2.tabs.should.eql(["t2_1", "t2_2", "t4", "t2_3"]); - }); - }); + it("should add removed tab by existing widget", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addPanelWidget(nineZone, "left", "w2", ["t2"]); + nineZone = addTab(nineZone, "t1"); + nineZone = addTab(nineZone, "t2"); + const widgetDef = new WidgetDef({ id: "t3" }); + widgetDef.tabLocation = { + ...widgetDef.tabLocation, + widgetId: "w2", + }; + const sut = setWidgetState(nineZone, widgetDef, WidgetState.Open); + sut.widgets.w2.tabs.should.eql(["t3", "t2"]); + }); - describe("WidgetState.Closed", () => { - it("should not minimize if tab is not active", () => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1", "t2"], undefined, { activeTabId: "t2" }); - nineZone = addTab(nineZone, "t1"); - nineZone = addTab(nineZone, "t2"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); - sut.should.eq(nineZone); + it("should add removed tab to existing panel widget", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addPanelWidget(nineZone, "left", "w2", ["t2_1", "t2_2", "t2_3"]); + nineZone = addPanelWidget(nineZone, "left", "w3", ["t3"]); + nineZone = addTab(nineZone, "t1"); + nineZone = addTab(nineZone, "t2_1"); + nineZone = addTab(nineZone, "t2_2"); + nineZone = addTab(nineZone, "t2_3"); + nineZone = addTab(nineZone, "t3"); + const widgetDef = new WidgetDef({ id: "t4" }); + widgetDef.tabLocation = { + ...widgetDef.tabLocation, + widgetIndex: 1, + tabIndex: 2, + }; + const sut = setWidgetState(nineZone, widgetDef, WidgetState.Open); + sut.widgets.w2.tabs.should.eql(["t2_1", "t2_2", "t4", "t2_3"]); + }); }); - it("should minimize floating widget", () => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); - sut.widgets.w1.minimized.should.true; - }); + describe("WidgetState.Closed", () => { + it("should not minimize if tab is not active", () => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1", "t2"], undefined, { activeTabId: "t2" }); + nineZone = addTab(nineZone, "t1"); + nineZone = addTab(nineZone, "t2"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); + sut.should.eq(nineZone); + }); - it("should minimize panel widget", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addPanelWidget(nineZone, "left", "w2", ["t2"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); - sut.widgets.w1.minimized.should.true; - }); + it("should minimize floating widget", () => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); + sut.widgets.w1.minimized.should.true; + }); - it("should not minimize single panel widget", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); - sut.widgets.w1.minimized.should.false; + it("should minimize panel widget", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addPanelWidget(nineZone, "left", "w2", ["t2"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); + sut.widgets.w1.minimized.should.true; + }); + + it("should not minimize single panel widget", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Closed); + sut.widgets.w1.minimized.should.false; + }); + + it("should add removed tab", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t2" }), WidgetState.Closed); + sut.panels.left.widgets.length.should.eq(2); + }); }); - it("should add removed tab", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t2" }), WidgetState.Closed); - sut.panels.left.widgets.length.should.eq(2); + describe("WidgetState.Hidden", () => { + it("should not update if tab is not found", () => { + const nineZone = createNineZoneState(); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Hidden); + sut.should.eq(nineZone); + }); + + it("should hide the widget", () => { + let nineZone = createNineZoneState(); + nineZone = addPanelWidget(nineZone, "left", "w1", ["t1", "t2"]); + nineZone = addTab(nineZone, "t1"); + const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Hidden); + sut.widgets.w1.tabs.should.eql(["t2"]); + }); + + it("should use default panel side for a floating widget", () => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const widgetDef = new WidgetDef({ id: "t1" }); + setWidgetState(nineZone, widgetDef, WidgetState.Hidden); + widgetDef.tabLocation.side.should.eq("left"); + widgetDef.tabLocation.widgetIndex.should.eq(0); + }); }); + }); - describe("WidgetState.Hidden", () => { + describe("showWidget ", () => { it("should not update if tab is not found", () => { const nineZone = createNineZoneState(); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Hidden); + const sut = showWidget(nineZone, "t1"); sut.should.eq(nineZone); }); - it("should hide the widget", () => { - let nineZone = createNineZoneState(); - nineZone = addPanelWidget(nineZone, "left", "w1", ["t1", "t2"]); - nineZone = addTab(nineZone, "t1"); - const sut = setWidgetState(nineZone, new WidgetDef({ id: "t1" }), WidgetState.Hidden); - sut.widgets.w1.tabs.should.eql(["t2"]); - }); - - it("should use default panel side for a floating widget", () => { + it("should bring floating widget to front", () => { let nineZone = createNineZoneState(); nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); + nineZone = addFloatingWidget(nineZone, "w2", ["t2"]); nineZone = addTab(nineZone, "t1"); - const widgetDef = new WidgetDef({ id: "t1" }); - setWidgetState(nineZone, widgetDef, WidgetState.Hidden); - widgetDef.tabLocation.side.should.eq("left"); - widgetDef.tabLocation.widgetIndex.should.eq(0); + const sut = showWidget(nineZone, "t1"); + sut.floatingWidgets.allIds[0].should.eq("w2"); + sut.floatingWidgets.allIds[1].should.eq("w1"); }); }); -}); - -describe("showWidget ", () => { - it("should not update if tab is not found", () => { - const nineZone = createNineZoneState(); - const sut = showWidget(nineZone, "t1"); - sut.should.eq(nineZone); - }); + describe("expandWidget ", () => { + it("should not update if tab is not found", () => { + const nineZone = createNineZoneState(); + const sut = expandWidget(nineZone, "t1"); + sut.should.eq(nineZone); + }); - it("should bring floating widget to front", () => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); - nineZone = addFloatingWidget(nineZone, "w2", ["t2"]); - nineZone = addTab(nineZone, "t1"); - const sut = showWidget(nineZone, "t1"); - sut.floatingWidgets.allIds[0].should.eq("w2"); - sut.floatingWidgets.allIds[1].should.eq("w1"); + it("should expand floating widget", () => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1"], undefined, { minimized: true }); + nineZone = addTab(nineZone, "t1"); + const sut = expandWidget(nineZone, "t1"); + sut.widgets.w1.minimized.should.false; + }); }); -}); -describe("expandWidget ", () => { - it("should not update if tab is not found", () => { - const nineZone = createNineZoneState(); - const sut = expandWidget(nineZone, "t1"); - sut.should.eq(nineZone); - }); + describe("restoreNineZoneState", () => { + it("should log error if widgetDef is not found", () => { + const spy = sinon.spy(Logger, "logError"); + const frontstageDef = new FrontstageDef(); + const savedState = { + ...createSavedNineZoneState(), + tabs: { + t1: createSavedTabState("t1"), + }, + }; + restoreNineZoneState(frontstageDef, savedState); + spy.calledOnce.should.true; + spy.firstCall.args[2]!().should.matchSnapshot(); + }); - it("should expand floating widget", () => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1"], undefined, { minimized: true }); - nineZone = addTab(nineZone, "t1"); - const sut = expandWidget(nineZone, "t1"); - sut.widgets.w1.minimized.should.false; - }); -}); + it("should remove tab if widgetDef is not found", () => { + const frontstageDef = new FrontstageDef(); + sinon.stub(frontstageDef, "findWidgetDef").withArgs("t2").returns(new WidgetDef({})); + let state = createNineZoneState(); + state = addPanelWidget(state, "left", "w1", ["t1", "t2"]); + state = addTab(state, "t1"); + state = addTab(state, "t2"); + const savedState = { + ...createSavedNineZoneState(state), + tabs: { + t1: createSavedTabState("t1"), + t2: createSavedTabState("t2"), + }, + }; + const newState = restoreNineZoneState(frontstageDef, savedState); + (newState.tabs.t1 === undefined).should.true; + newState.widgets.w1.tabs.indexOf("t1").should.eq(-1); + newState.widgets.w1.tabs.indexOf("t2").should.eq(0); + }); -describe("restoreNineZoneState", () => { - it("should log error if widgetDef is not found", () => { - const spy = sinon.spy(Logger, "logError"); - const frontstageDef = new FrontstageDef(); - const savedState = { - ...createSavedNineZoneState(), - tabs: { - t1: createSavedTabState("t1"), - }, - }; - restoreNineZoneState(frontstageDef, savedState); - spy.calledOnce.should.true; - spy.firstCall.args[2]!().should.matchSnapshot(); - }); + it("should restore tabs", () => { + const frontstageDef = new FrontstageDef(); + const widgetDef = new WidgetDef({}); + sinon.stub(frontstageDef, "findWidgetDef").returns(widgetDef); + const savedState = { + ...createSavedNineZoneState(), + tabs: { + t1: createSavedTabState("t1"), + }, + }; + const sut = restoreNineZoneState(frontstageDef, savedState); + sut.should.matchSnapshot(); + }); - it("should remove tab if widgetDef is not found", () => { - const frontstageDef = new FrontstageDef(); - sinon.stub(frontstageDef, "findWidgetDef").withArgs("t2").returns(new WidgetDef({})); - let state = createNineZoneState(); - state = addPanelWidget(state, "left", "w1", ["t1", "t2"]); - state = addTab(state, "t1"); - state = addTab(state, "t2"); - const savedState = { - ...createSavedNineZoneState(state), - tabs: { - t1: createSavedTabState("t1"), - t2: createSavedTabState("t2"), - }, - }; - const newState = restoreNineZoneState(frontstageDef, savedState); - (newState.tabs.t1 === undefined).should.true; - newState.widgets.w1.tabs.indexOf("t1").should.eq(-1); - newState.widgets.w1.tabs.indexOf("t2").should.eq(0); - }); + it("should RESIZE", () => { + sinon.stub(FrontstageManager, "nineZoneSize").get(() => new Size(10, 20)); + const frontstageDef = new FrontstageDef(); + const savedState = { + ...createSavedNineZoneState({ + size: { + width: 1, + height: 2, + }, + }), + tabs: { + t1: createSavedTabState("t1"), + }, + }; - it("should restore tabs", () => { - const frontstageDef = new FrontstageDef(); - const widgetDef = new WidgetDef({}); - sinon.stub(frontstageDef, "findWidgetDef").returns(widgetDef); - const savedState = { - ...createSavedNineZoneState(), - tabs: { - t1: createSavedTabState("t1"), - }, - }; - const sut = restoreNineZoneState(frontstageDef, savedState); - sut.should.matchSnapshot(); - }); + const sut = restoreNineZoneState(frontstageDef, savedState); + sut.size.should.eql({ width: 10, height: 20 }); + }); - it("should RESIZE", () => { - sinon.stub(FrontstageManager, "nineZoneSize").get(() => new Size(10, 20)); - const frontstageDef = new FrontstageDef(); - const savedState = { - ...createSavedNineZoneState({ - size: { - width: 1, - height: 2, + it("should not RESIZE", () => { + const frontstageDef = new FrontstageDef(); + const savedState = { + ...createSavedNineZoneState({ + size: { + width: 1, + height: 2, + }, + }), + tabs: { + t1: createSavedTabState("t1"), }, - }), - tabs: { - t1: createSavedTabState("t1"), - }, - }; - - const sut = restoreNineZoneState(frontstageDef, savedState); - sut.size.should.eql({ width: 10, height: 20 }); - }); + }; - it("should not RESIZE", () => { - const frontstageDef = new FrontstageDef(); - const savedState = { - ...createSavedNineZoneState({ - size: { - width: 1, - height: 2, - }, - }), - tabs: { - t1: createSavedTabState("t1"), - }, - }; - - const sut = restoreNineZoneState(frontstageDef, savedState); - sut.size.should.eql({ width: 1, height: 2 }); + const sut = restoreNineZoneState(frontstageDef, savedState); + sut.size.should.eql({ width: 1, height: 2 }); + }); }); -}); -describe("packNineZoneState", () => { - it("should remove labels", () => { - let nineZone = createNineZoneState(); - nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); - nineZone = addTab(nineZone, "t1"); - const sut = packNineZoneState(nineZone); - sut.should.matchSnapshot(); + describe("packNineZoneState", () => { + it("should remove labels", () => { + let nineZone = createNineZoneState(); + nineZone = addFloatingWidget(nineZone, "w1", ["t1"]); + nineZone = addTab(nineZone, "t1"); + const sut = packNineZoneState(nineZone); + sut.should.matchSnapshot(); + }); }); -}); - -describe("useUpdateNineZoneSize", () => { - it("should update size of nine zone state when new frontstage is activated", () => { - const { rerender } = renderHook((props) => useUpdateNineZoneSize(props), { initialProps: new FrontstageDef() }); - const newFrontstageDef = new FrontstageDef(); - newFrontstageDef.nineZoneState = createNineZoneState(); + describe("useUpdateNineZoneSize", () => { + it("should update size of nine zone state when new frontstage is activated", () => { + const { rerender } = renderHook((props) => useUpdateNineZoneSize(props), { initialProps: new FrontstageDef() }); - sinon.stub(FrontstageManager, "nineZoneSize").get(() => new Size(10, 20)); - rerender(newFrontstageDef); + const newFrontstageDef = new FrontstageDef(); + newFrontstageDef.nineZoneState = createNineZoneState(); - newFrontstageDef.nineZoneState.size.should.eql({ width: 10, height: 20 }); - }); + sinon.stub(FrontstageManager, "nineZoneSize").get(() => new Size(10, 20)); + rerender(newFrontstageDef); - it("should not update size if FrontstageManager.nineZoneSize is not initialized", () => { - const { rerender } = renderHook((props) => useUpdateNineZoneSize(props), { initialProps: new FrontstageDef() }); + newFrontstageDef.nineZoneState.size.should.eql({ width: 10, height: 20 }); + }); - const newFrontstageDef = new FrontstageDef(); - newFrontstageDef.nineZoneState = createNineZoneState({ size: { height: 1, width: 2 } }); + it("should not update size if FrontstageManager.nineZoneSize is not initialized", () => { + const { rerender } = renderHook((props) => useUpdateNineZoneSize(props), { initialProps: new FrontstageDef() }); - rerender(newFrontstageDef); + const newFrontstageDef = new FrontstageDef(); + newFrontstageDef.nineZoneState = createNineZoneState({ size: { height: 1, width: 2 } }); - newFrontstageDef.nineZoneState.size.should.eql({ height: 1, width: 2 }); - }); -}); + rerender(newFrontstageDef); -describe("addMissingWidgets", () => { - it("should add centerLeft widgets", () => { - const state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ id: "w1" }); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - sinon.stub(frontstageDef, "centerLeft").get(() => zoneDef); - const newState = addMissingWidgets(frontstageDef, state); - should().exist(newState.tabs.w1); + newFrontstageDef.nineZoneState.size.should.eql({ height: 1, width: 2 }); + }); }); - it("should add bottomLeft widgets", () => { - const state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ id: "w1" }); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - sinon.stub(frontstageDef, "bottomLeft").get(() => zoneDef); - const newState = addMissingWidgets(frontstageDef, state); - should().exist(newState.tabs.w1); - }); + describe("addMissingWidgets", () => { + it("should add centerLeft widgets", () => { + const state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ id: "w1" }); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + sinon.stub(frontstageDef, "centerLeft").get(() => zoneDef); + const newState = addMissingWidgets(frontstageDef, state); + should().exist(newState.tabs.w1); + }); - it("should add centerRight widgets", () => { - const state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ id: "w1" }); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); - const newState = addMissingWidgets(frontstageDef, state); - should().exist(newState.tabs.w1); - }); + it("should add bottomLeft widgets", () => { + const state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ id: "w1" }); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + sinon.stub(frontstageDef, "bottomLeft").get(() => zoneDef); + const newState = addMissingWidgets(frontstageDef, state); + should().exist(newState.tabs.w1); + }); - it("should add bottomRight widgets", () => { - const state = createNineZoneState(); - const frontstageDef = new FrontstageDef(); - const zoneDef = new ZoneDef(); - const widgetDef = new WidgetDef({ id: "w1" }); - sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); - sinon.stub(frontstageDef, "bottomRight").get(() => zoneDef); - const newState = addMissingWidgets(frontstageDef, state); - should().exist(newState.tabs.w1); - }); + it("should add centerRight widgets", () => { + const state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ id: "w1" }); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + sinon.stub(frontstageDef, "centerRight").get(() => zoneDef); + const newState = addMissingWidgets(frontstageDef, state); + should().exist(newState.tabs.w1); + }); - it("should add leftPanel widgets", () => { - let state = createNineZoneState(); - state = addPanelWidget(state, "left", "start", ["start1"]); - state = addPanelWidget(state, "left", "middle", ["middle1"]); - state = addPanelWidget(state, "left", "end", ["end1"]); - state = addTab(state, "start1"); - state = addTab(state, "middle1"); - state = addTab(state, "end1"); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - panelDef.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - panelZones: { - start: { - widgets: [ - , - ], - }, - middle: { - widgets: [ - , - ], - }, - end: { - widgets: [ - , - ], - }, - }, - }, StagePanelLocation.Left); - sinon.stub(frontstageDef, "leftPanel").get(() => panelDef); - const newState = addMissingWidgets(frontstageDef, state); - newState.widgets.start.tabs.should.eql(["start1", "ws1"]); - newState.widgets.middle.tabs.should.eql(["middle1", "wm1"]); - newState.widgets.end.tabs.should.eql(["end1", "w1", "we1"]); - }); + it("should add bottomRight widgets", () => { + const state = createNineZoneState(); + const frontstageDef = new FrontstageDef(); + const zoneDef = new ZoneDef(); + const widgetDef = new WidgetDef({ id: "w1" }); + sinon.stub(zoneDef, "widgetDefs").get(() => [widgetDef]); + sinon.stub(frontstageDef, "bottomRight").get(() => zoneDef); + const newState = addMissingWidgets(frontstageDef, state); + should().exist(newState.tabs.w1); + }); - it("should add rightPanel widgets", () => { - let state = createNineZoneState(); - state = addPanelWidget(state, "right", "start", ["start1"]); - state = addPanelWidget(state, "right", "middle", ["middle1"]); - state = addPanelWidget(state, "right", "end", ["end1"]); - state = addTab(state, "start1"); - state = addTab(state, "middle1"); - state = addTab(state, "end1"); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - panelDef.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - panelZones: { - start: { - widgets: [ - , - ], - }, - middle: { - widgets: [ - , - ], - }, - end: { - widgets: [ - , - ], + it("should add leftPanel widgets", () => { + let state = createNineZoneState(); + state = addPanelWidget(state, "left", "start", ["start1"]); + state = addPanelWidget(state, "left", "middle", ["middle1"]); + state = addPanelWidget(state, "left", "end", ["end1"]); + state = addTab(state, "start1"); + state = addTab(state, "middle1"); + state = addTab(state, "end1"); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + panelDef.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + panelZones: { + start: { + widgets: [ + , + ], + }, + middle: { + widgets: [ + , + ], + }, + end: { + widgets: [ + , + ], + }, }, - }, - }, StagePanelLocation.Right); - sinon.stub(frontstageDef, "rightPanel").get(() => panelDef); - const newState = addMissingWidgets(frontstageDef, state); - newState.widgets.start.tabs.should.eql(["start1", "ws1"]); - newState.widgets.middle.tabs.should.eql(["middle1", "wm1"]); - newState.widgets.end.tabs.should.eql(["end1", "w1", "we1"]); - }); + }, StagePanelLocation.Left); + sinon.stub(frontstageDef, "leftPanel").get(() => panelDef); + const newState = addMissingWidgets(frontstageDef, state); + newState.widgets.start.tabs.should.eql(["start1", "ws1"]); + newState.widgets.middle.tabs.should.eql(["middle1", "wm1"]); + newState.widgets.end.tabs.should.eql(["end1", "w1", "we1"]); + }); - it("should add topPanel widgets", () => { - let state = createNineZoneState(); - state = addPanelWidget(state, "top", "start", ["start1"]); - state = addPanelWidget(state, "top", "end", ["end1"]); - state = addTab(state, "start1"); - state = addTab(state, "end1"); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - panelDef.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - panelZones: { - start: { - widgets: [ - , - ], - }, - end: { - widgets: [ - , - ], + it("should add rightPanel widgets", () => { + let state = createNineZoneState(); + state = addPanelWidget(state, "right", "start", ["start1"]); + state = addPanelWidget(state, "right", "middle", ["middle1"]); + state = addPanelWidget(state, "right", "end", ["end1"]); + state = addTab(state, "start1"); + state = addTab(state, "middle1"); + state = addTab(state, "end1"); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + panelDef.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + panelZones: { + start: { + widgets: [ + , + ], + }, + middle: { + widgets: [ + , + ], + }, + end: { + widgets: [ + , + ], + }, }, - }, - }, StagePanelLocation.Top); - const panelDef1 = new StagePanelDef(); - panelDef1.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - }, StagePanelLocation.TopMost); - sinon.stub(frontstageDef, "topPanel").get(() => panelDef); - sinon.stub(frontstageDef, "topMostPanel").get(() => panelDef1); - const newState = addMissingWidgets(frontstageDef, state); - newState.widgets.start.tabs.should.eql(["start1", "w1", "ws1"]); - newState.widgets.end.tabs.should.eql(["end1", "w2", "we1"]); - }); + }, StagePanelLocation.Right); + sinon.stub(frontstageDef, "rightPanel").get(() => panelDef); + const newState = addMissingWidgets(frontstageDef, state); + newState.widgets.start.tabs.should.eql(["start1", "ws1"]); + newState.widgets.middle.tabs.should.eql(["middle1", "wm1"]); + newState.widgets.end.tabs.should.eql(["end1", "w1", "we1"]); + }); - it("should add bottomPanel widgets", () => { - let state = createNineZoneState(); - state = addPanelWidget(state, "bottom", "start", ["start1"]); - state = addPanelWidget(state, "bottom", "end", ["end1"]); - state = addTab(state, "start1"); - state = addTab(state, "end1"); - const frontstageDef = new FrontstageDef(); - const panelDef = new StagePanelDef(); - panelDef.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - panelZones: { - start: { - widgets: [ - , - ], + it("should add topPanel widgets", () => { + let state = createNineZoneState(); + state = addPanelWidget(state, "top", "start", ["start1"]); + state = addPanelWidget(state, "top", "end", ["end1"]); + state = addTab(state, "start1"); + state = addTab(state, "end1"); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + panelDef.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + panelZones: { + start: { + widgets: [ + , + ], + }, + end: { + widgets: [ + , + ], + }, }, - end: { - widgets: [ - , - ], + }, StagePanelLocation.Top); + const panelDef1 = new StagePanelDef(); + panelDef1.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + }, StagePanelLocation.TopMost); + sinon.stub(frontstageDef, "topPanel").get(() => panelDef); + sinon.stub(frontstageDef, "topMostPanel").get(() => panelDef1); + const newState = addMissingWidgets(frontstageDef, state); + newState.widgets.start.tabs.should.eql(["start1", "w1", "ws1"]); + newState.widgets.end.tabs.should.eql(["end1", "w2", "we1"]); + }); + + it("should add bottomPanel widgets", () => { + let state = createNineZoneState(); + state = addPanelWidget(state, "bottom", "start", ["start1"]); + state = addPanelWidget(state, "bottom", "end", ["end1"]); + state = addTab(state, "start1"); + state = addTab(state, "end1"); + const frontstageDef = new FrontstageDef(); + const panelDef = new StagePanelDef(); + panelDef.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + panelZones: { + start: { + widgets: [ + , + ], + }, + end: { + widgets: [ + , + ], + }, }, - }, - }, StagePanelLocation.Bottom); - const panelDef1 = new StagePanelDef(); - panelDef1.initializeFromProps({ - resizable: true, - widgets: [ - , - ], - }, StagePanelLocation.BottomMost); - sinon.stub(frontstageDef, "bottomPanel").get(() => panelDef); - sinon.stub(frontstageDef, "bottomMostPanel").get(() => panelDef1); - const newState = addMissingWidgets(frontstageDef, state); - newState.widgets.start.tabs.should.eql(["start1", "w1", "ws1"]); - newState.widgets.end.tabs.should.eql(["end1", "w2", "we1"]); - }); -}); - -describe("dynamic widgets", () => { - const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; - const localStorageMock = storageMock(); - - stubRaf(); - beforeEach(async () => { - Object.defineProperty(window, "localStorage", { - get: () => localStorageMock, + }, StagePanelLocation.Bottom); + const panelDef1 = new StagePanelDef(); + panelDef1.initializeFromProps({ + resizable: true, + widgets: [ + , + ], + }, StagePanelLocation.BottomMost); + sinon.stub(frontstageDef, "bottomPanel").get(() => panelDef); + sinon.stub(frontstageDef, "bottomMostPanel").get(() => panelDef1); + const newState = addMissingWidgets(frontstageDef, state); + newState.widgets.start.tabs.should.eql(["start1", "w1", "ws1"]); + newState.widgets.end.tabs.should.eql(["end1", "w2", "we1"]); + }); + }); + + describe("dynamic widgets", () => { + stubRaf(); + beforeEach(async () => { + await TestUtils.initializeUiFramework(); + await NoRenderApp.startup(); + }); + + afterEach(() => { + UiItemsManager.unregister("TestUi2Provider"); + FrontstageManager.clearFrontstageDefs(); + FrontstageManager.setActiveFrontstageDef(undefined); }); - await TestUtils.initializeUiFramework(); - await NoRenderApp.startup(); - }); - - afterEach(() => { - UiItemsManager.unregister("TestUi2Provider"); - FrontstageManager.clearFrontstageDefs(); - FrontstageManager.setActiveFrontstageDef(undefined); - }); + afterEach(() => { + TestUtils.terminateUiFramework(); + IModelApp.shutdown(); + }); - afterEach(() => { - Object.defineProperty(window, "localStorage", localStorageToRestore); - TestUtils.terminateUiFramework(); - IModelApp.shutdown(); - }); + it("should render pre-loaded extension widgets when state is initialized", async () => { + UiItemsManager.register(new TestUi2Provider()); - it("should render pre-loaded extension widgets when state is initialized", async () => { - UiItemsManager.register(new TestUi2Provider()); + const frontstageProvider = new TestFrontstageUi2(); + FrontstageManager.addFrontstageProvider(frontstageProvider); + await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); + const { findByText } = render(); + await findByText("Left Start 1"); + await findByText("TestUi2Provider RM1"); + await findByText("TestUi2Provider W1"); + }); - const frontstageProvider = new TestFrontstageUi2(); - FrontstageManager.addFrontstageProvider(frontstageProvider); - await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); - const { findByText } = render(); - await findByText("Left Start 1"); - await findByText("TestUi2Provider RM1"); - await findByText("TestUi2Provider W1"); - }); + it("should render pre-loaded extension widgets when state is restored", async () => { + UiItemsManager.register(new TestUi2Provider()); - it("should render pre-loaded extension widgets when state is restored", async () => { - UiItemsManager.register(new TestUi2Provider()); + const spy = sinon.spy(localStorageMock, "getItem"); + let state = createNineZoneState(); + state = addPanelWidget(state, "left", "leftStart", ["LeftStart1"]); + state = addTab(state, "LeftStart1"); + const setting = createFrontstageState(state); - const spy = sinon.spy(localStorageMock, "getItem"); - let state = createNineZoneState(); - state = addPanelWidget(state, "left", "leftStart", ["LeftStart1"]); - state = addTab(state, "LeftStart1"); - const setting = createFrontstageState(state); + const uiSettings = new UiSettingsStub(); + sinon.stub(uiSettings, "getSetting").resolves({ + status: UiSettingsStatus.Success, + setting, + }); - const uiSettings = new UiSettingsStub(); - sinon.stub(uiSettings, "getSetting").resolves({ - status: UiSettingsStatus.Success, - setting, - }); + const frontstageProvider = new TestFrontstageUi2(); + FrontstageManager.addFrontstageProvider(frontstageProvider); + await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); + const { findByText } = render(, { + wrapper: (props) => , + }); + await findByText("Left Start 1"); + await findByText("TestUi2Provider RM1"); + await findByText("TestUi2Provider W1"); - const frontstageProvider = new TestFrontstageUi2(); - FrontstageManager.addFrontstageProvider(frontstageProvider); - await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); - const { findByText } = render(, { - wrapper: (props) => , + sinon.assert.notCalled(spy); }); - await findByText("Left Start 1"); - await findByText("TestUi2Provider RM1"); - await findByText("TestUi2Provider W1"); - - sinon.assert.notCalled(spy); - }); - it("should render loaded extension widgets", async () => { - const frontstageProvider = new TestFrontstageUi2(); - FrontstageManager.addFrontstageProvider(frontstageProvider); - await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); - const { findByText } = render(); - await findByText("Left Start 1"); + it("should render loaded extension widgets", async () => { + const frontstageProvider = new TestFrontstageUi2(); + FrontstageManager.addFrontstageProvider(frontstageProvider); + await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); + const { findByText } = render(); + await findByText("Left Start 1"); - act(() => { - UiItemsManager.register(new TestUi2Provider()); + act(() => { + UiItemsManager.register(new TestUi2Provider()); + }); + await findByText("TestUi2Provider RM1"); + await findByText("TestUi2Provider W1"); }); - await findByText("TestUi2Provider RM1"); - await findByText("TestUi2Provider W1"); - }); - it("should stop rendering unloaded extension widgets", async () => { - const frontstageProvider = new TestFrontstageUi2(); - FrontstageManager.addFrontstageProvider(frontstageProvider); - await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); - const frontstageDef = FrontstageManager.activeFrontstageDef!; - render(); + it("should stop rendering unloaded extension widgets", async () => { + const frontstageProvider = new TestFrontstageUi2(); + FrontstageManager.addFrontstageProvider(frontstageProvider); + await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); + const frontstageDef = FrontstageManager.activeFrontstageDef!; + render(); - act(() => { - UiItemsManager.register(new TestUi2Provider()); - }); + act(() => { + UiItemsManager.register(new TestUi2Provider()); + }); - await TestUtils.flushAsyncOperations(); - should().exist(frontstageDef.nineZoneState!.tabs.LeftStart1, "LeftStart1"); - should().exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderRM1, "TestUi2ProviderRM1"); - should().exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderW1, "TestUi2ProviderW1"); - frontstageDef.nineZoneState!.widgets.rightMiddle.tabs.should.eql(["TestUi2ProviderRM1"], "rigthMiddle widget tabs"); - frontstageDef.nineZoneState!.widgets.leftStart.tabs.should.eql(["LeftStart1", "TestUi2ProviderW1"], "leftStart widget tabs"); + await TestUtils.flushAsyncOperations(); + should().exist(frontstageDef.nineZoneState!.tabs.LeftStart1, "LeftStart1"); + should().exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderRM1, "TestUi2ProviderRM1"); + should().exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderW1, "TestUi2ProviderW1"); + frontstageDef.nineZoneState!.widgets.rightMiddle.tabs.should.eql(["TestUi2ProviderRM1"], "rigthMiddle widget tabs"); + frontstageDef.nineZoneState!.widgets.leftStart.tabs.should.eql(["LeftStart1", "TestUi2ProviderW1"], "leftStart widget tabs"); - act(() => { - UiItemsManager.unregister("TestUi2Provider"); - }); + act(() => { + UiItemsManager.unregister("TestUi2Provider"); + }); - await TestUtils.flushAsyncOperations(); - should().exist(frontstageDef.nineZoneState!.tabs.LeftStart1, "LeftStart1 after unregister"); - should().not.exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderRM1, "TestUi2ProviderRM1 after unregister"); - should().not.exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderW1, "TestUi2ProviderW1 after unregister"); - should().not.exist(frontstageDef.nineZoneState!.widgets.rightMiddle, "rigthMiddle widget"); - frontstageDef.nineZoneState!.widgets.leftStart.tabs.should.eql(["LeftStart1"], "leftStart widget tabs"); - }); + await TestUtils.flushAsyncOperations(); + should().exist(frontstageDef.nineZoneState!.tabs.LeftStart1, "LeftStart1 after unregister"); + should().not.exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderRM1, "TestUi2ProviderRM1 after unregister"); + should().not.exist(frontstageDef.nineZoneState!.tabs.TestUi2ProviderW1, "TestUi2ProviderW1 after unregister"); + should().not.exist(frontstageDef.nineZoneState!.widgets.rightMiddle, "rigthMiddle widget"); + frontstageDef.nineZoneState!.widgets.leftStart.tabs.should.eql(["LeftStart1"], "leftStart widget tabs"); + }); - it("should render from 1.0 definition", async () => { - const frontstageProvider = new TestFrontstageUi1(); - FrontstageManager.addFrontstageProvider(frontstageProvider); - await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); - const frontstageDef = FrontstageManager.activeFrontstageDef!; - render(); + it("should render from 1.0 definition", async () => { + const frontstageProvider = new TestFrontstageUi1(); + FrontstageManager.addFrontstageProvider(frontstageProvider); + await FrontstageManager.setActiveFrontstageDef(frontstageProvider.frontstageDef); + const frontstageDef = FrontstageManager.activeFrontstageDef!; + render(); - await TestUtils.flushAsyncOperations(); - const state = frontstageDef.nineZoneState!; + await TestUtils.flushAsyncOperations(); + const state = frontstageDef.nineZoneState!; - state.panels.left.widgets.should.eql(["leftStart", "leftMiddle", "leftEnd"]); - state.panels.right.widgets.should.eql(["rightStart", "rightMiddle", "rightEnd"]); - state.panels.top.widgets.should.eql(["topStart", "topEnd"]); - state.panels.bottom.widgets.should.eql(["bottomStart", "bottomEnd"]); + state.panels.left.widgets.should.eql(["leftStart", "leftMiddle", "leftEnd"]); + state.panels.right.widgets.should.eql(["rightStart", "rightMiddle", "rightEnd"]); + state.panels.top.widgets.should.eql(["topStart", "topEnd"]); + state.panels.bottom.widgets.should.eql(["bottomStart", "bottomEnd"]); - state.widgets.leftStart.tabs.should.eql(["CenterLeft1", "LeftStart1"]); - state.widgets.leftMiddle.tabs.should.eql(["BottomLeft1", "LeftMiddle1"]); - state.widgets.leftEnd.tabs.should.eql(["Left1", "LeftEnd1"]); + state.widgets.leftStart.tabs.should.eql(["CenterLeft1", "LeftStart1"]); + state.widgets.leftMiddle.tabs.should.eql(["BottomLeft1", "LeftMiddle1"]); + state.widgets.leftEnd.tabs.should.eql(["Left1", "LeftEnd1"]); - state.widgets.rightStart.tabs.should.eql(["CenterRight1", "RightStart1"]); - state.widgets.rightMiddle.tabs.should.eql(["BottomRight1", "RightMiddle1"]); - state.widgets.rightEnd.tabs.should.eql(["Right1", "RightEnd1"]); + state.widgets.rightStart.tabs.should.eql(["CenterRight1", "RightStart1"]); + state.widgets.rightMiddle.tabs.should.eql(["BottomRight1", "RightMiddle1"]); + state.widgets.rightEnd.tabs.should.eql(["Right1", "RightEnd1"]); - state.widgets.topStart.tabs.should.eql(["Top1", "TopStart1"]); - state.widgets.topEnd.tabs.should.eql(["TopMost1", "TopEnd1"]); + state.widgets.topStart.tabs.should.eql(["Top1", "TopStart1"]); + state.widgets.topEnd.tabs.should.eql(["TopMost1", "TopEnd1"]); - state.widgets.bottomStart.tabs.should.eql(["Bottom1", "BottomStart1"]); - state.widgets.bottomEnd.tabs.should.eql(["BottomMost1", "BottomEnd1"]); + state.widgets.bottomStart.tabs.should.eql(["Bottom1", "BottomStart1"]); + state.widgets.bottomEnd.tabs.should.eql(["BottomMost1", "BottomEnd1"]); + }); }); }); diff --git a/ui/framework/src/test/widgets/NavigationWidget.test.snap b/ui/framework/src/test/widgets/NavigationWidget.test.snap index b8343cf8793a..8f3740adf692 100644 --- a/ui/framework/src/test/widgets/NavigationWidget.test.snap +++ b/ui/framework/src/test/widgets/NavigationWidget.test.snap @@ -83,3 +83,87 @@ exports[`NavigationWidget NavigationWidget should render correctly 1`] = ` } /> `; + +exports[`NavigationWidget localStorage Wrapper NavigationWidget NavigationWidget should render correctly 1`] = ` + + + + + } + panelAlignment={0} + /> + } + navigationWidgetDef={ + NavigationWidgetDef { + "_classId": undefined, + "_defaultState": 4, + "_fillZone": false, + "_handleSyncUiEvent": [Function], + "_id": "navigationWidget", + "_isFloatingStateSupported": false, + "_isFloatingStateWindowResizable": true, + "_isFreeform": false, + "_isStatusBar": false, + "_isToolSettings": false, + "_label": "", + "_navigationAidId": "", + "_onWidgetStateChanged": undefined, + "_preferredPanelSize": undefined, + "_priority": 0, + "_restoreTransientState": undefined, + "_saveTransientState": undefined, + "_state": 4, + "_stateChanged": false, + "_syncEventIds": Array [], + "_tabLocation": Object { + "side": "left", + "tabIndex": 0, + "widgetId": "", + "widgetIndex": 0, + }, + "_toolbarBaseName": "[]NavigationWidget", + "_tooltip": "", + "_widgetType": 1, + "horizontalDirection": 4, + "horizontalItems": undefined, + "horizontalPanelAlignment": 1, + "verticalDirection": 1, + "verticalItems": undefined, + "verticalPanelAlignment": 0, + } + } + verticalToolbar={ + + + + + } + panelAlignment={0} + /> + } +/> +`; diff --git a/ui/framework/src/test/widgets/NavigationWidget.test.tsx b/ui/framework/src/test/widgets/NavigationWidget.test.tsx index 6909fdc4522a..5092506cc545 100644 --- a/ui/framework/src/test/widgets/NavigationWidget.test.tsx +++ b/ui/framework/src/test/widgets/NavigationWidget.test.tsx @@ -17,46 +17,61 @@ import { ConfigurableUiManager } from "../../ui-framework/configurableui/Configu import { CoreTools } from "../../ui-framework/tools/CoreToolDefinitions"; import { FrameworkVersion } from "../../ui-framework/hooks/useFrameworkVersion"; import { NavigationAidControl } from "../../ui-framework/navigationaids/NavigationAidControl"; -import TestUtils from "../TestUtils"; +import TestUtils, { storageMock } from "../TestUtils"; import { UiShowHideManager } from "../../ui-framework/utils/UiShowHideManager"; -describe("NavigationWidget", () => { +describe("NavigationWidget localStorage Wrapper", () => { + + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + const localStorageMock = storageMock(); before(async () => { - await TestUtils.initializeUiFramework(); + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); }); after(() => { - TestUtils.terminateUiFramework(); + Object.defineProperty(window, "localStorage", localStorageToRestore); }); - const widgetProps: AnyWidgetProps = { - id: "navigationWidget", - classId: "NavigationWidget", - defaultState: WidgetState.Open, - isFreeform: true, - iconSpec: "icon-home", - labelKey: "SampleApp:Test.my-label", - navigationAidId: "StandardRotationNavigationAid", - horizontalDirection: Direction.Top, - verticalDirection: Direction.Left, - }; + describe("NavigationWidget", () => { - it("NavigationWidgetDef from WidgetProps", () => { + before(async () => { + await TestUtils.initializeUiFramework(); + }); - const widgetDef = new NavigationWidgetDef(widgetProps); // eslint-disable-line deprecation/deprecation - expect(widgetDef).to.be.instanceof(NavigationWidgetDef); // eslint-disable-line deprecation/deprecation + after(() => { + TestUtils.terminateUiFramework(); + }); - const navigationWidgetDef = widgetDef; + const widgetProps: AnyWidgetProps = { + id: "navigationWidget", + classId: "NavigationWidget", + defaultState: WidgetState.Open, + isFreeform: true, + iconSpec: "icon-home", + labelKey: "SampleApp:Test.my-label", + navigationAidId: "StandardRotationNavigationAid", + horizontalDirection: Direction.Top, + verticalDirection: Direction.Left, + }; - const reactNode = navigationWidgetDef.reactNode; - expect(reactNode).to.not.be.undefined; + it("NavigationWidgetDef from WidgetProps", () => { - const cornerNode = navigationWidgetDef.renderCornerItem(); - expect(cornerNode).to.not.be.undefined; - }); + const widgetDef = new NavigationWidgetDef(widgetProps); // eslint-disable-line deprecation/deprecation + expect(widgetDef).to.be.instanceof(NavigationWidgetDef); // eslint-disable-line deprecation/deprecation + + const navigationWidgetDef = widgetDef; + + const reactNode = navigationWidgetDef.reactNode; + expect(reactNode).to.not.be.undefined; - const horizontalToolbar = + const cornerNode = navigationWidgetDef.renderCornerItem(); + expect(cornerNode).to.not.be.undefined; + }); + + const horizontalToolbar = { } />; - const verticalToolbar = + const verticalToolbar = { } />; - it("NavigationWidget should render", () => { - mount( - , - ); - }); + it("NavigationWidget should render", () => { + mount( + , + ); + }); - it("NavigationWidget should render correctly", () => { - shallow( - , - ).should.matchSnapshot(); - }); + it("NavigationWidget should render correctly", () => { + shallow( + , + ).should.matchSnapshot(); + }); - it("NavigationWidget should render with an item list", () => { - const hItemList = new ItemList([CoreTools.selectElementCommand]); - const vItemList = new ItemList([CoreTools.fitViewCommand]); + it("NavigationWidget should render with an item list", () => { + const hItemList = new ItemList([CoreTools.selectElementCommand]); + const vItemList = new ItemList([CoreTools.fitViewCommand]); - mount( - , - ); - }); + mount( + , + ); + }); - it("NavigationWidget should support update", () => { - const wrapper = mount( - , - ); - expect(wrapper.find(ToolButton).length).to.eq(4); - - wrapper.setProps({ verticalToolbar: undefined }); - wrapper.update(); - expect(wrapper.find(ToolButton).length).to.eq(2); - }); + it("NavigationWidget should support update", () => { + const wrapper = mount( + , + ); + expect(wrapper.find(ToolButton).length).to.eq(4); + + wrapper.setProps({ verticalToolbar: undefined }); + wrapper.update(); + expect(wrapper.find(ToolButton).length).to.eq(2); + }); - class TestContentControl extends ContentControl { - constructor(info: ConfigurableCreateInfo, options: any) { - super(info, options); + class TestContentControl extends ContentControl { + constructor(info: ConfigurableCreateInfo, options: any) { + super(info, options); - this.reactNode =
; + this.reactNode =
; + } } - } - class TestNavigationAidControl extends NavigationAidControl { - constructor(info: ConfigurableCreateInfo, options: any) { - super(info, options); + class TestNavigationAidControl extends NavigationAidControl { + constructor(info: ConfigurableCreateInfo, options: any) { + super(info, options); - this.reactNode =
Test Navigation Aid
; + this.reactNode =
Test Navigation Aid
; + } } - } - it("NavigationWidgetDef with invalid navigation aid should throw Error", () => { - const def = new NavigationWidgetDef({ // eslint-disable-line deprecation/deprecation - navigationAidId: "Aid1", + it("NavigationWidgetDef with invalid navigation aid should throw Error", () => { + const def = new NavigationWidgetDef({ // eslint-disable-line deprecation/deprecation + navigationAidId: "Aid1", + }); + ConfigurableUiManager.registerControl("Aid1", TestContentControl); + expect(() => def.renderCornerItem()).to.throw(Error); + ConfigurableUiManager.unregisterControl("Aid1"); }); - ConfigurableUiManager.registerControl("Aid1", TestContentControl); - expect(() => def.renderCornerItem()).to.throw(Error); - ConfigurableUiManager.unregisterControl("Aid1"); - }); - it("NavigationWidgetDef should handle updateNavigationAid", () => { - const def = new NavigationWidgetDef({ // eslint-disable-line deprecation/deprecation - navigationAidId: "Aid1", - }); - ConfigurableUiManager.registerControl("Aid1", TestNavigationAidControl); + it("NavigationWidgetDef should handle updateNavigationAid", () => { + const def = new NavigationWidgetDef({ // eslint-disable-line deprecation/deprecation + navigationAidId: "Aid1", + }); + ConfigurableUiManager.registerControl("Aid1", TestNavigationAidControl); - const element = def.reactNode; - expect(def.reactNode).to.eq(element); - const wrapper = mount(element as React.ReactElement); + const element = def.reactNode; + expect(def.reactNode).to.eq(element); + const wrapper = mount(element as React.ReactElement); - const connection = moq.Mock.ofType(); - FrontstageManager.setActiveNavigationAid("Aid1", connection.object); - wrapper.update(); + const connection = moq.Mock.ofType(); + FrontstageManager.setActiveNavigationAid("Aid1", connection.object); + wrapper.update(); - FrontstageManager.setActiveToolId(CoreTools.selectElementCommand.toolId); + FrontstageManager.setActiveToolId(CoreTools.selectElementCommand.toolId); - ConfigurableUiManager.unregisterControl("Aid1"); - }); + ConfigurableUiManager.unregisterControl("Aid1"); + }); - it("NavigationAidHost should render in 2.0 mode", () => { - mount( - - - ); - }); + it("NavigationAidHost should render in 2.0 mode", () => { + mount( + + + ); + }); - it("NavigationAidHost should render in 2.0 mode with snapWidgetOpacity", () => { - UiShowHideManager.snapWidgetOpacity = true; - mount( - - - ); - UiShowHideManager.snapWidgetOpacity = false; + it("NavigationAidHost should render in 2.0 mode with snapWidgetOpacity", () => { + UiShowHideManager.snapWidgetOpacity = true; + mount( + + + ); + UiShowHideManager.snapWidgetOpacity = false; + }); }); - }); diff --git a/ui/framework/src/test/widgets/ToolWidgetComposer.test.snap b/ui/framework/src/test/widgets/ToolWidgetComposer.test.snap index 4e69e853db6a..84b983b00829 100644 --- a/ui/framework/src/test/widgets/ToolWidgetComposer.test.snap +++ b/ui/framework/src/test/widgets/ToolWidgetComposer.test.snap @@ -1,5 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`FrameworkAccuDraw localStorage Wrapper ToolWidgetComposer ToolWidgetComposer should render correctly 1`] = ` + + + +`; + +exports[`FrameworkAccuDraw localStorage Wrapper ToolWidgetComposer ToolWidgetComposer with should render 1`] = ` + + + } + onMouseEnter={[Function]} + /> + +`; + exports[`ToolWidgetComposer ToolWidgetComposer should render correctly 1`] = ` { +describe("FrameworkAccuDraw localStorage Wrapper", () => { + + const localStorageToRestore = Object.getOwnPropertyDescriptor(window, "localStorage")!; + const localStorageMock = storageMock(); before(async () => { - await TestUtils.initializeUiFramework(); + Object.defineProperty(window, "localStorage", { + get: () => localStorageMock, + }); }); after(() => { - TestUtils.terminateUiFramework(); + Object.defineProperty(window, "localStorage", localStorageToRestore); }); - it("ToolWidgetComposer should render", () => { - mount(); - }); + describe("ToolWidgetComposer", () => { - it("ToolWidgetComposer should render correctly", () => { - shallow().should.matchSnapshot(); - }); + before(async () => { + await TestUtils.initializeUiFramework(); + }); - it("ToolWidgetComposer with should render", () => { - shallow(} />).should.matchSnapshot(); - }); + after(() => { + TestUtils.terminateUiFramework(); + }); - it("BackstageAppButtonProps should render", () => { - const wrapper = mount(); - wrapper.setProps({ icon: "icon-bentley" }); - }); + it("ToolWidgetComposer should render", () => { + mount(); + }); - it("BackstageAppButtonProps should update with default icon", () => { - const wrapper = mount(); - wrapper.setProps({ icon: undefined }); - }); + it("ToolWidgetComposer should render correctly", () => { + shallow().should.matchSnapshot(); + }); - it("BackstageAppButton should render in 2.0 mode", () => { - mount( - - - ); - }); + it("ToolWidgetComposer with should render", () => { + shallow(} />).should.matchSnapshot(); + }); + + it("BackstageAppButtonProps should render", () => { + const wrapper = mount(); + wrapper.setProps({ icon: "icon-bentley" }); + }); + + it("BackstageAppButtonProps should update with default icon", () => { + const wrapper = mount(); + wrapper.setProps({ icon: undefined }); + }); + it("BackstageAppButton should render in 2.0 mode", () => { + mount( + + + ); + }); + + }); }); diff --git a/ui/framework/src/ui-framework.ts b/ui/framework/src/ui-framework.ts index 6fa3c490b552..76756cf57d32 100644 --- a/ui/framework/src/ui-framework.ts +++ b/ui/framework/src/ui-framework.ts @@ -145,6 +145,7 @@ export * from "./ui-framework/selection/SelectionContextItemDef"; export * from "./ui-framework/selection/HideIsolateEmphasizeManager"; export * from "./ui-framework/selection/ClearEmphasisStatusField"; +export * from "./ui-framework/settings/ui/UiSettingsPage"; export * from "./ui-framework/settings/quantityformatting/QuantityFormat"; export * from "./ui-framework/shared/ActionButtonItemDef"; @@ -224,7 +225,8 @@ export * from "./ui-framework/uiadmin/FrameworkUiAdmin"; export * from "./ui-framework/uiprovider/DefaultDialogGridContainer"; -export * from "./ui-framework/uisettings/IModelAppUiSettings"; +export * from "./ui-framework/uisettings/AppUiSettings"; +export * from "./ui-framework/uisettings/UserSettingsStorage"; export * from "./ui-framework/uisettings/useUiSettings"; export * from "./ui-framework/utils/ViewUtilities"; diff --git a/ui/framework/src/ui-framework/UiFramework.ts b/ui/framework/src/ui-framework/UiFramework.ts index b42e403f8a93..80a0b347fe98 100644 --- a/ui/framework/src/ui-framework/UiFramework.ts +++ b/ui/framework/src/ui-framework/UiFramework.ts @@ -6,6 +6,8 @@ * @module Utilities */ +// cSpell:ignore configurableui clientservices + import { Store } from "redux"; import { GuidString, Logger, ProcessDetector } from "@bentley/bentleyjs-core"; import { isFrontendAuthorizationClient } from "@bentley/frontend-authorization-client"; @@ -16,7 +18,7 @@ import { Presentation } from "@bentley/presentation-frontend"; import { TelemetryEvent } from "@bentley/telemetry-client"; import { getClassName, UiError } from "@bentley/ui-abstract"; import { UiComponents } from "@bentley/ui-components"; -import { LocalUiSettings, SettingsManager, UiEvent, UiSettings } from "@bentley/ui-core"; +import { LocalSettingsStorage, SettingsManager, UiEvent, UiSettingsStorage } from "@bentley/ui-core"; import { BackstageManager } from "./backstage/BackstageManager"; import { DefaultIModelServices } from "./clientservices/DefaultIModelServices"; import { DefaultProjectServices } from "./clientservices/DefaultProjectServices"; @@ -34,11 +36,21 @@ import * as keyinPaletteTools from "./tools/KeyinPaletteTools"; import * as restoreLayoutTools from "./tools/RestoreLayoutTool"; import * as openSettingTools from "./tools/OpenSettingsTool"; import * as toolSettingTools from "./tools/ToolSettingsTools"; -import { UiShowHideManager } from "./utils/UiShowHideManager"; +import { UiShowHideManager, UiShowHideSettingsProvider } from "./utils/UiShowHideManager"; import { WidgetManager } from "./widgets/WidgetManager"; // cSpell:ignore Mobi +/** Interface to be implemented but any classes that wants to load their user settings when the UiSetting storage class is set. + * @beta + */ +export interface UserSettingsProvider { + /** Unique provider Id */ + providerId: string; + /** Function to load settings from settings storage */ + loadUserSettings(storage: UiSettingsStorage): Promise; +} + /** UiVisibility Event Args interface. * @public */ @@ -70,8 +82,8 @@ export class FrameworkVersionChangedEvent extends UiEvent = new Map(); + + /** Registers class that will be informed when the UserSettingsStorage location has been set or changed. This allows + * classes to load any previously saved settings from the new storage location. Common storage locations are the browser's + * local storage, or the iTwin Product Settings cloud storage available via the SettingsAdmin see `IModelApp.settingsAdmin`. + * @alpha + */ + public static registerUserSettingsProvider(entry: UserSettingsProvider) { + if (this._uiSettingsProviderRegistry.has(entry.providerId)) + return false; + + this._uiSettingsProviderRegistry.set(entry.providerId, entry); + return true; + } /** Get Show Ui event. * @public @@ -135,7 +160,7 @@ export class UiFramework { if (frameworkStateKey && store) UiFramework._frameworkStateKeyInStore = frameworkStateKey; - // set up namespace and register akk tools from package + // set up namespace and register all tools from package const frameworkNamespace = UiFramework._i18n.registerNamespace(UiFramework.i18nNamespace); [ restoreLayoutTools, @@ -174,10 +199,13 @@ export class UiFramework { UiFramework._initialized = true; + // initialize any standalone settings providers that don't need to have defaults set by iModelApp + UiShowHideSettingsProvider.initialize (); + return readFinishedPromise; } - /** Unregisters the UiFramework internationalization service namespace */ + /** Un-registers the UiFramework internationalization service namespace */ public static terminate() { UiFramework._store = undefined; UiFramework._frameworkStateKeyInStore = "frameworkState"; @@ -218,8 +246,12 @@ export class UiFramework { * @beta */ public static get frameworkState(): FrameworkState | undefined { - // eslint-disable-next-line dot-notation - return UiFramework.store.getState()[UiFramework.frameworkStateKey]; + try { + // eslint-disable-next-line dot-notation + return UiFramework.store.getState()[UiFramework.frameworkStateKey]; + } catch (_e){ + return undefined; + } } /** The Redux store */ @@ -322,7 +354,7 @@ export class UiFramework { } public static setAccudrawSnapMode(snapMode: SnapMode) { - UiFramework.store.dispatch({ type: ConfigurableUiActionId.SetSnapMode, payload: snapMode }); + UiFramework.dispatchActionToStore(ConfigurableUiActionId.SetSnapMode, snapMode, true); } public static getAccudrawSnapMode(): SnapMode { @@ -377,8 +409,18 @@ export class UiFramework { } /** @beta */ - public static setUiSettings(uiSettings: UiSettings, immediateSync = false) { - UiFramework._uiSettings = uiSettings; + public static async setUiSettingsStorage(storage: UiSettingsStorage, immediateSync = false) { + if (UiFramework._uiSettingsStorage === storage) + return; + + UiFramework._uiSettingsStorage = storage; + + // let any registered providers to load values from the new storage location + const providerKeys = [...this._uiSettingsProviderRegistry.keys()]; + for await (const key of providerKeys) { + await this._uiSettingsProviderRegistry.get(key)!.loadUserSettings(storage); + } + // istanbul ignore next if (immediateSync) SyncUiEventDispatcher.dispatchImmediateSyncUiEvent(SyncUiEventId.UiSettingsChanged); @@ -387,10 +429,8 @@ export class UiFramework { } /** @beta */ - public static getUiSettings(): UiSettings { - if (undefined === UiFramework._uiSettings) - UiFramework._uiSettings = new LocalUiSettings(); - return UiFramework._uiSettings; + public static getUiSettingsStorage(): UiSettingsStorage { + return UiFramework._uiSettingsStorage; } /** @beta */ @@ -446,7 +486,10 @@ export class UiFramework { } public static setColorTheme(theme: string) { - UiFramework.store.dispatch({ type: ConfigurableUiActionId.SetTheme, payload: theme }); + if (UiFramework.getColorTheme() === theme) + return; + + UiFramework.dispatchActionToStore(ConfigurableUiActionId.SetTheme, theme, true); } public static getColorTheme(): string { @@ -454,7 +497,10 @@ export class UiFramework { } public static setWidgetOpacity(opacity: number) { - UiFramework.store.dispatch({ type: ConfigurableUiActionId.SetWidgetOpacity, payload: opacity }); + if (UiFramework.getWidgetOpacity() === opacity) + return; + + UiFramework.dispatchActionToStore(ConfigurableUiActionId.SetWidgetOpacity, opacity, true); } public static getWidgetOpacity(): number { @@ -468,9 +514,23 @@ export class UiFramework { /** Returns the Ui Version. * @beta */ - // istanbul ignore next public static get uiVersion(): string { - return UiFramework._uiVersion; + return UiFramework.frameworkState ? UiFramework.frameworkState.configurableUiState.frameworkVersion : this._uiVersion; + } + + public static setUiVersion(version: string) { + if (UiFramework.uiVersion === version) + return; + + UiFramework.dispatchActionToStore(ConfigurableUiActionId.SetFrameworkVersion, version === "1"?"1":"2", true); + } + + public static get useDragInteraction(): boolean { + return UiFramework.frameworkState ? UiFramework.frameworkState.configurableUiState.useDragInteraction : false; + } + + public static setUseDragInteraction(useDragInteraction: boolean) { + UiFramework.dispatchActionToStore(ConfigurableUiActionId.SetDragInteraction, useDragInteraction, true); } /** Send logging message to the telemetry system @@ -490,15 +550,6 @@ export class UiFramework { // eslint-disable-next-line @typescript-eslint/no-floating-promises UiFramework.postTelemetry(`Ui Version changed to ${args.version} `, "F2772C81-962D-4755-807C-2D675A5FF399"); UiFramework._uiVersion = args.version; - - // If Ui Version 1, save widget opacity - // istanbul ignore if - if (args.oldVersion === "1") - UiFramework._version1WidgetOpacity = UiFramework.getWidgetOpacity(); - - // If Ui Version 1, restore widget opacity; otherwise, set widget opacity to 1.0 to basically turn the feature off. - // This fixes use of "backdrop-filter: blur(10px)"" CSS. - UiFramework.setWidgetOpacity(args.version === "1" ? UiFramework._version1WidgetOpacity : 1.0); }; // istanbul ignore next diff --git a/ui/framework/src/ui-framework/accudraw/AccuDrawFieldContainer.tsx b/ui/framework/src/ui-framework/accudraw/AccuDrawFieldContainer.tsx index ec94876dd293..d79a29e02620 100644 --- a/ui/framework/src/ui-framework/accudraw/AccuDrawFieldContainer.tsx +++ b/ui/framework/src/ui-framework/accudraw/AccuDrawFieldContainer.tsx @@ -14,7 +14,7 @@ import { AccuDrawSetFieldFocusEventArgs, AccuDrawSetFieldLockEventArgs, AccuDrawSetModeEventArgs, AccuDrawUiAdmin, IconSpecUtilities, } from "@bentley/ui-abstract"; -import { CommonProps, IconSpec, Orientation, UiSettings } from "@bentley/ui-core"; +import { CommonProps, IconSpec, Orientation, UiSettingsStorage } from "@bentley/ui-core"; import { AccuDrawInputField } from "./AccuDrawInputField"; import { CompassMode, IModelApp, ItemField, ScreenViewport, SelectedViewportChangedArgs } from "@bentley/imodeljs-frontend"; import { KeyboardShortcutManager } from "../keyboardshortcut/KeyboardShortcut"; @@ -30,8 +30,8 @@ import { ColorDef } from "@bentley/imodeljs-common"; export interface AccuDrawFieldContainerProps extends CommonProps { /** Orientation of the fields */ orientation: Orientation; - /** Optional parameter for persistent UI settings. Defaults to LocalUiSettings. */ - uiSettings?: UiSettings; + /** Optional parameter for persistent UI settings. Defaults to LocalSettingsStorage. */ + uiSettingsStorage?: UiSettingsStorage; /** @internal */ showZOverride?: boolean; } @@ -52,7 +52,7 @@ const defaultDistanceIcon = IconSpecUtilities.createSvgIconSpec(distanceIconSvg) /** @alpha */ export function AccuDrawFieldContainer(props: AccuDrawFieldContainerProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { className, style, orientation, uiSettings, showZOverride, ...otherProps } = props; + const { className, style, orientation, uiSettingsStorage, showZOverride, ...otherProps } = props; const [containerIndex] = React.useState(() => ++AccuDrawContainerIndex); const xInputRef = React.useRef(null); diff --git a/ui/framework/src/ui-framework/accudraw/FrameworkAccuDraw.ts b/ui/framework/src/ui-framework/accudraw/FrameworkAccuDraw.ts index 83721d0eb311..5449798aa95d 100644 --- a/ui/framework/src/ui-framework/accudraw/FrameworkAccuDraw.ts +++ b/ui/framework/src/ui-framework/accudraw/FrameworkAccuDraw.ts @@ -8,10 +8,11 @@ import { AccuDraw, BeButtonEvent, CompassMode, IModelApp, ItemField, NotifyMessageDetails, OutputMessagePriority, QuantityType, RotationMode } from "@bentley/imodeljs-frontend"; import { AccuDrawField, AccuDrawMode, AccuDrawSetFieldValueFromUiEventArgs, AccuDrawUiAdmin, ConditionalBooleanValue } from "@bentley/ui-abstract"; -import { UiFramework } from "../UiFramework"; +import { UiFramework, UserSettingsProvider } from "../UiFramework"; import { SyncUiEventDispatcher, SyncUiEventId } from "../syncui/SyncUiEventDispatcher"; import { AccuDrawUiSettings } from "./AccuDrawUiSettings"; import { BeUiEvent } from "@bentley/bentleyjs-core"; +import { UiSettings, UiSettingsStatus } from "@bentley/ui-core"; // cspell:ignore dont @@ -49,9 +50,12 @@ const rotationModeToKeyMap = new Map([ export class AccuDrawUiSettingsChangedEvent extends BeUiEvent<{}> { } /** @internal */ -export class FrameworkAccuDraw extends AccuDraw { +export class FrameworkAccuDraw extends AccuDraw implements UserSettingsProvider { private static _displayNotifications = false; private static _uiSettings: AccuDrawUiSettings | undefined; + private static _settingsNamespace = "AppUiSettings"; + private static _notificationsKey = "AccuDrawNotifications"; + public readonly providerId = "FrameworkAccuDraw"; /** Determines if AccuDraw.rotationMode === RotationMode.Top */ public static readonly isTopRotationConditional = new ConditionalBooleanValue(() => IModelApp.accuDraw.rotationMode === RotationMode.Top, [SyncUiEventId.AccuDrawRotationChanged]); @@ -75,7 +79,16 @@ export class FrameworkAccuDraw extends AccuDraw { /** Determines if notifications should be displayed for AccuDraw changes */ public static get displayNotifications(): boolean { return FrameworkAccuDraw._displayNotifications; } - public static set displayNotifications(v: boolean) { FrameworkAccuDraw._displayNotifications = v; } + public static set displayNotifications(v: boolean) { + FrameworkAccuDraw._displayNotifications = v; + void UiFramework.getUiSettingsStorage().saveSetting (this._settingsNamespace, this._notificationsKey, v); + } + + public async loadUserSettings(storage: UiSettings): Promise { + const result = await storage.getSetting (FrameworkAccuDraw._settingsNamespace, FrameworkAccuDraw._notificationsKey); + if (result.status === UiSettingsStatus.Success) + FrameworkAccuDraw._displayNotifications = result.setting; + } /** AccuDraw User Interface settings */ public static get uiSettings(): AccuDrawUiSettings | undefined { return FrameworkAccuDraw._uiSettings; } @@ -87,6 +100,7 @@ export class FrameworkAccuDraw extends AccuDraw { constructor() { super(); AccuDrawUiAdmin.onAccuDrawSetFieldValueFromUiEvent.addListener(this.handleSetFieldValueFromUiEvent); + UiFramework.registerUserSettingsProvider(this); } private handleSetFieldValueFromUiEvent = async (args: AccuDrawSetFieldValueFromUiEventArgs) => { diff --git a/ui/framework/src/ui-framework/configurableui/state.ts b/ui/framework/src/ui-framework/configurableui/state.ts index eb7ed6cb3b0e..f04102b999e5 100644 --- a/ui/framework/src/ui-framework/configurableui/state.ts +++ b/ui/framework/src/ui-framework/configurableui/state.ts @@ -21,6 +21,8 @@ export enum ConfigurableUiActionId { SetTheme = "configurableui:set_theme", SetToolPrompt = "configurableui:set_toolprompt", SetWidgetOpacity = "configurableui:set_widget_opacity", + SetDragInteraction = "configurableui:set-drag-interaction", + SetFrameworkVersion = "configurableui:set-framework-version", } /** The portion of state managed by the ConfigurableUiReducer. @@ -31,6 +33,8 @@ export interface ConfigurableUiState { toolPrompt: string; theme: string; widgetOpacity: number; + useDragInteraction: boolean; + frameworkVersion: string; } /** used on first call of ConfigurableUiReducer */ @@ -39,6 +43,8 @@ const initialState: ConfigurableUiState = { toolPrompt: "", theme: SYSTEM_PREFERRED_COLOR_THEME, widgetOpacity: WIDGET_OPACITY_DEFAULT, + useDragInteraction: false, + frameworkVersion: "1", }; /** An object with a function that creates each ConfigurableUiReducer that can be handled by our reducer. @@ -55,6 +61,8 @@ export const ConfigurableUiActions = { // eslint-disable-line @typescript-esli setWidgetOpacity: // istanbul ignore next (opacity: number) => createAction(ConfigurableUiActionId.SetWidgetOpacity, opacity), + setDragInteraction: (dragInteraction: boolean) => createAction(ConfigurableUiActionId.SetDragInteraction, dragInteraction), + setFrameworkVersion: (frameworkVersion: string) => createAction(ConfigurableUiActionId.SetFrameworkVersion, frameworkVersion), }; /** Union of ConfigurableUi Redux actions @@ -65,35 +73,28 @@ export type ConfigurableUiActionsUnion = ActionsUnion(null); const keyinSeparator = "--#--"; const [historyKeyins, setHistoryKeyins] = React.useState([]); - const uiSettings = useUiSettingsContext(); + const uiSettingsStorage = useUiSettingsStorageContext(); React.useEffect(() => { async function fetchState() { - const settingsResult = await uiSettings.getSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY); + const settingsResult = await uiSettingsStorage.getSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY); // istanbul ignore else if (UiSettingsStatus.Success === settingsResult.status) { const filteredHistory = (settingsResult.setting as string[]).filter((keyin)=>{ @@ -64,18 +64,18 @@ export function KeyinPalettePanel({ keyins, onKeyinExecuted, historyLength: allo } fetchState(); // eslint-disable-line @typescript-eslint/no-floating-promises - }, [uiSettings]); + }, [uiSettingsStorage]); // istanbul ignore next const storeHistoryKeyins = React.useCallback(async (value: string[]) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises - const result = await uiSettings.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, value); + const result = await uiSettingsStorage.saveSetting(KEYIN_PALETTE_NAMESPACE, KEYIN_HISTORY_KEY, value); if (result.status !== UiSettingsStatus.Success) { const briefMessage = UiFramework.translate("keyinbrowser.couldNotSaveHistory"); const errorDetails = new NotifyMessageDetails(OutputMessagePriority.Error, briefMessage); IModelApp.notifications.outputMessage(errorDetails); } - }, [uiSettings]); + }, [uiSettingsStorage]); const allKeyins = React.useMemo(() => { const availableKeyins = []; diff --git a/ui/framework/src/ui-framework/settings/quantityformatting/QuantityFormat.tsx b/ui/framework/src/ui-framework/settings/quantityformatting/QuantityFormat.tsx index a39bd487f540..8a0414b841e2 100644 --- a/ui/framework/src/ui-framework/settings/quantityformatting/QuantityFormat.tsx +++ b/ui/framework/src/ui-framework/settings/quantityformatting/QuantityFormat.tsx @@ -50,7 +50,7 @@ export function getQuantityFormatsSettingsManagerEntry(itemPriority: number, opt itemPriority, tabId: "uifw:Quantity", label: UiFramework.translate("settings.quantity-formatting.label"), subLabel: UiFramework.translate("settings.quantity-formatting.subLabel"), - page: , isDisabled: false, icon: "icon-measure", @@ -62,7 +62,7 @@ export function getQuantityFormatsSettingsManagerEntry(itemPriority: number, opt /** UI Component shown in settings page to set the active Presentation Unit System and to set format overrides. * @beta */ -export function QuantityFormatSettingsPanel({initialQuantityType, availableUnitSystems}: QuantityFormatterSettingsOptions) { +export function QuantityFormatSettingsPage({initialQuantityType, availableUnitSystems}: QuantityFormatterSettingsOptions) { const [activeUnitSystemKey, setActiveUnitSystemKey] = React.useState(IModelApp.quantityFormatter.activeUnitSystem); const [activeQuantityType, setActiveQuantityType] = React.useState(getQuantityTypeKey(initialQuantityType)); const [activeFormatterSpec, setActiveFormatterSpec] = diff --git a/ui/framework/src/ui-framework/settings/quantityformatting/UnitSystemSelector.tsx b/ui/framework/src/ui-framework/settings/quantityformatting/UnitSystemSelector.tsx index 0217d97defcb..cdbf482a7c88 100644 --- a/ui/framework/src/ui-framework/settings/quantityformatting/UnitSystemSelector.tsx +++ b/ui/framework/src/ui-framework/settings/quantityformatting/UnitSystemSelector.tsx @@ -11,14 +11,18 @@ import { UiFramework } from "../../UiFramework"; import { Select } from "@bentley/ui-core"; import { UnitSystemKey } from "@bentley/imodeljs-frontend"; -/** @alpha */ +/** Props for [[UnitSystemSelector]] + * @beta + */ export interface UnitSystemSelectorProps { selectedUnitSystemKey: UnitSystemKey; onUnitSystemSelected: (unitSystem: UnitSystemKey) => void; availableUnitSystems: Set; } -/** @alpha */ +/** Select control to set the "active" Presentation Unit System. This setting determine what units are display for quantity values (i.e. foot vs meter). + * @alpha + */ export function UnitSystemSelector(props: UnitSystemSelectorProps) { // eslint-disable-line @typescript-eslint/naming-convention const label = React.useRef (UiFramework.translate("presentationUnitSystem.selector-label")); const { selectedUnitSystemKey, onUnitSystemSelected, availableUnitSystems } = props; diff --git a/ui/framework/src/ui-framework/settings/ui/UiSettingsPage.scss b/ui/framework/src/ui-framework/settings/ui/UiSettingsPage.scss new file mode 100644 index 000000000000..e946aa3b2151 --- /dev/null +++ b/ui/framework/src/ui-framework/settings/ui/UiSettingsPage.scss @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +@import "~@bentley/ui-core/lib/ui-core/style/themecolors"; +@import "~@bentley/ui-core/lib/ui-core/style/typography"; +@import "~@bentley/ui-core/lib/ui-core/base/base"; + +// cspell:ignore leftpanel rightpanel + +$settings-leftpanel-width: 225px; +$settings-rightpanel-width: 220px; + +.components-settings-container { + height: calc(100% - 50px); + overflow: hidden; + } + +.uifw-settings { + background: $buic-background-dialog; + color: $buic-text-color; + border-radius: 3px; + font-size: $uicore-font-size; + display: flex; + flex-direction: column; +} + +.uifw-settings-item { + display: flex; + + > .panel { + flex: 1; + display: flex; + box-sizing: border-box; + } + + > .left-panel { + flex-direction: column; + min-width: $settings-leftpanel-width; + padding: 20px 15px 20px 15px; + + > .title { + font-size: 18px; + } + + > .description { + margin-top: 10px; + font-size: 12px; + color: $buic-text-color-muted; + } + } + + > .right-panel { + display:flex; + align-items: center; + min-width: $settings-rightpanel-width; + padding: 30px 50px 30px 30px; + + .toggle { + margin-right: 10px; + } + + .select-theme-container { + flex: 1; + } + } +} diff --git a/ui/framework/src/ui-framework/settings/ui/UiSettingsPage.tsx b/ui/framework/src/ui-framework/settings/ui/UiSettingsPage.tsx new file mode 100644 index 000000000000..8f48b2c29001 --- /dev/null +++ b/ui/framework/src/ui-framework/settings/ui/UiSettingsPage.tsx @@ -0,0 +1,208 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ +/** @packageDocumentation + * @module Settings + */ + +// cSpell:ignore configurableui checkmark + +import widowSettingsIconSvg from "@bentley/icons-generic/icons/window-settings.svg?sprite"; +import "./UiSettingsPage.scss"; +import * as React from "react"; +import { Select, SelectOption, SettingsTabEntry, Slider, Toggle } from "@bentley/ui-core"; +import { UiFramework } from "../../UiFramework"; +import { ColorTheme, SYSTEM_PREFERRED_COLOR_THEME } from "../../theme/ThemeManager"; +import { UiShowHideManager } from "../../utils/UiShowHideManager"; +import { SyncUiEventArgs, SyncUiEventDispatcher, SyncUiEventId } from "../../syncui/SyncUiEventDispatcher"; +import { IconSpecUtilities } from "@bentley/ui-abstract"; + +/** UiSettingsPage displaying the active UI settings. This page lets users set the following settings. + * + * - theme - Dark, Light, or based on OS preference. + * - auto hide - Starts a timer and blanks out ui components that overlay content if there is no mouse movement for a period of time. + * - drag interaction - If set, toolbar group buttons require a press and drag or a long press to open. In this mode a child action + * item is shown as the group button and is activated when button is clicked. If a different child item is selected, it becomes the + * active group button item. + * - use proximity - Changes the opacity of toolbar from transparent to opaque as the mouse moves closer. + * - snap widget opacity - triggers an abrupt change from transparent to opaque for tool and navigation widgets, instead of a gradual change based on mouse location. + * - widget opacity - determines how transparent floating widgets in V2 and all widgets in V1 become when the mouse in not in them. + * - UI version - if allowed by props, the UI version can be toggled between V1 and V2. + * + * @beta + */ +export function UiSettingsPage({allowSettingUiFrameworkVersion}: {allowSettingUiFrameworkVersion: boolean}) { + const themeTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.themeTitle")); + const themeDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.themeDescription")); + const autoHideTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.autoHideTitle")); + const autoHideDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.autoHideDescription")); + const dragInteractionTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.dragInteractionTitle")); + const dragInteractionDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.dragInteractionDescription")); + const useNewUiTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.newUiTitle")); + const useNewUiDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.newUiDescription")); + const useProximityOpacityTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.useProximityOpacityTitle")); + const useProximityOpacityDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.useProximityOpacityDescription")); + const snapWidgetOpacityTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.snapWidgetOpacityTitle")); + const snapWidgetOpacityDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.snapWidgetOpacityDescription")); + const darkLabel = React.useRef(UiFramework.translate("settings.uiSettingsPage.dark")); + const lightLabel = React.useRef(UiFramework.translate("settings.uiSettingsPage.light")); + const systemPreferredLabel = React.useRef(UiFramework.translate("settings.uiSettingsPage.systemPreferred")); + const widgetOpacityTitle = React.useRef(UiFramework.translate("settings.uiSettingsPage.widgetOpacityTitle")); + const widgetOpacityDescription = React.useRef(UiFramework.translate("settings.uiSettingsPage.widgetOpacityDescription")); + + const [theme, setTheme] = React.useState(()=>UiFramework.getColorTheme()); + const [uiVersion, setUiVersion] = React.useState(()=>UiFramework.uiVersion); + const [useDragInteraction, setUseDragInteraction] = React.useState(()=>UiFramework.useDragInteraction); + const [widgetOpacity, setWidgetOpacity] = React.useState(()=>UiFramework.getWidgetOpacity()); + const [autoHideUi, setAutoHideUi] = React.useState(()=>UiShowHideManager.autoHideUi); + const [useProximityOpacity, setUseProximityOpacity] = React.useState(()=>UiShowHideManager.useProximityOpacity); + const [snapWidgetOpacity, setSnapWidgetOpacity] = React.useState(()=>UiShowHideManager.snapWidgetOpacity); + + React.useEffect(() => { + const syncIdsOfInterest = ["configurableui:set_theme", "configurableui:set_widget_opacity", + "configurableui:set-drag-interaction","configurableui:set-framework-version", SyncUiEventId.ShowHideManagerSettingChange ]; + + const handleSyncUiEvent = (args: SyncUiEventArgs) => { + // istanbul ignore else + if (syncIdsOfInterest.some((value: string): boolean => args.eventIds.has(value))) { + if (UiFramework.getColorTheme() !== theme) + setTheme(UiFramework.getColorTheme()); + if (UiShowHideManager.autoHideUi !== autoHideUi) + setAutoHideUi(UiShowHideManager.autoHideUi); + if (UiFramework.uiVersion !== uiVersion) + setUiVersion(UiFramework.uiVersion); + if (UiFramework.useDragInteraction !== useDragInteraction) + setUseDragInteraction(UiFramework.useDragInteraction); + if (UiFramework.getWidgetOpacity() !== widgetOpacity) + setWidgetOpacity(UiFramework.getWidgetOpacity()); + if (UiShowHideManager.autoHideUi !== autoHideUi) + setAutoHideUi(UiShowHideManager.autoHideUi); + if (UiShowHideManager.useProximityOpacity !== useProximityOpacity) + setUseProximityOpacity(UiShowHideManager.useProximityOpacity); + if (UiShowHideManager.snapWidgetOpacity !== snapWidgetOpacity) + setSnapWidgetOpacity(UiShowHideManager.snapWidgetOpacity); + } + }; + return SyncUiEventDispatcher.onSyncUiEvent.addListener(handleSyncUiEvent); + }, [autoHideUi, snapWidgetOpacity, theme, uiVersion, useDragInteraction, useProximityOpacity, widgetOpacity]); + + const defaultThemeOption = { label: systemPreferredLabel.current, value: SYSTEM_PREFERRED_COLOR_THEME }; + const themeOptions: Array = [ + defaultThemeOption, + { label: lightLabel.current, value: ColorTheme.Light }, + { label: darkLabel.current, value: ColorTheme.Dark }, + ]; + + const onThemeChange = React.useCallback((e: React.ChangeEvent) => { + e.preventDefault(); + UiFramework.setColorTheme(e.target.value); + }, []); + + const onAutoHideChange = React.useCallback(async () => { + UiShowHideManager.autoHideUi = !UiShowHideManager.autoHideUi; + },[]); + + const onUseProximityOpacityChange = React.useCallback(async () => { + UiShowHideManager.useProximityOpacity = !UiShowHideManager.useProximityOpacity; + },[]); + + const onSnapWidgetOpacityChange = React.useCallback(async () => { + UiShowHideManager.snapWidgetOpacity = !UiShowHideManager.snapWidgetOpacity; + },[]); + + const onWidgetOpacityChange = React.useCallback(async (values: readonly number[]) => { + // istanbul ignore else + if (values.length > 0) { + UiFramework.setWidgetOpacity(values[0]); + } + },[]); + const onToggleFrameworkVersion = React.useCallback(async () => { + UiFramework.setUiVersion(UiFramework.uiVersion === "2"?"1":"2"); + },[]); + + const onToggleDragInteraction = React.useCallback(async () => { + UiFramework.setUseDragInteraction(!UiFramework.useDragInteraction); + },[]); + + const currentTheme = UiFramework.getColorTheme(); + + return ( +
+ +