@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 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, val) => {
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 = val;
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: DashViewSpec;
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: DashViewSpec;
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
- ...it,
106
- resizeHandles: this.getView(it.i).autoHeight ? ['e'] : ['e', 's', 'se']
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 as any);
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: string, data: PlainObject}) {
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: string, data: PlainObject}) {
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: string[], newParams: PlainObject) {
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: {id: string, data: PlainObject}): PlainObject {
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: string, data: PlainObject}, isAdd) {
121
- let {url, dataRoot} = this;
122
- if (!isAdd) url += '/' + rec.id;
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 = {id: rec.id, ...this.editableDataForRecord(rec)};
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}}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "55.1.0",
3
+ "version": "55.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",