@xh/hoist 77.0.1 → 77.1.1
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 +27 -10
- package/admin/tabs/cluster/instances/services/ServiceModel.ts +7 -1
- package/admin/tabs/general/config/ConfigPanelModel.ts +31 -27
- package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -6
- package/admin/tabs/userData/prefs/editor/PrefEditorModel.ts +26 -19
- package/build/types/core/persist/PersistOptions.d.ts +2 -2
- package/build/types/data/StoreRecord.d.ts +8 -0
- package/build/types/desktop/cmp/rest/RestGridModel.d.ts +5 -4
- package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +3 -3
- package/cmp/chart/impl/copyToClipboard.ts +6 -13
- package/cmp/grid/Grid.ts +1 -1
- package/cmp/tab/TabModel.ts +2 -0
- package/cmp/treemap/TreeMap.ts +10 -2
- package/cmp/treemap/TreeMapModel.ts +2 -2
- package/core/persist/PersistOptions.ts +2 -2
- package/data/Store.ts +29 -8
- package/data/StoreRecord.ts +23 -10
- package/data/cube/View.ts +1 -1
- package/desktop/cmp/rest/RestGridModel.ts +6 -9
- package/kit/highcharts/index.ts +2 -2
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 77.1.1 - 2025-11-12
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
* New method `StoreRecord.getModifiedValues()` to gather edited data from a store record.
|
|
7
|
+
|
|
8
|
+
### 🐞 Bug Fixes
|
|
9
|
+
* StoreRecord will no longer report `isModified` as `true` if a field has been edited and
|
|
10
|
+
then returned to its original value in a subsequent edit.
|
|
11
|
+
* Restore support for `TabModel.content` being nullable to support dynamic tab content.
|
|
12
|
+
* Remove stray context menu from appearing when clicking on column group headers and other grid
|
|
13
|
+
empty space.
|
|
14
|
+
|
|
3
15
|
## 77.0.1 - 2025-10-29
|
|
4
16
|
|
|
5
17
|
### 💥 Breaking Changes
|
|
6
18
|
|
|
7
|
-
*
|
|
8
|
-
|
|
19
|
+
* Removed the `disableXssProtection` flag supported by `AppSpec` and `FieldSpec` and replaced with
|
|
20
|
+
its opposite, `enableXssProtection`, now an opt-in feature.
|
|
9
21
|
* While store-based XSS protection via DomPurify is still available to apps that can display
|
|
10
22
|
untrusted or potentially malicious data, this is an uncommon use case for Hoist apps and was
|
|
11
23
|
deemed to not provide enough benefit relative to potential performance pitfalls for most
|
|
@@ -17,20 +29,25 @@
|
|
|
17
29
|
|
|
18
30
|
### 🐞 Bug Fixes
|
|
19
31
|
|
|
20
|
-
*
|
|
21
|
-
*
|
|
32
|
+
* Fixed regressions in grid context menus for filtering and copy/paste introduced by AG Grid v34.
|
|
33
|
+
* Note: AG Grid v34+ no longer supports HTML markup in context menus. Applications setting the
|
|
34
|
+
`text` or `secondaryText` properties of `RecordGridAction` to markup should be sure to use
|
|
35
|
+
React nodes for formatting instead.
|
|
36
|
+
* Fixed `AgGridModel.getExpandState()` not returning a full representation of expanded groups -
|
|
37
|
+
an issue that primarily affected linked tree map visualizations.
|
|
38
|
+
|
|
39
|
+
### ⚙️ Technical
|
|
40
|
+
|
|
41
|
+
* Support Grails 7 service name conventions in admin client (backward compatible)
|
|
22
42
|
|
|
23
|
-
* Note: As of v34, AgGrid no longer supports html markup in context menus. Applications setting
|
|
24
|
-
the `text` or `secondaryText` properties of `RecordGridAction` to markup should be sure to use
|
|
25
|
-
react nodes for formatting instead.
|
|
26
43
|
|
|
27
44
|
## 76.2.0 - 2025-10-22
|
|
28
45
|
|
|
29
46
|
### ⚙️ Technical
|
|
30
47
|
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `waitFor`
|
|
48
|
+
* Implemented minor performance improvements within `Store` for large data sets.
|
|
49
|
+
* Added new `ViewRowData.cubeRowType` property to support identifying bucketed rows.
|
|
50
|
+
* Improved `waitFor` to accept a `null` value for its timeout.
|
|
34
51
|
|
|
35
52
|
## 76.1.0 - 2025-10-17
|
|
36
53
|
|
|
@@ -152,7 +152,13 @@ export class ServiceModel extends BaseInstanceModel {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
private processRawData(r: PlainObject) {
|
|
155
|
-
|
|
155
|
+
// For Grails <=6, plugin is prefix in name.
|
|
156
|
+
// For Grails >7, we provide class to determine provider
|
|
157
|
+
// TODO: simplify when Hoist v34+ required.
|
|
158
|
+
const provider =
|
|
159
|
+
r.name.startsWith('hoistCore') || r.className?.startsWith('io.xh.hoist')
|
|
160
|
+
? 'Hoist'
|
|
161
|
+
: 'App';
|
|
156
162
|
const displayName = lowerFirst(r.name.replace('hoistCore', ''));
|
|
157
163
|
return {provider, displayName, ...r};
|
|
158
164
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
8
8
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
9
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
10
|
-
import {hbox, hspacer} from '@xh/hoist/cmp/layout';
|
|
10
|
+
import {br, fragment, hbox, hspacer} from '@xh/hoist/cmp/layout';
|
|
11
11
|
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
12
12
|
import {FieldSpec} from '@xh/hoist/data';
|
|
13
13
|
import {defaultReadonlyRenderer} from '@xh/hoist/desktop/cmp/form';
|
|
@@ -17,14 +17,14 @@ import {
|
|
|
17
17
|
cloneAction,
|
|
18
18
|
deleteAction,
|
|
19
19
|
editAction,
|
|
20
|
-
RestGridModel
|
|
21
|
-
RestStore
|
|
20
|
+
RestGridModel
|
|
22
21
|
} from '@xh/hoist/desktop/cmp/rest';
|
|
22
|
+
import {Icon} from '@xh/hoist/icon';
|
|
23
23
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
24
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
24
25
|
import {isNil, truncate} from 'lodash';
|
|
25
26
|
import {DifferModel} from '../../../differ/DifferModel';
|
|
26
27
|
import {RegroupDialogModel} from '../../../regroup/RegroupDialogModel';
|
|
27
|
-
import {Icon} from '@xh/hoist/icon';
|
|
28
28
|
|
|
29
29
|
export class ConfigPanelModel extends HoistModel {
|
|
30
30
|
override persistWith = {localStorageKey: 'xhAdminConfigState'};
|
|
@@ -43,18 +43,27 @@ export class ConfigPanelModel extends HoistModel {
|
|
|
43
43
|
super();
|
|
44
44
|
makeObservable(this);
|
|
45
45
|
|
|
46
|
-
const
|
|
46
|
+
const {regroupAction} = this.regroupDialogModel,
|
|
47
|
+
required = true,
|
|
47
48
|
enableCreate = true,
|
|
48
49
|
hidden = true;
|
|
49
50
|
|
|
50
51
|
this.gridModel = new RestGridModel({
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// Core config
|
|
53
|
+
autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
|
|
53
54
|
colChooserModel: true,
|
|
54
55
|
enableExport: true,
|
|
55
56
|
exportOptions: {filename: exportFilenameWithDate('configs')},
|
|
57
|
+
filterFields: ['name', 'value', 'groupName', 'note'],
|
|
58
|
+
groupBy: 'groupName',
|
|
59
|
+
persistWith: this.persistWith,
|
|
60
|
+
prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
|
|
61
|
+
readonly: AppModel.readonly,
|
|
56
62
|
selModel: 'multiple',
|
|
57
|
-
|
|
63
|
+
sortBy: 'name',
|
|
64
|
+
unit: 'config',
|
|
65
|
+
// Store + fields
|
|
66
|
+
store: {
|
|
58
67
|
url: 'rest/configAdmin',
|
|
59
68
|
reloadLookupsOnLoad: true,
|
|
60
69
|
fieldDefaults: {enableXssProtection: false},
|
|
@@ -83,25 +92,8 @@ export class ConfigPanelModel extends HoistModel {
|
|
|
83
92
|
editable: false
|
|
84
93
|
}
|
|
85
94
|
]
|
|
86
|
-
}),
|
|
87
|
-
actionWarning: {
|
|
88
|
-
del: records =>
|
|
89
|
-
`Are you sure you want to delete ${records.length} config(s)? Deleting configs can break running apps.`
|
|
90
95
|
},
|
|
91
|
-
|
|
92
|
-
menuActions: [
|
|
93
|
-
addAction,
|
|
94
|
-
editAction,
|
|
95
|
-
cloneAction,
|
|
96
|
-
deleteAction,
|
|
97
|
-
'-',
|
|
98
|
-
this.regroupDialogModel.regroupAction
|
|
99
|
-
],
|
|
100
|
-
prepareCloneFn: ({clone}) => (clone.name = `${clone.name}_CLONE`),
|
|
101
|
-
unit: 'config',
|
|
102
|
-
filterFields: ['name', 'value', 'groupName', 'note'],
|
|
103
|
-
sortBy: 'name',
|
|
104
|
-
groupBy: 'groupName',
|
|
96
|
+
// Cols + editors
|
|
105
97
|
columns: [
|
|
106
98
|
{...Col.groupName, hidden},
|
|
107
99
|
{...Col.name},
|
|
@@ -138,7 +130,19 @@ export class ConfigPanelModel extends HoistModel {
|
|
|
138
130
|
{field: 'clientVisible'},
|
|
139
131
|
{field: 'lastUpdated'},
|
|
140
132
|
{field: 'lastUpdatedBy'}
|
|
141
|
-
]
|
|
133
|
+
],
|
|
134
|
+
// Actions
|
|
135
|
+
actionWarning: {
|
|
136
|
+
del: records =>
|
|
137
|
+
fragment(
|
|
138
|
+
`Are you sure you want to delete ${pluralize('selected config', records.length, true)}?`,
|
|
139
|
+
br(),
|
|
140
|
+
br(),
|
|
141
|
+
`Deleting configs can break running apps.`
|
|
142
|
+
)
|
|
143
|
+
},
|
|
144
|
+
menuActions: [addAction, editAction, cloneAction, deleteAction, '-', regroupAction],
|
|
145
|
+
toolbarActions: [addAction, editAction, cloneAction, deleteAction]
|
|
142
146
|
});
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -25,12 +25,19 @@ export class UserPreferenceModel extends HoistModel {
|
|
|
25
25
|
hidden = true;
|
|
26
26
|
|
|
27
27
|
this.gridModel = new RestGridModel({
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Core config
|
|
29
|
+
autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
|
|
30
30
|
colChooserModel: true,
|
|
31
31
|
enableExport: true,
|
|
32
32
|
exportOptions: {filename: exportFilenameWithDate('user-prefs')},
|
|
33
|
+
filterFields: ['name', 'username'],
|
|
34
|
+
groupBy: 'groupName',
|
|
35
|
+
persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
|
|
36
|
+
readonly: AppModel.readonly,
|
|
33
37
|
selModel: 'multiple',
|
|
38
|
+
sortBy: 'name',
|
|
39
|
+
unit: 'user preference',
|
|
40
|
+
// Store + fields
|
|
34
41
|
store: {
|
|
35
42
|
url: 'rest/userPreferenceAdmin',
|
|
36
43
|
reloadLookupsOnLoad: true,
|
|
@@ -55,10 +62,7 @@ export class UserPreferenceModel extends HoistModel {
|
|
|
55
62
|
{...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
|
|
56
63
|
]
|
|
57
64
|
},
|
|
58
|
-
|
|
59
|
-
groupBy: 'groupName',
|
|
60
|
-
unit: 'user preference',
|
|
61
|
-
filterFields: ['name', 'username'],
|
|
65
|
+
// Cols + editors
|
|
62
66
|
columns: [
|
|
63
67
|
{...Col.name},
|
|
64
68
|
{...Col.type},
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
8
8
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
9
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
10
|
+
import {br, fragment} from '@xh/hoist/cmp/layout';
|
|
10
11
|
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
11
12
|
import {FieldSpec} from '@xh/hoist/data';
|
|
12
13
|
import {textArea} from '@xh/hoist/desktop/cmp/input';
|
|
13
14
|
import {addAction, deleteAction, editAction, RestGridModel} from '@xh/hoist/desktop/cmp/rest';
|
|
14
15
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
16
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
15
17
|
import {DifferModel} from '../../../../differ/DifferModel';
|
|
16
18
|
import {RegroupDialogModel} from '../../../../regroup/RegroupDialogModel';
|
|
17
19
|
|
|
@@ -32,18 +34,26 @@ export class PrefEditorModel extends HoistModel {
|
|
|
32
34
|
super();
|
|
33
35
|
makeObservable(this);
|
|
34
36
|
|
|
35
|
-
const
|
|
37
|
+
const {regroupAction} = this.regroupDialogModel,
|
|
38
|
+
required = true,
|
|
36
39
|
enableCreate = true,
|
|
37
40
|
hidden = true;
|
|
38
41
|
|
|
39
42
|
this.gridModel = new RestGridModel({
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
// Core config
|
|
44
|
+
autosizeOptions: {mode: 'managed', includeCollapsedChildren: true},
|
|
42
45
|
colChooserModel: true,
|
|
43
46
|
enableExport: true,
|
|
44
47
|
exportOptions: {filename: exportFilenameWithDate('prefs')},
|
|
48
|
+
filterFields: ['name', 'groupName'],
|
|
49
|
+
groupBy: 'groupName',
|
|
50
|
+
persistWith: this.persistWith,
|
|
51
|
+
readonly: AppModel.readonly,
|
|
45
52
|
selModel: 'multiple',
|
|
46
53
|
showRefreshButton: true,
|
|
54
|
+
sortBy: 'name',
|
|
55
|
+
unit: 'preference',
|
|
56
|
+
// Store + fields
|
|
47
57
|
store: {
|
|
48
58
|
url: 'rest/preferenceAdmin',
|
|
49
59
|
reloadLookupsOnLoad: true,
|
|
@@ -68,21 +78,7 @@ export class PrefEditorModel extends HoistModel {
|
|
|
68
78
|
{...(Col.lastUpdatedBy.field as FieldSpec), editable: false}
|
|
69
79
|
]
|
|
70
80
|
},
|
|
71
|
-
|
|
72
|
-
groupBy: 'groupName',
|
|
73
|
-
unit: 'preference',
|
|
74
|
-
filterFields: ['name', 'groupName'],
|
|
75
|
-
actionWarning: {
|
|
76
|
-
del: records =>
|
|
77
|
-
`Are you sure you want to delete ${records.length} preference(s)? Deleting preferences can break running apps.`
|
|
78
|
-
},
|
|
79
|
-
menuActions: [
|
|
80
|
-
addAction,
|
|
81
|
-
editAction,
|
|
82
|
-
deleteAction,
|
|
83
|
-
'-',
|
|
84
|
-
this.regroupDialogModel.regroupAction
|
|
85
|
-
],
|
|
81
|
+
// Cols + Editors
|
|
86
82
|
columns: [
|
|
87
83
|
{...Col.name},
|
|
88
84
|
{...Col.type},
|
|
@@ -100,7 +96,18 @@ export class PrefEditorModel extends HoistModel {
|
|
|
100
96
|
{field: 'notes', formField: {item: textArea({height: 100})}},
|
|
101
97
|
{field: 'lastUpdated'},
|
|
102
98
|
{field: 'lastUpdatedBy'}
|
|
103
|
-
]
|
|
99
|
+
],
|
|
100
|
+
// Actions
|
|
101
|
+
actionWarning: {
|
|
102
|
+
del: records =>
|
|
103
|
+
fragment(
|
|
104
|
+
`Are you sure you want to delete ${pluralize('selected preference', records.length, true)}?`,
|
|
105
|
+
br(),
|
|
106
|
+
br(),
|
|
107
|
+
`Deleting preference definitions can break running apps.`
|
|
108
|
+
)
|
|
109
|
+
},
|
|
110
|
+
menuActions: [addAction, editAction, deleteAction, '-', regroupAction]
|
|
104
111
|
});
|
|
105
112
|
}
|
|
106
113
|
|
|
@@ -35,12 +35,12 @@ export interface PersistOptions {
|
|
|
35
35
|
viewManagerModel?: ViewManagerModel;
|
|
36
36
|
/**
|
|
37
37
|
* Function returning blob of data to be used for reading state.
|
|
38
|
-
* Ignored if `prefKey`, `localStorageKey
|
|
38
|
+
* Ignored if `prefKey`, `localStorageKey`, `dashViewModel` or 'viewManagerModel' are provided.
|
|
39
39
|
*/
|
|
40
40
|
getData?: () => any;
|
|
41
41
|
/**
|
|
42
42
|
* Function to be used to write blob of data representing state.
|
|
43
|
-
* Ignored if `prefKey`, `localStorageKey
|
|
43
|
+
* Ignored if `prefKey`, `localStorageKey`, `dashViewModel` or 'viewManagerModel' are provided.
|
|
44
44
|
*/
|
|
45
45
|
setData?: (data: object) => void;
|
|
46
46
|
}
|
|
@@ -94,6 +94,14 @@ export declare class StoreRecord {
|
|
|
94
94
|
* Field in the Store. Useful for cloning/iterating over all values (including defaults).
|
|
95
95
|
*/
|
|
96
96
|
getValues(): PlainObject;
|
|
97
|
+
/**
|
|
98
|
+
* Get a map of modified values only.
|
|
99
|
+
*
|
|
100
|
+
* If record has no modifications, this method will return null.
|
|
101
|
+
* If modifications are returned, the returned object will include id,
|
|
102
|
+
* for convenience.
|
|
103
|
+
*/
|
|
104
|
+
getModifiedValues(): PlainObject;
|
|
97
105
|
/**
|
|
98
106
|
* Construct a StoreRecord from a pre-processed `data` source object.
|
|
99
107
|
*
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { RowDoubleClickedEvent } from '@xh/hoist/kit/ag-grid';
|
|
2
1
|
import { BaseFieldConfig } from '@xh/hoist/cmp/form';
|
|
3
2
|
import { GridConfig, GridModel } from '@xh/hoist/cmp/grid';
|
|
4
3
|
import { ElementSpec, HoistModel, PlainObject } from '@xh/hoist/core';
|
|
5
4
|
import '@xh/hoist/desktop/register';
|
|
6
5
|
import { RecordAction, RecordActionSpec, StoreRecord } from '@xh/hoist/data';
|
|
6
|
+
import { RowDoubleClickedEvent } from '@xh/hoist/kit/ag-grid';
|
|
7
7
|
import { ExportOptions } from '@xh/hoist/svc';
|
|
8
|
+
import { ReactNode } from 'react';
|
|
8
9
|
import { FormFieldProps } from '../form';
|
|
9
10
|
import { RestStore, RestStoreConfig } from './data/RestStore';
|
|
10
11
|
import { RestFormModel } from './impl/RestFormModel';
|
|
@@ -22,9 +23,9 @@ export interface RestGridConfig extends GridConfig {
|
|
|
22
23
|
showRefreshButton?: boolean;
|
|
23
24
|
/** Warning to display before actions on a selection of records. */
|
|
24
25
|
actionWarning?: {
|
|
25
|
-
add?:
|
|
26
|
-
del?:
|
|
27
|
-
edit?:
|
|
26
|
+
add?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
|
|
27
|
+
del?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
|
|
28
|
+
edit?: ReactNode | ((recs: StoreRecord[]) => ReactNode);
|
|
28
29
|
};
|
|
29
30
|
/** Name that describes records in this grid. */
|
|
30
31
|
unit?: string;
|
|
@@ -16,9 +16,9 @@ export declare class RestFormModel extends HoistModel {
|
|
|
16
16
|
dialogRef: import("react").RefObject<HTMLElement>;
|
|
17
17
|
get unit(): string;
|
|
18
18
|
get actionWarning(): {
|
|
19
|
-
add?:
|
|
20
|
-
del?:
|
|
21
|
-
edit?:
|
|
19
|
+
add?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
|
|
20
|
+
del?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
|
|
21
|
+
edit?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
|
|
22
22
|
};
|
|
23
23
|
get actions(): (import("@xh/hoist/data").RecordActionSpec | import("@xh/hoist/data").RecordAction)[];
|
|
24
24
|
get editors(): RestGridEditor[];
|
|
@@ -24,14 +24,14 @@ export function installCopyToClipboard(Highcharts) {
|
|
|
24
24
|
try {
|
|
25
25
|
const blobPromise = convertChartToPngAsync(this),
|
|
26
26
|
clipboardItemInput = new window.ClipboardItem({
|
|
27
|
-
// Safari requires an unresolved promise.
|
|
27
|
+
// Safari requires an unresolved promise. See https://bugs.webkit.org/show_bug.cgi?id=222262 for discussion
|
|
28
28
|
'image/png': Highcharts.isSafari ? blobPromise : await blobPromise
|
|
29
29
|
});
|
|
30
30
|
await window.navigator.clipboard.write([clipboardItemInput]);
|
|
31
31
|
XH.successToast('Chart copied to clipboard');
|
|
32
32
|
} catch (e) {
|
|
33
33
|
XH.handleException(e, {showAlert: false, logOnServer: true});
|
|
34
|
-
XH.dangerToast('Error: Chart could not be copied.
|
|
34
|
+
XH.dangerToast('Error: Chart could not be copied. This error has been logged.');
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
});
|
|
@@ -41,14 +41,7 @@ export function installCopyToClipboard(Highcharts) {
|
|
|
41
41
|
// Implementation
|
|
42
42
|
//------------------
|
|
43
43
|
async function convertChartToPngAsync(chart) {
|
|
44
|
-
const svg =
|
|
45
|
-
chart.getSVGForLocalExport(
|
|
46
|
-
chart.options.exporting,
|
|
47
|
-
{},
|
|
48
|
-
() => reject('Cannot fallback to export server'),
|
|
49
|
-
svg => resolve(svg)
|
|
50
|
-
)
|
|
51
|
-
),
|
|
44
|
+
const svg = chart.getSVG(),
|
|
52
45
|
svgUrl = svgToDataUrl(svg),
|
|
53
46
|
pngDataUrl = await svgUrlToPngDataUrlAsync(svgUrl),
|
|
54
47
|
ret = await loadBlob(pngDataUrl);
|
|
@@ -65,7 +58,7 @@ function memoryCleanup(svgUrl) {
|
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
/**
|
|
68
|
-
* Convert dataUri
|
|
61
|
+
* Convert dataUri to blob
|
|
69
62
|
*/
|
|
70
63
|
async function loadBlob(dataUrl) {
|
|
71
64
|
const fetched = await fetch(dataUrl);
|
|
@@ -84,7 +77,7 @@ function svgToDataUrl(svg) {
|
|
|
84
77
|
try {
|
|
85
78
|
// Safari requires data URI since it doesn't allow navigation to blob
|
|
86
79
|
// URLs.
|
|
87
|
-
// foreignObjects
|
|
80
|
+
// foreignObjects don't work well in Blobs in Chrome (#14780).
|
|
88
81
|
if (!isWebKitButNotChrome && svg.indexOf('<foreignObject') === -1) {
|
|
89
82
|
return domurl.createObjectURL(
|
|
90
83
|
new window.Blob([svg], {
|
|
@@ -94,7 +87,7 @@ function svgToDataUrl(svg) {
|
|
|
94
87
|
}
|
|
95
88
|
} catch (e) {}
|
|
96
89
|
|
|
97
|
-
//
|
|
90
|
+
// Safari, Firefox, or SVGs with foreignObect returns this
|
|
98
91
|
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
|
|
99
92
|
}
|
|
100
93
|
|
package/cmp/grid/Grid.ts
CHANGED
|
@@ -205,10 +205,10 @@ export class GridLocalModel extends HoistModel {
|
|
|
205
205
|
defaultColDef: {
|
|
206
206
|
sortable: true,
|
|
207
207
|
resizable: true,
|
|
208
|
-
suppressHeaderContextMenu: true,
|
|
209
208
|
suppressHeaderMenuButton: true,
|
|
210
209
|
menuTabs: ['filterMenuTab']
|
|
211
210
|
},
|
|
211
|
+
getMainMenuItems: () => [],
|
|
212
212
|
popupParent: document.querySelector('body'),
|
|
213
213
|
suppressAggFuncInHeader: true,
|
|
214
214
|
icons: {
|
package/cmp/tab/TabModel.ts
CHANGED
|
@@ -174,6 +174,8 @@ export class TabModel extends HoistModel {
|
|
|
174
174
|
// Implementation
|
|
175
175
|
//------------------
|
|
176
176
|
private parseContent(content: Content | TabContainerConfig | TabConfig[]): Content {
|
|
177
|
+
if (!content) return null;
|
|
178
|
+
|
|
177
179
|
// Recognize if content is a child container spec.
|
|
178
180
|
let childConfig: TabContainerConfig = null;
|
|
179
181
|
if (isArray(content)) {
|
package/cmp/treemap/TreeMap.ts
CHANGED
|
@@ -172,7 +172,6 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
172
172
|
this.prevConfig = cloneDeep(chartCfg);
|
|
173
173
|
this.createChart(config);
|
|
174
174
|
}
|
|
175
|
-
|
|
176
175
|
this.updateLabelVisibility();
|
|
177
176
|
}
|
|
178
177
|
|
|
@@ -199,9 +198,18 @@ class TreeMapLocalModel extends HoistModel {
|
|
|
199
198
|
});
|
|
200
199
|
}
|
|
201
200
|
|
|
201
|
+
// Reload series data by fully removing and re-adding the series.
|
|
202
|
+
// When treemap clustering is enabled, `setData()` & `series.update()` does not properly clear old cluster nodes,
|
|
203
|
+
// causing overlap or stale rendering. Removing and re-adding the series forces a full rebuild
|
|
204
|
+
// of the layout and clustering state, ensuring the chart is correctly redrawn.
|
|
202
205
|
@logWithDebug
|
|
203
206
|
reloadSeriesData(newData) {
|
|
204
|
-
|
|
207
|
+
const {chart} = this;
|
|
208
|
+
if (!chart) return;
|
|
209
|
+
const oldSeries = chart.series[0],
|
|
210
|
+
series = Highcharts.merge(oldSeries.userOptions, {data: newData});
|
|
211
|
+
oldSeries.remove(false);
|
|
212
|
+
chart.addSeries(series, true);
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
startResize = ({width, height}) => {
|
|
@@ -465,7 +465,7 @@ export class TreeMapModel extends HoistModel {
|
|
|
465
465
|
//----------------------
|
|
466
466
|
defaultOnClick = (record, e) => {
|
|
467
467
|
const {gridModel} = this;
|
|
468
|
-
if (!gridModel) return;
|
|
468
|
+
if (!gridModel || !record) return;
|
|
469
469
|
|
|
470
470
|
// Select nodes in grid
|
|
471
471
|
const {selModel} = gridModel;
|
|
@@ -477,7 +477,7 @@ export class TreeMapModel extends HoistModel {
|
|
|
477
477
|
};
|
|
478
478
|
|
|
479
479
|
defaultOnDoubleClick = record => {
|
|
480
|
-
if (!this.gridModel?.treeMode || isEmpty(record
|
|
480
|
+
if (!this.gridModel?.treeMode || isEmpty(record?.children)) return;
|
|
481
481
|
this.toggleNodeExpanded(record.treePath);
|
|
482
482
|
};
|
|
483
483
|
}
|
|
@@ -59,13 +59,13 @@ export interface PersistOptions {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Function returning blob of data to be used for reading state.
|
|
62
|
-
* Ignored if `prefKey`, `localStorageKey
|
|
62
|
+
* Ignored if `prefKey`, `localStorageKey`, `dashViewModel` or 'viewManagerModel' are provided.
|
|
63
63
|
*/
|
|
64
64
|
getData?: () => any;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Function to be used to write blob of data representing state.
|
|
68
|
-
* Ignored if `prefKey`, `localStorageKey
|
|
68
|
+
* Ignored if `prefKey`, `localStorageKey`, `dashViewModel` or 'viewManagerModel' are provided.
|
|
69
69
|
*/
|
|
70
70
|
setData?: (data: object) => void;
|
|
71
71
|
}
|
package/data/Store.ts
CHANGED
|
@@ -515,10 +515,12 @@ export class Store extends HoistBase {
|
|
|
515
515
|
|
|
516
516
|
return new StoreRecord({
|
|
517
517
|
id,
|
|
518
|
-
data: parsedData,
|
|
519
518
|
store: this,
|
|
519
|
+
raw: null,
|
|
520
|
+
data: parsedData,
|
|
521
|
+
committedData: null,
|
|
520
522
|
parent,
|
|
521
|
-
|
|
523
|
+
isSummary: false
|
|
522
524
|
});
|
|
523
525
|
});
|
|
524
526
|
|
|
@@ -585,13 +587,22 @@ export class Store extends HoistBase {
|
|
|
585
587
|
const currentRec = this.getOrThrow(id),
|
|
586
588
|
updatedData = this.parseUpdate(currentRec.data, mod);
|
|
587
589
|
|
|
590
|
+
// If after parsing, data is deep equal, its a no-op
|
|
591
|
+
if (equal(updatedData, currentRec.data)) return;
|
|
592
|
+
|
|
593
|
+
// Previously updated record might now be reverted to clean, normalize
|
|
594
|
+
const committedData =
|
|
595
|
+
currentRec.isModified && equal(currentRec.committedData, updatedData)
|
|
596
|
+
? updatedData
|
|
597
|
+
: currentRec.committedData;
|
|
598
|
+
|
|
588
599
|
const updatedRec = new StoreRecord({
|
|
589
600
|
id: currentRec.id,
|
|
601
|
+
store: currentRec.store,
|
|
590
602
|
raw: currentRec.raw,
|
|
591
603
|
data: updatedData,
|
|
604
|
+
committedData: committedData,
|
|
592
605
|
parent: currentRec.parent,
|
|
593
|
-
store: currentRec.store,
|
|
594
|
-
committedData: currentRec.committedData,
|
|
595
606
|
isSummary: currentRec.isSummary
|
|
596
607
|
});
|
|
597
608
|
|
|
@@ -1036,7 +1047,15 @@ export class Store extends HoistBase {
|
|
|
1036
1047
|
}
|
|
1037
1048
|
|
|
1038
1049
|
data = this.parseRaw(data);
|
|
1039
|
-
const ret = new StoreRecord({
|
|
1050
|
+
const ret = new StoreRecord({
|
|
1051
|
+
id,
|
|
1052
|
+
store: this,
|
|
1053
|
+
raw,
|
|
1054
|
+
data,
|
|
1055
|
+
committedData: data,
|
|
1056
|
+
parent,
|
|
1057
|
+
isSummary
|
|
1058
|
+
});
|
|
1040
1059
|
|
|
1041
1060
|
// Finalize summary only. Non-summary finalized by RecordSet
|
|
1042
1061
|
if (isSummary) ret.finalize();
|
|
@@ -1093,7 +1112,7 @@ export class Store extends HoistBase {
|
|
|
1093
1112
|
return ret;
|
|
1094
1113
|
}
|
|
1095
1114
|
|
|
1096
|
-
private parseUpdate(data, update) {
|
|
1115
|
+
private parseUpdate(data: PlainObject, update: PlainObject): PlainObject {
|
|
1097
1116
|
const {_fieldMap} = this;
|
|
1098
1117
|
|
|
1099
1118
|
// a) clone the existing object
|
|
@@ -1151,9 +1170,11 @@ export class Store extends HoistBase {
|
|
|
1151
1170
|
|
|
1152
1171
|
const ret = new StoreRecord({
|
|
1153
1172
|
id: recToRevert.id,
|
|
1154
|
-
raw: recToRevert.raw,
|
|
1155
|
-
data: {...recToRevert.committedData},
|
|
1156
1173
|
store: this,
|
|
1174
|
+
raw: recToRevert.raw,
|
|
1175
|
+
data: recToRevert.committedData,
|
|
1176
|
+
committedData: recToRevert.committedData,
|
|
1177
|
+
parent: null,
|
|
1157
1178
|
isSummary: true
|
|
1158
1179
|
});
|
|
1159
1180
|
ret.finalize();
|
package/data/StoreRecord.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {PlainObject} from '@xh/hoist/core';
|
|
8
8
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
9
|
-
import {isNil, flatMap, isMatch} from 'lodash';
|
|
9
|
+
import {isNil, flatMap, isMatch, isEmpty, pickBy} from 'lodash';
|
|
10
10
|
import {Store} from './Store';
|
|
11
11
|
import {ValidationState} from './validation/ValidationState';
|
|
12
12
|
import {RecordValidator} from './impl/RecordValidator';
|
|
13
13
|
import {Field} from './Field';
|
|
14
|
+
import equal from 'fast-deep-equal';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Wrapper object for each data element within a {@link Store}. Records must be assigned a unique ID
|
|
@@ -184,6 +185,26 @@ export class StoreRecord {
|
|
|
184
185
|
return ret;
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Get a map of modified values only.
|
|
190
|
+
*
|
|
191
|
+
* If record has no modifications, this method will return null.
|
|
192
|
+
* If modifications are returned, the returned object will include id,
|
|
193
|
+
* for convenience.
|
|
194
|
+
*/
|
|
195
|
+
getModifiedValues(): PlainObject {
|
|
196
|
+
if (!this.isModified) return null;
|
|
197
|
+
|
|
198
|
+
const {data, committedData} = this,
|
|
199
|
+
ret = pickBy(data, (v, k) => !equal(v, committedData[k]));
|
|
200
|
+
if (!isEmpty(ret)) {
|
|
201
|
+
ret.id = this.id;
|
|
202
|
+
return ret;
|
|
203
|
+
} else {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
187
208
|
/**
|
|
188
209
|
* Construct a StoreRecord from a pre-processed `data` source object.
|
|
189
210
|
*
|
|
@@ -195,15 +216,7 @@ export class StoreRecord {
|
|
|
195
216
|
* @internal
|
|
196
217
|
*/
|
|
197
218
|
constructor(config: StoreRecordConfig) {
|
|
198
|
-
const {
|
|
199
|
-
id,
|
|
200
|
-
store,
|
|
201
|
-
data,
|
|
202
|
-
raw = null,
|
|
203
|
-
committedData = data,
|
|
204
|
-
parent,
|
|
205
|
-
isSummary = false
|
|
206
|
-
} = config;
|
|
219
|
+
const {id, store, raw, data, committedData, parent, isSummary} = config;
|
|
207
220
|
throwIf(
|
|
208
221
|
isNil(id),
|
|
209
222
|
"Record needs an ID. Use 'Store.idSpec' to specify a unique ID for each record."
|