@xh/hoist 55.1.0 → 55.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 +18 -0
- package/data/cube/View.ts +6 -4
- package/data/cube/row/AggregateRow.ts +2 -1
- package/desktop/cmp/dash/DashViewModel.ts +5 -5
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +16 -6
- package/desktop/cmp/dash/canvas/DashCanvasViewModel.ts +7 -2
- package/desktop/cmp/dash/canvas/DashCanvasViewSpec.ts +15 -0
- package/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +2 -2
- package/desktop/cmp/rest/RestGridModel.ts +2 -2
- package/desktop/cmp/rest/data/RestStore.ts +13 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v55.2.0 - 2023-02-10
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
* `DashCanvas` enhancements:
|
|
7
|
+
* Views now support minimum and maximum dimensions.
|
|
8
|
+
* Views now expose an `allowDuplicate` flag for controlling the `Duplicate` menu item visibility.
|
|
9
|
+
|
|
10
|
+
### 🐞 Bug Fixes
|
|
11
|
+
* Fixed a bug with Cube views having dimensions containing non-string or `null` values. Rows grouped
|
|
12
|
+
by these dimensions would report values for the dimension which were incorrectly stringified (e.g.
|
|
13
|
+
`null` vs. `'null'` or `'5'` vs. `5`). This has been fixed. Note that the stringified value is
|
|
14
|
+
still reported for the rows' `cubeLabel` value, and will be used for the purposes of grouping.
|
|
15
|
+
|
|
16
|
+
### ⚙️ Typescript API Adjustments
|
|
17
|
+
|
|
18
|
+
* Improved signatures of `RestStore` APIs.
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
## v55.1.0 - 2023-02-09
|
|
4
22
|
|
|
5
23
|
Version 55 is the first major update of the toolkit after our transition to typescript. In addition
|
package/data/cube/View.ts
CHANGED
|
@@ -268,7 +268,7 @@ export class View extends HoistBase {
|
|
|
268
268
|
if (includeRoot) {
|
|
269
269
|
newRows = [
|
|
270
270
|
this.cachedRow(rootId, newRows,
|
|
271
|
-
() => new AggregateRow(this, rootId, newRows, null, 'Total', {})
|
|
271
|
+
() => new AggregateRow(this, rootId, newRows, null, 'Total', 'Total', {})
|
|
272
272
|
)
|
|
273
273
|
];
|
|
274
274
|
} else if (!query.includeLeaves && newRows[0]?.isLeaf) {
|
|
@@ -308,15 +308,17 @@ export class View extends HoistBase {
|
|
|
308
308
|
groups = groupBy(records, (it) => it.data[dimName]);
|
|
309
309
|
|
|
310
310
|
appliedDimensions = {...appliedDimensions};
|
|
311
|
-
return map(groups, (groupRecords,
|
|
311
|
+
return map(groups, (groupRecords, strVal) => {
|
|
312
|
+
const val = groupRecords[0].data[dimName],
|
|
313
|
+
id = rootId + `${dimName}=[${strVal}]`;
|
|
314
|
+
|
|
312
315
|
appliedDimensions[dimName] = val;
|
|
313
|
-
const id = rootId + `${dimName}=[${val}]`;
|
|
314
316
|
|
|
315
317
|
let children = this.groupAndInsertRecords(groupRecords, dimensions.slice(1), id, appliedDimensions, leafMap);
|
|
316
318
|
children = this.bucketRows(children, id, appliedDimensions);
|
|
317
319
|
|
|
318
320
|
return this.cachedRow(id, children,
|
|
319
|
-
() => new AggregateRow(this, id, children, dim, val, appliedDimensions)
|
|
321
|
+
() => new AggregateRow(this, id, children, dim, val, strVal, appliedDimensions)
|
|
320
322
|
);
|
|
321
323
|
});
|
|
322
324
|
}
|
|
@@ -25,6 +25,7 @@ export class AggregateRow extends BaseRow {
|
|
|
25
25
|
children: BaseRow[],
|
|
26
26
|
dim: CubeField,
|
|
27
27
|
val: any,
|
|
28
|
+
strVal: string,
|
|
28
29
|
appliedDimensions: PlainObject
|
|
29
30
|
) {
|
|
30
31
|
super(view, id);
|
|
@@ -32,7 +33,7 @@ export class AggregateRow extends BaseRow {
|
|
|
32
33
|
|
|
33
34
|
this.dim = dim;
|
|
34
35
|
this.dimName = dimName;
|
|
35
|
-
this.data.cubeLabel =
|
|
36
|
+
this.data.cubeLabel = strVal;
|
|
36
37
|
this.data.cubeDimension = dimName;
|
|
37
38
|
|
|
38
39
|
this.initAggregate(children, dimName, val, appliedDimensions);
|
|
@@ -31,12 +31,12 @@ export type DashViewState = Record<string, any>;
|
|
|
31
31
|
* Content hosted within this view can use this model at runtime to access and set state
|
|
32
32
|
* for the view or access other information.
|
|
33
33
|
*/
|
|
34
|
-
export class DashViewModel extends HoistModel {
|
|
34
|
+
export class DashViewModel<T extends DashViewSpec = DashViewSpec> extends HoistModel {
|
|
35
35
|
|
|
36
36
|
id: string;
|
|
37
37
|
|
|
38
38
|
/** DashViewSpec used to create this view. */
|
|
39
|
-
viewSpec:
|
|
39
|
+
viewSpec: T;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Parent DashContainerModel or DashCanvasModel. Provided by the container when
|
|
@@ -74,7 +74,7 @@ export class DashViewModel extends HoistModel {
|
|
|
74
74
|
title,
|
|
75
75
|
viewState = null,
|
|
76
76
|
containerModel
|
|
77
|
-
}: DashViewConfig) {
|
|
77
|
+
}: DashViewConfig<T>) {
|
|
78
78
|
super();
|
|
79
79
|
makeObservable(this);
|
|
80
80
|
throwIf(!id, 'DashViewModel requires an id');
|
|
@@ -99,9 +99,9 @@ export class DashViewModel extends HoistModel {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/** @internal */
|
|
102
|
-
export interface DashViewConfig {
|
|
102
|
+
export interface DashViewConfig<T extends DashViewSpec = DashViewSpec> {
|
|
103
103
|
id: string;
|
|
104
|
-
viewSpec:
|
|
104
|
+
viewSpec: T;
|
|
105
105
|
icon?: ReactElement;
|
|
106
106
|
title?: string;
|
|
107
107
|
viewState?: DashViewState;
|
|
@@ -101,10 +101,19 @@ export class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashCanvasIte
|
|
|
101
101
|
private isLoadingState: boolean;
|
|
102
102
|
|
|
103
103
|
get rglLayout() {
|
|
104
|
-
return this.layout.map(it =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
return this.layout.map(it => {
|
|
105
|
+
const dashCanvasView = this.getView(it.i),
|
|
106
|
+
{autoHeight, viewSpec} = dashCanvasView;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...it,
|
|
110
|
+
resizeHandles: autoHeight ? ['e'] : ['e', 's', 'se'],
|
|
111
|
+
maxH: viewSpec.maxHeight,
|
|
112
|
+
minH: viewSpec.minHeight,
|
|
113
|
+
maxW: viewSpec.maxWidth,
|
|
114
|
+
minW: viewSpec.minWidth
|
|
115
|
+
};
|
|
116
|
+
});
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
constructor({
|
|
@@ -135,6 +144,7 @@ export class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashCanvasIte
|
|
|
135
144
|
omit: false,
|
|
136
145
|
unique: false,
|
|
137
146
|
allowAdd: true,
|
|
147
|
+
allowDuplicate: true,
|
|
138
148
|
allowRemove: true,
|
|
139
149
|
allowRename: true,
|
|
140
150
|
height: 5,
|
|
@@ -339,8 +349,8 @@ export class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashCanvasIte
|
|
|
339
349
|
prevLayout = previousViewId ? this.getViewLayout(previousViewId) : null,
|
|
340
350
|
x = prevLayout?.x ?? layout?.x ?? 0,
|
|
341
351
|
y = prevLayout?.y ?? layout?.y ?? this.rows,
|
|
342
|
-
h = layout?.h ?? viewSpec.height ?? 1,
|
|
343
|
-
w = layout?.w ?? viewSpec.width ?? 1;
|
|
352
|
+
h = layout?.h ?? viewSpec.height ?? viewSpec.minHeight ?? 1,
|
|
353
|
+
w = layout?.w ?? viewSpec.width ?? viewSpec.minWidth ?? 1;
|
|
344
354
|
|
|
345
355
|
this.setLayout([...this.layout, {i: id, x, y, h, w}]);
|
|
346
356
|
this.viewModels = [...this.viewModels, model];
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {DashCanvasViewSpec} from '@xh/hoist/desktop/cmp/dash';
|
|
7
8
|
import {DashViewConfig, DashViewModel} from '../DashViewModel';
|
|
8
9
|
import '@xh/hoist/desktop/register';
|
|
9
10
|
import {createObservableRef} from '@xh/hoist/utils/react';
|
|
@@ -13,7 +14,10 @@ import {ReactNode} from 'react';
|
|
|
13
14
|
/**
|
|
14
15
|
* Model for a content item within a DashCanvas.
|
|
15
16
|
*/
|
|
16
|
-
export class DashCanvasViewModel extends DashViewModel {
|
|
17
|
+
export class DashCanvasViewModel extends DashViewModel<DashCanvasViewSpec> {
|
|
18
|
+
|
|
19
|
+
/** True (default) to allow duplicating the view. */
|
|
20
|
+
@observable allowDuplicate: boolean;
|
|
17
21
|
|
|
18
22
|
/** Hide the Header Panel for the view? Default false. */
|
|
19
23
|
@observable hidePanelHeader: boolean;
|
|
@@ -29,9 +33,10 @@ export class DashCanvasViewModel extends DashViewModel {
|
|
|
29
33
|
|
|
30
34
|
ref = createObservableRef<HTMLElement>();
|
|
31
35
|
|
|
32
|
-
constructor(cfg: DashViewConfig) {
|
|
36
|
+
constructor(cfg: DashViewConfig<DashCanvasViewSpec>) {
|
|
33
37
|
super(cfg);
|
|
34
38
|
makeObservable(this);
|
|
39
|
+
this.allowDuplicate = cfg.viewSpec.allowDuplicate ?? true;
|
|
35
40
|
this.hidePanelHeader = !!cfg.viewSpec.hidePanelHeader;
|
|
36
41
|
this.hideMenuButton = !!cfg.viewSpec.hideMenuButton;
|
|
37
42
|
this.autoHeight = !!cfg.viewSpec.autoHeight;
|
|
@@ -21,6 +21,18 @@ export interface DashCanvasViewSpec extends DashViewSpec {
|
|
|
21
21
|
/** Initial width of view when added to canvas (default 5). */
|
|
22
22
|
width?: number
|
|
23
23
|
|
|
24
|
+
/** Maximum height of the view (default undefined). */
|
|
25
|
+
maxHeight?: number;
|
|
26
|
+
|
|
27
|
+
/** Minimum height of the view (default undefined). */
|
|
28
|
+
minHeight?: number;
|
|
29
|
+
|
|
30
|
+
/** Maximum width of the view (default undefined). */
|
|
31
|
+
maxWidth?: number
|
|
32
|
+
|
|
33
|
+
/** Minimum width of the view (default undefined). */
|
|
34
|
+
minWidth?: number
|
|
35
|
+
|
|
24
36
|
/** True to hide the panel header (default false). */
|
|
25
37
|
hidePanelHeader?: boolean;
|
|
26
38
|
|
|
@@ -29,4 +41,7 @@ export interface DashCanvasViewSpec extends DashViewSpec {
|
|
|
29
41
|
|
|
30
42
|
/** True to set height automatically based on content height (default false). */
|
|
31
43
|
autoHeight?: boolean;
|
|
44
|
+
|
|
45
|
+
/** True (default) to allow duplicating the view. */
|
|
46
|
+
allowDuplicate?: boolean;
|
|
32
47
|
}
|
|
@@ -58,7 +58,7 @@ const headerMenu = hoistCmp.factory<DashCanvasViewModel>(
|
|
|
58
58
|
({model}) => {
|
|
59
59
|
if (model.hideMenuButton) return null;
|
|
60
60
|
|
|
61
|
-
const {viewState, viewSpec, id, containerModel, positionParams, title} = model,
|
|
61
|
+
const {allowDuplicate, viewState, viewSpec, id, containerModel, positionParams, title} = model,
|
|
62
62
|
{contentLocked, renameLocked} = containerModel,
|
|
63
63
|
|
|
64
64
|
addMenuItems = createViewMenuItems({
|
|
@@ -101,7 +101,7 @@ const headerMenu = hoistCmp.factory<DashCanvasViewModel>(
|
|
|
101
101
|
{
|
|
102
102
|
text: 'Duplicate',
|
|
103
103
|
icon: Icon.copy(),
|
|
104
|
-
hidden: contentLocked || viewSpec.unique,
|
|
104
|
+
hidden: !allowDuplicate || contentLocked || viewSpec.unique,
|
|
105
105
|
actionFn: () =>
|
|
106
106
|
containerModel.addViewInternal(viewSpec.id, {
|
|
107
107
|
layout: getDuplicateLayout(positionParams, model),
|
|
@@ -185,7 +185,7 @@ export class RestGridModel extends HoistModel {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
cloneRecord(record: StoreRecord) {
|
|
188
|
-
const clone = this.store.editableDataForRecord(record
|
|
188
|
+
const clone = this.store.editableDataForRecord(record);
|
|
189
189
|
this.prepareCloneFn?.({record, clone});
|
|
190
190
|
this.formModel.openClone(clone);
|
|
191
191
|
}
|
|
@@ -247,4 +247,4 @@ export class RestGridModel extends HoistModel {
|
|
|
247
247
|
private parseStore(store: RestStore|RestStoreConfig) {
|
|
248
248
|
return store instanceof RestStore ? store : this.markManaged(new RestStore(store));
|
|
249
249
|
}
|
|
250
|
-
}
|
|
250
|
+
}
|
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
* Copyright © 2022 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {PlainObject, XH} from '@xh/hoist/core';
|
|
8
|
-
import {
|
|
9
|
-
StoreRecord,
|
|
10
|
-
UrlStore,
|
|
11
|
-
UrlStoreConfig
|
|
12
|
-
} from '@xh/hoist/data';
|
|
8
|
+
import {StoreRecord, StoreRecordId, UrlStore, UrlStoreConfig} from '@xh/hoist/data';
|
|
13
9
|
import '@xh/hoist/desktop/register';
|
|
14
|
-
import {filter, keyBy, mapValues} from 'lodash';
|
|
10
|
+
import {filter, isNil, keyBy, mapValues} from 'lodash';
|
|
15
11
|
import {RestField, RestFieldSpec} from './RestField';
|
|
16
12
|
|
|
17
13
|
|
|
@@ -81,17 +77,17 @@ export class RestStore extends UrlStore {
|
|
|
81
77
|
return resp;
|
|
82
78
|
}
|
|
83
79
|
|
|
84
|
-
async addRecordAsync(rec: {id
|
|
80
|
+
async addRecordAsync(rec: {id?: StoreRecordId, data: PlainObject}) {
|
|
85
81
|
return this.saveRecordInternalAsync(rec, true)
|
|
86
82
|
.linkTo(this.loadModel);
|
|
87
83
|
}
|
|
88
84
|
|
|
89
|
-
async saveRecordAsync(rec: {id:
|
|
85
|
+
async saveRecordAsync(rec: {id: StoreRecordId, data: PlainObject}) {
|
|
90
86
|
return this.saveRecordInternalAsync(rec, false)
|
|
91
87
|
.linkTo(this.loadModel);
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
async bulkUpdateRecordsAsync(ids:
|
|
90
|
+
async bulkUpdateRecordsAsync(ids: StoreRecordId[], newParams: PlainObject) {
|
|
95
91
|
const {url} = this,
|
|
96
92
|
resp = await XH.fetchService.putJson({
|
|
97
93
|
url: `${url}/bulkUpdate`,
|
|
@@ -104,7 +100,7 @@ export class RestStore extends UrlStore {
|
|
|
104
100
|
return resp;
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
editableDataForRecord(record: {
|
|
103
|
+
editableDataForRecord(record: {data: PlainObject}): PlainObject {
|
|
108
104
|
const {data} = record,
|
|
109
105
|
editable = keyBy(filter(this.fields, 'editable'), 'name');
|
|
110
106
|
return mapValues(editable, (v, k) => data[k]);
|
|
@@ -117,12 +113,15 @@ export class RestStore extends UrlStore {
|
|
|
117
113
|
//--------------------------------
|
|
118
114
|
// Implementation
|
|
119
115
|
//--------------------------------
|
|
120
|
-
private async saveRecordInternalAsync(rec: {id
|
|
121
|
-
let {url, dataRoot} = this
|
|
122
|
-
|
|
116
|
+
private async saveRecordInternalAsync(rec: {id?: StoreRecordId, data: PlainObject}, isAdd) {
|
|
117
|
+
let {url, dataRoot} = this,
|
|
118
|
+
{id} = rec;
|
|
119
|
+
|
|
120
|
+
if (!isAdd) url += '/' + id;
|
|
123
121
|
|
|
124
122
|
// Only include editable fields in the request data
|
|
125
|
-
const data =
|
|
123
|
+
const data = this.editableDataForRecord(rec);
|
|
124
|
+
if (!isNil(id)) data.id = id;
|
|
126
125
|
|
|
127
126
|
const fetchMethod = isAdd ? 'postJson' : 'putJson',
|
|
128
127
|
response = await XH.fetchService[fetchMethod]({url, body: {data}}),
|