@xh/hoist 80.0.0-SNAPSHOT.1767647247785 → 80.0.0-SNAPSHOT.1767973504648
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 +24 -4
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +3 -3
- package/admin/tabs/cluster/instances/services/ServiceModel.ts +2 -2
- package/appcontainer/AppContainerModel.ts +5 -5
- package/appcontainer/AppStateModel.ts +1 -1
- package/appcontainer/FeedbackDialogModel.ts +3 -5
- package/build/types/appcontainer/AppContainerModel.d.ts +2 -2
- package/build/types/cmp/grid/GridModel.d.ts +2 -2
- package/build/types/cmp/layout/Tags.d.ts +2 -0
- package/build/types/cmp/tab/TabContainerModel.d.ts +1 -1
- package/build/types/core/XH.d.ts +2 -1
- package/build/types/desktop/cmp/button/FieldsetCollapseButton.d.ts +10 -0
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +46 -6
- package/build/types/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWell.d.ts +17 -0
- package/build/types/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWellModel.d.ts +11 -0
- package/build/types/desktop/cmp/form/CollapsibleFieldset.d.ts +11 -0
- package/cmp/grid/GridModel.ts +2 -2
- package/cmp/layout/Tags.ts +2 -0
- package/cmp/tab/TabContainerModel.ts +1 -1
- package/cmp/viewmanager/ViewManagerModel.ts +2 -2
- package/core/XH.ts +12 -4
- package/desktop/appcontainer/AppContainer.ts +1 -1
- package/desktop/appcontainer/LoginPanel.ts +2 -2
- package/desktop/cmp/button/FieldsetCollapseButton.ts +44 -0
- package/desktop/cmp/dash/canvas/DashCanvas.ts +21 -4
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +140 -24
- package/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWell.scss +26 -0
- package/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWell.ts +126 -0
- package/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWellModel.ts +65 -0
- package/desktop/cmp/form/CollapsibleFieldset.scss +14 -0
- package/desktop/cmp/form/CollapsibleFieldset.ts +65 -0
- package/desktop/cmp/viewmanager/ViewManager.ts +2 -2
- package/mobile/appcontainer/AppContainer.ts +1 -1
- package/mobile/appcontainer/LoginPanel.ts +2 -2
- package/package.json +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/impl/MenuItems.ts +3 -3
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {wait} from '@xh/hoist/promise';
|
|
7
8
|
import type {LayoutItem} from 'react-grid-layout';
|
|
8
9
|
import {Persistable, PersistableState, PersistenceProvider, XH} from '@xh/hoist/core';
|
|
9
10
|
import {required} from '@xh/hoist/data';
|
|
@@ -17,6 +18,7 @@ import {createObservableRef} from '@xh/hoist/utils/react';
|
|
|
17
18
|
import {
|
|
18
19
|
defaultsDeep,
|
|
19
20
|
find,
|
|
21
|
+
omit,
|
|
20
22
|
uniqBy,
|
|
21
23
|
times,
|
|
22
24
|
without,
|
|
@@ -41,11 +43,12 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
|
|
|
41
43
|
rowHeight?: number;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
|
-
* Whether views should "compact" vertically or
|
|
46
|
+
* Whether views should "compact" vertically, horizontally or wrap
|
|
45
47
|
* to condense space. Default `true` defaults to vertical compaction.
|
|
48
|
+
* Use `wrap` with caution. It only works well if all items are 1 row high.
|
|
46
49
|
* See react-grid-layout docs for more information.
|
|
47
|
-
|
|
48
|
-
compact?: boolean | 'vertical' | 'horizontal';
|
|
50
|
+
*/
|
|
51
|
+
compact?: boolean | 'vertical' | 'horizontal' | 'wrap';
|
|
49
52
|
|
|
50
53
|
/** Between items [x,y] in pixels. Default `[10, 10]`. */
|
|
51
54
|
margin?: [number, number];
|
|
@@ -60,6 +63,35 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
|
|
|
60
63
|
* Whether a grid background should be shown. Default false.
|
|
61
64
|
*/
|
|
62
65
|
showGridBackground?: boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the canvas should accept drag-and-drop of views from outside
|
|
69
|
+
* the canvas. Default false.
|
|
70
|
+
*/
|
|
71
|
+
allowsDrop?: boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optional callback to invoke after a view is successfully dropped onto the canvas.
|
|
75
|
+
*/
|
|
76
|
+
onDropDone?: (viewModel: DashCanvasViewModel) => void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Optional callback to invoke when an item is dragged over the canvas. This may be used to
|
|
80
|
+
* customize how the size of the dropping placeholder is calculated. The callback should
|
|
81
|
+
* return an object with optional properties indicating the desired width, height (in grid units),
|
|
82
|
+
* and offset (in pixels) of the dropping placeholder. The method's signature is the same as
|
|
83
|
+
* the `onDropDragOver` prop of ReactGridLayout.
|
|
84
|
+
* Returning `false` will prevent the dropping placeholder from being shown, and prevents a drop.
|
|
85
|
+
* Returning `void` will use the default behavior, which is to size the placeholder as per the
|
|
86
|
+
* `dropConfig.defaultItem` specification.
|
|
87
|
+
*/
|
|
88
|
+
onDropDragOver?: (e: DragEvent) => OnDropDragOverResult;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether an overlay with an Add View button should be rendered
|
|
92
|
+
* when the canvas is empty. Default true.
|
|
93
|
+
*/
|
|
94
|
+
showAddViewButtonWhenEmpty?: boolean;
|
|
63
95
|
}
|
|
64
96
|
|
|
65
97
|
export interface DashCanvasItemState {
|
|
@@ -76,6 +108,16 @@ export interface DashCanvasItemLayout {
|
|
|
76
108
|
h: number;
|
|
77
109
|
}
|
|
78
110
|
|
|
111
|
+
export type OnDropDragOverResult =
|
|
112
|
+
| {
|
|
113
|
+
w?: number;
|
|
114
|
+
h?: number;
|
|
115
|
+
dragOffsetX?: number;
|
|
116
|
+
dragOffsetY?: number;
|
|
117
|
+
}
|
|
118
|
+
| false
|
|
119
|
+
| void;
|
|
120
|
+
|
|
79
121
|
/**
|
|
80
122
|
* Model for {@link DashCanvas}, managing all configurable options for the component and publishing
|
|
81
123
|
* the observable state of its current widgets and their layout.
|
|
@@ -89,16 +131,21 @@ export class DashCanvasModel
|
|
|
89
131
|
//------------------------------
|
|
90
132
|
@bindable columns: number;
|
|
91
133
|
@bindable rowHeight: number;
|
|
92
|
-
@bindable compact: 'vertical' | 'horizontal';
|
|
134
|
+
@bindable compact: 'vertical' | 'horizontal' | 'wrap';
|
|
93
135
|
@bindable.ref margin: [number, number]; // [x, y]
|
|
94
136
|
@bindable.ref containerPadding: [number, number]; // [x, y]
|
|
95
137
|
@bindable showGridBackground: boolean;
|
|
96
138
|
@bindable rglHeight: number;
|
|
139
|
+
@bindable showAddViewButtonWhenEmpty: boolean;
|
|
97
140
|
|
|
98
141
|
//-----------------------------
|
|
99
142
|
// Public properties
|
|
100
143
|
//-----------------------------
|
|
144
|
+
DROPPING_ELEM_ID = '__dropping-elem__';
|
|
101
145
|
maxRows: number;
|
|
146
|
+
allowsDrop: boolean;
|
|
147
|
+
onDropDone: (viewModel: DashCanvasViewModel) => void;
|
|
148
|
+
draggedInView: DashCanvasItemState;
|
|
102
149
|
|
|
103
150
|
/** Current number of rows in canvas */
|
|
104
151
|
get rows(): number {
|
|
@@ -118,21 +165,27 @@ export class DashCanvasModel
|
|
|
118
165
|
private isLoadingState: boolean;
|
|
119
166
|
|
|
120
167
|
get rglLayout() {
|
|
121
|
-
return this.layout
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
return this.layout
|
|
169
|
+
.map(it => {
|
|
170
|
+
const dashCanvasView = this.getView(it.i);
|
|
171
|
+
|
|
172
|
+
// `dashCanvasView` will not be found if `it` is a dropping element.
|
|
173
|
+
if (!dashCanvasView) return null;
|
|
174
|
+
|
|
175
|
+
const {autoHeight, viewSpec} = dashCanvasView;
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
...it,
|
|
179
|
+
resizeHandles: autoHeight
|
|
180
|
+
? ['w', 'e']
|
|
181
|
+
: ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne'],
|
|
182
|
+
maxH: viewSpec.maxHeight,
|
|
183
|
+
minH: viewSpec.minHeight,
|
|
184
|
+
maxW: viewSpec.maxWidth,
|
|
185
|
+
minW: viewSpec.minWidth
|
|
186
|
+
};
|
|
187
|
+
})
|
|
188
|
+
.filter(Boolean);
|
|
136
189
|
}
|
|
137
190
|
|
|
138
191
|
constructor({
|
|
@@ -152,7 +205,11 @@ export class DashCanvasModel
|
|
|
152
205
|
maxRows = Infinity,
|
|
153
206
|
containerPadding = margin,
|
|
154
207
|
extraMenuItems,
|
|
155
|
-
showGridBackground = false
|
|
208
|
+
showGridBackground = false,
|
|
209
|
+
showAddViewButtonWhenEmpty = true,
|
|
210
|
+
allowsDrop = false,
|
|
211
|
+
onDropDone,
|
|
212
|
+
onDropDragOver
|
|
156
213
|
}: DashCanvasConfig) {
|
|
157
214
|
super();
|
|
158
215
|
makeObservable(this);
|
|
@@ -200,6 +257,10 @@ export class DashCanvasModel
|
|
|
200
257
|
this.addViewButtonText = addViewButtonText;
|
|
201
258
|
this.extraMenuItems = extraMenuItems;
|
|
202
259
|
this.showGridBackground = showGridBackground;
|
|
260
|
+
this.showAddViewButtonWhenEmpty = showAddViewButtonWhenEmpty;
|
|
261
|
+
this.allowsDrop = allowsDrop;
|
|
262
|
+
this.onDropDone = onDropDone;
|
|
263
|
+
if (onDropDragOver) this.onDropDragOver = onDropDragOver;
|
|
203
264
|
|
|
204
265
|
this.loadState(initialState);
|
|
205
266
|
this.state = this.buildState();
|
|
@@ -337,6 +398,59 @@ export class DashCanvasModel
|
|
|
337
398
|
this.getView(id)?.ensureVisible();
|
|
338
399
|
}
|
|
339
400
|
|
|
401
|
+
onDrop(rglLayout: LayoutItem[], layoutItem: LayoutItem, evt: Event) {
|
|
402
|
+
throwIf(
|
|
403
|
+
!this.draggedInView,
|
|
404
|
+
`No draggedInView set on DashCanvasModel prior to onDrop operation.
|
|
405
|
+
Typically a developer would set this in response to dragstart events from
|
|
406
|
+
a DashViewTray or similar component.`
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const droppingItem: any = rglLayout.find(it => it.i === this.DROPPING_ELEM_ID);
|
|
410
|
+
if (!droppingItem) {
|
|
411
|
+
// if `onDropDragOver` returned false, we won't have a dropping item
|
|
412
|
+
// and we cancel the drop
|
|
413
|
+
this.draggedInView = null;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const {viewSpecId, title, state} = this.draggedInView,
|
|
418
|
+
layout = omit(layoutItem, 'i'),
|
|
419
|
+
newViewModel: DashCanvasViewModel = this.addViewInternal(viewSpecId, {
|
|
420
|
+
title,
|
|
421
|
+
state,
|
|
422
|
+
layout
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Change ID of dropping item to the new view's id
|
|
426
|
+
// so that the new view goes where the dropping item is.
|
|
427
|
+
droppingItem.i = newViewModel.id;
|
|
428
|
+
|
|
429
|
+
// must wait a tick for RGL to settle
|
|
430
|
+
wait().then(() => {
|
|
431
|
+
this.draggedInView = null;
|
|
432
|
+
this.onRglLayoutChange(rglLayout);
|
|
433
|
+
this.onDropDone?.(newViewModel);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
setDraggedInView(view?: DashCanvasItemState) {
|
|
438
|
+
this.draggedInView = view;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
onDropDragOver(evt: DragEvent): OnDropDragOverResult {
|
|
442
|
+
if (!this.draggedInView) return false;
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
w: this.draggedInView.layout.w,
|
|
446
|
+
h: this.draggedInView.layout.h
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
getViewsBySpecId(id) {
|
|
451
|
+
return this.viewModels.filter(it => it.viewSpec.id === id);
|
|
452
|
+
}
|
|
453
|
+
|
|
340
454
|
//------------------------
|
|
341
455
|
// Persistable Interface
|
|
342
456
|
//------------------------
|
|
@@ -413,6 +527,12 @@ export class DashCanvasModel
|
|
|
413
527
|
|
|
414
528
|
onRglLayoutChange(rglLayout: LayoutItem[]) {
|
|
415
529
|
rglLayout = rglLayout.map(it => pick(it, ['i', 'x', 'y', 'w', 'h']));
|
|
530
|
+
|
|
531
|
+
// Early out if RGL is changing layout as user is dragging droppable
|
|
532
|
+
// item around the canvas. This will be called again once dragging
|
|
533
|
+
// has stopped and user has dropped the item onto the canvas.
|
|
534
|
+
if (rglLayout.some(it => it.i === this.DROPPING_ELEM_ID)) return;
|
|
535
|
+
|
|
416
536
|
this.setLayout(rglLayout);
|
|
417
537
|
}
|
|
418
538
|
|
|
@@ -496,10 +616,6 @@ export class DashCanvasModel
|
|
|
496
616
|
return some(this.viewSpecs, {id});
|
|
497
617
|
}
|
|
498
618
|
|
|
499
|
-
private getViewsBySpecId(id) {
|
|
500
|
-
return this.viewModels.filter(it => it.viewSpec.id === id);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
619
|
private getNextAvailablePosition({
|
|
504
620
|
width,
|
|
505
621
|
height,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.xh-dash-canvas-widget-well {
|
|
2
|
+
padding: 0 var(--xh-pad-px);
|
|
3
|
+
|
|
4
|
+
.xh-collapsible-fieldset {
|
|
5
|
+
margin: var(--xh-pad-half-px) 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.xh-dash-canvas-draggable-widget {
|
|
9
|
+
border: var(--xh-border-dotted);
|
|
10
|
+
background-color: var(--xh-bg-alt);
|
|
11
|
+
padding: var(--xh-pad-half-px);
|
|
12
|
+
margin: var(--xh-pad-half-px) 0;
|
|
13
|
+
cursor: grab;
|
|
14
|
+
|
|
15
|
+
&.is-dragging {
|
|
16
|
+
cursor: grabbing;
|
|
17
|
+
// lighten background color of left behind placeholder
|
|
18
|
+
// when dragging
|
|
19
|
+
opacity: 0.25;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&:active {
|
|
23
|
+
cursor: grabbing;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {uniqBy} from 'lodash';
|
|
9
|
+
import classNames from 'classnames';
|
|
10
|
+
import type {ReactElement} from 'react';
|
|
11
|
+
import {div, vframe} from '@xh/hoist/cmp/layout';
|
|
12
|
+
import {creates, hoistCmp, HoistProps, uses} from '@xh/hoist/core';
|
|
13
|
+
import {DashCanvasModel, DashCanvasViewSpec} from '@xh/hoist/desktop/cmp/dash';
|
|
14
|
+
import {DashCanvasWidgetWellModel} from '@xh/hoist/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWellModel';
|
|
15
|
+
import {collapsibleFieldset} from '@xh/hoist/desktop/cmp/form/CollapsibleFieldset';
|
|
16
|
+
|
|
17
|
+
import './DashCanvasWidgetWell.scss';
|
|
18
|
+
|
|
19
|
+
export interface DashCanvasWidgetWellProps extends HoistProps {
|
|
20
|
+
/** DashCanvasModel for which this widget well should allow the user to add views from. */
|
|
21
|
+
dashCanvasModel?: DashCanvasModel;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Widget Well from which to add items to a DashCanvas by drag-and-drop.
|
|
26
|
+
*
|
|
27
|
+
* Available view specs are listed in their defined order,
|
|
28
|
+
* grouped by their 'groupName' property if present.
|
|
29
|
+
*
|
|
30
|
+
* Typically, an app developer would place this inside a collapsible panel to the side of
|
|
31
|
+
* a DashCanvas.
|
|
32
|
+
*/
|
|
33
|
+
export const [DashCanvasWidgetWell, dashCanvasWidgetWell] =
|
|
34
|
+
hoistCmp.withFactory<DashCanvasWidgetWellProps>({
|
|
35
|
+
displayName: 'DashCanvasWidgetWell',
|
|
36
|
+
model: creates(DashCanvasWidgetWellModel),
|
|
37
|
+
className: 'xh-dash-canvas-widget-well',
|
|
38
|
+
render({dashCanvasModel, className}) {
|
|
39
|
+
if (!dashCanvasModel) return;
|
|
40
|
+
|
|
41
|
+
return vframe({
|
|
42
|
+
className: classNames(className),
|
|
43
|
+
overflow: 'auto',
|
|
44
|
+
items: createDraggableItems(dashCanvasModel)
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
//---------------------------
|
|
50
|
+
// Implementation
|
|
51
|
+
//---------------------------
|
|
52
|
+
const draggableWidget = hoistCmp.factory<DashCanvasWidgetWellModel>({
|
|
53
|
+
displayName: 'DraggableWidget',
|
|
54
|
+
model: uses(DashCanvasWidgetWellModel),
|
|
55
|
+
render({model, viewSpec}) {
|
|
56
|
+
const {id, icon, title} = viewSpec as DashCanvasViewSpec;
|
|
57
|
+
return div({
|
|
58
|
+
id: `draggableFor-${id}`,
|
|
59
|
+
className: 'xh-dash-canvas-draggable-widget',
|
|
60
|
+
draggable: true,
|
|
61
|
+
unselectable: 'on',
|
|
62
|
+
onDragStart: e => model.onDragStart(e),
|
|
63
|
+
onDragEnd: e => model.onDragEnd(e),
|
|
64
|
+
items: [icon, ' ', title]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Used to create draggable items (for adding views)
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
function createDraggableItems(dashCanvasModel: DashCanvasModel): any[] {
|
|
74
|
+
if (!dashCanvasModel.ref.current) return [];
|
|
75
|
+
|
|
76
|
+
const groupedItems = {},
|
|
77
|
+
ungroupedItems = [];
|
|
78
|
+
|
|
79
|
+
const addToGroup = (item, icon, groupName) => {
|
|
80
|
+
const group = groupedItems[groupName];
|
|
81
|
+
if (group) {
|
|
82
|
+
group.push({item, icon});
|
|
83
|
+
} else {
|
|
84
|
+
groupedItems[groupName] = [{item, icon}];
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
dashCanvasModel.viewSpecs
|
|
89
|
+
.filter(viewSpec => {
|
|
90
|
+
return (
|
|
91
|
+
viewSpec.allowAdd &&
|
|
92
|
+
(!viewSpec.unique || !dashCanvasModel.getViewsBySpecId(viewSpec.id).length)
|
|
93
|
+
);
|
|
94
|
+
})
|
|
95
|
+
.forEach(viewSpec => {
|
|
96
|
+
const {groupName} = viewSpec,
|
|
97
|
+
item = draggableWidget({viewSpec});
|
|
98
|
+
|
|
99
|
+
if (groupName) {
|
|
100
|
+
addToGroup(item, viewSpec.icon, groupName);
|
|
101
|
+
} else {
|
|
102
|
+
ungroupedItems.push(item);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
...Object.keys(groupedItems).map(group => {
|
|
108
|
+
const label = group,
|
|
109
|
+
items = groupedItems[group],
|
|
110
|
+
sameIcons =
|
|
111
|
+
uniqBy<{item: ReactElement; icon: ReactElement}>(
|
|
112
|
+
items,
|
|
113
|
+
it => it.icon.props.iconName
|
|
114
|
+
).length === 1,
|
|
115
|
+
icon = sameIcons ? items[0].icon : null;
|
|
116
|
+
|
|
117
|
+
return collapsibleFieldset({
|
|
118
|
+
icon,
|
|
119
|
+
collapsed: false,
|
|
120
|
+
label,
|
|
121
|
+
items: items.map(it => it.item)
|
|
122
|
+
});
|
|
123
|
+
}),
|
|
124
|
+
...ungroupedItems
|
|
125
|
+
];
|
|
126
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {DragEvent} from 'react';
|
|
8
|
+
import {DashCanvasModel} from '@xh/hoist/desktop/cmp/dash';
|
|
9
|
+
import {HoistModel, managed} from '@xh/hoist/core';
|
|
10
|
+
import '@xh/hoist/desktop/register';
|
|
11
|
+
import {makeObservable, observable} from '@xh/hoist/mobx';
|
|
12
|
+
import {runInAction} from 'mobx';
|
|
13
|
+
|
|
14
|
+
export class DashCanvasWidgetWellModel extends HoistModel {
|
|
15
|
+
@managed
|
|
16
|
+
@observable.ref
|
|
17
|
+
dashCanvasModel: DashCanvasModel;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
makeObservable(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override onLinked() {
|
|
25
|
+
this.addReaction({
|
|
26
|
+
track: () => this.componentProps,
|
|
27
|
+
run: () =>
|
|
28
|
+
runInAction(() => (this.dashCanvasModel = this.componentProps.dashCanvasModel)),
|
|
29
|
+
fireImmediately: true
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
onDragStart(evt: DragEvent<HTMLDivElement>) {
|
|
34
|
+
const target = evt.target as HTMLElement;
|
|
35
|
+
if (!target) return;
|
|
36
|
+
|
|
37
|
+
this.dashCanvasModel.showAddViewButtonWhenEmpty = false;
|
|
38
|
+
evt.dataTransfer.effectAllowed = 'move';
|
|
39
|
+
target.classList.add('is-dragging');
|
|
40
|
+
|
|
41
|
+
const viewSpecId: string = target.getAttribute('id').split('draggableFor-')[1],
|
|
42
|
+
viewSpec = this.dashCanvasModel.viewSpecs.find(it => it.id === viewSpecId),
|
|
43
|
+
{width, height} = viewSpec,
|
|
44
|
+
widget = {
|
|
45
|
+
viewSpecId,
|
|
46
|
+
layout: {
|
|
47
|
+
x: 0,
|
|
48
|
+
y: 0,
|
|
49
|
+
w: width,
|
|
50
|
+
h: height
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
this.dashCanvasModel.setDraggedInView(widget);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onDragEnd(evt: DragEvent<HTMLDivElement>) {
|
|
58
|
+
this.dashCanvasModel.showAddViewButtonWhenEmpty = true;
|
|
59
|
+
|
|
60
|
+
const target = evt.target as HTMLElement;
|
|
61
|
+
if (!target) return;
|
|
62
|
+
|
|
63
|
+
target.classList.remove('is-dragging');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.xh-collapsible-fieldset {
|
|
9
|
+
&--collapsed {
|
|
10
|
+
border-bottom: none;
|
|
11
|
+
border-left: none;
|
|
12
|
+
border-right: none;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {fieldsetCollapseButton} from '@xh/hoist/desktop/cmp/button/FieldsetCollapseButton';
|
|
9
|
+
import classNames from 'classnames';
|
|
10
|
+
import {castArray} from 'lodash';
|
|
11
|
+
import {type FieldsetHTMLAttributes, type ReactElement, type ReactNode, useState} from 'react';
|
|
12
|
+
import {hoistCmp, HoistProps} from '@xh/hoist/core';
|
|
13
|
+
import {fieldset} from '@xh/hoist/cmp/layout';
|
|
14
|
+
|
|
15
|
+
import './CollapsibleFieldset.scss';
|
|
16
|
+
|
|
17
|
+
export interface CollapsibleFieldsetProps
|
|
18
|
+
extends FieldsetHTMLAttributes<HTMLFieldSetElement>, HoistProps {
|
|
19
|
+
icon?: ReactElement;
|
|
20
|
+
label: ReactNode;
|
|
21
|
+
clickHandler?: () => void;
|
|
22
|
+
collapsed?: boolean;
|
|
23
|
+
hideItemCount?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const [CollapsibleFieldset, collapsibleFieldset] =
|
|
27
|
+
hoistCmp.withFactory<CollapsibleFieldsetProps>({
|
|
28
|
+
displayName: 'FieldsetCollapseButton',
|
|
29
|
+
model: false,
|
|
30
|
+
className: 'xh-collapsible-fieldset',
|
|
31
|
+
render({icon, label, collapsed, children, hideItemCount, className, disabled, ...rest}) {
|
|
32
|
+
const [isCollapsed, setIsCollapsed] = useState<boolean>(collapsed === true),
|
|
33
|
+
items = castArray(children),
|
|
34
|
+
itemCount = hideItemCount === true ? '' : ` (${items.length})`,
|
|
35
|
+
classes = [];
|
|
36
|
+
|
|
37
|
+
if (isCollapsed) {
|
|
38
|
+
classes.push('xh-collapsible-fieldset--collapsed');
|
|
39
|
+
} else {
|
|
40
|
+
classes.push('xh-collapsible-fieldset--expanded');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (disabled) {
|
|
44
|
+
classes.push('xh-collapsible-fieldset--disabled');
|
|
45
|
+
} else {
|
|
46
|
+
classes.push('xh-collapsible-fieldset--enabled');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return fieldset({
|
|
50
|
+
className: classNames(className, classes),
|
|
51
|
+
items: [
|
|
52
|
+
fieldsetCollapseButton({
|
|
53
|
+
icon,
|
|
54
|
+
text: `${label}${itemCount}`,
|
|
55
|
+
clickHandler: val => setIsCollapsed(val),
|
|
56
|
+
collapsed: isCollapsed,
|
|
57
|
+
disabled
|
|
58
|
+
}),
|
|
59
|
+
...(isCollapsed ? [] : items)
|
|
60
|
+
],
|
|
61
|
+
disabled,
|
|
62
|
+
...rest
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
@@ -86,7 +86,7 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
|
|
|
86
86
|
buttonSide = 'right',
|
|
87
87
|
extraMenuItems = []
|
|
88
88
|
}: ViewManagerProps) {
|
|
89
|
-
const {
|
|
89
|
+
const {loadObserver} = model,
|
|
90
90
|
locModel = useLocalModel(() => new ViewManagerLocalModel(model)),
|
|
91
91
|
save = saveButton({model: locModel, mode: showSaveButton, ...saveButtonProps}),
|
|
92
92
|
revert = revertButton({model: locModel, mode: showRevertButton, ...revertButtonProps}),
|
|
@@ -103,7 +103,7 @@ export const [ViewManager, viewManager] = hoistCmp.withFactory<ViewManagerProps>
|
|
|
103
103
|
}),
|
|
104
104
|
...menuButtonProps
|
|
105
105
|
}),
|
|
106
|
-
content:
|
|
106
|
+
content: loadObserver.isPending
|
|
107
107
|
? box({
|
|
108
108
|
item: spinner({compact: true}),
|
|
109
109
|
alignItems: 'center',
|
|
@@ -143,7 +143,7 @@ const appContainerView = hoistCmp.factory<AppContainerModel>({
|
|
|
143
143
|
});
|
|
144
144
|
|
|
145
145
|
const appLoadMask = hoistCmp.factory<AppContainerModel>(({model}) =>
|
|
146
|
-
mask({bind: model.
|
|
146
|
+
mask({bind: model.appLoadObserver, spinner: true})
|
|
147
147
|
);
|
|
148
148
|
|
|
149
149
|
const bannerList = hoistCmp.factory<AppContainerModel>({
|
|
@@ -26,7 +26,7 @@ export const loginPanel = hoistCmp.factory({
|
|
|
26
26
|
|
|
27
27
|
render({model}) {
|
|
28
28
|
const {loginMessage} = XH.appSpec,
|
|
29
|
-
{isValid,
|
|
29
|
+
{isValid, loadObserver, warning, loginInProgress} = model;
|
|
30
30
|
|
|
31
31
|
return panel({
|
|
32
32
|
className: 'xh-login',
|
|
@@ -34,7 +34,7 @@ export const loginPanel = hoistCmp.factory({
|
|
|
34
34
|
toolbar(filler(), XH.clientAppName, filler()),
|
|
35
35
|
panel({
|
|
36
36
|
className: 'xh-login__body',
|
|
37
|
-
mask:
|
|
37
|
+
mask: loadObserver,
|
|
38
38
|
items: [
|
|
39
39
|
form({
|
|
40
40
|
className: 'xh-login__fields',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xh/hoist",
|
|
3
|
-
"version": "80.0.0-SNAPSHOT.
|
|
3
|
+
"version": "80.0.0-SNAPSHOT.1767973504648",
|
|
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",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"moment": "~2.30.1",
|
|
63
63
|
"numbro": "~2.5.0",
|
|
64
64
|
"onsenui": "~2.12.8",
|
|
65
|
-
"qs": "~6.14.
|
|
65
|
+
"qs": "~6.14.1",
|
|
66
66
|
"react-beautiful-dnd": "~13.1.0",
|
|
67
67
|
"react-dates": "~21.8.0",
|
|
68
68
|
"react-dropzone": "~10.2.2",
|
|
69
|
-
"react-grid-layout": "2.
|
|
69
|
+
"react-grid-layout": "2.2.2",
|
|
70
70
|
"react-markdown": "~10.1.0",
|
|
71
71
|
"react-onsenui": "~1.13.2",
|
|
72
72
|
"react-popper": "~2.3.0",
|