Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 14 additions & 2 deletions desktop/cmp/dash/DashViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*
* Copyright © 2025 Extremely Heavy Industries Inc.
*/
import {isNil} from 'lodash';
import {ReactElement} from 'react';
import {
HoistModel,
managed,
Expand All @@ -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;
Expand Down Expand Up @@ -44,9 +45,20 @@ export class DashViewModel<T extends DashViewSpec = DashViewSpec> 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;

Expand Down
6 changes: 3 additions & 3 deletions desktop/cmp/dash/canvas/impl/DashCanvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
37 changes: 22 additions & 15 deletions desktop/cmp/dash/container/DashContainerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -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));
}
});
}
Expand All @@ -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(`<form class="title-form"><input type="text"/></form>`);

// Attach listeners
Expand All @@ -579,7 +574,6 @@ export class DashContainerModel
$formEl.submit(() => {
const title = $inputEl.val();
if (title.length) {
$titleEl.text(title);
viewModel.title = title;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down
Loading