@xh/hoist 79.0.0-SNAPSHOT.1765207715965 → 79.0.0-SNAPSHOT.1765213676051

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.
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import type {DragOverEvent, Layout} from 'react-grid-layout';
7
8
  import {Persistable, PersistableState, PersistenceProvider, XH} from '@xh/hoist/core';
8
9
  import {required} from '@xh/hoist/data';
9
10
  import {DashCanvasViewModel, DashCanvasViewSpec, DashConfig, DashViewState, DashModel} from '../';
@@ -16,6 +17,7 @@ import {createObservableRef} from '@xh/hoist/utils/react';
16
17
  import {
17
18
  defaultsDeep,
18
19
  find,
20
+ omit,
19
21
  uniqBy,
20
22
  times,
21
23
  without,
@@ -50,6 +52,31 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
50
52
 
51
53
  /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
52
54
  containerPadding?: [number, number];
55
+
56
+ /**
57
+ * Whether the canvas should accept drag-and-drop of views from outside
58
+ * the canvas. Default false.
59
+ */
60
+ droppable?: boolean;
61
+
62
+ /**
63
+ * Optional Callback to invoke after a view is successfully dropped onto the canvas.
64
+ */
65
+ onDropDone?: (viewModel: DashCanvasViewModel) => void;
66
+
67
+ /**
68
+ * Optional callback to invoke when an item is dragged over the canvas. This may be used to
69
+ * customize how the size of the dropping placeholder is calculated. The callback should
70
+ * return an object with optional 'w' and 'h' properties indicating the desired width and height
71
+ * (in grid units) of the dropping placeholder. If not provided, Hoist's own default logic will be used.
72
+ */
73
+ onDropDragOver?: (e: DragOverEvent) => {w?: number; h?: number} | false | undefined;
74
+
75
+ /**
76
+ * Whether an overlay with an Add View button should be rendered
77
+ * when the canvas is empty. Default true.
78
+ */
79
+ showAddViewButtonWhenEmpty?: boolean;
53
80
  }
54
81
 
55
82
  export interface DashCanvasItemState {
@@ -86,7 +113,12 @@ export class DashCanvasModel
86
113
  //-----------------------------
87
114
  // Public properties
88
115
  //-----------------------------
116
+ DROPPING_ELEM_ID = '__dropping-elem__';
89
117
  maxRows: number;
118
+ showAddViewButtonWhenEmpty: boolean;
119
+ droppable: boolean;
120
+ onDropDone: (viewModel: DashCanvasViewModel) => void;
121
+ draggedInView: DashCanvasItemState;
90
122
 
91
123
  /** Current number of rows in canvas */
92
124
  get rows(): number {
@@ -106,21 +138,27 @@ export class DashCanvasModel
106
138
  private isLoadingState: boolean;
107
139
 
108
140
  get rglLayout() {
109
- return this.layout.map(it => {
110
- const dashCanvasView = this.getView(it.i),
111
- {autoHeight, viewSpec} = dashCanvasView;
112
-
113
- return {
114
- ...it,
115
- resizeHandles: autoHeight
116
- ? ['w', 'e']
117
- : ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
118
- maxH: viewSpec.maxHeight,
119
- minH: viewSpec.minHeight,
120
- maxW: viewSpec.maxWidth,
121
- minW: viewSpec.minWidth
122
- };
123
- });
141
+ return this.layout
142
+ .map(it => {
143
+ const dashCanvasView = this.getView(it.i);
144
+
145
+ // `dashCanvasView` will not be found if `it` is a dropping element.
146
+ if (!dashCanvasView) return null;
147
+
148
+ const {autoHeight, viewSpec} = dashCanvasView;
149
+
150
+ return {
151
+ ...it,
152
+ resizeHandles: autoHeight
153
+ ? ['w', 'e']
154
+ : ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
155
+ maxH: viewSpec.maxHeight,
156
+ minH: viewSpec.minHeight,
157
+ maxW: viewSpec.maxWidth,
158
+ minW: viewSpec.minWidth
159
+ };
160
+ })
161
+ .filter(Boolean);
124
162
  }
125
163
 
126
164
  constructor({
@@ -139,7 +177,11 @@ export class DashCanvasModel
139
177
  margin = [10, 10],
140
178
  maxRows = Infinity,
141
179
  containerPadding = margin,
142
- extraMenuItems
180
+ extraMenuItems,
181
+ showAddViewButtonWhenEmpty = true,
182
+ droppable = false,
183
+ onDropDone,
184
+ onDropDragOver
143
185
  }: DashCanvasConfig) {
144
186
  super();
145
187
  makeObservable(this);
@@ -187,6 +229,11 @@ export class DashCanvasModel
187
229
  this.emptyText = emptyText;
188
230
  this.addViewButtonText = addViewButtonText;
189
231
  this.extraMenuItems = extraMenuItems;
232
+ this.showAddViewButtonWhenEmpty = showAddViewButtonWhenEmpty;
233
+ this.droppable = droppable;
234
+ this.onDropDone = onDropDone;
235
+ // Override default onDropDragOver if provided
236
+ if (onDropDragOver) this.onDropDragOver = onDropDragOver;
190
237
 
191
238
  this.loadState(initialState);
192
239
  this.state = this.buildState();
@@ -312,6 +359,31 @@ export class DashCanvasModel
312
359
  this.getView(id)?.ensureVisible();
313
360
  }
314
361
 
362
+ onDrop(rglLayout: Layout[], layoutItem: Layout, evt: Event) {
363
+ throwIf(
364
+ !this.draggedInView,
365
+ `No draggedInView set on DashCanvasModel prior to onDrop operation.
366
+ Typically a developer would set this in response to dragstart events from
367
+ a DashViewTray or similar component.`
368
+ );
369
+
370
+ const {viewSpecId, title, state} = this.draggedInView,
371
+ layout = omit(layoutItem, 'i'),
372
+ newViewModel = this.doDrop(viewSpecId, {title, state, layout}, rglLayout);
373
+
374
+ this.draggedInView = null;
375
+ this.onDropDone?.(newViewModel);
376
+ }
377
+
378
+ setDraggedInView(view?: DashCanvasItemState) {
379
+ this.draggedInView = view;
380
+ }
381
+
382
+ onDropDragOver(e: DragOverEvent): {w?: number; h?: number} | false | undefined {
383
+ if (!this.draggedInView) return false;
384
+ return {w: 6, h: 6};
385
+ }
386
+
315
387
  //------------------------
316
388
  // Persistable Interface
317
389
  //------------------------
@@ -327,6 +399,24 @@ export class DashCanvasModel
327
399
  //------------------------
328
400
  // Implementation
329
401
  //------------------------
402
+ @action
403
+ private doDrop(
404
+ specId: string,
405
+ opts: {
406
+ title: string;
407
+ state: any;
408
+ layout: DashCanvasItemLayout;
409
+ },
410
+ rglLayout: Layout[]
411
+ ): DashCanvasViewModel {
412
+ const newViewModel: DashCanvasViewModel = this.addViewInternal(specId, opts),
413
+ droppingItem: any = rglLayout.find(it => it.i === this.DROPPING_ELEM_ID);
414
+
415
+ droppingItem.i = newViewModel.id;
416
+ this.onRglLayoutChange(rglLayout);
417
+ return newViewModel;
418
+ }
419
+
330
420
  private getLayoutFromPosition(position: string, specId: string) {
331
421
  switch (position) {
332
422
  case 'first':
@@ -384,8 +474,14 @@ export class DashCanvasModel
384
474
  return model;
385
475
  }
386
476
 
387
- onRglLayoutChange(rglLayout) {
477
+ onRglLayoutChange(rglLayout: Layout[]) {
388
478
  rglLayout = rglLayout.map(it => pick(it, ['i', 'x', 'y', 'w', 'h']));
479
+
480
+ // Early out if RGL is changing layout as user is dragging droppable
481
+ // item around the canvas. This will be called again once dragging
482
+ // has stopped and user has dropped the item onto the canvas.
483
+ if (rglLayout.some(it => it.i === this.DROPPING_ELEM_ID)) return;
484
+
389
485
  this.setLayout(rglLayout);
390
486
  }
391
487
 
@@ -4,7 +4,6 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import composeRefs from '@seznam/compose-react-refs/composeRefs';
8
7
  import {GridModel} from '@xh/hoist/cmp/grid';
9
8
  import {hbox, span, vbox} from '@xh/hoist/cmp/layout';
10
9
  import {hoistCmp, LayoutProps, useLocalModel} from '@xh/hoist/core';
@@ -61,7 +60,7 @@ export interface GridFindFieldProps extends TextInputProps, LayoutProps {
61
60
  export const [GridFindField, gridFindField] = hoistCmp.withFactory<GridFindFieldProps>({
62
61
  displayName: 'GridFindField',
63
62
  className: 'xh-grid-find-field',
64
- render({className, model, ...props}, ref) {
63
+ render({className, model, ...props}) {
65
64
  let [layoutProps, restProps] = splitLayoutProps(props);
66
65
  const impl = useLocalModel(GridFindFieldImplModel);
67
66
 
@@ -73,7 +72,7 @@ export const [GridFindField, gridFindField] = hoistCmp.withFactory<GridFindField
73
72
  textInput({
74
73
  model: impl,
75
74
  bind: 'query',
76
- ref: composeRefs(impl.inputRef, ref),
75
+ ref: impl.inputRef,
77
76
  commitOnChange: true,
78
77
  leftIcon: Icon.search(),
79
78
  enableClear: true,
@@ -8,7 +8,6 @@ import {GridModel} from '@xh/hoist/cmp/grid';
8
8
  import {HoistModel, managed} from '@xh/hoist/core';
9
9
  import {LeftRightChooserModel} from '@xh/hoist/desktop/cmp/leftrightchooser';
10
10
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
11
- import {sortBy} from 'lodash';
12
11
 
13
12
  /**
14
13
  * State management for the ColChooser component.
@@ -56,8 +55,6 @@ export class ColChooserModel extends HoistModel {
56
55
  rightTitle: 'Displayed Columns',
57
56
  rightEmptyText: 'No columns will be visible.',
58
57
  leftSorted: true,
59
- leftSortBy: 'text',
60
- rightSorted: true,
61
58
  rightGroupingEnabled: false,
62
59
  onChange: () => {
63
60
  if (this.commitOnChange) this.commit();
@@ -98,7 +95,7 @@ export class ColChooserModel extends HoistModel {
98
95
  }
99
96
  });
100
97
 
101
- gridModel.updateColumnState(colChanges);
98
+ gridModel.applyColumnStateChanges(colChanges);
102
99
  if (autosizeOnCommit && colChanges.length) gridModel.autosizeAsync({showMask: true});
103
100
  }
104
101
 
@@ -114,33 +111,19 @@ export class ColChooserModel extends HoistModel {
114
111
  //------------------------
115
112
  syncChooserData() {
116
113
  const {gridModel, lrModel} = this,
117
- hasGrouping = gridModel.getLeafColumns().some(it => it.chooserGroup),
118
- columnState = sortBy(gridModel.columnState, it => {
119
- const {pinned} = it;
120
- if (pinned === 'left') {
121
- return 0;
122
- }
123
-
124
- if (pinned === 'right') {
125
- return 2;
126
- }
127
-
128
- return 1;
129
- });
130
-
131
- const data = columnState.map((it, idx) => {
132
- const visible = !it.hidden,
133
- col = gridModel.getColumn(it.colId);
114
+ columns = gridModel.getLeafColumns(),
115
+ hasGrouping = columns.some(it => it.chooserGroup);
134
116
 
117
+ const data = columns.map(it => {
118
+ const visible = gridModel.isColumnVisible(it.colId);
135
119
  return {
136
120
  value: it.colId,
137
- text: col.chooserName,
138
- description: col.chooserDescription,
139
- group: hasGrouping ? (col.chooserGroup ?? 'Ungrouped') : null,
140
- exclude: col.excludeFromChooser,
141
- locked: visible && !col.hideable,
142
- side: visible ? 'right' : 'left',
143
- sortValue: idx
121
+ text: it.chooserName,
122
+ description: it.chooserDescription,
123
+ group: hasGrouping ? (it.chooserGroup ?? 'Ungrouped') : null,
124
+ exclude: it.excludeFromChooser,
125
+ locked: visible && !it.hideable,
126
+ side: visible ? 'right' : 'left'
144
127
  } as const;
145
128
  });
146
129
 
@@ -26,10 +26,10 @@ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
26
26
  import {Icon} from '@xh/hoist/icon';
27
27
  import {menu, menuItem, popover} from '@xh/hoist/kit/blueprint';
28
28
  import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd';
29
- import {elemWithin, getTestId} from '@xh/hoist/utils/js';
29
+ import {apiDeprecated, elemWithin, getTestId} from '@xh/hoist/utils/js';
30
30
  import {splitLayoutProps} from '@xh/hoist/utils/react';
31
31
  import classNames from 'classnames';
32
- import {isEmpty, isNil} from 'lodash';
32
+ import {isEmpty, isNil, isUndefined} from 'lodash';
33
33
  import './GroupingChooser.scss';
34
34
  import {ReactNode} from 'react';
35
35
 
@@ -55,6 +55,9 @@ export interface GroupingChooserProps extends ButtonProps<GroupingChooserModel>
55
55
  /** Position of popover relative to target button. */
56
56
  popoverPosition?: 'bottom' | 'top';
57
57
 
58
+ /** @deprecated - use `editorTitle` instead */
59
+ popoverTitle?: ReactNode;
60
+
58
61
  /**
59
62
  * Width in pixels of the popover menu itself.
60
63
  * If unspecified, will default based on favorites enabled status + side.
@@ -86,6 +89,7 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
86
89
  favoritesTitle = 'Favorites',
87
90
  popoverWidth,
88
91
  popoverMinHeight,
92
+ popoverTitle,
89
93
  popoverPosition = 'bottom',
90
94
  styleButtonAsInput = true,
91
95
  testId,
@@ -100,6 +104,15 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
100
104
  favesClassNameMod = `faves-${persistFavorites ? favoritesSide : 'disabled'}`,
101
105
  favesTB = isTB(favoritesSide);
102
106
 
107
+ if (!isUndefined(popoverTitle)) {
108
+ apiDeprecated('GroupingChooser.popoverTitle', {
109
+ msg: `Update to use 'editorTitle' instead`,
110
+ v: `v78`,
111
+ source: GroupingChooser
112
+ });
113
+ editorTitle = popoverTitle;
114
+ }
115
+
103
116
  popoverWidth = popoverWidth || (persistFavorites && !favesTB ? 500 : 250);
104
117
 
105
118
  return box({
@@ -4,9 +4,9 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
- import {GridModel, GridSorterLike} from '@xh/hoist/cmp/grid';
7
+ import {GridModel} from '@xh/hoist/cmp/grid';
8
8
  import {div} from '@xh/hoist/cmp/layout';
9
- import {HoistModel, HSide, managed, Some, XH} from '@xh/hoist/core';
9
+ import {HoistModel, HSide, managed, XH} from '@xh/hoist/core';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {Icon} from '@xh/hoist/icon';
12
12
  import {bindable, computed, makeObservable} from '@xh/hoist/mobx';
@@ -29,14 +29,12 @@ export interface LeftRightChooserConfig {
29
29
 
30
30
  leftTitle?: string;
31
31
  leftSorted?: boolean;
32
- leftSortBy?: Some<GridSorterLike>;
33
32
  leftGroupingEnabled?: boolean;
34
33
  leftGroupingExpanded?: boolean;
35
34
  leftEmptyText?: string;
36
35
 
37
36
  rightTitle?: string;
38
37
  rightSorted?: boolean;
39
- rightSortBy?: Some<GridSorterLike>;
40
38
  rightGroupingEnabled?: boolean;
41
39
  rightGroupingExpanded?: boolean;
42
40
  rightEmptyText?: string;
@@ -66,9 +64,6 @@ export interface LeftRightChooserItem {
66
64
 
67
65
  /* True to exclude the item from the chooser entirely. */
68
66
  exclude?: boolean;
69
-
70
- /* Value to use for sorting. If unset then sort order will be based solely on the text value. */
71
- sortValue?: any;
72
67
  }
73
68
 
74
69
  /**
@@ -127,14 +122,12 @@ export class LeftRightChooserModel extends HoistModel {
127
122
  ungroupedName = 'Ungrouped',
128
123
  leftTitle = 'Available',
129
124
  leftSorted = false,
130
- leftSortBy = ['sortValue', 'text'],
131
125
  leftGroupingEnabled = true,
132
126
  leftGroupingExpanded = true,
133
127
  leftEmptyText = null,
134
128
  readonly = false,
135
129
  rightTitle = 'Selected',
136
130
  rightSorted = false,
137
- rightSortBy = ['sortValue', 'text'],
138
131
  rightGroupingEnabled = true,
139
132
  rightGroupingExpanded = true,
140
133
  rightEmptyText = null,
@@ -161,8 +154,7 @@ export class LeftRightChooserModel extends HoistModel {
161
154
  {name: 'group', type: 'string'},
162
155
  {name: 'side', type: 'string'},
163
156
  {name: 'locked', type: 'bool'},
164
- {name: 'exclude', type: 'bool'},
165
- {name: 'sortValue'}
157
+ {name: 'exclude', type: 'bool'}
166
158
  ]
167
159
  };
168
160
 
@@ -187,19 +179,15 @@ export class LeftRightChooserModel extends HoistModel {
187
179
  field: 'group',
188
180
  headerName: 'Group',
189
181
  hidden: true
190
- },
191
- sortValueCol = {
192
- field: 'sortValue',
193
- hidden: true
194
182
  };
195
183
 
196
184
  this.leftModel = new GridModel({
197
185
  store,
198
186
  selModel: 'multiple',
199
- sortBy: leftSorted ? leftSortBy : null,
187
+ sortBy: leftSorted ? 'text' : null,
200
188
  emptyText: leftEmptyText,
201
189
  onRowDoubleClicked: e => this.onRowDoubleClicked(e),
202
- columns: [leftTextCol, groupCol, sortValueCol],
190
+ columns: [leftTextCol, groupCol],
203
191
  contextMenu: false,
204
192
  expandLevel: leftGroupingExpanded ? 1 : 0,
205
193
  xhImpl: true
@@ -208,10 +196,10 @@ export class LeftRightChooserModel extends HoistModel {
208
196
  this.rightModel = new GridModel({
209
197
  store,
210
198
  selModel: 'multiple',
211
- sortBy: rightSorted ? rightSortBy : null,
199
+ sortBy: rightSorted ? 'text' : null,
212
200
  emptyText: rightEmptyText,
213
201
  onRowDoubleClicked: e => this.onRowDoubleClicked(e),
214
- columns: [rightTextCol, groupCol, sortValueCol],
202
+ columns: [rightTextCol, groupCol],
215
203
  contextMenu: false,
216
204
  expandLevel: rightGroupingExpanded ? 1 : 0,
217
205
  xhImpl: true
@@ -132,7 +132,7 @@ export class ColChooserModel extends HoistModel {
132
132
  return {colId, hidden, pinned};
133
133
  });
134
134
 
135
- gridModel.updateColumnState(colChanges);
135
+ gridModel.applyColumnStateChanges(colChanges);
136
136
  if (autosizeOnCommit) gridModel.autosizeAsync({showMask: true});
137
137
  }
138
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "79.0.0-SNAPSHOT.1765207715965",
3
+ "version": "79.0.0-SNAPSHOT.1765213676051",
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",
@@ -79,7 +79,7 @@ export class GridAutosizeService extends HoistService {
79
79
 
80
80
  runInAction(() => {
81
81
  // Apply calculated widths to grid.
82
- gridModel.updateColumnState(requiredWidths);
82
+ gridModel.applyColumnStateChanges(requiredWidths);
83
83
  this.logDebug(
84
84
  `Auto-sized ${requiredWidths.length} columns`,
85
85
  `${records.length} records`
@@ -94,7 +94,7 @@ export class GridAutosizeService extends HoistService {
94
94
  fillMode,
95
95
  asManuallySized
96
96
  );
97
- gridModel.updateColumnState(fillWidths);
97
+ gridModel.applyColumnStateChanges(fillWidths);
98
98
  this.logDebug(`Auto-sized ${fillWidths.length} columns using fillMode`);
99
99
  }
100
100
  });