@xh/hoist 55.1.0 → 55.2.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 CHANGED
@@ -1,37 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## v55.2.1 - 2023-02-24
4
+
5
+ ### 🐞 Bug Fixes
6
+
7
+ * Fixed issue where a resizable `Panel` splitter could be rendered incorrectly while dragging.
8
+
9
+ ## v55.2.0 - 2023-02-10
10
+
11
+ ### 🎁 New Features
12
+
13
+ * `DashCanvas` enhancements:
14
+ * Views now support minimum and maximum dimensions.
15
+ * Views now expose an `allowDuplicate` flag for controlling the `Duplicate` menu item
16
+ visibility.
17
+
18
+ ### 🐞 Bug Fixes
19
+
20
+ * Fixed a bug with Cube views having dimensions containing non-string or `null` values. Rows grouped
21
+ by these dimensions would report values for the dimension which were incorrectly stringified (e.g.
22
+ `'null'` vs. `null` or `'5'` vs. `5`). This has been fixed. Note that the stringified value is
23
+ still reported for the rows' `cubeLabel` value, and will be used for the purposes of grouping.
24
+
25
+ ### ⚙️ Typescript API Adjustments
26
+
27
+ * Improved signatures of `RestStore` APIs.
28
+
3
29
  ## v55.1.0 - 2023-02-09
4
30
 
5
- Version 55 is the first major update of the toolkit after our transition to typescript. In addition
6
- to a host of runtime fixes and features, it also contains a good number of important typescript
7
- typing adjustments, which are listed below. In also includes a helpful
31
+ Version 55 is the first major update of the toolkit after our transition to Typescript. In addition
32
+ to a host of runtime fixes and features, it also contains a good number of important Typescript
33
+ typing adjustments, which are listed below. It also includes a helpful
8
34
  [Typescript upgrade guide](https://github.com/xh/hoist-react/blob/develop/docs/upgrade-to-typescript.md).
9
35
 
10
36
  ### 🎁 New Features
11
37
 
12
38
  * Grid exports can now be tracked in the admin activity tab by setting `exportOptions.track` to
13
39
  true (defaults to false).
14
- * Miscellaneous performance improvements to the cube package
15
- * The implementation of the `Cube.omitFn` feature has been enhanced. This function will now be
16
- called on *all* non-leaf nodes, not just single child nodes. This allows for more flexible
40
+ * Miscellaneous performance improvements to the cube package.
41
+ * The implementation of the `Cube.omitFn` feature has been enhanced. This function will now be
42
+ called on *all* non-leaf nodes, not just single child nodes. This allows for more flexible
17
43
  editing of the shape of the resulting hierarchical data emitted by cube views.
18
44
 
19
45
  ### 🐞 Bug Fixes
46
+
20
47
  * Fixed: grid cell editors would drop a single character edit.
21
48
  * Fixed: grid date input editor's popup did not position correctly in a grid with pinned columns.
22
49
  * Fixed issue with `DashContainer` flashing its "empty" text briefly before loading.
23
50
  * Several Hoist TypeScript types, interfaces, and signatures have been improved or corrected (typing
24
51
  changes only).
25
52
  * Fix bug where a `className` provided to a `Panel` with `modalSupport` would be dropped when in a
26
- modal state. Note this necessitated an additional layer in the `Panel` DOM hierarchy. Highly
53
+ modal state. Note this necessitated an additional layer in the `Panel` DOM hierarchy. Highly
27
54
  specific CSS selectors may be affected.
28
55
  * Fix bug where `TileFrame` would not pass through the keys of its children.
29
56
 
30
57
  ### 💥 Breaking Changes
31
- * The semantics of `Cube.omitFn` have changed such that it will now be called on all aggregate nodes,
32
- not just nodes with a single child. Applications may need to adjust any implementation of this
33
- function accordingly.
34
- * `hoistCmp.containerFactory` and `hoistCmp.withContainerFactory` are removed in favor of
58
+
59
+ * The semantics of `Cube.omitFn` have changed such that it will now be called on all aggregate
60
+ nodes, not just nodes with a single child. Applications may need to adjust any implementation of
61
+ this function accordingly.
62
+ * `hoistCmp.containerFactory` and `hoistCmp.withContainerFactory` are removed in favor of
35
63
  the basic `hoistCmp.factory` and `hoistCmp.withFactory` respectively. See typescript
36
64
  API adjustments below.
37
65
 
@@ -39,22 +67,19 @@ typing adjustments, which are listed below. In also includes a helpful
39
67
 
40
68
  The following Typescript API were adjusted in v55.
41
69
 
42
- * Removed the distinction between `StandardElementFactory` and `ContainerElementFactory`. This
70
+ * Removed the distinction between `StandardElementFactory` and `ContainerElementFactory`. This
43
71
  distinction was deemed to be unnecessary, and overcomplicated the understanding of Hoist.
44
- Applications should simply continue to use `ElementFactory` instead. `hoistCmp.containerFactory` and
45
- `hoistCmp.withContainerFactory` are also removed in favor of the basic `hoistCmp.factory` and
72
+ Applications should simply continue to use `ElementFactory` instead. `hoistCmp.containerFactory`
73
+ and `hoistCmp.withContainerFactory` are also removed in favor of the basic `hoistCmp.factory` and
46
74
  `hoistCmp.withFactory` respectively.
47
-
48
75
  * `HoistProps.modelConfig` now references the type declaration of `HoistModel.config`. See
49
76
  `PanelModel` and `TabContainerModel` for examples.
50
-
51
77
  * The new `SelectOption` type has been made multi-platform and moved to `@xh/hoist/core`.
52
78
 
53
79
  **Note** that we do not intend to make such extensive Typescript changes going forward post-v55.0.
54
80
  These changes were deemed critical and worth adjusting in our first typescript update, and before
55
81
  typescript has been widely adopted in production Hoist apps.
56
82
 
57
-
58
83
  ### ⚙️ Technical
59
84
 
60
85
  * Hoist's `Icon` enumeration has been re-organized slightly to better separate icons that describe
@@ -7,27 +7,27 @@
7
7
  import {ColumnRenderer} from '@xh/hoist/cmp/grid';
8
8
  import {HoistInputProps} from '@xh/hoist/cmp/input';
9
9
  import {PlainObject} from '@xh/hoist/core';
10
+ import {FieldFilterOperator, parseFilter, View} from '@xh/hoist/data';
10
11
  import {
11
12
  BaseFilterFieldSpec,
12
13
  BaseFilterFieldSpecConfig
13
14
  } from '@xh/hoist/data/filter/BaseFilterFieldSpec';
14
- import {FieldFilterOperator, parseFilter, View} from '@xh/hoist/data';
15
15
  import {castArray, compact, flatten, isDate, isEmpty, uniqBy} from 'lodash';
16
16
  import {GridFilterModel} from './GridFilterModel';
17
17
 
18
-
19
18
  export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
20
- /** GridFilterModel instance which owns this fieldSpec. */
19
+ /** GridFilterModel instance owning this fieldSpec. */
21
20
  filterModel?: GridFilterModel;
21
+
22
22
  /**
23
- * function returning a formatted string for each value in this values filter display.
23
+ * Function returning a formatted string for each value in this values filter display.
24
24
  * If not provided, the Column's renderer will be used.
25
25
  */
26
26
  renderer?: ColumnRenderer;
27
27
 
28
28
  /**
29
- * Props to pass through to the HoistInput components used on the custom filter tab. Note
30
- * that the HoistInput component used is decided by fieldType.
29
+ * Props to pass through to the HoistInput components used on the custom filter tab.
30
+ * Note that the HoistInput component used is decided by fieldType.
31
31
  */
32
32
  inputProps?: HoistInputProps;
33
33
 
@@ -36,8 +36,8 @@ export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
36
36
  }
37
37
 
38
38
  /**
39
- * Apps should NOT instantiate this class directly. Provide a config for this object
40
- * to the GridModel's `filterModel` property instead.
39
+ * Apps should NOT instantiate this class directly.
40
+ * Instead, provide a config for this object to the GridModel's `filterModel` config.
41
41
  */
42
42
  export class GridFilterFieldSpec extends BaseFilterFieldSpec {
43
43
 
@@ -53,7 +53,7 @@ export class GridFilterFieldSpec extends BaseFilterFieldSpec {
53
53
  inputProps,
54
54
  defaultOp,
55
55
  ...rest
56
- }:GridFilterFieldSpecConfig) {
56
+ }: GridFilterFieldSpecConfig) {
57
57
  super(rest);
58
58
 
59
59
  this.filterModel = filterModel;
@@ -106,9 +106,7 @@ export class GridFilterFieldSpec extends BaseFilterFieldSpec {
106
106
  this.valueCount = allValues.length;
107
107
  }
108
108
 
109
- /**
110
- * Recursively modify a Filter|CompoundFilter to remove all FieldFilters that reference this column
111
- */
109
+ // Recursively modify a Filter|CompoundFilter to remove all FieldFilters referencing this column
112
110
  cleanFilter(filter) {
113
111
  if (!filter) return filter;
114
112
 
@@ -5,15 +5,22 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {GridFilterModelConfig} from '@xh/hoist/cmp/grid';
8
+ import {GridFilterFieldSpec, GridFilterModelConfig} from '@xh/hoist/cmp/grid';
9
9
  import {HoistModel, managed} from '@xh/hoist/core';
10
- import {action, bindable, observable, makeObservable} from '@xh/hoist/mobx';
11
- import {CompoundFilter, FieldFilter, Filter,
12
- FilterLike, flattenFilter, Store, View, withFilterByField, withFilterByTypes} from '@xh/hoist/data';
10
+ import {
11
+ CompoundFilter,
12
+ FieldFilter,
13
+ Filter,
14
+ FilterLike,
15
+ flattenFilter,
16
+ Store,
17
+ View,
18
+ withFilterByField,
19
+ withFilterByTypes
20
+ } from '@xh/hoist/data';
21
+ import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
13
22
  import {wait} from '@xh/hoist/promise';
14
- import {find, isString, isNil, castArray, uniq, every, compact} from 'lodash';
15
-
16
- import {GridFilterFieldSpec} from './GridFilterFieldSpec';
23
+ import {castArray, compact, every, find, isNil, isString, uniq} from 'lodash';
17
24
  import {GridModel} from '../GridModel';
18
25
 
19
26
 
@@ -174,4 +181,4 @@ export class GridFilterModel extends HoistModel {
174
181
  const results = compact(filter.filters.map(it => this.getOuterCompoundFilter(it, field)));
175
182
  return results.length === 1 ? results[0] : null;
176
183
  }
177
- }
184
+ }
@@ -90,7 +90,7 @@ export class TabModel extends HoistModel {
90
90
  @bindable.ref icon: ReactElement;
91
91
  @bindable.ref tooltip: ReactNode;
92
92
  @observable disabled: boolean;
93
- excludeFromSwitcher: boolean;
93
+ @bindable excludeFromSwitcher: boolean;
94
94
  showRemoveAction: boolean;
95
95
  content: Content;
96
96
 
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistBase, managed, LoadSupport, LoadSpec, Loadable} from './';
7
+ import {HoistBase, managed, LoadSupport, LoadSpec, Loadable, PlainObject} from './';
8
8
 
9
9
  /**
10
10
  * Core superclass for Services in Hoist. Services are special classes used in both Hoist and
@@ -64,12 +64,12 @@ export class HoistService extends HoistBase implements Loadable {
64
64
  @managed
65
65
  loadSupport: LoadSupport;
66
66
 
67
- get loadModel() {return this.loadSupport?.loadModel}
68
- get lastLoadRequested() {return this.loadSupport?.lastLoadRequested}
69
- get lastLoadCompleted() {return this.loadSupport?.lastLoadCompleted}
70
- get lastLoadException() {return this.loadSupport?.lastLoadException}
71
- async refreshAsync(meta?: object) {return this.loadSupport?.refreshAsync(meta)}
72
- async autoRefreshAsync(meta?: object) {return this.loadSupport?.autoRefreshAsync(meta)}
67
+ get loadModel() {return this.loadSupport?.loadModel}
68
+ get lastLoadRequested() {return this.loadSupport?.lastLoadRequested}
69
+ get lastLoadCompleted() {return this.loadSupport?.lastLoadCompleted}
70
+ get lastLoadException() {return this.loadSupport?.lastLoadException}
71
+ async refreshAsync(meta?: PlainObject) {return this.loadSupport?.refreshAsync(meta)}
72
+ async autoRefreshAsync(meta?: PlainObject) {return this.loadSupport?.autoRefreshAsync(meta)}
73
73
  async doLoadAsync(loadSpec: LoadSpec) {}
74
74
  async loadAsync(loadSpec?: LoadSpec|Partial<LoadSpec>) {
75
75
  return this.loadSupport?.loadAsync(loadSpec);
@@ -37,10 +37,9 @@ export interface ExceptionHandlerOptions {
37
37
  showAlert?: boolean;
38
38
 
39
39
  /**
40
- * If `showAlert`, which type of alert to display.
41
- * Valid options are 'dialog'|'toast'. Defaults to ExceptionHandler.ALERT_TYPE.
40
+ * If `showAlert`, which type of alert to display. Defaults to ExceptionHandler.ALERT_TYPE.
42
41
  */
43
- alertType?: string;
42
+ alertType?: 'dialog'|'toast';
44
43
 
45
44
  /**
46
45
  * Force user to fully refresh the app in order to dismiss - default false, excepting
@@ -80,9 +79,8 @@ export class ExceptionHandler {
80
79
 
81
80
  /**
82
81
  * Default type of alert to use to display exceptions with `showAlert`.
83
- * Valid options are 'dialog'|'toast'.
84
82
  */
85
- static ALERT_TYPE: string = 'dialog';
83
+ static ALERT_TYPE: 'dialog'|'toast' = 'dialog';
86
84
 
87
85
  /**
88
86
  * Default props provided to toast, when alert type is 'toast'
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import {LoadSupport} from './';
9
+ import {PlainObject} from '../types/Types';
9
10
 
10
11
  /**
11
12
  * Object describing a load/refresh request in Hoist.
@@ -46,7 +47,7 @@ export class LoadSpec {
46
47
  owner: LoadSupport;
47
48
 
48
49
  /** Application specific information about the load request */
49
- meta: object;
50
+ meta: PlainObject;
50
51
 
51
52
  /** True if a more recent request to load this object's owner has *started*. */
52
53
  get isStale(): boolean {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {forOwn, has, isFunction} from 'lodash';
8
8
  import {warnIf} from '@xh/hoist/utils/js';
9
- import {DefaultHoistProps, HoistBase, managed} from '../';
9
+ import {DefaultHoistProps, HoistBase, managed, PlainObject} from '../';
10
10
  import {ModelSelector} from './';
11
11
  import {LoadSupport, LoadSpec, Loadable} from '../load';
12
12
  import {observable, action, makeObservable} from '@xh/hoist/mobx';
@@ -94,12 +94,12 @@ export abstract class HoistModel extends HoistBase implements Loadable {
94
94
  @managed
95
95
  loadSupport: LoadSupport;
96
96
 
97
- get loadModel() {return this.loadSupport?.loadModel}
98
- get lastLoadRequested() {return this.loadSupport?.lastLoadRequested}
99
- get lastLoadCompleted() {return this.loadSupport?.lastLoadCompleted}
100
- get lastLoadException() {return this.loadSupport?.lastLoadException}
101
- async refreshAsync(meta?: object) {return this.loadSupport?.refreshAsync(meta)}
102
- async autoRefreshAsync(meta?: object) {return this.loadSupport?.autoRefreshAsync(meta)}
97
+ get loadModel() {return this.loadSupport?.loadModel}
98
+ get lastLoadRequested() {return this.loadSupport?.lastLoadRequested}
99
+ get lastLoadCompleted() {return this.loadSupport?.lastLoadCompleted}
100
+ get lastLoadException() {return this.loadSupport?.lastLoadException}
101
+ async refreshAsync(meta?: PlainObject) {return this.loadSupport?.refreshAsync(meta)}
102
+ async autoRefreshAsync(meta?: PlainObject) {return this.loadSupport?.autoRefreshAsync(meta)}
103
103
  async doLoadAsync(loadSpec: LoadSpec) {}
104
104
  async loadAsync(loadSpec?: LoadSpec|Partial<LoadSpec>) {
105
105
  return this.loadSupport?.loadAsync(loadSpec);
package/data/cube/View.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  Cube,
11
11
  CubeField,
12
12
  Filter,
13
+ FilterLike,
13
14
  Query,
14
15
  QueryConfig,
15
16
  Store,
@@ -17,15 +18,14 @@ import {
17
18
  StoreRecordId
18
19
  } from '@xh/hoist/data';
19
20
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
20
- import {throwIf, logWithDebug} from '@xh/hoist/utils/js';
21
21
  import {shallowEqualArrays} from '@xh/hoist/utils/impl';
22
- import {castArray, forEach, groupBy, isEmpty, isNil, map, find} from 'lodash';
22
+ import {logWithDebug, throwIf} from '@xh/hoist/utils/js';
23
+ import {castArray, find, forEach, groupBy, isEmpty, isNil, map} from 'lodash';
23
24
  import {AggregationContext} from './aggregate/AggregationContext';
24
-
25
25
  import {AggregateRow} from './row/AggregateRow';
26
+ import {BaseRow} from './row/BaseRow';
26
27
  import {BucketRow} from './row/BucketRow';
27
28
  import {LeafRow} from './row/LeafRow';
28
- import {BaseRow} from './row/BaseRow';
29
29
 
30
30
  export interface ViewConfig {
31
31
  /** Query to be used to construct this view. */
@@ -177,7 +177,7 @@ export class View extends HoistBase {
177
177
  }
178
178
 
179
179
  /** Update the filter on the current Query.*/
180
- setFilter(filter: Filter) {
180
+ setFilter(filter: FilterLike) {
181
181
  this.updateQuery({filter});
182
182
  }
183
183
 
@@ -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),
@@ -159,6 +159,7 @@ export class PanelModel extends HoistModel {
159
159
  // Implementation
160
160
  //-----------------
161
161
  _resizeRef;
162
+ splitterRef = createRef<HTMLDivElement>();
162
163
 
163
164
  constructor({
164
165
  collapsible = true,
@@ -30,7 +30,8 @@ export const splitter = hoistCmp.factory({
30
30
  icon: Icon[chevron](),
31
31
  onClick: () => panelModel.toggleCollapsed(),
32
32
  omit: !showSplitterCollapseButton || !collapsible
33
- })
33
+ }),
34
+ ref: panelModel.splitterRef
34
35
  };
35
36
 
36
37
  return cmp(cfg);
@@ -155,8 +155,8 @@ export class DraggerModel extends HoistModel {
155
155
 
156
156
  private getDraggableSplitter() {
157
157
  // clone .xh-resizable-splitter to get its styling
158
- const splitter = this.panelEl.querySelector('.xh-resizable-splitter'),
159
- ret = splitter.cloneNode();
158
+ const splitter = this.panelModel.splitterRef.current,
159
+ ret = splitter.cloneNode() as HTMLDivElement;
160
160
 
161
161
  ret.style.position = 'absolute';
162
162
  ret.style.display = 'none'; // display = none needed to prevent flash
@@ -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.1",
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",