From 5af61b3ed98e8114e56fd7fd447bffb764b0ef9b Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Thu, 23 May 2024 15:01:37 -0400 Subject: [PATCH 1/5] Test new DynamicTabSwitcher in main appBar --- client-app/src/desktop/AppComponent.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/client-app/src/desktop/AppComponent.ts b/client-app/src/desktop/AppComponent.ts index 88071880a..273104134 100755 --- a/client-app/src/desktop/AppComponent.ts +++ b/client-app/src/desktop/AppComponent.ts @@ -4,7 +4,7 @@ import {webSocketIndicator} from '@xh/hoist/cmp/websocket'; import {hoistCmp, uses} from '@xh/hoist/core'; import {appBar, appBarSeparator} from '@xh/hoist/desktop/cmp/appbar'; import {panel} from '@xh/hoist/desktop/cmp/panel'; -import {tabSwitcher} from '@xh/hoist/desktop/cmp/tab'; +import {dynamicTabSwitcher} from '@xh/hoist/desktop/cmp/tab'; import {welcomeMsg} from '../core/cmp/WelcomeMsg'; // @ts-ignore import xhLogo from '../core/img/xh-toolbox-logo.png'; @@ -21,7 +21,14 @@ export const AppComponent = hoistCmp({ tbar: appBar({ icon: img({src: xhLogo, onClick: () => model.goHome()}), title: null, - leftItems: [tabSwitcher({enableOverflow: true})], + leftItems: [ + dynamicTabSwitcher({ + modelConfig: { + persistWith: {prefKey: 'appState'}, + tabContainerModel: model.tabModel + } + }) + ], rightItems: [ webSocketIndicator({iconOnly: true, marginRight: 4}), appBarSeparator() From 9581f9df888c22d63ff73468eebdb8eb0b87d7e6 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Mon, 15 Sep 2025 11:27:14 -0400 Subject: [PATCH 2/5] Enhance Dynamic Tab Switcher example in main AppComponent / model --- client-app/src/desktop/AppComponent.ts | 9 +--- client-app/src/desktop/AppModel.ts | 69 +++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/client-app/src/desktop/AppComponent.ts b/client-app/src/desktop/AppComponent.ts index 273104134..40091510e 100755 --- a/client-app/src/desktop/AppComponent.ts +++ b/client-app/src/desktop/AppComponent.ts @@ -21,14 +21,7 @@ export const AppComponent = hoistCmp({ tbar: appBar({ icon: img({src: xhLogo, onClick: () => model.goHome()}), title: null, - leftItems: [ - dynamicTabSwitcher({ - modelConfig: { - persistWith: {prefKey: 'appState'}, - tabContainerModel: model.tabModel - } - }) - ], + leftItems: [dynamicTabSwitcher()], rightItems: [ webSocketIndicator({iconOnly: true, marginRight: 4}), appBarSeparator() diff --git a/client-app/src/desktop/AppModel.ts b/client-app/src/desktop/AppModel.ts index c8640a50d..9c3951575 100755 --- a/client-app/src/desktop/AppModel.ts +++ b/client-app/src/desktop/AppModel.ts @@ -1,4 +1,5 @@ -import {TabContainerModel} from '@xh/hoist/cmp/tab'; +import {span} from '@xh/hoist/cmp/layout'; +import {TabContainerModel, TabModel} from '@xh/hoist/cmp/tab'; import {LoadSpec, managed, XH} from '@xh/hoist/core'; import { autoRefreshAppOption, @@ -6,7 +7,11 @@ import { themeAppOption } from '@xh/hoist/desktop/cmp/appOption'; import {switchInput} from '@xh/hoist/desktop/cmp/input'; +import {DynamicTabSwitcherModel} from '@xh/hoist/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel'; +import {fmtDateTimeSec} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; +import {isEmpty} from 'lodash'; +import {BaseAppModel} from '../BaseAppModel'; import {GitHubService} from '../core/svc/GitHubService'; import {PortfolioService} from '../core/svc/PortfolioService'; import { @@ -16,6 +21,7 @@ import { simpleTreeMapPanel, splitTreeMapPanel } from './tabs/charts'; +import {examplesTab} from './tabs/examples/ExamplesTab'; import {formPanel, inputsPanel, toolbarFormPanel} from './tabs/forms'; import { agGridView, @@ -30,6 +36,7 @@ import { treeGridWithCheckboxPanel, zoneGridPanel } from './tabs/grids'; +import {homeTab} from './tabs/home/HomeTab'; import { dashCanvasPanel, dashContainerPanel, @@ -39,8 +46,6 @@ import { tileFrameContainerPanel, vboxContainerPanel } from './tabs/layout'; -import {examplesTab} from './tabs/examples/ExamplesTab'; -import {homeTab} from './tabs/home/HomeTab'; import {mobileTab} from './tabs/mobile/MobileTab'; import { appNotificationsPanel, @@ -69,9 +74,6 @@ import { panelSizingPanel, toolbarPanel } from './tabs/panels'; -import {fmtDateTimeSec} from '@xh/hoist/format'; -import {span} from '@xh/hoist/cmp/layout'; -import {BaseAppModel} from '../BaseAppModel'; export class AppModel extends BaseAppModel { /** Singleton instance reference - installed by XH upon init. */ @@ -216,6 +218,61 @@ export class AppModel extends BaseAppModel { ] }); + @managed + switcherModel: DynamicTabSwitcherModel = new DynamicTabSwitcherModel({ + initialFavorites: [...this.tabModel.tabs.map(it => it.id), 'github'], + persistWith: {prefKey: 'appState'}, + tabContainerModel: this.tabModel, + extraTabs: [ + { + id: 'github', + title: 'XH Github', + tooltip: 'Open XH Github in new window', + icon: Icon.icon({iconName: 'github', prefix: 'fab'}), + actionFn: () => XH.openWindow('https://github.com/xh') + } + ], + extraMenuItems: [ + { + text: 'Open Tab in New Window', + icon: Icon.openExternal(), + actionFn: (_, {tab}) => { + if (tab instanceof TabModel) { + const {params} = XH.router.getState(); + XH.openWindow( + window.origin + + XH.router.buildPath(tab.containerModel.route + '.' + tab.id, params) + ); + } + }, + prepareFn: (me, {tab}) => { + if (!(tab instanceof TabModel)) { + me.hidden = true; + } + } + }, + '-', + { + text: 'More Tabs...', + prepareFn: me => { + const tabs = this.tabModel.tabs.filter( + tab => !this.switcherModel.enabledVisibleTabs.includes(tab) + ); + if (isEmpty(tabs)) { + me.hidden = true; + } else { + me.hidden = false; + me.items = tabs.map(tab => ({ + text: tab.title, + icon: tab.icon, + actionFn: () => this.tabModel.activateTab(tab) + })); + } + } + } + ] + }); + override async initAsync() { await super.initAsync(); await XH.installServicesAsync(GitHubService, PortfolioService); From 67d82bbd92772b2623091006874274f38dd351c9 Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Wed, 12 Nov 2025 16:14:06 -0500 Subject: [PATCH 3/5] Integrate dynamicTabSwitcher into existing tab API --- client-app/src/desktop/AppComponent.ts | 5 +- client-app/src/desktop/AppModel.ts | 396 +++++++++--------- .../layout/tabContainer/tabs/CustomExample.ts | 4 +- .../tabContainer/tabs/DynamicExample.ts | 16 +- 4 files changed, 217 insertions(+), 204 deletions(-) diff --git a/client-app/src/desktop/AppComponent.ts b/client-app/src/desktop/AppComponent.ts index 40091510e..a508541c6 100755 --- a/client-app/src/desktop/AppComponent.ts +++ b/client-app/src/desktop/AppComponent.ts @@ -40,7 +40,10 @@ export const AppComponent = hoistCmp({ onKeyDown: () => model.goHome() } ], - item: tabContainer(), + item: tabContainer({ + switcher: false, + childTabContainerProps: {switcher: {orientation: 'left', className: 'tb-switcher'}} + }), mask: 'onLoad' }); } diff --git a/client-app/src/desktop/AppModel.ts b/client-app/src/desktop/AppModel.ts index 9c3951575..3da56054b 100755 --- a/client-app/src/desktop/AppModel.ts +++ b/client-app/src/desktop/AppModel.ts @@ -7,7 +7,6 @@ import { themeAppOption } from '@xh/hoist/desktop/cmp/appOption'; import {switchInput} from '@xh/hoist/desktop/cmp/input'; -import {DynamicTabSwitcherModel} from '@xh/hoist/desktop/cmp/tab/dynamic/DynamicTabSwitcherModel'; import {fmtDateTimeSec} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; import {isEmpty} from 'lodash'; @@ -80,198 +79,7 @@ export class AppModel extends BaseAppModel { static instance: AppModel; @managed - tabModel: TabContainerModel = new TabContainerModel({ - route: 'default', - track: true, - switcher: false, - tabs: [ - {id: 'home', icon: Icon.home(), content: homeTab}, - { - id: 'grids', - icon: Icon.grid(), - content: { - switcher: {orientation: 'left', className: 'tb-switcher'}, - tabs: [ - {id: 'standard', content: standardGridPanel}, - {id: 'tree', content: treeGridPanel}, - {id: 'columnFiltering', content: columnFilteringPanel}, - {id: 'inlineEditing', content: inlineEditingPanel}, - {id: 'zoneGrid', title: 'Zone Grid', content: zoneGridPanel}, - {id: 'dataview', title: 'DataView', content: dataViewPanel}, - { - id: 'treeWithCheckBox', - title: 'Tree w/CheckBox', - content: treeGridWithCheckboxPanel - }, - { - id: 'groupedCols', - title: 'Grouped Columns', - content: columnGroupsGridPanel - }, - {id: 'externalSort', content: externalSortGridPanel}, - {id: 'rest', title: 'REST Editor', content: restGridPanel}, - {id: 'agGrid', title: 'ag-Grid Wrapper', content: agGridView} - ] - } - }, - { - id: 'panels', - icon: Icon.window(), - content: { - switcher: {orientation: 'left', className: 'tb-switcher'}, - tabs: [ - {id: 'intro', content: basicPanel}, - {id: 'toolbars', content: toolbarPanel}, - {id: 'sizing', content: panelSizingPanel}, - {id: 'mask', content: maskPanel}, - {id: 'loadingIndicator', content: loadingIndicatorPanel} - ] - } - }, - { - id: 'layout', - icon: Icon.layout(), - content: { - switcher: {orientation: 'left', className: 'tb-switcher'}, - tabs: [ - {id: 'hbox', title: 'HBox', content: hboxContainerPanel}, - {id: 'vbox', title: 'VBox', content: vboxContainerPanel}, - { - id: 'tabPanel', - title: 'TabContainer', - content: tabPanelContainerPanel - }, - { - id: 'dashContainer', - title: 'DashContainer', - content: dashContainerPanel - }, - {id: 'dashCanvas', title: 'DashCanvas', content: dashCanvasPanel}, - {id: 'dock', title: 'DockContainer', content: dockContainerPanel}, - {id: 'tileFrame', title: 'TileFrame', content: tileFrameContainerPanel} - ] - } - }, - { - id: 'forms', - icon: Icon.edit(), - content: { - switcher: {orientation: 'left', className: 'tb-switcher'}, - tabs: [ - {id: 'form', title: 'FormModel', content: formPanel}, - {id: 'inputs', title: 'Hoist Inputs', content: inputsPanel}, - {id: 'toolbarForm', title: 'Toolbar Forms', content: toolbarFormPanel} - ] - } - }, - { - id: 'charts', - icon: Icon.chartLine(), - content: { - switcher: {orientation: 'left', className: 'tb-switcher'}, - tabs: [ - {id: 'line', content: lineChartPanel}, - {id: 'ohlc', title: 'OHLC', content: ohlcChartPanel}, - {id: 'simpleTreeMap', title: 'TreeMap', content: simpleTreeMapPanel}, - {id: 'gridTreeMap', title: 'Grid TreeMap', content: gridTreeMapPanel}, - {id: 'splitTreeMap', title: 'Split TreeMap', content: splitTreeMapPanel} - ] - } - }, - {id: 'mobile', icon: Icon.mobile(), content: mobileTab}, - { - id: 'other', - icon: Icon.boxFull(), - content: { - switcher: {orientation: 'left', className: 'tb-switcher'}, - tabs: [ - {id: 'appNotifications', content: appNotificationsPanel}, - {id: 'buttons', content: buttonsPanel}, - {id: 'clock', content: clockPanel}, - {id: 'customPackage', content: customPackagePanel}, - {id: 'errorMessage', title: 'ErrorMessage', content: errorMessagePanel}, - { - id: 'exceptionHandler', - title: 'Exception Handling', - content: exceptionHandlerPanel - }, - {id: 'jsx', title: 'Factories vs. JSX', content: jsxPanel}, - {id: 'fileChooser', title: 'FileChooser', content: fileChooserPanel}, - {id: 'formatDates', content: dateFormatsPanel}, - {id: 'formatNumbers', content: numberFormatsPanel}, - {id: 'icons', content: iconsPanel}, - {id: 'inspector', content: inspectorPanel}, - { - id: 'leftRightChooser', - title: 'LeftRightChooser', - content: leftRightChooserPanel - }, - {id: 'pinPad', title: 'PIN Pad', content: pinPadPanel}, - {id: 'placeholder', title: 'Placeholder', content: placeholderPanel}, - {id: 'popups', content: popupsPanel}, - {id: 'timestamp', content: relativeTimestampPanel}, - {id: 'simpleRouting', content: simpleRoutingPanel} - ] - } - }, - {id: 'examples', icon: Icon.books(), content: examplesTab} - ] - }); - - @managed - switcherModel: DynamicTabSwitcherModel = new DynamicTabSwitcherModel({ - initialFavorites: [...this.tabModel.tabs.map(it => it.id), 'github'], - persistWith: {prefKey: 'appState'}, - tabContainerModel: this.tabModel, - extraTabs: [ - { - id: 'github', - title: 'XH Github', - tooltip: 'Open XH Github in new window', - icon: Icon.icon({iconName: 'github', prefix: 'fab'}), - actionFn: () => XH.openWindow('https://github.com/xh') - } - ], - extraMenuItems: [ - { - text: 'Open Tab in New Window', - icon: Icon.openExternal(), - actionFn: (_, {tab}) => { - if (tab instanceof TabModel) { - const {params} = XH.router.getState(); - XH.openWindow( - window.origin + - XH.router.buildPath(tab.containerModel.route + '.' + tab.id, params) - ); - } - }, - prepareFn: (me, {tab}) => { - if (!(tab instanceof TabModel)) { - me.hidden = true; - } - } - }, - '-', - { - text: 'More Tabs...', - prepareFn: me => { - const tabs = this.tabModel.tabs.filter( - tab => !this.switcherModel.enabledVisibleTabs.includes(tab) - ); - if (isEmpty(tabs)) { - me.hidden = true; - } else { - me.hidden = false; - me.items = tabs.map(tab => ({ - text: tab.title, - icon: tab.icon, - actionFn: () => this.tabModel.activateTab(tab) - })); - } - } - } - ] - }); + tabModel: TabContainerModel = this.createTabContainerModel(); override async initAsync() { await super.initAsync(); @@ -439,4 +247,206 @@ export class AppModel extends BaseAppModel { } ]; } + + // ------------------------------- + // Implementation + // ------------------------------- + private createTabContainerModel(): TabContainerModel { + const tabs = [ + {id: 'home', icon: Icon.home(), content: homeTab}, + { + id: 'grids', + icon: Icon.grid(), + content: { + // switcher: {orientation: 'left', className: 'tb-switcher'}, // TODO revisit + tabs: [ + {id: 'standard', content: standardGridPanel}, + {id: 'tree', content: treeGridPanel}, + {id: 'columnFiltering', content: columnFilteringPanel}, + {id: 'inlineEditing', content: inlineEditingPanel}, + {id: 'zoneGrid', title: 'Zone Grid', content: zoneGridPanel}, + {id: 'dataview', title: 'DataView', content: dataViewPanel}, + { + id: 'treeWithCheckBox', + title: 'Tree w/CheckBox', + content: treeGridWithCheckboxPanel + }, + { + id: 'groupedCols', + title: 'Grouped Columns', + content: columnGroupsGridPanel + }, + {id: 'externalSort', content: externalSortGridPanel}, + {id: 'rest', title: 'REST Editor', content: restGridPanel}, + {id: 'agGrid', title: 'ag-Grid Wrapper', content: agGridView} + ] + } + }, + { + id: 'panels', + icon: Icon.window(), + content: { + // switcher: {orientation: 'left', className: 'tb-switcher'}, // TODO revisit + tabs: [ + {id: 'intro', content: basicPanel}, + {id: 'toolbars', content: toolbarPanel}, + {id: 'sizing', content: panelSizingPanel}, + {id: 'mask', content: maskPanel}, + {id: 'loadingIndicator', content: loadingIndicatorPanel} + ] + } + }, + { + id: 'layout', + icon: Icon.layout(), + content: { + // switcher: {orientation: 'left', className: 'tb-switcher'}, // TODO revisit + tabs: [ + {id: 'hbox', title: 'HBox', content: hboxContainerPanel}, + {id: 'vbox', title: 'VBox', content: vboxContainerPanel}, + { + id: 'tabPanel', + title: 'TabContainer', + content: tabPanelContainerPanel + }, + { + id: 'dashContainer', + title: 'DashContainer', + content: dashContainerPanel + }, + {id: 'dashCanvas', title: 'DashCanvas', content: dashCanvasPanel}, + {id: 'dock', title: 'DockContainer', content: dockContainerPanel}, + {id: 'tileFrame', title: 'TileFrame', content: tileFrameContainerPanel} + ] + } + }, + { + id: 'forms', + icon: Icon.edit(), + content: { + // switcher: {orientation: 'left', className: 'tb-switcher'}, // TODO revisit + tabs: [ + {id: 'form', title: 'FormModel', content: formPanel}, + {id: 'inputs', title: 'Hoist Inputs', content: inputsPanel}, + {id: 'toolbarForm', title: 'Toolbar Forms', content: toolbarFormPanel} + ] + } + }, + { + id: 'charts', + icon: Icon.chartLine(), + content: { + // switcher: {orientation: 'left', className: 'tb-switcher'}, // TODO revisit + tabs: [ + {id: 'line', content: lineChartPanel}, + {id: 'ohlc', title: 'OHLC', content: ohlcChartPanel}, + {id: 'simpleTreeMap', title: 'TreeMap', content: simpleTreeMapPanel}, + {id: 'gridTreeMap', title: 'Grid TreeMap', content: gridTreeMapPanel}, + {id: 'splitTreeMap', title: 'Split TreeMap', content: splitTreeMapPanel} + ] + } + }, + {id: 'mobile', icon: Icon.mobile(), content: mobileTab}, + { + id: 'other', + icon: Icon.boxFull(), + content: { + // switcher: {orientation: 'left', className: 'tb-switcher'}, // TODO revisit + tabs: [ + {id: 'appNotifications', content: appNotificationsPanel}, + {id: 'buttons', content: buttonsPanel}, + {id: 'clock', content: clockPanel}, + {id: 'customPackage', content: customPackagePanel}, + {id: 'errorMessage', title: 'ErrorMessage', content: errorMessagePanel}, + { + id: 'exceptionHandler', + title: 'Exception Handling', + content: exceptionHandlerPanel + }, + {id: 'jsx', title: 'Factories vs. JSX', content: jsxPanel}, + {id: 'fileChooser', title: 'FileChooser', content: fileChooserPanel}, + {id: 'formatDates', content: dateFormatsPanel}, + {id: 'formatNumbers', content: numberFormatsPanel}, + {id: 'icons', content: iconsPanel}, + {id: 'inspector', content: inspectorPanel}, + { + id: 'leftRightChooser', + title: 'LeftRightChooser', + content: leftRightChooserPanel + }, + {id: 'pinPad', title: 'PIN Pad', content: pinPadPanel}, + {id: 'placeholder', title: 'Placeholder', content: placeholderPanel}, + {id: 'popups', content: popupsPanel}, + {id: 'timestamp', content: relativeTimestampPanel}, + {id: 'simpleRouting', content: simpleRoutingPanel} + ] + } + }, + {id: 'examples', icon: Icon.books(), content: examplesTab} + ], + actionTabs = [ + { + id: 'github', + title: 'XH Github', + tooltip: 'Open XH Github in new window', + icon: Icon.icon({iconName: 'github', prefix: 'fab'}), + actionFn: () => XH.openWindow('https://github.com/xh') + } + ]; + return new TabContainerModel({ + persistWith: {localStorageKey: 'tabState'}, + route: 'default', + track: true, + tabs, + dynamicTabSwitcherModel: { + initialFavorites: [...tabs, ...actionTabs].map(it => it.id), + actionTabs, + extraMenuItems: [ + { + text: 'Open Tab in New Window', + icon: Icon.openExternal(), + actionFn: (_, {tab}) => { + if (tab instanceof TabModel) { + const {params} = XH.router.getState(); + XH.openWindow( + window.origin + + XH.router.buildPath( + tab.containerModel.route + '.' + tab.id, + params + ) + ); + } + }, + prepareFn: (me, {tab}) => { + if (!(tab instanceof TabModel)) { + me.hidden = true; + } + } + }, + '-', + { + text: 'More Tabs...', + prepareFn: me => { + const tabs = this.tabModel.tabs.filter( + tab => + !this.tabModel.dynamicTabSwitcherModel.enabledVisibleTabs.includes( + tab + ) + ); + if (isEmpty(tabs)) { + me.hidden = true; + } else { + me.hidden = false; + me.items = tabs.map(tab => ({ + text: tab.title, + icon: tab.icon, + actionFn: () => this.tabModel.activateTab(tab) + })); + } + } + } + ] + } + }); + } } diff --git a/client-app/src/desktop/tabs/layout/tabContainer/tabs/CustomExample.ts b/client-app/src/desktop/tabs/layout/tabContainer/tabs/CustomExample.ts index d00f1dba1..50859f58a 100644 --- a/client-app/src/desktop/tabs/layout/tabContainer/tabs/CustomExample.ts +++ b/client-app/src/desktop/tabs/layout/tabContainer/tabs/CustomExample.ts @@ -17,12 +17,12 @@ export const customExample = hoistCmp.factory({ onClick: () => model.detachedTabModel.activateTab(childModel.id) }) ), - item: tabContainer({model: model.detachedTabModel}) + item: tabContainer({model: model.detachedTabModel, switcher: false}) }); } }); class CustomExampleModel extends HoistModel { @managed - detachedTabModel = new TabContainerModel(createContainerModelConfig({switcher: false})); + detachedTabModel = new TabContainerModel(createContainerModelConfig({})); } diff --git a/client-app/src/desktop/tabs/layout/tabContainer/tabs/DynamicExample.ts b/client-app/src/desktop/tabs/layout/tabContainer/tabs/DynamicExample.ts index 51d60b9f6..bf41520fa 100644 --- a/client-app/src/desktop/tabs/layout/tabContainer/tabs/DynamicExample.ts +++ b/client-app/src/desktop/tabs/layout/tabContainer/tabs/DynamicExample.ts @@ -27,7 +27,13 @@ export const dynamicExample = hoistCmp.factory({ }), button({icon: Icon.xCircle(), text: 'Clear', onClick: () => model.clearDynamic()}) ], - item: tabContainer({model: model.dynamicModel}) + item: tabContainer({ + model: model.dynamicModel, + switcher: { + orientation: 'top', + enableOverflow: true + } + }) }); } }); @@ -36,13 +42,7 @@ class DynamicExampleModel extends HoistModel { id = 0; @managed - dynamicModel = new TabContainerModel({ - tabs: [], - switcher: { - orientation: 'top', - enableOverflow: true - } - }); + dynamicModel = new TabContainerModel({tabs: []}); constructor() { super(); From 1f2693ea866fe8dd527355ae9b797764f12ee47e Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Wed, 19 Nov 2025 16:28:11 -0500 Subject: [PATCH 4/5] CR Lee --- client-app/src/desktop/AppComponent.ts | 2 +- client-app/src/desktop/AppModel.ts | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client-app/src/desktop/AppComponent.ts b/client-app/src/desktop/AppComponent.ts index a508541c6..13165ee11 100755 --- a/client-app/src/desktop/AppComponent.ts +++ b/client-app/src/desktop/AppComponent.ts @@ -42,7 +42,7 @@ export const AppComponent = hoistCmp({ ], item: tabContainer({ switcher: false, - childTabContainerProps: {switcher: {orientation: 'left', className: 'tb-switcher'}} + childContainerProps: {switcher: {orientation: 'left', className: 'tb-switcher'}} }), mask: 'onLoad' }); diff --git a/client-app/src/desktop/AppModel.ts b/client-app/src/desktop/AppModel.ts index 3da56054b..48cf65faa 100755 --- a/client-app/src/desktop/AppModel.ts +++ b/client-app/src/desktop/AppModel.ts @@ -398,7 +398,7 @@ export class AppModel extends BaseAppModel { route: 'default', track: true, tabs, - dynamicTabSwitcherModel: { + switcher: { initialFavorites: [...tabs, ...actionTabs].map(it => it.id), actionTabs, extraMenuItems: [ @@ -427,11 +427,9 @@ export class AppModel extends BaseAppModel { { text: 'More Tabs...', prepareFn: me => { - const tabs = this.tabModel.tabs.filter( + const tabs = [...this.tabModel.tabs, ...actionTabs].filter( tab => - !this.tabModel.dynamicTabSwitcherModel.enabledVisibleTabs.includes( - tab - ) + !this.tabModel.dynamicTabSwitcherModel.visibleTabs.includes(tab) ); if (isEmpty(tabs)) { me.hidden = true; @@ -440,7 +438,16 @@ export class AppModel extends BaseAppModel { me.items = tabs.map(tab => ({ text: tab.title, icon: tab.icon, - actionFn: () => this.tabModel.activateTab(tab) + actionFn: () => { + if (tab instanceof TabModel) { + this.tabModel.activateTab(tab); + } else { + tab.actionFn(); + this.tabModel.dynamicTabSwitcherModel.toggleTabFavorite( + tab.id + ); + } + } })); } } From ac4eda4ff461e99035f9291243e88c165013c03d Mon Sep 17 00:00:00 2001 From: Greg Solomon Date: Wed, 19 Nov 2025 18:26:58 -0500 Subject: [PATCH 5/5] CR Anselm --- client-app/src/desktop/AppModel.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client-app/src/desktop/AppModel.ts b/client-app/src/desktop/AppModel.ts index 48cf65faa..24d9b4fef 100755 --- a/client-app/src/desktop/AppModel.ts +++ b/client-app/src/desktop/AppModel.ts @@ -428,8 +428,10 @@ export class AppModel extends BaseAppModel { text: 'More Tabs...', prepareFn: me => { const tabs = [...this.tabModel.tabs, ...actionTabs].filter( - tab => - !this.tabModel.dynamicTabSwitcherModel.visibleTabs.includes(tab) + ({id}) => + !this.tabModel.dynamicTabSwitcherModel.visibleTabs.some( + it => it.id === id + ) ); if (isEmpty(tabs)) { me.hidden = true;