@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 +8 -0
- package/build/types/core/elem.d.ts +3 -3
- package/build/types/desktop/cmp/dash/canvas/DashCanvas.d.ts +3 -2
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +45 -3
- package/build/types/desktop/cmp/panel/Panel.d.ts +2 -2
- package/build/types/desktop/cmp/rest/RestGrid.d.ts +3 -3
- package/build/types/mobile/cmp/panel/Panel.d.ts +2 -2
- package/core/elem.ts +5 -5
- package/desktop/cmp/dash/canvas/DashCanvas.ts +69 -35
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +135 -21
- package/desktop/cmp/panel/Panel.ts +2 -2
- package/desktop/cmp/rest/RestGrid.ts +4 -5
- package/mobile/cmp/panel/Panel.ts +2 -2
- package/package.json +2 -3
- package/tsconfig.tsbuildinfo +1 -1
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,
|
|
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?:
|
|
29
|
+
items?: ReactNode;
|
|
30
30
|
/** Equivalent to `items`, offered for code clarity when only one child is needed. */
|
|
31
|
-
item?:
|
|
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?:
|
|
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.
|
|
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:
|
|
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?:
|
|
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?:
|
|
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
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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?:
|
|
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,
|
|
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?:
|
|
48
|
+
items?: ReactNode;
|
|
49
49
|
|
|
50
50
|
/** Equivalent to `items`, offered for code clarity when only one child is needed. */
|
|
51
|
-
item?:
|
|
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
|
|
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[]
|
|
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?:
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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(
|
|
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.
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 =
|
|
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?:
|
|
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?:
|
|
90
|
+
bbar?: ReactNode;
|
|
91
91
|
|
|
92
92
|
/** Title text added to the panel's header. */
|
|
93
93
|
title?: ReactNode;
|