diff --git a/CHANGELOG.md b/CHANGELOG.md index c209b1e2b..d8aa051ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## 77.0.0-SNAPSHOT - unreleased +### 🎁 New Features + +* `DashCanvasView` and `DashContainerView` components now have a public `@bindable` `titleDetails` + property on their models to support displaying additional information in the title bar of these + components. `titleDetails` is not persisted, and is expected to be set programmatically by the + application as needed. + ## 76.0.0 - 2025-09-26 ### 💥 Breaking Changes (upgrade difficulty: 🟠 MEDIUM - AG Grid update, Hoist React upgrade) diff --git a/desktop/cmp/dash/DashViewModel.ts b/desktop/cmp/dash/DashViewModel.ts index 24b58d7a8..c4c194afd 100644 --- a/desktop/cmp/dash/DashViewModel.ts +++ b/desktop/cmp/dash/DashViewModel.ts @@ -4,6 +4,8 @@ * * Copyright © 2025 Extremely Heavy Industries Inc. */ +import {isNil} from 'lodash'; +import {ReactElement} from 'react'; import { HoistModel, managed, @@ -16,7 +18,6 @@ import { import '@xh/hoist/desktop/register'; import {makeObservable, bindable} from '@xh/hoist/mobx'; import {throwIf} from '@xh/hoist/utils/js'; -import {ReactElement} from 'react'; import {DashViewSpec} from './DashViewSpec'; export type DashViewState = PlainObject; @@ -44,9 +45,20 @@ export class DashViewModel extends HoistM */ containerModel: any; - /** Title with which to initialize the view. */ + /** Title with which to initialize the view. Value is persisted. */ @bindable title: string; + /** + * Additional info that will be displayed after the title. + * Applications can bind to this property to provide dynamic title details. + * Value is not persisted. + **/ + @bindable titleDetails: string; + + get fullTitle(): string { + return [this.title, this.titleDetails].filter(it => !isNil(it)).join(' '); + } + /** Icon with which to initialize the view. */ @bindable.ref icon: ReactElement; diff --git a/desktop/cmp/dash/canvas/impl/DashCanvasView.ts b/desktop/cmp/dash/canvas/impl/DashCanvasView.ts index 1e16da0ae..1db124b55 100644 --- a/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +++ b/desktop/cmp/dash/canvas/impl/DashCanvasView.ts @@ -34,13 +34,13 @@ export const dashCanvasView = hoistCmp.factory({ model: uses(DashCanvasViewModel, {publishMode: 'limited'}), render({model, className}) { - const {viewSpec, ref, hidePanelHeader, headerItems, autoHeight} = model, + const {viewSpec, ref, hidePanelHeader, headerItems, autoHeight, fullTitle, icon} = model, headerProps = hidePanelHeader ? {} : { compactHeader: true, - title: model.title, - icon: model.icon, + title: fullTitle, + icon, headerItems: [...headerItems, headerMenu({model})] }; return panel({ diff --git a/desktop/cmp/dash/container/DashContainerModel.ts b/desktop/cmp/dash/container/DashContainerModel.ts index 50f88ce5e..248b2f084 100644 --- a/desktop/cmp/dash/container/DashContainerModel.ts +++ b/desktop/cmp/dash/container/DashContainerModel.ts @@ -338,7 +338,7 @@ export class DashContainerModel renameView(id: string) { const view = this.getItemByViewModel(id); if (!view) return; - this.showTitleForm(view.tab.element); + this.showTitleForm(view.tab.element, this.getViewModel(id)); } onResize() { @@ -528,10 +528,10 @@ export class DashContainerModel const $el = item.tab.element, // Note: this is a jquery element stack = item.parent, - $titleEl = $el.find('.lm_title').first(), + $titleEl = this.getTitleElement($el), iconSelector = 'svg.svg-inline--fa', viewSpec = this.getViewSpec(item.config.component), - {icon, title} = viewModel; + {icon} = viewModel; $el.off('contextmenu').contextmenu(e => { const index = stack.contentItems.indexOf(item); @@ -551,14 +551,9 @@ export class DashContainerModel } } - if (title) { - const currentTitle = $titleEl.text(); - if (currentTitle !== title) $titleEl.text(title); - } - if (viewSpec.allowRename) { this.insertTitleForm($el, viewModel); - $titleEl.off('dblclick').dblclick(() => this.showTitleForm($el)); + $titleEl.off('dblclick').dblclick(() => this.showTitleForm($el, viewModel)); } }); } @@ -568,7 +563,7 @@ export class DashContainerModel if ($el.find(formSelector).length) return; // Create and insert form - const $titleEl = $el.find('.lm_title').first(); + const $titleEl = this.getTitleElement($el); $titleEl.after(`
`); // Attach listeners @@ -579,7 +574,6 @@ export class DashContainerModel $formEl.submit(() => { const title = $inputEl.val(); if (title.length) { - $titleEl.text(title); viewModel.title = title; } @@ -588,12 +582,11 @@ export class DashContainerModel }); } - private showTitleForm($tabEl) { + private showTitleForm($tabEl, viewModel: DashViewModel) { if (this.renameLocked) return; - const $titleEl = $tabEl.find('.lm_title').first(), - $inputEl = $tabEl.find('.title-form input').first(), - currentTitle = $titleEl.text(); + const $inputEl = $tabEl.find('.title-form input').first(), + currentTitle = viewModel.title; $tabEl.addClass('show-title-form'); $inputEl.val(currentTitle); @@ -647,6 +640,16 @@ export class DashContainerModel containerModel: this }); + model.addReaction({ + track: () => model.fullTitle, + run: () => { + const item = this.getItemByViewModel(id), + $titleEl = this.getTitleElement(item.tab.element); + + $titleEl.text(model.fullTitle); + } + }); + this.addViewModel(model); return modelLookupContextProvider({ value: this.modelLookupContext, @@ -662,6 +665,10 @@ export class DashContainerModel return ret; } + private getTitleElement($el) { + return $el.find('.lm_title').first(); + } + @action private destroyGoldenLayout() { XH.safeDestroy(this.goldenLayout);