@xh/hoist 76.0.0 → 76.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -39
- package/appcontainer/ImpersonationBarModel.ts +8 -3
- package/build/types/appcontainer/ImpersonationBarModel.d.ts +2 -0
- package/build/types/cmp/chart/ChartModel.d.ts +2 -0
- package/build/types/core/model/HoistModel.d.ts +1 -1
- package/build/types/data/cube/Query.d.ts +1 -0
- package/build/types/desktop/cmp/dash/DashViewModel.d.ts +9 -2
- package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -0
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.d.ts +4 -2
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.d.ts +10 -3
- package/cmp/chart/ChartModel.ts +8 -2
- package/cmp/dataview/DataView.ts +1 -1
- package/core/model/HoistModel.ts +1 -1
- package/data/cube/Query.ts +2 -0
- package/data/cube/View.ts +18 -12
- package/desktop/appcontainer/AppContainer.ts +15 -10
- package/desktop/appcontainer/ImpersonationBar.ts +17 -6
- package/desktop/appcontainer/VersionBar.ts +5 -2
- package/desktop/cmp/dash/DashViewModel.ts +14 -2
- package/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +3 -3
- package/desktop/cmp/dash/container/DashContainerModel.ts +22 -15
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilter.ts +0 -1
- package/desktop/cmp/grid/impl/filter/headerfilter/HeaderFilterModel.ts +12 -4
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.scss +12 -4
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.ts +8 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +42 -10
- package/desktop/cmp/viewmanager/dialog/ManageDialog.ts +1 -1
- package/mobile/appcontainer/ImpersonationBar.ts +1 -1
- package/package.json +5 -5
- package/styles/vars.scss +2 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,54 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 76.
|
|
3
|
+
## 76.1.0 - 2025-10-17
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### 🎁 New Features
|
|
6
6
|
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
your licensed grid modules at their new import paths.
|
|
7
|
+
* Added a public `@bindable titleDetails` config to `DashViewModel` to support displaying additional
|
|
8
|
+
information in the title bar of dashboard widgets. The new property is not persisted, allowing
|
|
9
|
+
apps to programmatically show dynamic info in a widget header without perturbing its saved state.
|
|
10
|
+
* Enhanced grid column filtering to support sorting the list of available values.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
function is no longer stringified, but is instead the raw field value for the group.
|
|
12
|
+
### ⚙️ Technical
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
** [Upgrade to v32](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-32/)
|
|
18
|
-
** [Upgrade to v33](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-33/)
|
|
19
|
-
** [Upgrade to v34](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-34/)
|
|
14
|
+
* Autofocus the user input when the impersonation bar is shown.
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
(Most applications do not create `TabModels` directly, but it is possible.)
|
|
23
|
-
* The `Exception` class and `HoistException` type have been moved from `@xh\hoist\core` to a new
|
|
24
|
-
lower level package `@xh\hoist\exception`. This new structure is not expected to effect most
|
|
25
|
-
applications, and was put in place to reduce the risk of circular dependencies between internal
|
|
26
|
-
hoist packages.
|
|
16
|
+
### 📚 Libraries
|
|
27
17
|
|
|
28
|
-
|
|
18
|
+
* @auth0/auth0-spa-js `2.4 → 2.7`
|
|
19
|
+
* @azure/msal-browser `4.23 → 4.25`
|
|
20
|
+
* dompurify `3.2 → 3.3`
|
|
21
|
+
* mobx `6.13 → 6.15`
|
|
29
22
|
|
|
30
|
-
|
|
23
|
+
## 76.0.0 - 2025-09-26
|
|
24
|
+
|
|
25
|
+
### 💥 Breaking Changes (upgrade difficulty: 🟠 MEDIUM - AG Grid update, Hoist React upgrade)
|
|
26
|
+
|
|
27
|
+
* Hoist v76 **upgrades AG Grid to v34** (from v31), covering three major AG Grid releases with their
|
|
28
|
+
own potentially breaking changes.
|
|
29
|
+
* Fortunately, internal Hoist updates to our managed API wrappers mean that most apps will see
|
|
30
|
+
very minimal changes, although there are required adjustments to app-level `package.json` to
|
|
31
|
+
install updated grid dependencies and `Bootstrap.ts` to import and register your licensed grid
|
|
32
|
+
modules at their new import paths.
|
|
33
|
+
* Applications implementing `groupRowRenderer` should note that the `value` property passed
|
|
34
|
+
to this function is no longer stringified, but is instead the raw field value for the group.
|
|
35
|
+
* See AG's upgrade guides for more details:
|
|
36
|
+
* [Upgrade to v32](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-32/)
|
|
37
|
+
* [Upgrade to v33](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-33/)
|
|
38
|
+
* [Upgrade to v34](https://www.ag-grid.com/react-data-grid/upgrading-to-ag-grid-34/)
|
|
39
|
+
* Modified the `TabModel` constructor to take its owning container as a second argument.
|
|
40
|
+
* Apps very rarely create `TabModels` directly, so this unlikely to require changes.
|
|
41
|
+
* Moved the `Exception` class and `HoistException` type from `@xh\hoist\core` to a new lower-level
|
|
42
|
+
package `@xh\hoist\exception` to reduce the risk of circule dependencies within Hoist.
|
|
43
|
+
* Apps rarely interact with these directly, so also unlikely to require changes.
|
|
44
|
+
|
|
45
|
+
### 🎁 New Features
|
|
46
|
+
|
|
47
|
+
* Added `extraConfirmText` + `extraConfirmLabel` configs to `MessageOptions`. Use these new options
|
|
31
48
|
to require the specified text to be re-typed by a user when confirming a potentially destructive
|
|
32
|
-
or disruptive action.
|
|
33
|
-
* Updated grid column filters to apply on `Enter` / dismiss on `Esc
|
|
49
|
+
or disruptive action. Note their usage within Hoist's Admin Console when deleting a role.
|
|
50
|
+
* Updated grid column filters to apply on `Enter` / dismiss on `Esc`. Tweaked the filter popup
|
|
34
51
|
toolbar for clarity.
|
|
35
|
-
* Added new ability to specify nested tab containers in a single declarative config.
|
|
52
|
+
* Added new ability to specify nested tab containers in a single declarative config. Apps may now
|
|
36
53
|
provide a spec for a nested tab container directly to the `TabConfig.content` property.
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
on `GridModel`. Set to true to ensure that the grid will have a single horizontal scrollbar
|
|
49
|
-
spanning the width of all columns, including any pinned columns.
|
|
50
|
-
* New `@sharePendingPromise` decorator for returning a shared Promise across concurrent async calls.
|
|
51
|
-
|
|
54
|
+
* Improved `ViewManager` features:
|
|
55
|
+
* Enabled globally sharing a new view directly from the 'Save/Save As' dialog.
|
|
56
|
+
* Simplified presentation and management of view visibility via new "Visibility" control.
|
|
57
|
+
* Removed support for the `isDefaultPinned` attribute on global views. All global views will be
|
|
58
|
+
pinned (i.e. show up in user menus) by default. Users can still explicitly "unpin" any global
|
|
59
|
+
views to remove them from their menus.
|
|
60
|
+
* Added a `validEmails` constraint rule to validate one or more email addresses in an input field.
|
|
61
|
+
* Added `DashCanvas.rglOptions` prop - passed through to the underlying `react-grid-layout`.
|
|
62
|
+
* Promoted experimental grid feature `enableFullWidthScroll` to a first-class `GridModel` config.
|
|
63
|
+
Set to true to ensure that the grid will have a single horizontal scrollbar spanning the width of
|
|
64
|
+
all columns, including any pinned columns.
|
|
52
65
|
|
|
53
66
|
### 🐞 Bug Fixes
|
|
54
67
|
|
|
@@ -64,6 +77,9 @@
|
|
|
64
77
|
|
|
65
78
|
### ⚙️ Technical
|
|
66
79
|
|
|
80
|
+
* Added a new `@sharePendingPromise` decorator for returning a shared Promise across concurrent
|
|
81
|
+
async calls. Calls made to a decorated method while a prior call with the same args is still
|
|
82
|
+
pending won't kick off a new call, but will instead receive the same Promise as the first call.
|
|
67
83
|
* Added `XH.logLevel` to define a minimum logging severity threshold for Hoist's client-side logging
|
|
68
84
|
utilities. Defaulted to 'info' to prevent possible memory and performance impacts of verbose
|
|
69
85
|
logging on 'debug'. Change at runtime via new `XH.setLogLevel()` when troubleshooting. See
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {HoistInputModel} from '@xh/hoist/cmp/input';
|
|
7
8
|
import {HoistModel, XH} from '@xh/hoist/core';
|
|
8
|
-
import {action,
|
|
9
|
+
import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
|
|
9
10
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
11
|
+
import {createRef} from 'react';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* @internal
|
|
@@ -18,6 +20,9 @@ export class ImpersonationBarModel extends HoistModel {
|
|
|
18
20
|
@observable.ref targets: string[] = [];
|
|
19
21
|
@bindable pendingTarget: string = null;
|
|
20
22
|
|
|
23
|
+
// For managed focus of desktop select.
|
|
24
|
+
inputRef = createRef<HoistInputModel>();
|
|
25
|
+
|
|
21
26
|
constructor() {
|
|
22
27
|
super();
|
|
23
28
|
makeObservable(this);
|
|
@@ -58,7 +63,7 @@ export class ImpersonationBarModel extends HoistModel {
|
|
|
58
63
|
@action
|
|
59
64
|
toggleVisibility() {
|
|
60
65
|
if (this.isOpen) {
|
|
61
|
-
this.hide();
|
|
66
|
+
XH.identityService.isImpersonating ? this.inputRef.current?.focus() : this.hide();
|
|
62
67
|
} else {
|
|
63
68
|
this.show();
|
|
64
69
|
}
|
|
@@ -73,7 +78,7 @@ export class ImpersonationBarModel extends HoistModel {
|
|
|
73
78
|
try {
|
|
74
79
|
await XH.identityService.impersonateAsync(pendingTarget);
|
|
75
80
|
} catch (e) {
|
|
76
|
-
this.pendingTarget =
|
|
81
|
+
this.pendingTarget = null;
|
|
77
82
|
XH.handleException(e, {logOnServer: false}); // likely to be an unknown user
|
|
78
83
|
}
|
|
79
84
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HoistInputModel } from '@xh/hoist/cmp/input';
|
|
1
2
|
import { HoistModel } from '@xh/hoist/core';
|
|
2
3
|
/**
|
|
3
4
|
* @internal
|
|
@@ -7,6 +8,7 @@ export declare class ImpersonationBarModel extends HoistModel {
|
|
|
7
8
|
showRequested: boolean;
|
|
8
9
|
targets: string[];
|
|
9
10
|
pendingTarget: string;
|
|
11
|
+
inputRef: import("react").RefObject<HoistInputModel>;
|
|
10
12
|
constructor();
|
|
11
13
|
init(): void;
|
|
12
14
|
get isOpen(): boolean;
|
|
@@ -26,6 +26,8 @@ export declare class ChartModel extends HoistModel {
|
|
|
26
26
|
* be done via {@link setHighchartsConfig} or {@link setSeries}.
|
|
27
27
|
*/
|
|
28
28
|
highchart: any;
|
|
29
|
+
/** True if this chart has no series to display */
|
|
30
|
+
get empty(): boolean;
|
|
29
31
|
constructor(config?: ChartConfig);
|
|
30
32
|
/**
|
|
31
33
|
* Update the Highcharts instance configuration.
|
|
@@ -9,7 +9,7 @@ import { Class } from 'type-fest';
|
|
|
9
9
|
*
|
|
10
10
|
* The most common use of `HoistModel` is to support Hoist components. Components can be configured
|
|
11
11
|
* to create or lookup an instance of an appropriate model subclass using the `model` config passed
|
|
12
|
-
* to {@link
|
|
12
|
+
* to {@link hoistCmp.factory}. Hoist will automatically pass the resolved model instance as a
|
|
13
13
|
* prop to the component's `render()` function, where the model's properties can be read/rendered
|
|
14
14
|
* and any imperative APIs wired to buttons, callbacks, and other handlers.
|
|
15
15
|
*
|
|
@@ -97,6 +97,7 @@ export declare class Query {
|
|
|
97
97
|
readonly fields: CubeField[];
|
|
98
98
|
readonly dimensions: CubeField[];
|
|
99
99
|
readonly filter: Filter;
|
|
100
|
+
readonly hasFilter: boolean;
|
|
100
101
|
readonly includeRoot: boolean;
|
|
101
102
|
readonly includeLeaves: boolean;
|
|
102
103
|
readonly provideLeaves: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
1
2
|
import { HoistModel, MenuItemLike, PlainObject, RefreshMode, RenderMode } from '@xh/hoist/core';
|
|
2
3
|
import '@xh/hoist/desktop/register';
|
|
3
|
-
import { ReactElement } from 'react';
|
|
4
4
|
import { DashViewSpec } from './DashViewSpec';
|
|
5
5
|
export type DashViewState = PlainObject;
|
|
6
6
|
/**
|
|
@@ -23,8 +23,15 @@ export declare class DashViewModel<T extends DashViewSpec = DashViewSpec> extend
|
|
|
23
23
|
* constructing these models - no need to specify manually.
|
|
24
24
|
*/
|
|
25
25
|
containerModel: any;
|
|
26
|
-
/** Title with which to initialize the view. */
|
|
26
|
+
/** Title with which to initialize the view. Value is persisted. */
|
|
27
27
|
title: string;
|
|
28
|
+
/**
|
|
29
|
+
* Additional info that will be displayed after the title.
|
|
30
|
+
* Applications can bind to this property to provide dynamic title details.
|
|
31
|
+
* Value is not persisted.
|
|
32
|
+
**/
|
|
33
|
+
titleDetails: string;
|
|
34
|
+
get fullTitle(): string;
|
|
28
35
|
/** Icon with which to initialize the view. */
|
|
29
36
|
icon: ReactElement;
|
|
30
37
|
/** State with which to initialize the view. */
|
|
@@ -150,6 +150,7 @@ export declare class DashContainerModel extends DashModel<DashContainerViewSpec,
|
|
|
150
150
|
private showTitleForm;
|
|
151
151
|
private hideTitleForm;
|
|
152
152
|
private createGoldenLayout;
|
|
153
|
+
private getTitleElement;
|
|
153
154
|
private destroyGoldenLayout;
|
|
154
155
|
destroy(): void;
|
|
155
156
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { Column, GridFilterFieldSpec, GridFilterModel, GridModel } from '@xh/hoist/cmp/grid';
|
|
1
2
|
import { TabContainerModel } from '@xh/hoist/cmp/tab';
|
|
2
3
|
import { HoistModel } from '@xh/hoist/core';
|
|
3
4
|
import { CompoundFilter, FieldFilter, FieldType, Filter, FilterLike, Store } from '@xh/hoist/data';
|
|
4
|
-
import {
|
|
5
|
+
import { ColumnHeaderFilterModel } from '../ColumnHeaderFilterModel';
|
|
5
6
|
import { CustomTabModel } from './custom/CustomTabModel';
|
|
6
7
|
import { ValuesTabModel } from './values/ValuesTabModel';
|
|
7
|
-
import { ColumnHeaderFilterModel } from '../ColumnHeaderFilterModel';
|
|
8
8
|
export declare class HeaderFilterModel extends HoistModel {
|
|
9
9
|
xhImpl: boolean;
|
|
10
10
|
fieldSpec: GridFilterFieldSpec;
|
|
@@ -14,7 +14,9 @@ export declare class HeaderFilterModel extends HoistModel {
|
|
|
14
14
|
customTabModel: CustomTabModel;
|
|
15
15
|
get filterModel(): GridFilterModel;
|
|
16
16
|
get field(): string;
|
|
17
|
+
get gridModel(): GridModel;
|
|
17
18
|
get store(): Store;
|
|
19
|
+
get column(): Column;
|
|
18
20
|
get fieldType(): FieldType;
|
|
19
21
|
get currentGridFilter(): Filter;
|
|
20
22
|
get columnFilters(): FieldFilter[];
|
|
@@ -5,12 +5,16 @@ import { HeaderFilterModel } from '../HeaderFilterModel';
|
|
|
5
5
|
export declare class ValuesTabModel extends HoistModel {
|
|
6
6
|
xhImpl: boolean;
|
|
7
7
|
headerFilterModel: HeaderFilterModel;
|
|
8
|
-
/** Checkbox grid to display enumerated set of values */
|
|
8
|
+
/** Checkbox grid to display enumerated set of values. */
|
|
9
9
|
gridModel: GridModel;
|
|
10
|
-
/** List of currently checked values
|
|
10
|
+
/** List of currently checked values. */
|
|
11
11
|
pendingValues: any[];
|
|
12
|
-
/** Bound search term for `StoreFilterField
|
|
12
|
+
/** Bound search term for `StoreFilterField`. */
|
|
13
13
|
filterText: string;
|
|
14
|
+
/**
|
|
15
|
+
* Merge current filter with pendingValues on commit.
|
|
16
|
+
* Used when commitOnChange is false.
|
|
17
|
+
*/
|
|
14
18
|
combineCurrentFilters: boolean;
|
|
15
19
|
/** FieldFilter output by this model. */
|
|
16
20
|
get filter(): FieldFilterSpec;
|
|
@@ -22,15 +26,18 @@ export declare class ValuesTabModel extends HoistModel {
|
|
|
22
26
|
get values(): any[];
|
|
23
27
|
get valueCount(): number;
|
|
24
28
|
get hasHiddenValues(): boolean;
|
|
29
|
+
get sortIcon(): any;
|
|
25
30
|
constructor(headerFilterModel: HeaderFilterModel);
|
|
26
31
|
syncWithFilter(): void;
|
|
27
32
|
reset(): void;
|
|
28
33
|
setRecsChecked(isChecked: boolean, values: any[]): void;
|
|
34
|
+
toggleSort(): void;
|
|
29
35
|
toggleAllRecsChecked(): void;
|
|
30
36
|
private onFilterTextChange;
|
|
31
37
|
private onCombineCurrentFiltersToggle;
|
|
32
38
|
private getFilter;
|
|
33
39
|
private doSyncWithFilter;
|
|
34
40
|
private syncGrid;
|
|
41
|
+
private initGridSortBy;
|
|
35
42
|
private createGridModel;
|
|
36
43
|
}
|
package/cmp/chart/ChartModel.ts
CHANGED
|
@@ -8,8 +8,8 @@ import {type MouseEvent} from 'react';
|
|
|
8
8
|
import type {ChartContextMenuSpec, ChartMenuToken} from '@xh/hoist/cmp/chart/Types';
|
|
9
9
|
import {getContextMenuItems} from '@xh/hoist/cmp/chart/impl/ChartContextMenuItems';
|
|
10
10
|
import {HoistModel, PlainObject, Some, XH} from '@xh/hoist/core';
|
|
11
|
-
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
12
|
-
import {castArray, cloneDeep, isFunction, isNil} from 'lodash';
|
|
11
|
+
import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
12
|
+
import {castArray, cloneDeep, isEmpty, isFunction, isNil} from 'lodash';
|
|
13
13
|
import {mergeDeep} from '@xh/hoist/utils/js';
|
|
14
14
|
|
|
15
15
|
interface ChartConfig {
|
|
@@ -59,6 +59,12 @@ export class ChartModel extends HoistModel {
|
|
|
59
59
|
@observable.ref
|
|
60
60
|
highchart: any;
|
|
61
61
|
|
|
62
|
+
/** True if this chart has no series to display */
|
|
63
|
+
@computed
|
|
64
|
+
get empty(): boolean {
|
|
65
|
+
return isEmpty(this.series);
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
constructor(config?: ChartConfig) {
|
|
63
69
|
super();
|
|
64
70
|
makeObservable(this);
|
package/cmp/dataview/DataView.ts
CHANGED
|
@@ -82,7 +82,7 @@ class DataViewLocalModel extends HoistModel {
|
|
|
82
82
|
const {model} = this;
|
|
83
83
|
return {
|
|
84
84
|
headerHeight: 0,
|
|
85
|
-
|
|
85
|
+
suppressGroupChangesColumnVisibility: 'suppressShowOnUngroup',
|
|
86
86
|
getRowHeight: agParams => {
|
|
87
87
|
const {groupRowHeight, itemHeight} = model;
|
|
88
88
|
|
package/core/model/HoistModel.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {Class} from 'type-fest';
|
|
|
20
20
|
*
|
|
21
21
|
* The most common use of `HoistModel` is to support Hoist components. Components can be configured
|
|
22
22
|
* to create or lookup an instance of an appropriate model subclass using the `model` config passed
|
|
23
|
-
* to {@link
|
|
23
|
+
* to {@link hoistCmp.factory}. Hoist will automatically pass the resolved model instance as a
|
|
24
24
|
* prop to the component's `render()` function, where the model's properties can be read/rendered
|
|
25
25
|
* and any imperative APIs wired to buttons, callbacks, and other handlers.
|
|
26
26
|
*
|
package/data/cube/Query.ts
CHANGED
|
@@ -127,6 +127,7 @@ export class Query {
|
|
|
127
127
|
readonly fields: CubeField[];
|
|
128
128
|
readonly dimensions: CubeField[];
|
|
129
129
|
readonly filter: Filter;
|
|
130
|
+
readonly hasFilter: boolean;
|
|
130
131
|
readonly includeRoot: boolean;
|
|
131
132
|
readonly includeLeaves: boolean;
|
|
132
133
|
readonly provideLeaves: boolean;
|
|
@@ -164,6 +165,7 @@ export class Query {
|
|
|
164
165
|
this.omitFn = omitFn;
|
|
165
166
|
|
|
166
167
|
this._testFn = this.filter?.getTestFn(this.cube.store) ?? null;
|
|
168
|
+
this.hasFilter = this._testFn != null;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
clone(overrides: Partial<QueryConfig>) {
|
package/data/cube/View.ts
CHANGED
|
@@ -309,12 +309,12 @@ export class View extends HoistBase {
|
|
|
309
309
|
appliedDimensions: PlainObject,
|
|
310
310
|
leafMap: Map<StoreRecordId, LeafRow>
|
|
311
311
|
): BaseRow[] {
|
|
312
|
-
if (
|
|
312
|
+
if (!records?.length) return [];
|
|
313
313
|
|
|
314
314
|
const rootId = parentId + Cube.RECORD_ID_DELIMITER;
|
|
315
315
|
|
|
316
|
-
if (
|
|
317
|
-
return map(
|
|
316
|
+
if (!dimensions?.length) {
|
|
317
|
+
return records.map(r => {
|
|
318
318
|
const id = rootId + r.id,
|
|
319
319
|
leaf = this.cachedRow(id, null, () => new LeafRow(this, id, r));
|
|
320
320
|
leafMap.set(r.id, leaf);
|
|
@@ -355,16 +355,17 @@ export class View extends HoistBase {
|
|
|
355
355
|
parentId: string,
|
|
356
356
|
appliedDimensions: PlainObject
|
|
357
357
|
): BaseRow[] {
|
|
358
|
-
|
|
358
|
+
const {query} = this;
|
|
359
359
|
|
|
360
|
-
|
|
361
|
-
if (!
|
|
360
|
+
if (!query.bucketSpecFn) return rows;
|
|
361
|
+
if (!query.includeLeaves && rows[0]?.isLeaf) return rows;
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
const bucketSpec = query.bucketSpecFn(rows);
|
|
364
|
+
if (!bucketSpec) return rows;
|
|
364
365
|
|
|
365
366
|
const {name: bucketName, bucketFn} = bucketSpec,
|
|
366
|
-
buckets = {},
|
|
367
|
-
ret = [];
|
|
367
|
+
buckets: Record<string, BaseRow[]> = {},
|
|
368
|
+
ret: BaseRow[] = [];
|
|
368
369
|
|
|
369
370
|
// Determine which bucket to put this row into (if any)
|
|
370
371
|
rows.forEach(row => {
|
|
@@ -372,8 +373,8 @@ export class View extends HoistBase {
|
|
|
372
373
|
if (isNil(bucketVal)) {
|
|
373
374
|
ret.push(row);
|
|
374
375
|
} else {
|
|
375
|
-
|
|
376
|
-
|
|
376
|
+
const bucketRows = buckets[bucketVal] ??= [];
|
|
377
|
+
bucketRows.push(row);
|
|
377
378
|
}
|
|
378
379
|
});
|
|
379
380
|
|
|
@@ -454,8 +455,13 @@ export class View extends HoistBase {
|
|
|
454
455
|
|
|
455
456
|
private filterRecords() {
|
|
456
457
|
const {query, cube} = this,
|
|
458
|
+
{hasFilter} = query,
|
|
457
459
|
ret = new Map();
|
|
458
|
-
|
|
460
|
+
|
|
461
|
+
cube.store.records.forEach(r => {
|
|
462
|
+
if (!hasFilter || query.test(r)) ret.set(r.id, r);
|
|
463
|
+
});
|
|
464
|
+
|
|
459
465
|
this._recordMap = ret;
|
|
460
466
|
}
|
|
461
467
|
|
|
@@ -4,32 +4,34 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {HotkeyConfig} from '@blueprintjs/core/src/hooks/hotkeys/hotkeyConfig';
|
|
7
8
|
import {AppContainerModel} from '@xh/hoist/appcontainer/AppContainerModel';
|
|
9
|
+
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
8
10
|
import {fragment, frame, vframe, viewport} from '@xh/hoist/cmp/layout';
|
|
11
|
+
import {mask} from '@xh/hoist/cmp/mask';
|
|
9
12
|
import {createElement, hoistCmp, refreshContextView, uses, XH} from '@xh/hoist/core';
|
|
10
|
-
import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
|
|
11
13
|
import {changelogDialog} from '@xh/hoist/desktop/appcontainer/ChangelogDialog';
|
|
12
|
-
import {suspendPanel} from './suspend/SuspendPanel';
|
|
13
14
|
import {dockContainerImpl} from '@xh/hoist/desktop/cmp/dock/impl/DockContainer';
|
|
15
|
+
import {errorMessageImpl} from '@xh/hoist/desktop/cmp/error/impl/ErrorMessage';
|
|
14
16
|
import {colChooserDialog as colChooser} from '@xh/hoist/desktop/cmp/grid/impl/colchooser/ColChooserDialog';
|
|
15
17
|
import {ColChooserModel} from '@xh/hoist/desktop/cmp/grid/impl/colchooser/ColChooserModel';
|
|
16
|
-
import {zoneMapperDialog as zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapperDialog';
|
|
17
18
|
import {columnHeaderFilter} from '@xh/hoist/desktop/cmp/grid/impl/filter/ColumnHeaderFilter';
|
|
18
19
|
import {ColumnHeaderFilterModel} from '@xh/hoist/desktop/cmp/grid/impl/filter/ColumnHeaderFilterModel';
|
|
19
20
|
import {gridFilterDialog} from '@xh/hoist/desktop/cmp/grid/impl/filter/GridFilterDialog';
|
|
20
|
-
import {
|
|
21
|
+
import {maskImpl} from '@xh/hoist/desktop/cmp/mask/impl/Mask';
|
|
21
22
|
import {ModalSupportModel} from '@xh/hoist/desktop/cmp/modalsupport';
|
|
22
23
|
import {pinPadImpl} from '@xh/hoist/desktop/cmp/pinpad/impl/PinPad';
|
|
23
24
|
import {storeFilterFieldImpl} from '@xh/hoist/desktop/cmp/store/impl/StoreFilterField';
|
|
24
25
|
import {tabContainerImpl} from '@xh/hoist/desktop/cmp/tab/impl/TabContainer';
|
|
26
|
+
import {zoneMapperDialog as zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapperDialog';
|
|
25
27
|
import {useContextMenu, useHotkeys} from '@xh/hoist/desktop/hooks';
|
|
26
28
|
import {installDesktopImpls} from '@xh/hoist/dynamics/desktop';
|
|
27
29
|
import {inspectorPanel} from '@xh/hoist/inspector/InspectorPanel';
|
|
28
30
|
import {blueprintProvider} from '@xh/hoist/kit/blueprint';
|
|
29
|
-
import {
|
|
30
|
-
import {maskImpl} from '@xh/hoist/desktop/cmp/mask/impl/Mask';
|
|
31
|
+
import {consumeEvent} from '@xh/hoist/utils/js';
|
|
31
32
|
import {elementFromContent, useOnMount} from '@xh/hoist/utils/react';
|
|
32
33
|
import {isEmpty} from 'lodash';
|
|
34
|
+
import {ReactElement} from 'react';
|
|
33
35
|
import {aboutDialog} from './AboutDialog';
|
|
34
36
|
import {banner} from './Banner';
|
|
35
37
|
import {exceptionDialog} from './ExceptionDialog';
|
|
@@ -39,9 +41,9 @@ import {lockoutPanel} from './LockoutPanel';
|
|
|
39
41
|
import {loginPanel} from './LoginPanel';
|
|
40
42
|
import {messageSource} from './MessageSource';
|
|
41
43
|
import {optionsDialog} from './OptionsDialog';
|
|
44
|
+
import {suspendPanel} from './suspend/SuspendPanel';
|
|
42
45
|
import {toastSource} from './ToastSource';
|
|
43
46
|
import {versionBar} from './VersionBar';
|
|
44
|
-
import {ReactElement} from 'react';
|
|
45
47
|
|
|
46
48
|
installDesktopImpls({
|
|
47
49
|
tabContainerImpl,
|
|
@@ -183,9 +185,9 @@ const bannerList = hoistCmp.factory<AppContainerModel>({
|
|
|
183
185
|
}
|
|
184
186
|
});
|
|
185
187
|
|
|
186
|
-
function globalHotKeys(model) {
|
|
188
|
+
function globalHotKeys(model: AppContainerModel) {
|
|
187
189
|
const {impersonationBarModel, optionsDialogModel} = model,
|
|
188
|
-
ret = [
|
|
190
|
+
ret: HotkeyConfig[] = [
|
|
189
191
|
{
|
|
190
192
|
global: true,
|
|
191
193
|
combo: 'shift + r',
|
|
@@ -199,7 +201,10 @@ function globalHotKeys(model) {
|
|
|
199
201
|
global: true,
|
|
200
202
|
combo: 'shift + i',
|
|
201
203
|
label: 'Impersonate another user',
|
|
202
|
-
onKeyDown:
|
|
204
|
+
onKeyDown: e => {
|
|
205
|
+
consumeEvent(e); // avoid typing "i" in the impersonation bar select
|
|
206
|
+
impersonationBarModel.toggleVisibility();
|
|
207
|
+
}
|
|
203
208
|
});
|
|
204
209
|
}
|
|
205
210
|
if (optionsDialogModel.hasOptions) {
|
|
@@ -31,9 +31,12 @@ export const impersonationBar = hoistCmp.factory({
|
|
|
31
31
|
|
|
32
32
|
const {targets} = model;
|
|
33
33
|
|
|
34
|
-
let msg = `Logged in as ${authUsername}
|
|
34
|
+
let msg = `Logged in as ${authUsername}`,
|
|
35
|
+
placeholder = 'Select a user...';
|
|
36
|
+
|
|
35
37
|
if (isImpersonating) {
|
|
36
38
|
msg += ` › impersonating ${username}`;
|
|
39
|
+
placeholder = `Impersonating ${username}`;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
return toolbar({
|
|
@@ -53,10 +56,16 @@ export const impersonationBar = hoistCmp.factory({
|
|
|
53
56
|
bind: 'pendingTarget',
|
|
54
57
|
options: targets,
|
|
55
58
|
enableCreate: true,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
// Autofocus when shown to begin impersonation
|
|
60
|
+
autoFocus: !isImpersonating,
|
|
61
|
+
placeholder,
|
|
62
|
+
createMessageFn: q => `Impersonate new user "${q}"`,
|
|
63
|
+
minWidth: 150,
|
|
64
|
+
maxWidth: 350,
|
|
65
|
+
menuWidth: 350,
|
|
66
|
+
flex: 1,
|
|
67
|
+
onCommit: model.onCommit,
|
|
68
|
+
ref: model.inputRef
|
|
60
69
|
}),
|
|
61
70
|
button({
|
|
62
71
|
text: isImpersonating ? 'Exit Impersonation' : 'Cancel',
|
|
@@ -71,6 +80,7 @@ export const impersonationBar = hoistCmp.factory({
|
|
|
71
80
|
const showUseResponsiblyAlert = () => {
|
|
72
81
|
XH.alert({
|
|
73
82
|
title: 'Important Reminders',
|
|
83
|
+
icon: Icon.warning(),
|
|
74
84
|
message: fragment(
|
|
75
85
|
h3('With great power comes great responsibility.'),
|
|
76
86
|
ul(
|
|
@@ -94,7 +104,8 @@ const showUseResponsiblyAlert = () => {
|
|
|
94
104
|
)
|
|
95
105
|
),
|
|
96
106
|
confirmProps: {
|
|
97
|
-
text: 'I understand and will be careful'
|
|
107
|
+
text: 'I understand and will be careful',
|
|
108
|
+
autoFocus: false
|
|
98
109
|
}
|
|
99
110
|
});
|
|
100
111
|
};
|
|
@@ -18,13 +18,16 @@ export const versionBar = hoistCmp.factory({
|
|
|
18
18
|
const inspectorSvc = XH.inspectorService,
|
|
19
19
|
envSvc = XH.environmentService,
|
|
20
20
|
env = envSvc.get('appEnvironment'),
|
|
21
|
+
build = envSvc.get('clientBuild'),
|
|
21
22
|
version = envSvc.get('clientVersion'),
|
|
22
|
-
isAdminApp = window.location.pathname?.startsWith('/admin/')
|
|
23
|
+
isAdminApp = window.location.pathname?.startsWith('/admin/'),
|
|
24
|
+
versionAndBuild =
|
|
25
|
+
!build || build === 'UNKNOWN' ? version : `${version} (build ${build})`;
|
|
23
26
|
|
|
24
27
|
return box({
|
|
25
28
|
className: `xh-version-bar xh-version-bar--${env.toLowerCase()}`,
|
|
26
29
|
items: [
|
|
27
|
-
[XH.appName, env,
|
|
30
|
+
[XH.appName, env, versionAndBuild].join(' • '),
|
|
28
31
|
span({
|
|
29
32
|
className: 'xh-version-bar__spacer',
|
|
30
33
|
items: '|'
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {isNil} from 'lodash';
|
|
8
|
+
import {ReactElement} from 'react';
|
|
7
9
|
import {
|
|
8
10
|
HoistModel,
|
|
9
11
|
managed,
|
|
@@ -16,7 +18,6 @@ import {
|
|
|
16
18
|
import '@xh/hoist/desktop/register';
|
|
17
19
|
import {makeObservable, bindable} from '@xh/hoist/mobx';
|
|
18
20
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
19
|
-
import {ReactElement} from 'react';
|
|
20
21
|
import {DashViewSpec} from './DashViewSpec';
|
|
21
22
|
|
|
22
23
|
export type DashViewState = PlainObject;
|
|
@@ -44,9 +45,20 @@ export class DashViewModel<T extends DashViewSpec = DashViewSpec> extends HoistM
|
|
|
44
45
|
*/
|
|
45
46
|
containerModel: any;
|
|
46
47
|
|
|
47
|
-
/** Title with which to initialize the view. */
|
|
48
|
+
/** Title with which to initialize the view. Value is persisted. */
|
|
48
49
|
@bindable title: string;
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Additional info that will be displayed after the title.
|
|
53
|
+
* Applications can bind to this property to provide dynamic title details.
|
|
54
|
+
* Value is not persisted.
|
|
55
|
+
**/
|
|
56
|
+
@bindable titleDetails: string;
|
|
57
|
+
|
|
58
|
+
get fullTitle(): string {
|
|
59
|
+
return [this.title, this.titleDetails].filter(it => !isNil(it)).join(' ');
|
|
60
|
+
}
|
|
61
|
+
|
|
50
62
|
/** Icon with which to initialize the view. */
|
|
51
63
|
@bindable.ref icon: ReactElement;
|
|
52
64
|
|
|
@@ -34,13 +34,13 @@ export const dashCanvasView = hoistCmp.factory({
|
|
|
34
34
|
model: uses(DashCanvasViewModel, {publishMode: 'limited'}),
|
|
35
35
|
|
|
36
36
|
render({model, className}) {
|
|
37
|
-
const {viewSpec, ref, hidePanelHeader, headerItems, autoHeight} = model,
|
|
37
|
+
const {viewSpec, ref, hidePanelHeader, headerItems, autoHeight, fullTitle, icon} = model,
|
|
38
38
|
headerProps = hidePanelHeader
|
|
39
39
|
? {}
|
|
40
40
|
: {
|
|
41
41
|
compactHeader: true,
|
|
42
|
-
title:
|
|
43
|
-
icon
|
|
42
|
+
title: fullTitle,
|
|
43
|
+
icon,
|
|
44
44
|
headerItems: [...headerItems, headerMenu({model})]
|
|
45
45
|
};
|
|
46
46
|
return panel({
|