@xh/hoist 76.0.0 → 76.2.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 +63 -39
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +1 -1
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +1 -1
- package/admin/tabs/cluster/instances/logs/LogDisplayModel.ts +1 -1
- package/admin/tabs/userData/users/UserModel.ts +1 -0
- 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/Store.d.ts +1 -0
- package/build/types/data/cube/Query.d.ts +1 -0
- package/build/types/data/cube/ViewRowData.d.ts +2 -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/Store.ts +13 -3
- package/data/cube/Query.ts +2 -0
- package/data/cube/View.ts +18 -12
- package/data/cube/ViewRowData.ts +3 -0
- package/data/cube/row/AggregateRow.ts +1 -0
- package/data/cube/row/BucketRow.ts +1 -0
- package/data/cube/row/LeafRow.ts +1 -1
- 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/promise/Promise.ts +3 -3
- package/styles/vars.scss +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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({
|
|
@@ -338,7 +338,7 @@ export class DashContainerModel
|
|
|
338
338
|
renameView(id: string) {
|
|
339
339
|
const view = this.getItemByViewModel(id);
|
|
340
340
|
if (!view) return;
|
|
341
|
-
this.showTitleForm(view.tab.element);
|
|
341
|
+
this.showTitleForm(view.tab.element, this.getViewModel(id));
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
onResize() {
|
|
@@ -528,10 +528,10 @@ export class DashContainerModel
|
|
|
528
528
|
|
|
529
529
|
const $el = item.tab.element, // Note: this is a jquery element
|
|
530
530
|
stack = item.parent,
|
|
531
|
-
$titleEl = $el
|
|
531
|
+
$titleEl = this.getTitleElement($el),
|
|
532
532
|
iconSelector = 'svg.svg-inline--fa',
|
|
533
533
|
viewSpec = this.getViewSpec(item.config.component),
|
|
534
|
-
{icon
|
|
534
|
+
{icon} = viewModel;
|
|
535
535
|
|
|
536
536
|
$el.off('contextmenu').contextmenu(e => {
|
|
537
537
|
const index = stack.contentItems.indexOf(item);
|
|
@@ -551,14 +551,9 @@ export class DashContainerModel
|
|
|
551
551
|
}
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
if (title) {
|
|
555
|
-
const currentTitle = $titleEl.text();
|
|
556
|
-
if (currentTitle !== title) $titleEl.text(title);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
554
|
if (viewSpec.allowRename) {
|
|
560
555
|
this.insertTitleForm($el, viewModel);
|
|
561
|
-
$titleEl.off('dblclick').dblclick(() => this.showTitleForm($el));
|
|
556
|
+
$titleEl.off('dblclick').dblclick(() => this.showTitleForm($el, viewModel));
|
|
562
557
|
}
|
|
563
558
|
});
|
|
564
559
|
}
|
|
@@ -568,7 +563,7 @@ export class DashContainerModel
|
|
|
568
563
|
if ($el.find(formSelector).length) return;
|
|
569
564
|
|
|
570
565
|
// Create and insert form
|
|
571
|
-
const $titleEl = $el
|
|
566
|
+
const $titleEl = this.getTitleElement($el);
|
|
572
567
|
$titleEl.after(`<form class="title-form"><input type="text"/></form>`);
|
|
573
568
|
|
|
574
569
|
// Attach listeners
|
|
@@ -579,7 +574,6 @@ export class DashContainerModel
|
|
|
579
574
|
$formEl.submit(() => {
|
|
580
575
|
const title = $inputEl.val();
|
|
581
576
|
if (title.length) {
|
|
582
|
-
$titleEl.text(title);
|
|
583
577
|
viewModel.title = title;
|
|
584
578
|
}
|
|
585
579
|
|
|
@@ -588,12 +582,11 @@ export class DashContainerModel
|
|
|
588
582
|
});
|
|
589
583
|
}
|
|
590
584
|
|
|
591
|
-
private showTitleForm($tabEl) {
|
|
585
|
+
private showTitleForm($tabEl, viewModel: DashViewModel) {
|
|
592
586
|
if (this.renameLocked) return;
|
|
593
587
|
|
|
594
|
-
const $
|
|
595
|
-
|
|
596
|
-
currentTitle = $titleEl.text();
|
|
588
|
+
const $inputEl = $tabEl.find('.title-form input').first(),
|
|
589
|
+
currentTitle = viewModel.title;
|
|
597
590
|
|
|
598
591
|
$tabEl.addClass('show-title-form');
|
|
599
592
|
$inputEl.val(currentTitle);
|
|
@@ -647,6 +640,16 @@ export class DashContainerModel
|
|
|
647
640
|
containerModel: this
|
|
648
641
|
});
|
|
649
642
|
|
|
643
|
+
model.addReaction({
|
|
644
|
+
track: () => model.fullTitle,
|
|
645
|
+
run: () => {
|
|
646
|
+
const item = this.getItemByViewModel(id),
|
|
647
|
+
$titleEl = this.getTitleElement(item.tab.element);
|
|
648
|
+
|
|
649
|
+
$titleEl.text(model.fullTitle);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
650
653
|
this.addViewModel(model);
|
|
651
654
|
return modelLookupContextProvider({
|
|
652
655
|
value: this.modelLookupContext,
|
|
@@ -662,6 +665,10 @@ export class DashContainerModel
|
|
|
662
665
|
return ret;
|
|
663
666
|
}
|
|
664
667
|
|
|
668
|
+
private getTitleElement($el) {
|
|
669
|
+
return $el.find('.lm_title').first();
|
|
670
|
+
}
|
|
671
|
+
|
|
665
672
|
@action
|
|
666
673
|
private destroyGoldenLayout() {
|
|
667
674
|
XH.safeDestroy(this.goldenLayout);
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import {Column, GridFilterFieldSpec, GridFilterModel, GridModel} from '@xh/hoist/cmp/grid';
|
|
8
9
|
import {TabContainerModel} from '@xh/hoist/cmp/tab';
|
|
9
|
-
import {HoistModel,
|
|
10
|
+
import {HoistModel, lookup, managed} from '@xh/hoist/core';
|
|
10
11
|
import {
|
|
11
12
|
CompoundFilter,
|
|
12
13
|
FieldFilter,
|
|
@@ -19,12 +20,11 @@ import {
|
|
|
19
20
|
import {action, computed} from '@xh/hoist/mobx';
|
|
20
21
|
import {wait} from '@xh/hoist/promise';
|
|
21
22
|
import {isEmpty} from 'lodash';
|
|
22
|
-
import {
|
|
23
|
+
import {ColumnHeaderFilterModel} from '../ColumnHeaderFilterModel';
|
|
23
24
|
import {customTab} from './custom/CustomTab';
|
|
24
25
|
import {CustomTabModel} from './custom/CustomTabModel';
|
|
25
26
|
import {valuesTab} from './values/ValuesTab';
|
|
26
27
|
import {ValuesTabModel} from './values/ValuesTabModel';
|
|
27
|
-
import {ColumnHeaderFilterModel} from '../ColumnHeaderFilterModel';
|
|
28
28
|
|
|
29
29
|
export class HeaderFilterModel extends HoistModel {
|
|
30
30
|
override xhImpl = true;
|
|
@@ -46,8 +46,16 @@ export class HeaderFilterModel extends HoistModel {
|
|
|
46
46
|
return this.fieldSpec.field;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
get gridModel(): GridModel {
|
|
50
|
+
return this.filterModel.gridModel;
|
|
51
|
+
}
|
|
52
|
+
|
|
49
53
|
get store(): Store {
|
|
50
|
-
return this.
|
|
54
|
+
return this.gridModel.store;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get column(): Column {
|
|
58
|
+
return this.parent.column;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
get fieldType(): FieldType {
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
.xh-values-filter-tab {
|
|
2
|
-
|
|
3
|
-
padding: 5px 7px;
|
|
2
|
+
&__filter-controls {
|
|
4
3
|
border-bottom: 1px solid var(--xh-grid-header-border-color);
|
|
4
|
+
padding: 5px 7px;
|
|
5
5
|
row-gap: 5px;
|
|
6
|
+
|
|
6
7
|
.bp5-control-indicator {
|
|
7
8
|
font-size: 1em;
|
|
8
9
|
}
|
|
10
|
+
|
|
9
11
|
label {
|
|
10
12
|
font-size: var(--xh-grid-compact-header-font-size-px);
|
|
11
13
|
color: var(--xh-grid-header-text-color);
|
|
12
14
|
cursor: pointer;
|
|
13
15
|
}
|
|
16
|
+
|
|
17
|
+
&__sort-icon {
|
|
18
|
+
border-left: var(--xh-menu-border);
|
|
19
|
+
padding-left: var(--xh-pad-half-px);
|
|
20
|
+
color: var(--xh-grid-header-text-color);
|
|
21
|
+
}
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
&__hidden-values-message {
|
|
17
|
-
display: flex;
|
|
18
|
-
padding: var(--xh-pad-half-px);
|
|
19
25
|
background-color: var(--xh-bg-alt);
|
|
20
26
|
border-top: var(--xh-border-solid);
|
|
21
27
|
color: var(--xh-text-color-muted);
|
|
28
|
+
display: flex;
|
|
29
|
+
padding: var(--xh-pad-half-px);
|
|
22
30
|
|
|
23
31
|
.xh-icon {
|
|
24
32
|
margin-right: var(--xh-pad-half-px);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {isEmpty} from 'lodash';
|
|
8
8
|
import {grid} from '@xh/hoist/cmp/grid';
|
|
9
|
-
import {div, hframe, placeholder, label, vbox, vframe} from '@xh/hoist/cmp/layout';
|
|
9
|
+
import {div, hframe, placeholder, label, vbox, vframe, filler} from '@xh/hoist/cmp/layout';
|
|
10
10
|
import {storeFilterField} from '@xh/hoist/cmp/store';
|
|
11
11
|
import {XH, hoistCmp, uses} from '@xh/hoist/core';
|
|
12
12
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
@@ -60,7 +60,7 @@ const storeFilterSelect = hoistCmp.factory<ValuesTabModel>(({model}) => {
|
|
|
60
60
|
addToFilterId = XH.genId();
|
|
61
61
|
|
|
62
62
|
return vbox({
|
|
63
|
-
className: '
|
|
63
|
+
className: 'xh-values-filter-tab__filter-controls',
|
|
64
64
|
items: [
|
|
65
65
|
hframe(
|
|
66
66
|
checkbox({
|
|
@@ -73,6 +73,12 @@ const storeFilterSelect = hoistCmp.factory<ValuesTabModel>(({model}) => {
|
|
|
73
73
|
label({
|
|
74
74
|
htmlFor: selectAllId,
|
|
75
75
|
item: `(Select All${filterText ? ' Search Results' : ''})`
|
|
76
|
+
}),
|
|
77
|
+
filler(),
|
|
78
|
+
div({
|
|
79
|
+
className: 'xh-values-filter-tab__filter-controls__sort-icon',
|
|
80
|
+
item: model.sortIcon,
|
|
81
|
+
onClick: () => model.toggleSort()
|
|
76
82
|
})
|
|
77
83
|
),
|
|
78
84
|
hframe({
|
|
@@ -7,26 +7,27 @@
|
|
|
7
7
|
import {GridFilterModel, GridModel} from '@xh/hoist/cmp/grid';
|
|
8
8
|
import {HoistModel, managed} from '@xh/hoist/core';
|
|
9
9
|
import {FieldFilterSpec} from '@xh/hoist/data';
|
|
10
|
-
import {HeaderFilterModel} from '../HeaderFilterModel';
|
|
11
10
|
import {checkbox} from '@xh/hoist/desktop/cmp/input';
|
|
11
|
+
import {Icon} from '@xh/hoist/icon';
|
|
12
12
|
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
13
13
|
import {castArray, difference, flatten, isEmpty, map, partition, uniq, without} from 'lodash';
|
|
14
|
+
import {HeaderFilterModel} from '../HeaderFilterModel';
|
|
14
15
|
|
|
15
16
|
export class ValuesTabModel extends HoistModel {
|
|
16
17
|
override xhImpl = true;
|
|
17
18
|
|
|
18
19
|
headerFilterModel: HeaderFilterModel;
|
|
19
20
|
|
|
20
|
-
/** Checkbox grid to display enumerated set of values */
|
|
21
|
-
@managed
|
|
21
|
+
/** Checkbox grid to display enumerated set of values. */
|
|
22
|
+
@managed gridModel: GridModel;
|
|
22
23
|
|
|
23
|
-
/** List of currently checked values
|
|
24
|
+
/** List of currently checked values. */
|
|
24
25
|
@observable.ref pendingValues: any[] = [];
|
|
25
26
|
|
|
26
|
-
/** Bound search term for `StoreFilterField
|
|
27
|
+
/** Bound search term for `StoreFilterField`. */
|
|
27
28
|
@bindable filterText: string = null;
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/**
|
|
30
31
|
* Merge current filter with pendingValues on commit.
|
|
31
32
|
* Used when commitOnChange is false.
|
|
32
33
|
*/
|
|
@@ -80,12 +81,26 @@ export class ValuesTabModel extends HoistModel {
|
|
|
80
81
|
return this.values.length < this.valueCount;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
get sortIcon() {
|
|
85
|
+
const {sort, abs} = this.gridModel.sortBy[0];
|
|
86
|
+
if (sort === 'asc') {
|
|
87
|
+
if (abs) return Icon.sortAbsAsc();
|
|
88
|
+
return Icon.sortAsc();
|
|
89
|
+
}
|
|
90
|
+
if (sort === 'desc') {
|
|
91
|
+
if (abs) return Icon.sortAbsDesc();
|
|
92
|
+
return Icon.sortDesc();
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
83
97
|
constructor(headerFilterModel: HeaderFilterModel) {
|
|
84
98
|
super();
|
|
85
99
|
makeObservable(this);
|
|
86
100
|
|
|
87
101
|
this.headerFilterModel = headerFilterModel;
|
|
88
102
|
this.gridModel = this.createGridModel();
|
|
103
|
+
this.initGridSortBy();
|
|
89
104
|
|
|
90
105
|
this.addReaction(
|
|
91
106
|
{
|
|
@@ -125,6 +140,13 @@ export class ValuesTabModel extends HoistModel {
|
|
|
125
140
|
: without(this.pendingValues, ...values);
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
@action
|
|
144
|
+
toggleSort() {
|
|
145
|
+
const {colId, sort, abs} = this.gridModel.sortBy.find(it => it.colId === 'value'),
|
|
146
|
+
newSort = sort === 'asc' ? 'desc' : 'asc';
|
|
147
|
+
this.gridModel.setSortBy({colId, sort: newSort, abs});
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
toggleAllRecsChecked() {
|
|
129
151
|
const setAllToChecked = !this.allVisibleRecsChecked,
|
|
130
152
|
values = this.gridModel.store.records.map(it => it.get('value'));
|
|
@@ -244,13 +266,22 @@ export class ValuesTabModel extends HoistModel {
|
|
|
244
266
|
this.gridModel.loadData(data);
|
|
245
267
|
}
|
|
246
268
|
|
|
269
|
+
private initGridSortBy() {
|
|
270
|
+
const {gridModel: srcGridModel, column} = this.headerFilterModel,
|
|
271
|
+
srcColGridSorter = srcGridModel.sortBy.find(it => it.colId === column.colId);
|
|
272
|
+
|
|
273
|
+
this.gridModel.setSortBy({
|
|
274
|
+
colId: 'value',
|
|
275
|
+
sort: srcColGridSorter?.sort ?? 'asc',
|
|
276
|
+
abs: srcColGridSorter?.abs ?? false
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
247
280
|
private createGridModel() {
|
|
248
281
|
const {BLANK_PLACEHOLDER} = GridFilterModel,
|
|
249
282
|
{headerFilterModel, fieldSpec} = this,
|
|
250
|
-
{fieldType} = headerFilterModel,
|
|
251
|
-
renderer =
|
|
252
|
-
fieldSpec.renderer ??
|
|
253
|
-
(fieldType !== 'tags' ? this.headerFilterModel.parent.column.renderer : null);
|
|
283
|
+
{fieldType, column} = headerFilterModel,
|
|
284
|
+
renderer = fieldSpec.renderer ?? (fieldType !== 'tags' ? column.renderer : null);
|
|
254
285
|
|
|
255
286
|
return new GridModel({
|
|
256
287
|
store: {
|
|
@@ -301,6 +332,7 @@ export class ValuesTabModel extends HoistModel {
|
|
|
301
332
|
{
|
|
302
333
|
field: 'value',
|
|
303
334
|
align: 'left',
|
|
335
|
+
tooltip: true,
|
|
304
336
|
comparator: (v1, v2, sortDir, abs, {defaultComparator}) => {
|
|
305
337
|
const mul = sortDir === 'desc' ? -1 : 1;
|
|
306
338
|
if (v1 === BLANK_PLACEHOLDER) return 1 * mul;
|
|
@@ -36,7 +36,7 @@ export const impersonationBar = hoistCmp.factory({
|
|
|
36
36
|
enableFilter: true,
|
|
37
37
|
enableFullscreen: true,
|
|
38
38
|
placeholder: 'Select a user to impersonate...',
|
|
39
|
-
createMessageFn: q => `Impersonate ${q}`,
|
|
39
|
+
createMessageFn: q => `Impersonate new user "${q}"`,
|
|
40
40
|
onCommit: model.onCommit
|
|
41
41
|
}),
|
|
42
42
|
button({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "76.
|
|
3
|
+
"version": "76.2.0",
|
|
4
4
|
"description": "Hoist add-on for building and deploying React Applications.",
|
|
5
5
|
"repository": "github:xh/hoist-react",
|
|
6
6
|
"homepage": "https://xh.io",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@auth0/auth0-spa-js": "~2.
|
|
32
|
-
"@azure/msal-browser": "~4.
|
|
31
|
+
"@auth0/auth0-spa-js": "~2.7.0",
|
|
32
|
+
"@azure/msal-browser": "~4.25.0",
|
|
33
33
|
"@blueprintjs/core": "^5.10.5",
|
|
34
34
|
"@blueprintjs/datetime": "^5.3.7",
|
|
35
35
|
"@blueprintjs/datetime2": "^2.3.7",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"codemirror": "~5.65.0",
|
|
49
49
|
"core-js": "3.x",
|
|
50
50
|
"debounce-promise": "~3.1.0",
|
|
51
|
-
"dompurify": "~3.
|
|
51
|
+
"dompurify": "~3.3.0",
|
|
52
52
|
"downloadjs": "~1.4.7",
|
|
53
53
|
"fast-deep-equal": "~3.1.1",
|
|
54
54
|
"filesize": "~11.0.2",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"jwt-decode": "~4.0.0",
|
|
59
59
|
"lodash": "~4.17.21",
|
|
60
60
|
"lodash-inflection": "~1.5.0",
|
|
61
|
-
"mobx": "~6.
|
|
61
|
+
"mobx": "~6.15.0",
|
|
62
62
|
"mobx-react-lite": "~4.1.0",
|
|
63
63
|
"moment": "~2.30.1",
|
|
64
64
|
"numbro": "~2.5.0",
|
package/promise/Promise.ts
CHANGED
|
@@ -132,15 +132,15 @@ export function waitFor(
|
|
|
132
132
|
condition: () => boolean,
|
|
133
133
|
{interval = 50, timeout = 5 * SECONDS}: {interval?: number; timeout?: number} = {}
|
|
134
134
|
): Promise<void> {
|
|
135
|
-
if (
|
|
136
|
-
if (
|
|
135
|
+
if (interval <= 0) throw new Error('Invalid interval');
|
|
136
|
+
if (timeout != null && timeout <= 0) throw new Error('Invalid timeout');
|
|
137
137
|
|
|
138
138
|
const startTime = Date.now();
|
|
139
139
|
return new Promise((resolve, reject) => {
|
|
140
140
|
const resolveOnMet = () => {
|
|
141
141
|
if (condition()) {
|
|
142
142
|
resolve();
|
|
143
|
-
} else if (olderThan(startTime, timeout)) {
|
|
143
|
+
} else if (timeout != null && olderThan(startTime, timeout)) {
|
|
144
144
|
reject(Exception.timeout({interval: Date.now() - startTime}));
|
|
145
145
|
} else {
|
|
146
146
|
setTimeout(resolveOnMet, interval);
|
package/styles/vars.scss
CHANGED
|
@@ -522,8 +522,8 @@ body {
|
|
|
522
522
|
--xh-zone-grid-label-color: var(--zone-grid-label-color, inherit);
|
|
523
523
|
|
|
524
524
|
// Grid column-header-based filter popover (desktop only)
|
|
525
|
-
--xh-grid-filter-popover-height-px: var(--grid-filter-popover-height-px,
|
|
526
|
-
--xh-grid-filter-popover-width-px: var(--grid-filter-popover-width-px,
|
|
525
|
+
--xh-grid-filter-popover-height-px: var(--grid-filter-popover-height-px, 400px);
|
|
526
|
+
--xh-grid-filter-popover-width-px: var(--grid-filter-popover-width-px, 280px);
|
|
527
527
|
|
|
528
528
|
// Dark Grid
|
|
529
529
|
&.xh-dark {
|