@xh/hoist 79.0.0-SNAPSHOT.1764947672059 → 79.0.0-SNAPSHOT.1764952313480

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.
@@ -1,6 +1,6 @@
1
+ import { type ReactGridLayoutProps } from 'react-grid-layout';
1
2
  import { HoistProps, TestSupportProps } from '@xh/hoist/core';
2
3
  import '@xh/hoist/desktop/register';
3
- import type { ReactGridLayoutProps } from 'react-grid-layout';
4
4
  import { DashCanvasModel } from './DashCanvasModel';
5
5
  import 'react-grid-layout/css/styles.css';
6
6
  import './DashCanvas.scss';
@@ -1,3 +1,4 @@
1
+ import type { DragOverEvent, Layout } from 'react-grid-layout';
1
2
  import { Persistable, PersistableState } from '@xh/hoist/core';
2
3
  import { DashCanvasViewModel, DashCanvasViewSpec, DashConfig, DashViewState, DashModel } from '../';
3
4
  import '@xh/hoist/desktop/register';
@@ -20,6 +21,30 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
20
21
  maxRows?: number;
21
22
  /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
22
23
  containerPadding?: [number, number];
24
+ /**
25
+ * Whether the canvas should accept drag-and-drop of views from outside
26
+ * the canvas. Default false.
27
+ */
28
+ droppable?: boolean;
29
+ /**
30
+ * Optional Callback to invoke after a view is successfully dropped onto the canvas.
31
+ */
32
+ onDropDone?: (viewModel: DashCanvasViewModel) => void;
33
+ /**
34
+ * Optional callback to invoke when an item is dragged over the canvas. This may be used to
35
+ * customize how the size of the dropping placeholder is calculated. The callback should
36
+ * return an object with optional 'w' and 'h' properties indicating the desired width and height
37
+ * (in grid units) of the dropping placeholder. If not provided, Hoist's own default logic will be used.
38
+ */
39
+ onDropDragOver?: (e: DragOverEvent) => {
40
+ w?: number;
41
+ h?: number;
42
+ } | false | undefined;
43
+ /**
44
+ * Whether an overlay with an Add View button should be rendered
45
+ * when the canvas is empty. Default true.
46
+ */
47
+ showAddViewButtonWhenEmpty?: boolean;
23
48
  }
24
49
  export interface DashCanvasItemState {
25
50
  layout: DashCanvasItemLayout;
@@ -45,7 +70,12 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
45
70
  compact: boolean;
46
71
  margin: [number, number];
47
72
  containerPadding: [number, number];
73
+ DROPPING_ELEM_ID: string;
48
74
  maxRows: number;
75
+ showAddViewButtonWhenEmpty: boolean;
76
+ droppable: boolean;
77
+ onDropDone: (viewModel: DashCanvasViewModel) => void;
78
+ draggedInView: DashCanvasItemState;
49
79
  /** Current number of rows in canvas */
50
80
  get rows(): number;
51
81
  get isEmpty(): boolean;
@@ -54,7 +84,7 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
54
84
  isResizing: boolean;
55
85
  private isLoadingState;
56
86
  get rglLayout(): any[];
57
- constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems }: DashCanvasConfig);
87
+ constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems, showAddViewButtonWhenEmpty, droppable, onDropDone, onDropDragOver }: DashCanvasConfig);
58
88
  /** Removes all views from the canvas */
59
89
  clear(): void;
60
90
  /**
@@ -92,15 +122,22 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
92
122
  renameView(id: string): void;
93
123
  /** Scrolls a DashCanvasView into view. */
94
124
  ensureViewVisible(id: string): void;
125
+ onDrop(rglLayout: Layout[], layoutItem: Layout, evt: Event): void;
126
+ setDraggedInView(view?: DashCanvasItemState): void;
127
+ onDropDragOver(e: DragOverEvent): {
128
+ w?: number;
129
+ h?: number;
130
+ } | false | undefined;
95
131
  getPersistableState(): PersistableState<{
96
132
  state: DashCanvasItemState[];
97
133
  }>;
98
134
  setPersistableState(persistableState: PersistableState<{
99
135
  state: DashCanvasItemState[];
100
136
  }>): void;
137
+ private doDrop;
101
138
  private getLayoutFromPosition;
102
139
  private addViewInternal;
103
- onRglLayoutChange(rglLayout: any): void;
140
+ onRglLayoutChange(rglLayout: Layout[]): void;
104
141
  private setLayout;
105
142
  private loadState;
106
143
  private buildState;
@@ -4,6 +4,12 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import ReactGridLayout, {
8
+ type ReactGridLayoutProps,
9
+ type DragOverEvent,
10
+ type Layout,
11
+ WidthProvider
12
+ } from 'react-grid-layout';
7
13
  import {showContextMenu} from '@xh/hoist/kit/blueprint';
8
14
  import composeRefs from '@seznam/compose-react-refs';
9
15
  import {div, vbox, vspacer} from '@xh/hoist/cmp/layout';
@@ -20,8 +26,6 @@ import '@xh/hoist/desktop/register';
20
26
  import {Classes, overlay} from '@xh/hoist/kit/blueprint';
21
27
  import {consumeEvent, TEST_ID} from '@xh/hoist/utils/js';
22
28
  import classNames from 'classnames';
23
- import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
24
- import type {ReactGridLayoutProps} from 'react-grid-layout';
25
29
  import {DashCanvasModel} from './DashCanvasModel';
26
30
  import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu';
27
31
  import {dashCanvasView} from './impl/DashCanvasView';
@@ -87,7 +91,7 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
87
91
  draggableHandle:
88
92
  '.xh-dash-tab.xh-panel > .xh-panel__content > .xh-panel-header',
89
93
  draggableCancel: '.xh-button',
90
- onLayoutChange: layout => model.onRglLayoutChange(layout),
94
+ onLayoutChange: (layout: Layout[]) => model.onRglLayoutChange(layout),
91
95
  onResizeStart: () => (model.isResizing = true),
92
96
  onResizeStop: () => (model.isResizing = false),
93
97
  items: model.viewModels.map(vm =>
@@ -96,9 +100,13 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
96
100
  item: dashCanvasView({model: vm})
97
101
  })
98
102
  ),
103
+ isDroppable: model.droppable,
104
+ onDrop: (layout: Layout[], layoutItem: Layout, evt: Event) =>
105
+ model.onDrop(layout, layoutItem, evt),
106
+ onDropDragOver: (evt: DragOverEvent) => model.onDropDragOver(evt),
99
107
  ...rglOptions
100
108
  }),
101
- emptyContainerOverlay()
109
+ emptyContainerOverlay({omit: !model.showAddViewButtonWhenEmpty})
102
110
  ],
103
111
  [TEST_ID]: testId
104
112
  })
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "79.0.0-SNAPSHOT.1764947672059",
3
+ "version": "79.0.0-SNAPSHOT.1764952313480",
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",