From 0da547d3d31f49f4302b1c2034660871ead14476 Mon Sep 17 00:00:00 2001 From: Brian Ingles Date: Fri, 12 Jul 2024 12:33:44 -0500 Subject: [PATCH] refactor: Improve GL item config types (#2138) - Broke out explicit `ItemConfig` sub types and converted `ItemConfig` type into a union them all - Updated `createContentItem` to be more explicit in its mapping of config types to returned content items - Updated references to `ItemConfigType` with `ItemConfig` - Updated `Component`, `RowOrColumn`, and `Stack` classes to allow null `parent` resolves #2130 --- .../src/components/AppDashboards.tsx | 4 +- .../app-utils/src/storage/LayoutStorage.ts | 6 +- .../src/storage/UserLayoutUtils.test.ts | 4 +- .../code-studio/src/main/AppMainContainer.tsx | 8 +- packages/dashboard/src/Dashboard.tsx | 4 +- packages/dashboard/src/DashboardLayout.tsx | 4 +- packages/dashboard/src/PanelManager.ts | 6 +- packages/dashboard/src/layout/LayoutUtils.ts | 45 +++--- packages/embed-widget/src/App.tsx | 6 +- packages/golden-layout/src/LayoutManager.ts | 131 +++++++++++++----- packages/golden-layout/src/config/Config.ts | 4 +- .../golden-layout/src/config/ItemConfig.ts | 78 +++++++---- .../src/controls/BrowserPopout.ts | 8 +- .../golden-layout/src/controls/DragSource.ts | 6 +- packages/golden-layout/src/controls/Header.ts | 4 +- .../src/items/AbstractContentItem.ts | 25 +--- packages/golden-layout/src/items/Component.ts | 6 +- packages/golden-layout/src/items/Root.ts | 12 +- .../golden-layout/src/items/RowOrColumn.ts | 24 +++- packages/golden-layout/src/items/Stack.ts | 60 ++++---- tests/table-operations.spec.ts | 8 ++ 21 files changed, 273 insertions(+), 180 deletions(-) diff --git a/packages/app-utils/src/components/AppDashboards.tsx b/packages/app-utils/src/components/AppDashboards.tsx index 033aa8a068..7a7f8dcba4 100644 --- a/packages/app-utils/src/components/AppDashboards.tsx +++ b/packages/app-utils/src/components/AppDashboards.tsx @@ -10,7 +10,7 @@ import { useObjectFetcher, } from '@deephaven/jsapi-bootstrap'; import LayoutManager, { - ItemConfigType, + ItemConfig, Settings as LayoutSettings, } from '@deephaven/golden-layout'; import { LoadingOverlay } from '@deephaven/components'; @@ -18,7 +18,7 @@ import { LoadingOverlay } from '@deephaven/components'; interface AppDashboardsProps { dashboards: { id: string; - layoutConfig: ItemConfigType[]; + layoutConfig: ItemConfig[]; layoutSettings?: Partial; key?: string; }[]; diff --git a/packages/app-utils/src/storage/LayoutStorage.ts b/packages/app-utils/src/storage/LayoutStorage.ts index c096fafe77..406269640b 100644 --- a/packages/app-utils/src/storage/LayoutStorage.ts +++ b/packages/app-utils/src/storage/LayoutStorage.ts @@ -1,16 +1,16 @@ -import type { ItemConfigType } from '@deephaven/golden-layout'; +import type { ItemConfig } from '@deephaven/golden-layout'; import { FilterSet, Link } from '@deephaven/dashboard-core-plugins'; import { PluginDataMap } from '@deephaven/redux'; /** * Have a different version to support legacy layout exports */ -export type ExportedLayoutV1 = ItemConfigType[]; +export type ExportedLayoutV1 = ItemConfig[]; export type ExportedLayoutV2 = { filterSets: FilterSet[]; links: Link[]; - layoutConfig: ItemConfigType[]; + layoutConfig: ItemConfig[]; pluginDataMap?: PluginDataMap; version: 2; }; diff --git a/packages/app-utils/src/storage/UserLayoutUtils.test.ts b/packages/app-utils/src/storage/UserLayoutUtils.test.ts index 71518c7727..7b8041e3a7 100644 --- a/packages/app-utils/src/storage/UserLayoutUtils.test.ts +++ b/packages/app-utils/src/storage/UserLayoutUtils.test.ts @@ -1,5 +1,5 @@ import { FilterSet, Link } from '@deephaven/dashboard-core-plugins'; -import type { ItemConfigType } from '@deephaven/golden-layout'; +import type { ItemConfig } from '@deephaven/golden-layout'; import LayoutStorage, { ExportedLayout, ExportedLayoutV1, @@ -28,7 +28,7 @@ const filterSets: FilterSet[] = [ panels: [], }, ]; -const layoutConfig: ItemConfigType[] = [ +const layoutConfig: ItemConfig[] = [ { component: 'TestComponent', type: 'TestComponentType', diff --git a/packages/code-studio/src/main/AppMainContainer.tsx b/packages/code-studio/src/main/AppMainContainer.tsx index 3376b6d00f..eb96bb33de 100644 --- a/packages/code-studio/src/main/AppMainContainer.tsx +++ b/packages/code-studio/src/main/AppMainContainer.tsx @@ -77,7 +77,7 @@ import { EMPTY_ARRAY, } from '@deephaven/utils'; import GoldenLayout from '@deephaven/golden-layout'; -import type { ItemConfigType } from '@deephaven/golden-layout'; +import type { ItemConfig } from '@deephaven/golden-layout'; import { type PluginModuleMap, getDashboardPlugins } from '@deephaven/plugin'; import { AppDashboards, @@ -755,7 +755,7 @@ export class AppMainContainer extends Component< getDashboards(): { id: string; - layoutConfig: ItemConfigType[]; + layoutConfig: ItemConfig[]; key?: string; }[] { const { layoutIteration, tabs } = this.state; @@ -766,7 +766,7 @@ export class AppMainContainer extends Component< return [ { id: DEFAULT_DASHBOARD_ID, - layoutConfig: layoutConfig as ItemConfigType[], + layoutConfig: layoutConfig as ItemConfig[], key: `${DEFAULT_DASHBOARD_ID}-${layoutIteration}`, }, ...tabs @@ -774,7 +774,7 @@ export class AppMainContainer extends Component< .map(tab => ({ id: tab.key, layoutConfig: (allDashboardData[tab.key]?.layoutConfig ?? - EMPTY_ARRAY) as ItemConfigType[], + EMPTY_ARRAY) as ItemConfig[], key: `${tab.key}-${layoutIteration}`, })), ]; diff --git a/packages/dashboard/src/Dashboard.tsx b/packages/dashboard/src/Dashboard.tsx index fd3897d014..6a285844c7 100644 --- a/packages/dashboard/src/Dashboard.tsx +++ b/packages/dashboard/src/Dashboard.tsx @@ -9,7 +9,7 @@ import React, { } from 'react'; import throttle from 'lodash.throttle'; import GoldenLayout from '@deephaven/golden-layout'; -import type { ItemConfigType } from '@deephaven/golden-layout'; +import type { ItemConfig } from '@deephaven/golden-layout'; import { useResizeObserver } from '@deephaven/react-hooks'; import './layout/GoldenLayout.scss'; import LayoutUtils from './layout/LayoutUtils'; @@ -33,7 +33,7 @@ export type DashboardProps = { id?: string; children?: React.ReactNode | React.ReactNode[]; emptyDashboard?: React.ReactNode; - layoutConfig?: ItemConfigType[]; + layoutConfig?: ItemConfig[]; layoutSettings?: Record; onLayoutConfigChange?: () => void; onGoldenLayoutChange?: (goldenLayout: GoldenLayout) => void; diff --git a/packages/dashboard/src/DashboardLayout.tsx b/packages/dashboard/src/DashboardLayout.tsx index 98a3eea4ee..6410c4efa7 100644 --- a/packages/dashboard/src/DashboardLayout.tsx +++ b/packages/dashboard/src/DashboardLayout.tsx @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import GoldenLayout from '@deephaven/golden-layout'; import type { Container, - ItemConfigType, + ItemConfig, ReactComponentConfig, } from '@deephaven/golden-layout'; import Log from '@deephaven/log'; @@ -37,7 +37,7 @@ import { } from './DashboardPlugin'; import DashboardPanelWrapper from './DashboardPanelWrapper'; -export type DashboardLayoutConfig = ItemConfigType[]; +export type DashboardLayoutConfig = ItemConfig[]; const log = Log.module('DashboardLayout'); diff --git a/packages/dashboard/src/PanelManager.ts b/packages/dashboard/src/PanelManager.ts index a2ed5e8a52..9effe1832c 100644 --- a/packages/dashboard/src/PanelManager.ts +++ b/packages/dashboard/src/PanelManager.ts @@ -3,7 +3,7 @@ import GoldenLayout from '@deephaven/golden-layout'; import type { Container, ContentItem, - ItemConfigType, + ItemConfig, ReactComponentConfig, } from '@deephaven/golden-layout'; import Log from '@deephaven/log'; @@ -139,7 +139,7 @@ class PanelManager { return Array.from(this.openedMap.values()); } - getOpenedPanelConfigs(): (ItemConfigType | null)[] { + getOpenedPanelConfigs(): (ItemConfig | null)[] { return this.getOpenedPanels().map(panel => { const { glContainer } = panel.props; return LayoutUtils.getComponentConfigFromContainer(glContainer); @@ -278,7 +278,7 @@ class PanelManager { */ handleReopen( panelConfig: ClosedPanel, - replaceConfig?: Partial + replaceConfig?: Partial ): void { log.debug2('Reopen:', panelConfig, replaceConfig); diff --git a/packages/dashboard/src/layout/LayoutUtils.ts b/packages/dashboard/src/layout/LayoutUtils.ts index e63689f807..30c93a4a1c 100644 --- a/packages/dashboard/src/layout/LayoutUtils.ts +++ b/packages/dashboard/src/layout/LayoutUtils.ts @@ -16,7 +16,6 @@ import type { Container, ContentItem, ItemConfig, - ItemConfigType, ReactComponentConfig, Stack, Tab, @@ -38,12 +37,12 @@ export type StackItemConfig = ItemConfig & { activeItemIndex?: number; }; -function isComponentConfig(config: ItemConfigType): config is ComponentConfig { +function isComponentConfig(config: ItemConfig): config is ComponentConfig { return (config as ComponentConfig).componentName !== undefined; } export function isReactComponentConfig( - config: ItemConfigType + config: ItemConfig ): config is ReactComponentConfig { const reactConfig = config as ReactComponentConfig; // Golden layout sets the type to 'component' and componentName to 'lm-react-component' in `createContentItem`, then changes it back in `toConfig` @@ -60,13 +59,13 @@ function isHTMLElement(element: Element): element is HTMLElement { return (element as HTMLElement).focus !== undefined; } -function isStackItemConfig(config: ItemConfigType): config is StackItemConfig { +function isStackItemConfig(config: ItemConfig): config is StackItemConfig { return config.type === 'stack'; } class LayoutUtils { static DEFAULT_FOCUS_SELECTOR = 'input, select, textarea, button'; - static activateTab(root: ContentItem, config: Partial): void { + static activateTab(root: ContentItem, config: Partial): void { const stack = LayoutUtils.getStackForRoot(root, config, false); if (!stack) { log.error('Could not find stack for config', config); @@ -106,17 +105,20 @@ class LayoutUtils { * @returns The newly created stack. */ static addStack(parent: ContentItem, columnPreferred = true): Stack { - const type = columnPreferred ? 'column' : 'row'; if (isRoot(parent)) { + const rowOrColConfig: ItemConfig = { + type: columnPreferred ? 'column' : 'row', + }; + if (parent.contentItems == null || parent.contentItems.length === 0) { - parent.addChild({ type }); + parent.addChild(rowOrColConfig); } const child = parent.contentItems[0]; const isCorrectType = columnPreferred ? child.isColumn : child.isRow; if (!isCorrectType) { parent.removeChild(child, true); - parent.addChild({ type }); + parent.addChild(rowOrColConfig); // The addChild may cause the element that has focus to be removed from the DOM, which changes focus to the body // Try and maintain the focus as best we can. The unfocused element may still send a blur/focus event so that needs to be handled correctly. @@ -142,7 +144,10 @@ class LayoutUtils { ? newParent.isColumn : newParent.isRow; if (!isCorrectType) { - parent.addChild({ type: !columnPreferred ? 'column' : 'row' }); + const inverseRowOrColConfig: ItemConfig = { + type: !columnPreferred ? 'column' : 'row', + }; + parent.addChild(inverseRowOrColConfig); parent.removeChild(newParent, true); parent.contentItems[parent.contentItems.length - 1].addChild(newParent); newParent = parent.contentItems[parent.contentItems.length - 1]; @@ -197,7 +202,7 @@ class LayoutUtils { */ static getStackForConfig( item: ContentItem, - config: Partial = {}, + config: Partial = {}, allowEmptyStack = false ): Stack | null { if (allowEmptyStack && isStack(item) && item.contentItems.length === 0) { @@ -239,7 +244,7 @@ class LayoutUtils { */ static getStackForRoot( root: ContentItem, - config: Partial, + config: Partial, createIfNecessary = true, matchComponentType = true, allowEmptyStack = true @@ -248,7 +253,7 @@ class LayoutUtils { if (!stack && matchComponentType) { stack = this.getStackForConfig( root, - { component: config.component }, + { component: (config as { component?: string }).component }, allowEmptyStack ); } @@ -300,7 +305,7 @@ class LayoutUtils { */ static getContentItemInStack( stack: ContentItem | null, - config: Partial + config: Partial ): ContentItem | null { if (!stack) { return null; @@ -322,10 +327,10 @@ class LayoutUtils { * @returns Dehydrated config */ static dehydrateLayoutConfig( - config: ItemConfigType[], + config: ItemConfig[], dehydrateComponent: ( componentName: string, - config: ItemConfigType + config: ItemConfig ) => PanelConfig ): (PanelConfig | ItemConfig)[] { if (config == null || !config.length) { @@ -516,7 +521,7 @@ class LayoutUtils { config?: Partial; stack?: Stack; replaceExisting?: boolean; - replaceConfig?: Partial; + replaceConfig?: Partial; createNewStack?: boolean; focusElement?: string; dragEvent?: DragEvent; @@ -600,7 +605,7 @@ class LayoutUtils { */ static openComponentInStack( stack: Stack | null, - config: ItemConfigType & Record, + config: ItemConfig & Record, replaceExisting = true ): void { const maintainFocusElement = document.activeElement; // attempt to retain focus after dom manipulation, which can break focus @@ -676,7 +681,7 @@ class LayoutUtils { static renameComponent( root: ContentItem, - config: Partial, + config: Partial, newTitle: string ): void { const stack = LayoutUtils.getStackForRoot(root, config, false); @@ -727,7 +732,7 @@ class LayoutUtils { * @param config Panel config * @returns Panel state */ - static getPanelComponentState(config: ItemConfigType): unknown { + static getPanelComponentState(config: ItemConfig): unknown { if (isComponentConfig(config)) { return config.componentState?.panelState; } @@ -766,7 +771,7 @@ class LayoutUtils { */ static getComponentConfigFromContainer( container?: Container - ): ItemConfigType | null { + ): ItemConfig | null { if (container) { if (container.tab != null && container.tab.contentItem != null) { return container.tab.contentItem.config; diff --git a/packages/embed-widget/src/App.tsx b/packages/embed-widget/src/App.tsx index dd377d2d27..7c2367c581 100644 --- a/packages/embed-widget/src/App.tsx +++ b/packages/embed-widget/src/App.tsx @@ -10,7 +10,7 @@ import { useUser, } from '@deephaven/app-utils'; import type GoldenLayout from '@deephaven/golden-layout'; -import type { ItemConfigType } from '@deephaven/golden-layout'; +import type { ItemConfig } from '@deephaven/golden-layout'; import { ContextMenuRoot, ErrorBoundary, @@ -201,10 +201,10 @@ function App(): JSX.Element { const dashboardPlugins = useDashboardPlugins(); const layoutConfig = (allDashboardData[dashboardId]?.layoutConfig ?? - EMPTY_ARRAY) as ItemConfigType[]; + EMPTY_ARRAY) as ItemConfig[]; const hasMultipleComponents = useMemo(() => { - function getComponentCount(config: ItemConfigType[]) { + function getComponentCount(config: ItemConfig[]) { if (config.length === 0) { return 0; } diff --git a/packages/golden-layout/src/LayoutManager.ts b/packages/golden-layout/src/LayoutManager.ts index e473d6e82d..ad4a96bc5b 100644 --- a/packages/golden-layout/src/LayoutManager.ts +++ b/packages/golden-layout/src/LayoutManager.ts @@ -1,12 +1,18 @@ import $ from 'jquery'; import React from 'react'; import lm from './base'; -import { defaultConfig } from './config'; +import { + ColumnItemConfig, + defaultConfig, + DefaultItemConfig, + RootItemConfig, + RowItemConfig, + StackItemConfig, +} from './config'; import type { ItemConfig, Config, ComponentConfig, - ItemConfigType, ReactComponentConfig, } from './config'; import type { ItemContainer } from './container'; @@ -44,6 +50,31 @@ export type ComponentConstructor< new (container: ItemContainer, state: unknown): unknown; }; +/** + * Item configuration types that are supported inside of `createContentItem` to + * create content items. Note that `ReactComponentConfig` is a valid input type, + * but it gets converted to `ComponentConfig` inside the method before this + * constraint comes into play. + */ +type LayoutItemConfig = + | ColumnItemConfig + | RowItemConfig + | StackItemConfig + | ComponentConfig; + +/** + * Item configuration `type` values that are supported inside of + * `createContentItem` to create content items. Note that `react-component` + * is a valid input value, but it gets converted to `component` inside the + * method before this constraint comes into play. + */ +const LAYOUT_ITEM_CONFIG_TYPES = [ + 'column', + 'row', + 'stack', + 'component', +] as const satisfies Readonly; + /** * The main class that will be exposed as GoldenLayout. * @@ -56,6 +87,16 @@ export class LayoutManager extends EventEmitter { */ static __lm = lm; + /** + * Returns true if the given item config can be used to create a layout item. + * (Used internally by `createContentItem`). + */ + static isLayoutItemConfig(config: ItemConfig): config is LayoutItemConfig { + return (LAYOUT_ITEM_CONFIG_TYPES as Readonly).includes( + config.type + ); + } + /** * Takes a GoldenLayout configuration object and * replaces its keys and values recursively with @@ -119,10 +160,6 @@ export class LayoutManager extends EventEmitter { dropTargetIndicator: DropTargetIndicator | null = null; tabDropPlaceholder = $('
'); - private _typeToItem: { - [type: string]: new (...args: any[]) => AbstractContentItem; - }; - constructor( config: Config, container: JQuery | HTMLElement | undefined @@ -142,13 +179,6 @@ export class LayoutManager extends EventEmitter { if (this.isSubWindow) { $('body').css('visibility', 'hidden'); } - - this._typeToItem = { - column: RowOrColumn.bind(this, true), - row: RowOrColumn.bind(this, false), - stack: Stack, - component: Component, - }; } /** @@ -237,7 +267,9 @@ export class LayoutManager extends EventEmitter { * Content */ const next = function ( - configNode: ComponentConfig & { [key: string]: unknown }, + configNode: (ComponentConfig | ReactComponentConfig) & { + [key: string]: unknown; + }, item: AbstractContentItem & { config: Record; } @@ -257,7 +289,7 @@ export class LayoutManager extends EventEmitter { configNode.content = []; for (let i = 0; i < item.contentItems.length; i++) { - configNode.content[i] = {} as ItemConfigType; + configNode.content[i] = {} as ItemConfig; next( configNode.content[i] as ComponentConfig & Record, item.contentItems[i] as AbstractContentItem & { @@ -489,9 +521,32 @@ export class LayoutManager extends EventEmitter { * @returns Created item */ createContentItem( - config: Partial & { type: ItemConfig['type'] }, - parent?: AbstractContentItem - ) { + config: StackItemConfig, + parent: AbstractContentItem | null + ): Stack; + createContentItem( + config: ColumnItemConfig | RowItemConfig, + parent: AbstractContentItem | null + ): RowOrColumn; + createContentItem( + config: ComponentConfig | ReactComponentConfig, + parent: AbstractContentItem | null + ): Component | Stack; + // Default and Root configs will throw an error hence the `never` return type + createContentItem( + config: DefaultItemConfig | RootItemConfig, + parent: AbstractContentItem | null + ): never; + // This signature is necessary for this function to handle the broader + // `ItemConfig` type since it won't be able to narrow the result in such cases. + createContentItem( + config: ItemConfig, + parent: AbstractContentItem | null + ): Component | RowOrColumn | Stack; + createContentItem( + config: ItemConfig, + parent: AbstractContentItem | null + ): Component | RowOrColumn | Stack { var typeErrorMsg, contentItem; if (typeof config.type !== 'string') { @@ -499,17 +554,18 @@ export class LayoutManager extends EventEmitter { } if (config.type === 'react-component') { - config.type = 'component'; - config.componentName = 'lm-react-component'; + (config as unknown as ComponentConfig).type = 'component'; + (config as unknown as ComponentConfig).componentName = + 'lm-react-component'; } - if (!this._typeToItem[config.type]) { + if (!LayoutManager.isLayoutItemConfig(config)) { typeErrorMsg = "Unknown type '" + config.type + "'. " + 'Valid types are ' + - Object.keys(this._typeToItem).join(','); + LAYOUT_ITEM_CONFIG_TYPES.join(','); throw new ConfigurationError(typeErrorMsg); } @@ -536,8 +592,20 @@ export class LayoutManager extends EventEmitter { } config.id = config.id ?? getUniqueId(); - contentItem = new this._typeToItem[config.type](this, config, parent); - return contentItem; + + if (config.type === 'stack') { + return new Stack(this, config, parent); + } + + if (config.type === 'row') { + return new RowOrColumn(false, this, config, parent); + } + + if (config.type === 'column') { + return new RowOrColumn(true, this, config, parent); + } + + return new Component(this, config, parent); } /** @@ -552,16 +620,13 @@ export class LayoutManager extends EventEmitter { * @returns Created popout */ createPopout( - configOrContentItem: - | ItemConfigType - | AbstractContentItem - | ItemConfigType[], + configOrContentItem: ItemConfig | AbstractContentItem | ItemConfig[], dimensions?: { width: number; height: number; left: number; top: number }, parentId?: string, indexInParent?: number ): BrowserPopout | undefined { let config = configOrContentItem; - let configArray: ItemConfigType[] = []; + let configArray: ItemConfig[] = []; const isItem = configOrContentItem instanceof AbstractContentItem; const self = this; @@ -871,8 +936,7 @@ export class LayoutManager extends EventEmitter { */ _$normalizeContentItem( contentItemOrConfig: - | { type: ItemConfig['type'] } - | ItemConfigType + | ItemConfig | AbstractContentItem | (() => AbstractContentItem), parent?: AbstractContentItem @@ -890,7 +954,10 @@ export class LayoutManager extends EventEmitter { } if ($.isPlainObject(contentItemOrConfig) && contentItemOrConfig.type) { - var newContentItem = this.createContentItem(contentItemOrConfig, parent); + const newContentItem = this.createContentItem( + contentItemOrConfig, + parent ?? null + ); newContentItem.callDownwards('_$init'); return newContentItem; } else { diff --git a/packages/golden-layout/src/config/Config.ts b/packages/golden-layout/src/config/Config.ts index acc241edae..51ceceb7e2 100644 --- a/packages/golden-layout/src/config/Config.ts +++ b/packages/golden-layout/src/config/Config.ts @@ -1,10 +1,10 @@ -import type { ItemConfig, ItemConfigType } from './ItemConfig'; +import type { ItemConfig } from './ItemConfig'; export type Config = { settings: Partial; dimensions: Dimensions; labels: Labels; - content: ItemConfigType[]; + content: ItemConfig[]; maximisedItemId?: string; openPopouts?: PopoutConfig[]; }; diff --git a/packages/golden-layout/src/config/ItemConfig.ts b/packages/golden-layout/src/config/ItemConfig.ts index 24fee22965..3896724c5b 100644 --- a/packages/golden-layout/src/config/ItemConfig.ts +++ b/packages/golden-layout/src/config/ItemConfig.ts @@ -1,27 +1,20 @@ -import type { StackHeaderConfig } from '../items/Stack'; +/** @deprecated Use `ItemConfig` instead. */ +export type ItemConfigType = ItemConfig; -export type ItemConfigType = - | ItemConfig +export type ItemConfig = + | ColumnItemConfig | ComponentConfig - | ReactComponentConfig; - -export interface ItemConfig { - /** - * The type of the item. - */ - type: - | 'default' - | 'root' - | 'row' - | 'column' - | 'stack' - | 'component' - | 'react-component'; + | DefaultItemConfig + | ReactComponentConfig + | RootItemConfig + | RowItemConfig + | StackItemConfig; +export interface ItemConfigAttributes { /** * An array of configurations for items that will be created as children of this item. */ - content?: (ItemConfig | ItemConfigType)[]; + content?: ItemConfig[]; /** * The width of this item, relative to the other children of its parent in percent @@ -59,10 +52,43 @@ export interface ItemConfig { reorderEnabled?: boolean; - header?: StackHeaderConfig; + header?: StackItemHeaderConfig; +} + +export interface DefaultItemConfig extends ItemConfigAttributes { + type: 'default'; +} + +export interface RowItemConfig extends ItemConfigAttributes { + type: 'row'; +} + +export interface ColumnItemConfig extends ItemConfigAttributes { + type: 'column'; } -export interface ComponentConfig extends ItemConfig { +export interface RootItemConfig extends ItemConfigAttributes { + type: 'root'; +} + +export interface StackItemHeaderConfig { + show?: boolean | 'top' | 'left' | 'right' | 'bottom'; + popout?: string; + maximise?: string; + close?: string; + minimise?: string; +} + +export interface StackItemConfig extends ItemConfigAttributes { + type: 'stack'; + activeItemIndex?: number; + header?: StackItemHeaderConfig; + hasHeaders?: boolean; +} + +export interface ComponentConfig extends ItemConfigAttributes { + type: 'component'; + /** * The name of the component as specified in layout.registerComponent. Mandatory if type is 'component'. */ @@ -75,7 +101,9 @@ export interface ComponentConfig extends ItemConfig { componentState: Record; } -export interface ReactComponentConfig extends ItemConfig { +export interface ReactComponentConfig extends ItemConfigAttributes { + type: 'react-component'; + componentName?: string; /** * The name of the component as specified in layout.registerComponent. Mandatory if type is 'react-component' @@ -88,19 +116,17 @@ export interface ReactComponentConfig extends ItemConfig { props?: any; } -export function isGLComponentConfig( - item: ItemConfigType -): item is ComponentConfig { +export function isGLComponentConfig(item: ItemConfig): item is ComponentConfig { return (item as ComponentConfig).componentName !== undefined; } export function isReactComponentConfig( - item: ItemConfigType + item: ItemConfig ): item is ReactComponentConfig { return (item as ReactComponentConfig).component !== undefined; } -export const itemDefaultConfig: ItemConfig = Object.freeze({ +export const itemDefaultConfig: DefaultItemConfig = Object.freeze({ type: 'default', isClosable: true, isFocusOnShow: true, diff --git a/packages/golden-layout/src/controls/BrowserPopout.ts b/packages/golden-layout/src/controls/BrowserPopout.ts index bcbdc96ba5..0bdcda7141 100644 --- a/packages/golden-layout/src/controls/BrowserPopout.ts +++ b/packages/golden-layout/src/controls/BrowserPopout.ts @@ -1,5 +1,5 @@ import $ from 'jquery'; -import type { Config, PopoutConfig, ItemConfigType } from '../config'; +import type { Config, PopoutConfig, ItemConfig } from '../config'; import type Root from '../items/Root'; import type LayoutManager from '../LayoutManager'; import { getUniqueId, minifyConfig, EventEmitter } from '../utils'; @@ -31,7 +31,7 @@ type BrowserDimensions = { export default class BrowserPopout extends EventEmitter { isInitialised = false; - private _config: ItemConfigType[]; + private _config: ItemConfig[]; private _dimensions: BrowserDimensions; private _parentId: string; private _indexInParent: number; @@ -41,7 +41,7 @@ export default class BrowserPopout extends EventEmitter { private _id = null; constructor( - config: ItemConfigType[], + config: ItemConfig[], dimensions: BrowserDimensions, parentId: string, indexInParent: number, @@ -99,7 +99,7 @@ export default class BrowserPopout extends EventEmitter { */ popIn() { let index = this._indexInParent; - let childConfig: ItemConfigType | null = null; + let childConfig: ItemConfig | null = null; let parentItem: AbstractContentItem | null = null; if (this._parentId) { diff --git a/packages/golden-layout/src/controls/DragSource.ts b/packages/golden-layout/src/controls/DragSource.ts index c68f262258..b9011de4d2 100644 --- a/packages/golden-layout/src/controls/DragSource.ts +++ b/packages/golden-layout/src/controls/DragSource.ts @@ -1,5 +1,5 @@ import $ from 'jquery'; -import type { ItemConfigType } from '../config'; +import type { ItemConfig } from '../config'; import type LayoutManager from '../LayoutManager'; import { DragListener } from '../utils'; import DragProxy from './DragProxy'; @@ -14,13 +14,13 @@ import DragProxy from './DragProxy'; */ export default class DragSource { _element: JQuery; - _itemConfig: ItemConfigType | (() => ItemConfigType); + _itemConfig: ItemConfig | (() => ItemConfig); _layoutManager: LayoutManager; _dragListener: DragListener; constructor( element: JQuery, - itemConfig: ItemConfigType | (() => ItemConfigType), + itemConfig: ItemConfig | (() => ItemConfig), layoutManager: LayoutManager ) { this._element = element; diff --git a/packages/golden-layout/src/controls/Header.ts b/packages/golden-layout/src/controls/Header.ts index 20af2378c4..9a26e96a4c 100644 --- a/packages/golden-layout/src/controls/Header.ts +++ b/packages/golden-layout/src/controls/Header.ts @@ -1,6 +1,6 @@ import $ from 'jquery'; +import { StackItemHeaderConfig } from '../config'; import type { AbstractContentItem, Stack } from '../items'; -import type { StackHeaderConfig } from '../items/Stack'; import type LayoutManager from '../LayoutManager'; import { EventEmitter } from '../utils'; import HeaderButton from './HeaderButton'; @@ -533,7 +533,7 @@ export default class Header extends EventEmitter { */ _getHeaderSetting< N extends 'show' | 'popout' | 'maximise' | 'close' | 'minimise', - >(name: N): StackHeaderConfig[N] { + >(name: N): StackItemHeaderConfig[N] { if (name in this.parent._header) return this.parent._header[name]; } diff --git a/packages/golden-layout/src/items/AbstractContentItem.ts b/packages/golden-layout/src/items/AbstractContentItem.ts index 1a0f17e79c..de1f8c316e 100644 --- a/packages/golden-layout/src/items/AbstractContentItem.ts +++ b/packages/golden-layout/src/items/AbstractContentItem.ts @@ -1,7 +1,7 @@ import { animFrame, BubblingEvent, EventEmitter } from '../utils'; import { ConfigurationError } from '../errors'; import { itemDefaultConfig } from '../config'; -import type { ItemConfig, ItemConfigType } from '../config'; +import type { ItemConfig } from '../config'; import type LayoutManager from '../LayoutManager'; import type Tab from '../controls/Tab'; import type Stack from './Stack'; @@ -30,13 +30,6 @@ export type ItemArea = { contentItem: C; }; -type AbstractItemConfig = - | ItemConfig - | { - type: ItemConfig['type']; - content: ItemConfigType[]; - }; - /** * This is the baseclass that all content items inherit from. * Most methods provide a subset of what the sub-classes do. @@ -83,7 +76,7 @@ export default abstract class AbstractContentItem extends EventEmitter { constructor( layoutManager: LayoutManager, - config: AbstractItemConfig, + config: ItemConfig, parent: AbstractContentItem | null, element: JQuery ) { @@ -200,13 +193,7 @@ export default abstract class AbstractContentItem extends EventEmitter { * @param contentItem * @param index If omitted item will be appended */ - addChild( - contentItem: - | AbstractContentItem - | ItemConfigType - | { type: ItemConfig['type'] }, - index?: number - ) { + addChild(contentItem: AbstractContentItem | ItemConfig, index?: number) { contentItem = this.layoutManager._$normalizeContentItem(contentItem, this); if (index === undefined) { index = this.contentItems.length; @@ -565,7 +552,7 @@ export default abstract class AbstractContentItem extends EventEmitter { * PLEASE NOTE, please see addChild for adding contentItems add runtime * @param {configuration item node} config */ - _createContentItems(config: AbstractItemConfig) { + _createContentItems(config: ItemConfig) { var oContentItem; if (!(config.content instanceof Array)) { @@ -586,10 +573,10 @@ export default abstract class AbstractContentItem extends EventEmitter { * @param config * @returns extended config */ - _extendItemNode(config: AbstractItemConfig) { + _extendItemNode(config: TConfig) { for (let [key, value] of Object.entries(itemDefaultConfig)) { // This just appeases TS - const k = key as keyof AbstractItemConfig; + const k = key as keyof TConfig; if (config[k] === undefined) { config[k] = value; } diff --git a/packages/golden-layout/src/items/Component.ts b/packages/golden-layout/src/items/Component.ts index 145da9ce30..368b17cbd8 100644 --- a/packages/golden-layout/src/items/Component.ts +++ b/packages/golden-layout/src/items/Component.ts @@ -18,14 +18,14 @@ export default class Component extends AbstractContentItem { container: ItemContainer; - parent: AbstractContentItem; + parent: AbstractContentItem | null; instance: unknown; constructor( layoutManager: LayoutManager, config: ComponentConfig, - parent: AbstractContentItem + parent: AbstractContentItem | null ) { // Extend before super so the AbstractContentItem defualts aren't applied first Object.entries( @@ -66,7 +66,7 @@ export default class Component extends AbstractContentItem { } close() { - this.parent.removeChild(this); + this.parent?.removeChild(this); } setSize() { diff --git a/packages/golden-layout/src/items/Root.ts b/packages/golden-layout/src/items/Root.ts index 7963652012..c2f01e26af 100644 --- a/packages/golden-layout/src/items/Root.ts +++ b/packages/golden-layout/src/items/Root.ts @@ -1,5 +1,5 @@ import $ from 'jquery'; -import type { ComponentConfig, ItemConfigType, ItemConfig } from '../config'; +import type { ComponentConfig, ItemConfig } from '../config'; import LayoutManager from '../LayoutManager'; import AbstractContentItem, { isComponent, @@ -14,7 +14,7 @@ export default class Root extends AbstractContentItem { constructor( layoutManager: LayoutManager, - config: ComponentConfig | { content: ItemConfigType[] }, + config: ComponentConfig | { content: ItemConfig[] }, containerElement: JQuery ) { super( @@ -30,13 +30,7 @@ export default class Root extends AbstractContentItem { this._containerElement.append(this.element); } - addChild( - contentItem: - | AbstractContentItem - | ItemConfigType - | { type: ItemConfig['type'] }, - index?: number - ) { + addChild(contentItem: AbstractContentItem | ItemConfig, index?: number) { if (this.contentItems.length > 0) { throw new Error('Root node can only have a single child'); } diff --git a/packages/golden-layout/src/items/RowOrColumn.ts b/packages/golden-layout/src/items/RowOrColumn.ts index 063fb55ff1..315bd7b78a 100644 --- a/packages/golden-layout/src/items/RowOrColumn.ts +++ b/packages/golden-layout/src/items/RowOrColumn.ts @@ -3,13 +3,13 @@ import AbstractContentItem from './AbstractContentItem'; import { animFrame } from '../utils'; import { Splitter } from '../controls'; import type LayoutManager from '../LayoutManager'; -import type { ItemConfig, ItemConfigType } from '../config'; +import type { ColumnItemConfig, ItemConfig, RowItemConfig } from '../config'; export default class RowOrColumn extends AbstractContentItem { isRow: boolean; isColumn: boolean; childElementContainer: JQuery; - parent: AbstractContentItem; + parent: AbstractContentItem | null; private _splitter: Splitter[] = []; private _splitterSize: number; @@ -20,11 +20,23 @@ export default class RowOrColumn extends AbstractContentItem { private _splitterMinPosition: number | null = null; private _splitterMaxPosition: number | null = null; + constructor( + isColumn: true, + layoutManager: LayoutManager, + config: ColumnItemConfig, + parent: AbstractContentItem | null + ); + constructor( + isColumn: false, + layoutManager: LayoutManager, + config: RowItemConfig, + parent: AbstractContentItem | null + ); constructor( isColumn: boolean, layoutManager: LayoutManager, - config: ItemConfigType, - parent: AbstractContentItem + config: ColumnItemConfig | RowItemConfig, + parent: AbstractContentItem | null ) { super( layoutManager, @@ -58,7 +70,7 @@ export default class RowOrColumn extends AbstractContentItem { * children need to be added in one go and resize is called afterwards */ addChild( - contentItem: AbstractContentItem | { type: ItemConfig['type'] }, + contentItem: AbstractContentItem | ItemConfig, index?: number, _$suspendResize?: boolean ) { @@ -157,7 +169,7 @@ export default class RowOrColumn extends AbstractContentItem { if (this.contentItems.length === 1 && this.config.isClosable === true) { childItem = this.contentItems[0]; this.contentItems = []; - this.parent.replaceChild(this, childItem, true); + this.parent?.replaceChild(this, childItem, true); } else { this.callDownwards('setSize'); this.emitBubblingEvent('stateChanged'); diff --git a/packages/golden-layout/src/items/Stack.ts b/packages/golden-layout/src/items/Stack.ts index 3bf4b06656..d9bb5fb68f 100644 --- a/packages/golden-layout/src/items/Stack.ts +++ b/packages/golden-layout/src/items/Stack.ts @@ -1,9 +1,13 @@ import $ from 'jquery'; import AbstractContentItem, { isComponent } from './AbstractContentItem'; import type LayoutManager from '../LayoutManager'; -import type { ComponentConfig, ItemConfigType } from '../config'; +import type { + ItemConfig, + StackItemConfig, + StackItemHeaderConfig, +} from '../config'; import { Header } from '../controls'; -import type RowOrColumn from './RowOrColumn'; +import RowOrColumn from './RowOrColumn'; interface HoverDimensions { hoverArea: { @@ -31,47 +35,33 @@ type ContentAreaDimensions = { type BodySegment = keyof ContentAreaDimensions; -export interface StackHeaderConfig { - show?: boolean | 'top' | 'left' | 'right' | 'bottom'; - popout?: string; - maximise?: string; - close?: string; - minimise?: string; -} - export default class Stack extends AbstractContentItem { private _activeContentItem: AbstractContentItem | null = null; - _header: StackHeaderConfig; + _header: StackItemHeaderConfig; childElementContainer = $('
'); header: Header; - parent: RowOrColumn; + parent: AbstractContentItem | null; isStack = true; - private _dropZones = {}; private _dropSegment: string | null = null; _contentAreaDimensions: ContentAreaDimensions | null = null; private _dropIndex: number | undefined; _side: boolean | 'top' | 'left' | 'right' | 'bottom'; _sided: boolean = false; - config: ComponentConfig & { - activeItemIndex?: number; - }; + config: StackItemConfig; constructor( layoutManager: LayoutManager & { config: LayoutManager['config'] & { - header?: StackHeaderConfig; + header?: StackItemHeaderConfig; }; }, - config: ComponentConfig & { - header?: StackHeaderConfig; - hasHeaders?: boolean; - }, - parent: RowOrColumn + config: StackItemConfig, + parent: AbstractContentItem | null ) { super( layoutManager, @@ -176,7 +166,7 @@ export default class Stack extends AbstractContentItem { return this.header.activeContentItem; } - addChild(contentItem: AbstractContentItem | ItemConfigType, index?: number) { + addChild(contentItem: AbstractContentItem | ItemConfig, index?: number) { contentItem = this.layoutManager._$normalizeContentItem(contentItem, this); super.addChild(contentItem, index); this.childElementContainer.append(contentItem.element); @@ -276,8 +266,9 @@ export default class Stack extends AbstractContentItem { const insertBefore = this._dropSegment === 'top' || this._dropSegment === 'left'; const hasCorrectParent = - (isVertical && this.parent.isColumn) || - (isHorizontal && this.parent.isRow); + this.parent instanceof RowOrColumn && + ((isVertical && this.parent.isColumn) || + (isHorizontal && this.parent.isRow)); const type = isVertical ? 'column' : 'row'; const dimension = isVertical ? 'height' : 'width'; @@ -302,21 +293,24 @@ export default class Stack extends AbstractContentItem { * layd out in the correct way. Just add it as a child */ if (hasCorrectParent) { - const index = this.parent.contentItems.indexOf(this); - this.parent.addChild(contentItem, insertBefore ? index : index + 1, true); + const index = this.parent!.contentItems.indexOf(this); + + // Should be a `RowOrColumn` if `hasCorrectParent` is true + (this.parent as RowOrColumn).addChild( + contentItem, + insertBefore ? index : index + 1, + true + ); this.config[dimension] = (this.config[dimension] ?? 0) * 0.5; contentItem.config[dimension] = this.config[dimension]; - this.parent.callDownwards('setSize'); + this.parent!.callDownwards('setSize'); /* * This handles items that are dropped on top or bottom of a row or left / right of a column. We need * to create the appropriate contentItem for them to live in */ } else { - const rowOrColumn = this.layoutManager.createContentItem( - { type: type }, - this - ) as RowOrColumn; - this.parent.replaceChild(this, rowOrColumn); + const rowOrColumn = this.layoutManager.createContentItem({ type }, this); + this.parent?.replaceChild(this, rowOrColumn); rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true); rowOrColumn.addChild(this, insertBefore ? undefined : 0, true); diff --git a/tests/table-operations.spec.ts b/tests/table-operations.spec.ts index dbf77b23b4..8f2f9bbfe7 100644 --- a/tests/table-operations.spec.ts +++ b/tests/table-operations.spec.ts @@ -106,6 +106,14 @@ async function artificialWait(page: Page, tableNumber = 0) { test.beforeEach(async ({ page }) => { await gotoPage(page, ''); + + // Fail quickly if console errors are detected + page.on('console', msg => { + if (msg.type() === 'error') { + throw new Error(msg.text()); + } + }); + await openTable(page, 'all_types'); const tableOperationsMenu = page.locator(