@xh/hoist 79.0.0-SNAPSHOT.1766098199305 → 79.0.0-SNAPSHOT.1766100504431

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
@@ -20,6 +20,10 @@
20
20
  * `TabContainerConfig.switcher` has been repurposed to accept a `TabSwitcherConfig`. To pass
21
21
  `TabSwitcherProps` via a parent `TabContainer`, use `TabContainerProps.switcher`.
22
22
 
23
+ ### 🎁 New Features
24
+
25
+ * DashCanvas component now supports dragging and dropping widgets in from an external container.
26
+
23
27
  ### 🐞 Bug Fixes
24
28
 
25
29
  * Fixed column chooser to display columns in the same order as they appear in the grid.
@@ -34,6 +38,10 @@
34
38
  * `GroupingChooserProps.popoverTitle` - use `editorTitle`
35
39
  * `RelativeTimestampProps.options` - provide directly as top-level props
36
40
 
41
+ ### 📚 Libraries
42
+
43
+ * react-grid-layout `1.5.0 → 2.1.0`
44
+
37
45
  ## 78.1.4 - 2025-12-05
38
46
 
39
47
  ### 🐞 Bug Fixes
@@ -1,6 +1,6 @@
1
1
  import { TEST_ID } from '@xh/hoist/utils/js';
2
2
  import { ComponentType, JSX, Key, ReactElement, ReactNode } from 'react';
3
- import { PlainObject, Some, Thunkable } from './types/Types';
3
+ import { PlainObject, Thunkable } from './types/Types';
4
4
  /**
5
5
  * Alternative format for specifying React Elements in render functions. This type is designed to
6
6
  * provide a well-formatted, declarative, native javascript approach to configuring Elements and
@@ -26,9 +26,9 @@ import { PlainObject, Some, Thunkable } from './types/Types';
26
26
  */
27
27
  export type ElementSpec<P> = Omit<P, 'items' | 'item' | 'omit'> & {
28
28
  /** Child Element(s). Equivalent provided as Rest Arguments to React.createElement.*/
29
- items?: Some<ReactNode>;
29
+ items?: ReactNode;
30
30
  /** Equivalent to `items`, offered for code clarity when only one child is needed. */
31
- item?: Some<ReactNode>;
31
+ item?: ReactNode;
32
32
  /** True to exclude the Element. */
33
33
  omit?: Thunkable<boolean>;
34
34
  /** React key for this component. */
@@ -1,8 +1,9 @@
1
+ import { type GridLayoutProps } 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
+ import 'react-resizable/css/styles.css';
6
7
  import './DashCanvas.scss';
7
8
  export interface DashCanvasProps extends HoistProps<DashCanvasModel>, TestSupportProps {
8
9
  /**
@@ -11,7 +12,7 @@ export interface DashCanvasProps extends HoistProps<DashCanvasModel>, TestSuppor
11
12
  * {@link https://www.npmjs.com/package/react-grid-layout#grid-layout-props}
12
13
  * Note that some ReactGridLayout props are managed directly by DashCanvas and will be overridden if provided here.
13
14
  */
14
- rglOptions?: ReactGridLayoutProps;
15
+ rglOptions?: GridLayoutProps;
15
16
  }
16
17
  /**
17
18
  * Dashboard-style container that allows users to drag-and-drop child widgets into flexible layouts.
@@ -1,3 +1,5 @@
1
+ import { DragEvent } from 'react';
2
+ import type { LayoutItem } from 'react-grid-layout';
1
3
  import { Persistable, PersistableState } from '@xh/hoist/core';
2
4
  import { DashCanvasViewModel, DashCanvasViewSpec, DashConfig, DashViewState, DashModel } from '../';
3
5
  import '@xh/hoist/desktop/register';
@@ -18,8 +20,35 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
18
20
  margin?: [number, number];
19
21
  /** Maximum number of rows permitted for this container. Default `Infinity`. */
20
22
  maxRows?: number;
21
- /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
23
+ /** Padding inside the container [x, y] in pixels. Default `[0, 0]`. */
22
24
  containerPadding?: [number, number];
25
+ /**
26
+ * Whether the canvas should accept drag-and-drop of views from outside
27
+ * the canvas. Default false.
28
+ */
29
+ droppable?: boolean;
30
+ /**
31
+ * Optional Callback to invoke after a view is successfully dropped onto the canvas.
32
+ */
33
+ onDropDone?: (viewModel: DashCanvasViewModel) => void;
34
+ /**
35
+ * Optional callback to invoke when an item is dragged over the canvas. This may be used to
36
+ * customize how the size of the dropping placeholder is calculated. The callback should
37
+ * return an object with optional properties indicating the desired width, height (in grid units),
38
+ * and offset (in pixels) of the dropping placeholder.
39
+ * If not provided, Hoist's own default logic will be used.
40
+ */
41
+ onDropDragOver?: (e: DragEvent) => {
42
+ w?: number;
43
+ h?: number;
44
+ dragOffsetX?: number;
45
+ dragOffsetY?: number;
46
+ } | false | void;
47
+ /**
48
+ * Whether an overlay with an Add View button should be rendered
49
+ * when the canvas is empty. Default true.
50
+ */
51
+ showAddViewButtonWhenEmpty?: boolean;
23
52
  }
24
53
  export interface DashCanvasItemState {
25
54
  layout: DashCanvasItemLayout;
@@ -45,7 +74,12 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
45
74
  compact: boolean;
46
75
  margin: [number, number];
47
76
  containerPadding: [number, number];
77
+ showAddViewButtonWhenEmpty: boolean;
78
+ DROPPING_ELEM_ID: string;
48
79
  maxRows: number;
80
+ droppable: boolean;
81
+ onDropDone: (viewModel: DashCanvasViewModel) => void;
82
+ draggedInView: DashCanvasItemState;
49
83
  /** Current number of rows in canvas */
50
84
  get rows(): number;
51
85
  get isEmpty(): boolean;
@@ -54,7 +88,7 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
54
88
  isResizing: boolean;
55
89
  private isLoadingState;
56
90
  get rglLayout(): any[];
57
- constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems }: DashCanvasConfig);
91
+ constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems, showAddViewButtonWhenEmpty, droppable, onDropDone, onDropDragOver }: DashCanvasConfig);
58
92
  /** Removes all views from the canvas */
59
93
  clear(): void;
60
94
  /**
@@ -92,6 +126,14 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
92
126
  renameView(id: string): void;
93
127
  /** Scrolls a DashCanvasView into view. */
94
128
  ensureViewVisible(id: string): void;
129
+ onDrop(rglLayout: LayoutItem[], layoutItem: LayoutItem, evt: Event): void;
130
+ setDraggedInView(view?: DashCanvasItemState): void;
131
+ onDropDragOver(evt: DragEvent): {
132
+ w?: number;
133
+ h?: number;
134
+ dragOffsetX?: number;
135
+ dragOffsetY?: number;
136
+ } | false | void;
95
137
  getPersistableState(): PersistableState<{
96
138
  state: DashCanvasItemState[];
97
139
  }>;
@@ -100,7 +142,7 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
100
142
  }>): void;
101
143
  private getLayoutFromPosition;
102
144
  private addViewInternal;
103
- onRglLayoutChange(rglLayout: any): void;
145
+ onRglLayoutChange(rglLayout: LayoutItem[]): void;
104
146
  private setLayout;
105
147
  private loadState;
106
148
  private buildState;
@@ -42,12 +42,12 @@ export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'titl
42
42
  * A toolbar to be docked at the top of the panel.
43
43
  * If specified as an array, items will be passed as children to a Toolbar component.
44
44
  */
45
- tbar?: Some<ReactNode>;
45
+ tbar?: ReactNode;
46
46
  /**
47
47
  * A toolbar to be docked at the bottom of the panel.
48
48
  * If specified as an array, items will be passed as children to a Toolbar component.
49
49
  */
50
- bbar?: Some<ReactNode>;
50
+ bbar?: ReactNode;
51
51
  /** Title text added to the panel's header. */
52
52
  title?: ReactNode;
53
53
  /** Title to be used when the panel is collapsed. Defaults to `title`. */
@@ -1,4 +1,4 @@
1
- import { HoistProps, PlainObject, Some } from '@xh/hoist/core';
1
+ import { HoistProps, PlainObject } from '@xh/hoist/core';
2
2
  import { PanelProps } from '@xh/hoist/desktop/cmp/panel';
3
3
  import '@xh/hoist/desktop/register';
4
4
  import { ReactElement, ReactNode } from 'react';
@@ -14,7 +14,7 @@ export interface RestGridProps extends HoistProps<RestGridModel>, Omit<PanelProp
14
14
  * Optional components rendered adjacent to the top toolbar's action buttons.
15
15
  * See also {@link tbar} to take full control of the toolbar.
16
16
  */
17
- extraToolbarItems?: Some<ReactNode> | (() => Some<ReactNode>);
17
+ extraToolbarItems?: ReactNode | (() => ReactNode);
18
18
  /** Classname to be passed to RestForm. */
19
19
  formClassName?: string;
20
20
  /**
@@ -28,6 +28,6 @@ export interface RestGridProps extends HoistProps<RestGridModel>, Omit<PanelProp
28
28
  * configs `toolbarActions`, `filterFields`, and `showRefreshButton`. If specified as an array,
29
29
  * will be passed as children to a Toolbar component.
30
30
  */
31
- tbar?: Some<ReactNode>;
31
+ tbar?: ReactNode;
32
32
  }
33
33
  export declare const RestGrid: import("react").FC<RestGridProps>, restGrid: import("@xh/hoist/core").ElementFactory<RestGridProps>;
@@ -4,7 +4,7 @@ import { ReactNode, ReactElement } from 'react';
4
4
  import './Panel.scss';
5
5
  export interface PanelProps extends HoistProps, Omit<BoxProps, 'title'> {
6
6
  /** A toolbar to be docked at the bottom of the panel. */
7
- bbar?: Some<ReactNode>;
7
+ bbar?: ReactNode;
8
8
  /** CSS class name specific to the panel's header. */
9
9
  headerClassName?: string;
10
10
  /** Items to be added to the right-side of the panel's header. */
@@ -30,7 +30,7 @@ export interface PanelProps extends HoistProps, Omit<BoxProps, 'title'> {
30
30
  /** Allow the panel to scroll vertically */
31
31
  scrollable?: boolean;
32
32
  /** A toolbar to be docked at the top of the panel. */
33
- tbar?: Some<ReactNode>;
33
+ tbar?: ReactNode;
34
34
  /** Title text added to the panel's header. */
35
35
  title?: ReactNode;
36
36
  }
package/core/elem.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  ReactElement,
16
16
  ReactNode
17
17
  } from 'react';
18
- import {PlainObject, Some, Thunkable} from './types/Types';
18
+ import {PlainObject, Thunkable} from './types/Types';
19
19
 
20
20
  /**
21
21
  * Alternative format for specifying React Elements in render functions. This type is designed to
@@ -45,10 +45,10 @@ export type ElementSpec<P> = Omit<P, 'items' | 'item' | 'omit'> & {
45
45
  // Enhanced attributes to support element factory
46
46
  //---------------------------------------------
47
47
  /** Child Element(s). Equivalent provided as Rest Arguments to React.createElement.*/
48
- items?: Some<ReactNode>;
48
+ items?: ReactNode;
49
49
 
50
50
  /** Equivalent to `items`, offered for code clarity when only one child is needed. */
51
- item?: Some<ReactNode>;
51
+ item?: ReactNode;
52
52
 
53
53
  /** True to exclude the Element. */
54
54
  omit?: Thunkable<boolean>;
@@ -126,7 +126,7 @@ export function elementFactory<C extends ReactComponent>(component: C): ElementF
126
126
  export function elementFactory<P extends PlainObject>(component: ReactComponent): ElementFactory<P>;
127
127
  export function elementFactory(component: ReactComponent): ElementFactory {
128
128
  const ret = function (...args) {
129
- return createElement(component, normalizeArgs(args, component));
129
+ return createElement(component, normalizeArgs(args));
130
130
  };
131
131
  ret.isElementFactory = true;
132
132
  return ret;
@@ -135,7 +135,7 @@ export function elementFactory(component: ReactComponent): ElementFactory {
135
135
  //------------------------
136
136
  // Implementation
137
137
  //------------------------
138
- function normalizeArgs(args: any[], type: any) {
138
+ function normalizeArgs(args: any[]) {
139
139
  const len = args.length;
140
140
  if (len === 0) return {};
141
141
  if (len === 1) {
@@ -4,6 +4,16 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {omit} from 'lodash';
8
+ import {DragEvent} from 'react';
9
+ import ReactGridLayout, {
10
+ type LayoutItem,
11
+ type GridLayoutProps,
12
+ useContainerWidth,
13
+ noCompactor,
14
+ verticalCompactor
15
+ } from 'react-grid-layout';
16
+
7
17
  import {showContextMenu} from '@xh/hoist/kit/blueprint';
8
18
  import composeRefs from '@seznam/compose-react-refs';
9
19
  import {div, vbox, vspacer} from '@xh/hoist/cmp/layout';
@@ -20,13 +30,12 @@ import '@xh/hoist/desktop/register';
20
30
  import {Classes, overlay} from '@xh/hoist/kit/blueprint';
21
31
  import {consumeEvent, TEST_ID} from '@xh/hoist/utils/js';
22
32
  import classNames from 'classnames';
23
- import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
24
- import type {ReactGridLayoutProps} from 'react-grid-layout';
25
33
  import {DashCanvasModel} from './DashCanvasModel';
26
34
  import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu';
27
35
  import {dashCanvasView} from './impl/DashCanvasView';
28
36
 
29
37
  import 'react-grid-layout/css/styles.css';
38
+ import 'react-resizable/css/styles.css';
30
39
  import './DashCanvas.scss';
31
40
 
32
41
  export interface DashCanvasProps extends HoistProps<DashCanvasModel>, TestSupportProps {
@@ -36,7 +45,7 @@ export interface DashCanvasProps extends HoistProps<DashCanvasModel>, TestSuppor
36
45
  * {@link https://www.npmjs.com/package/react-grid-layout#grid-layout-props}
37
46
  * Note that some ReactGridLayout props are managed directly by DashCanvas and will be overridden if provided here.
38
47
  */
39
- rglOptions?: ReactGridLayoutProps;
48
+ rglOptions?: GridLayoutProps;
40
49
  }
41
50
 
42
51
  /**
@@ -58,7 +67,14 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
58
67
  render({className, model, rglOptions, testId}, ref) {
59
68
  const isDraggable = !model.layoutLocked,
60
69
  isResizable = !model.layoutLocked,
61
- [padX, padY] = model.containerPadding;
70
+ [padX, padY] = model.containerPadding,
71
+ topLevelRglOptions: Partial<GridLayoutProps> = omit(rglOptions ?? {}, [
72
+ 'gridConfig',
73
+ 'dragConfig',
74
+ 'resizeConfig',
75
+ 'dropConfig'
76
+ ]),
77
+ {width, containerRef, mounted} = useContainerWidth();
62
78
 
63
79
  return refreshContextView({
64
80
  model: model.refreshContextModel,
@@ -69,37 +85,55 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
69
85
  isResizable ? `${className}--resizable` : null
70
86
  ),
71
87
  style: {padding: `${padY}px ${padX}px`},
72
- ref: composeRefs(ref, model.ref),
88
+ ref: composeRefs(ref, model.ref, containerRef),
73
89
  onContextMenu: e => onContextMenu(e, model),
74
- items: [
75
- reactGridLayout({
76
- layout: model.rglLayout,
77
- cols: model.columns,
78
- rowHeight: model.rowHeight,
79
- isDraggable,
80
- isResizable,
81
- compactType: model.compact ? 'vertical' : null,
82
- margin: model.margin,
83
- maxRows: model.maxRows,
84
- containerPadding: [0, 0], // Workaround for https://github.com/react-grid-layout/react-grid-layout/issues/1990
85
- autoSize: true,
86
- isBounded: true,
87
- draggableHandle:
88
- '.xh-dash-tab.xh-panel > .xh-panel__content > .xh-panel-header',
89
- draggableCancel: '.xh-button',
90
- onLayoutChange: layout => model.onRglLayoutChange(layout),
91
- onResizeStart: () => (model.isResizing = true),
92
- onResizeStop: () => (model.isResizing = false),
93
- items: model.viewModels.map(vm =>
94
- div({
95
- key: vm.id,
96
- item: dashCanvasView({model: vm})
97
- })
98
- ),
99
- ...rglOptions
100
- }),
101
- emptyContainerOverlay()
102
- ],
90
+ items: mounted
91
+ ? [
92
+ reactGridLayout({
93
+ layout: model.rglLayout,
94
+ width,
95
+ gridConfig: {
96
+ cols: model.columns,
97
+ rowHeight: model.rowHeight,
98
+ margin: model.margin,
99
+ maxRows: model.maxRows,
100
+ ...(rglOptions?.gridConfig ?? {})
101
+ },
102
+ dragConfig: {
103
+ enabled: isDraggable,
104
+ handle: '.xh-dash-tab.xh-panel > .xh-panel__content > .xh-panel-header',
105
+ cancel: '.xh-button',
106
+ bounded: true,
107
+ ...(rglOptions?.dragConfig ?? {})
108
+ },
109
+ resizeConfig: {
110
+ enabled: isResizable,
111
+ ...(rglOptions?.resizeConfig ?? {})
112
+ },
113
+ dropConfig: {
114
+ enabled: model.contentLocked ? false : model.droppable,
115
+ defaultItem: {w: 6, h: 6},
116
+ ...(rglOptions?.dropConfig ?? {})
117
+ },
118
+ compactor: model.compact ? verticalCompactor : noCompactor,
119
+ onLayoutChange: (layout: LayoutItem[]) =>
120
+ model.onRglLayoutChange(layout),
121
+ onResizeStart: () => (model.isResizing = true),
122
+ onResizeStop: () => (model.isResizing = false),
123
+ children: model.viewModels.map(vm =>
124
+ div({
125
+ key: vm.id,
126
+ item: dashCanvasView({model: vm})
127
+ })
128
+ ),
129
+ onDropDragOver: (evt: DragEvent) => model.onDropDragOver(evt),
130
+ onDrop: (layout: LayoutItem[], layoutItem: LayoutItem, evt: Event) =>
131
+ model.onDrop(layout, layoutItem, evt),
132
+ ...topLevelRglOptions
133
+ }),
134
+ emptyContainerOverlay({omit: !model.showAddViewButtonWhenEmpty})
135
+ ]
136
+ : [],
103
137
  [TEST_ID]: testId
104
138
  })
105
139
  });
@@ -147,4 +181,4 @@ const onContextMenu = (e, model) => {
147
181
  }
148
182
  };
149
183
 
150
- const reactGridLayout = elementFactory(WidthProvider(ReactGridLayout));
184
+ const reactGridLayout = elementFactory<GridLayoutProps>(ReactGridLayout);
@@ -4,6 +4,9 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {wait} from '@xh/hoist/promise';
8
+ import {DragEvent} from 'react';
9
+ import type {LayoutItem} from 'react-grid-layout';
7
10
  import {Persistable, PersistableState, PersistenceProvider, XH} from '@xh/hoist/core';
8
11
  import {required} from '@xh/hoist/data';
9
12
  import {DashCanvasViewModel, DashCanvasViewSpec, DashConfig, DashViewState, DashModel} from '../';
@@ -16,6 +19,7 @@ import {createObservableRef} from '@xh/hoist/utils/react';
16
19
  import {
17
20
  defaultsDeep,
18
21
  find,
22
+ omit,
19
23
  uniqBy,
20
24
  times,
21
25
  without,
@@ -48,8 +52,42 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
48
52
  /** Maximum number of rows permitted for this container. Default `Infinity`. */
49
53
  maxRows?: number;
50
54
 
51
- /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
55
+ /** Padding inside the container [x, y] in pixels. Default `[0, 0]`. */
52
56
  containerPadding?: [number, number];
57
+
58
+ /**
59
+ * Whether the canvas should accept drag-and-drop of views from outside
60
+ * the canvas. Default false.
61
+ */
62
+ droppable?: boolean;
63
+
64
+ /**
65
+ * Optional Callback to invoke after a view is successfully dropped onto the canvas.
66
+ */
67
+ onDropDone?: (viewModel: DashCanvasViewModel) => void;
68
+
69
+ /**
70
+ * Optional callback to invoke when an item is dragged over the canvas. This may be used to
71
+ * customize how the size of the dropping placeholder is calculated. The callback should
72
+ * return an object with optional properties indicating the desired width, height (in grid units),
73
+ * and offset (in pixels) of the dropping placeholder.
74
+ * If not provided, Hoist's own default logic will be used.
75
+ */
76
+ onDropDragOver?: (e: DragEvent) =>
77
+ | {
78
+ w?: number;
79
+ h?: number;
80
+ dragOffsetX?: number;
81
+ dragOffsetY?: number;
82
+ }
83
+ | false
84
+ | void;
85
+
86
+ /**
87
+ * Whether an overlay with an Add View button should be rendered
88
+ * when the canvas is empty. Default true.
89
+ */
90
+ showAddViewButtonWhenEmpty?: boolean;
53
91
  }
54
92
 
55
93
  export interface DashCanvasItemState {
@@ -82,11 +120,16 @@ export class DashCanvasModel
82
120
  @bindable compact: boolean;
83
121
  @bindable.ref margin: [number, number]; // [x, y]
84
122
  @bindable.ref containerPadding: [number, number]; // [x, y]
123
+ @bindable showAddViewButtonWhenEmpty: boolean;
85
124
 
86
125
  //-----------------------------
87
126
  // Public properties
88
127
  //-----------------------------
128
+ DROPPING_ELEM_ID = '__dropping-elem__';
89
129
  maxRows: number;
130
+ droppable: boolean;
131
+ onDropDone: (viewModel: DashCanvasViewModel) => void;
132
+ draggedInView: DashCanvasItemState;
90
133
 
91
134
  /** Current number of rows in canvas */
92
135
  get rows(): number {
@@ -106,21 +149,27 @@ export class DashCanvasModel
106
149
  private isLoadingState: boolean;
107
150
 
108
151
  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
- });
152
+ return this.layout
153
+ .map(it => {
154
+ const dashCanvasView = this.getView(it.i);
155
+
156
+ // `dashCanvasView` will not be found if `it` is a dropping element.
157
+ if (!dashCanvasView) return null;
158
+
159
+ const {autoHeight, viewSpec} = dashCanvasView;
160
+
161
+ return {
162
+ ...it,
163
+ resizeHandles: autoHeight
164
+ ? ['w', 'e']
165
+ : ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
166
+ maxH: viewSpec.maxHeight,
167
+ minH: viewSpec.minHeight,
168
+ maxW: viewSpec.maxWidth,
169
+ minW: viewSpec.minWidth
170
+ };
171
+ })
172
+ .filter(Boolean);
124
173
  }
125
174
 
126
175
  constructor({
@@ -138,8 +187,12 @@ export class DashCanvasModel
138
187
  compact = true,
139
188
  margin = [10, 10],
140
189
  maxRows = Infinity,
141
- containerPadding = margin,
142
- extraMenuItems
190
+ containerPadding = [0, 0],
191
+ extraMenuItems,
192
+ showAddViewButtonWhenEmpty = true,
193
+ droppable = false,
194
+ onDropDone,
195
+ onDropDragOver
143
196
  }: DashCanvasConfig) {
144
197
  super();
145
198
  makeObservable(this);
@@ -182,11 +235,15 @@ export class DashCanvasModel
182
235
  this.maxRows = maxRows;
183
236
  this.containerPadding = containerPadding;
184
237
  this.margin = margin;
185
- this.containerPadding = containerPadding;
186
238
  this.compact = compact;
187
239
  this.emptyText = emptyText;
188
240
  this.addViewButtonText = addViewButtonText;
189
241
  this.extraMenuItems = extraMenuItems;
242
+ this.showAddViewButtonWhenEmpty = showAddViewButtonWhenEmpty;
243
+ this.droppable = droppable;
244
+ this.onDropDone = onDropDone;
245
+ // Override default onDropDragOver if provided
246
+ if (onDropDragOver) this.onDropDragOver = onDropDragOver;
190
247
 
191
248
  this.loadState(initialState);
192
249
  this.state = this.buildState();
@@ -312,6 +369,56 @@ export class DashCanvasModel
312
369
  this.getView(id)?.ensureVisible();
313
370
  }
314
371
 
372
+ onDrop(rglLayout: LayoutItem[], layoutItem: LayoutItem, evt: Event) {
373
+ throwIf(
374
+ !this.draggedInView,
375
+ `No draggedInView set on DashCanvasModel prior to onDrop operation.
376
+ Typically a developer would set this in response to dragstart events from
377
+ a DashViewTray or similar component.`
378
+ );
379
+
380
+ const {viewSpecId, title, state} = this.draggedInView,
381
+ layout = omit(layoutItem, 'i'),
382
+ newViewModel: DashCanvasViewModel = this.addViewInternal(viewSpecId, {
383
+ title,
384
+ state,
385
+ layout
386
+ }),
387
+ droppingItem: any = rglLayout.find(it => it.i === this.DROPPING_ELEM_ID);
388
+
389
+ // Change ID of dropping item to the new view's id
390
+ // so that the new view goes where the dropping item is.
391
+ droppingItem.i = newViewModel.id;
392
+
393
+ // must wait a tick for RGL to settle
394
+ wait().then(() => {
395
+ this.draggedInView = null;
396
+ this.onRglLayoutChange(rglLayout);
397
+ this.onDropDone?.(newViewModel);
398
+ });
399
+ }
400
+
401
+ setDraggedInView(view?: DashCanvasItemState) {
402
+ this.draggedInView = view;
403
+ }
404
+
405
+ onDropDragOver(evt: DragEvent):
406
+ | {
407
+ w?: number;
408
+ h?: number;
409
+ dragOffsetX?: number;
410
+ dragOffsetY?: number;
411
+ }
412
+ | false
413
+ | void {
414
+ if (!this.draggedInView) return false;
415
+
416
+ return {
417
+ w: this.draggedInView.layout.w,
418
+ h: this.draggedInView.layout.h
419
+ };
420
+ }
421
+
315
422
  //------------------------
316
423
  // Persistable Interface
317
424
  //------------------------
@@ -384,13 +491,19 @@ export class DashCanvasModel
384
491
  return model;
385
492
  }
386
493
 
387
- onRglLayoutChange(rglLayout) {
494
+ onRglLayoutChange(rglLayout: LayoutItem[]) {
388
495
  rglLayout = rglLayout.map(it => pick(it, ['i', 'x', 'y', 'w', 'h']));
496
+
497
+ // Early out if RGL is changing layout as user is dragging droppable
498
+ // item around the canvas. This will be called again once dragging
499
+ // has stopped and user has dropped the item onto the canvas.
500
+ if (rglLayout.some(it => it.i === this.DROPPING_ELEM_ID)) return;
501
+
389
502
  this.setLayout(rglLayout);
390
503
  }
391
504
 
392
505
  @action
393
- private setLayout(layout) {
506
+ private setLayout(layout: LayoutItem[]) {
394
507
  layout = sortBy(layout, 'i');
395
508
  const layoutChanged = !isEqual(layout, this.layout);
396
509
  if (!layoutChanged) return;
@@ -492,6 +605,7 @@ export class DashCanvasModel
492
605
  }
493
606
  }
494
607
  }
608
+
495
609
  const checkPosition = (originX, originY) => {
496
610
  for (let y = originY; y < originY + height; y++) {
497
611
  for (let x = originX; x < originX + width; x++) {
@@ -81,13 +81,13 @@ export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'titl
81
81
  * A toolbar to be docked at the top of the panel.
82
82
  * If specified as an array, items will be passed as children to a Toolbar component.
83
83
  */
84
- tbar?: Some<ReactNode>;
84
+ tbar?: ReactNode;
85
85
 
86
86
  /**
87
87
  * A toolbar to be docked at the bottom of the panel.
88
88
  * If specified as an array, items will be passed as children to a Toolbar component.
89
89
  */
90
- bbar?: Some<ReactNode>;
90
+ bbar?: ReactNode;
91
91
 
92
92
  /** Title text added to the panel's header. */
93
93
  title?: ReactNode;