@xh/hoist 80.0.0-SNAPSHOT.1767799665209 → 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.
Files changed (36) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +3 -3
  3. package/admin/tabs/cluster/instances/services/ServiceModel.ts +2 -2
  4. package/appcontainer/AppContainerModel.ts +5 -5
  5. package/appcontainer/AppStateModel.ts +1 -1
  6. package/appcontainer/FeedbackDialogModel.ts +3 -5
  7. package/build/types/appcontainer/AppContainerModel.d.ts +2 -2
  8. package/build/types/cmp/grid/GridModel.d.ts +2 -2
  9. package/build/types/cmp/layout/Tags.d.ts +2 -0
  10. package/build/types/cmp/tab/TabContainerModel.d.ts +1 -1
  11. package/build/types/core/XH.d.ts +2 -1
  12. package/build/types/desktop/cmp/button/FieldsetCollapseButton.d.ts +10 -0
  13. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +46 -6
  14. package/build/types/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWell.d.ts +17 -0
  15. package/build/types/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWellModel.d.ts +11 -0
  16. package/build/types/desktop/cmp/form/CollapsibleFieldset.d.ts +11 -0
  17. package/cmp/grid/GridModel.ts +2 -2
  18. package/cmp/layout/Tags.ts +2 -0
  19. package/cmp/tab/TabContainerModel.ts +1 -1
  20. package/cmp/viewmanager/ViewManagerModel.ts +2 -2
  21. package/core/XH.ts +12 -4
  22. package/desktop/appcontainer/AppContainer.ts +1 -1
  23. package/desktop/appcontainer/LoginPanel.ts +2 -2
  24. package/desktop/cmp/button/FieldsetCollapseButton.ts +44 -0
  25. package/desktop/cmp/dash/canvas/DashCanvas.ts +21 -4
  26. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +140 -24
  27. package/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWell.scss +26 -0
  28. package/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWell.ts +126 -0
  29. package/desktop/cmp/dash/canvas/widgetwell/DashCanvasWidgetWellModel.ts +65 -0
  30. package/desktop/cmp/form/CollapsibleFieldset.scss +14 -0
  31. package/desktop/cmp/form/CollapsibleFieldset.ts +65 -0
  32. package/desktop/cmp/viewmanager/ViewManager.ts +2 -2
  33. package/mobile/appcontainer/AppContainer.ts +1 -1
  34. package/mobile/appcontainer/LoginPanel.ts +2 -2
  35. package/package.json +3 -3
  36. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## 80.0.0-SNAPSHOT - unreleased
4
4
 
5
+ ### 💥 Breaking Changes
6
+
7
+ * Completed the refactoring away from `loadModel` to `loadObserver` started in v79:
8
+ * Renamed `XH.appLoadModel` to `XH.appLoadObserver`. The prior getter remains as an alias but is
9
+ deprecated and scheduled for removal in v82.
10
+ * Renamed `AppContainerModel.loadModel` to `loadObserver`. This is primarily an internal model,
11
+ so there is no deprecated alias. Any app usages should swap to `XH.appLoadObserver`.
12
+ * Removed additional references to deprecated `loadModel` within Hoist itself.
13
+
14
+ ### 🎁 New Features
15
+
16
+ * DashCanvas:
17
+ * supports dragging and dropping widgets in from an external container.
18
+ * supports new compacting strategy: 'wrap'
19
+ * new elementFactory tags: `fieldset`, `legend`
20
+
21
+ ### 📚 Libraries
22
+
23
+ * react-grid-layout `2.1 → 2.2.2`
24
+
5
25
  ## 79.0.0 - 2026-01-05
6
26
 
7
27
  ### 💥 Breaking Changes
@@ -69,8 +89,8 @@ this release, but is not strictly required.
69
89
 
70
90
  ### 📚 Libraries
71
91
 
72
- * @blueprintjs/core: `5.10 -> 6.3`
73
- * @blueprintjs/datetime: `5.3 -> 6.0`
92
+ * @blueprintjs/core: `5.10 6.3`
93
+ * @blueprintjs/datetime: `5.3 6.0`
74
94
  * react-grid-layout `1.5 → 2.1`
75
95
 
76
96
  ## 78.1.4 - 2025-12-05
@@ -856,8 +876,8 @@ build. That said, we *strongly* recommend taking these same changes into your ap
856
876
  ### 📚 Libraries
857
877
 
858
878
  * @azure/msal-browser `3.17 → 3.23`
859
- * mobx `6.9.1 -> 6.13.2`,
860
- * mobx-react-lite `3.4.3 -> 4.0.7`,
879
+ * mobx `6.9 6.13`,
880
+ * mobx-react-lite `3.4 4.0`,
861
881
 
862
882
  ## 67.0.0 - 2024-09-03
863
883
 
@@ -35,9 +35,9 @@ export const detailsPanel = hoistCmp.factory({
35
35
 
36
36
  const stats = hoistCmp.factory<DetailsModel>({
37
37
  render({model}) {
38
- const {stats, lastLoadException, loadModel} = model;
38
+ const {stats, lastLoadException, loadObserver} = model;
39
39
 
40
- if (!loadModel.isPending && lastLoadException) {
40
+ if (!loadObserver.isPending && lastLoadException) {
41
41
  return errorMessage({
42
42
  error: lastLoadException,
43
43
  detailsFn: e => XH.exceptionHandler.showExceptionDetails(e)
@@ -46,7 +46,7 @@ const stats = hoistCmp.factory<DetailsModel>({
46
46
 
47
47
  return isEmpty(stats)
48
48
  ? placeholder(
49
- ...(loadModel.isPending
49
+ ...(loadObserver.isPending
50
50
  ? []
51
51
  : [Icon.questionCircle(), 'This service does not report any admin stats.'])
52
52
  )
@@ -91,7 +91,7 @@ export class ServiceModel extends BaseInstanceModel {
91
91
  }
92
92
 
93
93
  async clearCachesAsync(entireCluster: boolean) {
94
- const {gridModel, instanceName, loadModel} = this,
94
+ const {gridModel, instanceName, loadObserver} = this,
95
95
  {selectedRecords} = gridModel;
96
96
 
97
97
  if (isEmpty(selectedRecords)) return;
@@ -125,7 +125,7 @@ export class ServiceModel extends BaseInstanceModel {
125
125
  instance: entireCluster ? null : instanceName,
126
126
  names: selectedRecords.map(it => it.data.name)
127
127
  }
128
- }).linkTo(loadModel);
128
+ }).linkTo(loadObserver);
129
129
  await this.refreshAsync();
130
130
  XH.successToast('Service caches cleared.');
131
131
  } catch (e) {
@@ -76,7 +76,7 @@ export class AppContainerModel extends HoistModel {
76
76
  //------------
77
77
  // Sub-models
78
78
  //------------
79
- @managed appLoadModel = TaskObserver.trackAll();
79
+ @managed appLoadObserver = TaskObserver.trackAll();
80
80
  @managed appStateModel = new AppStateModel();
81
81
  @managed pageStateModel = new PageStateModel();
82
82
  @managed routerModel = new RouterModel();
@@ -250,7 +250,7 @@ export class AppContainerModel extends HoistModel {
250
250
 
251
251
  // init all models other than Router
252
252
  const models = [
253
- this.appLoadModel,
253
+ this.appLoadObserver,
254
254
  this.appStateModel,
255
255
  this.pageStateModel,
256
256
  this.routerModel,
@@ -271,7 +271,7 @@ export class AppContainerModel extends HoistModel {
271
271
  ];
272
272
  models.forEach((m: any) => m.init?.());
273
273
 
274
- this.bindInitSequenceToAppLoadModel();
274
+ this.bindInitSequenceToAppLoadObserver();
275
275
 
276
276
  this.setDocTitle();
277
277
 
@@ -359,10 +359,10 @@ export class AppContainerModel extends HoistModel {
359
359
  this.appStateModel.setAppState(nextState);
360
360
  }
361
361
 
362
- private bindInitSequenceToAppLoadModel() {
362
+ private bindInitSequenceToAppLoadObserver() {
363
363
  const terminalStates: AppState[] = ['RUNNING', 'SUSPENDED', 'LOAD_FAILED', 'ACCESS_DENIED'],
364
364
  loadingPromise = mobxWhen(() => terminalStates.includes(this.appStateModel.state));
365
- loadingPromise.linkTo(this.appLoadModel);
365
+ loadingPromise.linkTo(this.appLoadObserver);
366
366
  }
367
367
 
368
368
  private setViewportContent(content: string) {
@@ -57,7 +57,7 @@ export class AppStateModel extends HoistModel {
57
57
  this.setAppState('SUSPENDED');
58
58
  XH.webSocketService.shutdown();
59
59
  Timer.cancelAll();
60
- XH.appContainerModel.appLoadModel.clear();
60
+ XH.appContainerModel.appLoadObserver.clear();
61
61
  }
62
62
 
63
63
  checkAccess(): boolean {
@@ -51,20 +51,18 @@ export class FeedbackDialogModel extends HoistModel {
51
51
  * Submit the feedback entry to the activity tracking system.
52
52
  */
53
53
  async submitAsync() {
54
- const {message} = this,
55
- {trackService} = XH;
56
-
54
+ const {message} = this;
57
55
  if (!message) this.hide();
58
56
 
59
57
  try {
60
- trackService.track({
58
+ XH.trackService.track({
61
59
  category: 'Feedback',
62
60
  message: 'User submitted feedback',
63
61
  data: {
64
62
  userMessage: this.message
65
63
  }
66
64
  });
67
- trackService.pushPendingAsync().linkTo(XH.appLoadModel);
65
+ await XH.trackService.pushPendingAsync().linkTo(XH.appLoadObserver);
68
66
  XH.successToast('Thank you - your feedback has been sent.');
69
67
  this.hide();
70
68
  } catch (e) {
@@ -23,7 +23,7 @@ export declare class AppContainerModel extends HoistModel {
23
23
  private initCalled;
24
24
  appSpec: AppSpec;
25
25
  appModel: HoistAppModel;
26
- appLoadModel: TaskObserver;
26
+ appLoadObserver: TaskObserver;
27
27
  appStateModel: AppStateModel;
28
28
  pageStateModel: PageStateModel;
29
29
  routerModel: RouterModel;
@@ -87,7 +87,7 @@ export declare class AppContainerModel extends HoistModel {
87
87
  private startRouter;
88
88
  private startOptionsDialog;
89
89
  private setAppState;
90
- private bindInitSequenceToAppLoadModel;
90
+ private bindInitSequenceToAppLoadObserver;
91
91
  private setViewportContent;
92
92
  private getViewportContent;
93
93
  }
@@ -553,9 +553,9 @@ export declare class GridModel extends HoistModel {
553
553
  autosizeAsync(overrideOpts?: Omit<GridAutosizeOptions, 'mode'>): Promise<void>;
554
554
  /**
555
555
  * Begin an inline editing session.
556
- * @param record - StoreRecord/ID to edit. If unspecified, the first selected StoreRecord
556
+ * @param opts.record - StoreRecord/ID to edit. If unspecified, the first selected StoreRecord
557
557
  * will be used, if any, or the first overall StoreRecord in the grid.
558
- * @param colId - ID of column on which to start editing. If unspecified, the first
558
+ * @param opts.colId - ID of column on which to start editing. If unspecified, the first
559
559
  * editable column will be used.
560
560
  */
561
561
  beginEditAsync(opts?: {
@@ -10,6 +10,7 @@ export declare const a: import("@xh/hoist/core").ElementFactory<import("react").
10
10
  export declare const br: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLBRElement>, HTMLBRElement>>;
11
11
  export declare const code: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>>;
12
12
  export declare const div: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>>;
13
+ export declare const fieldset: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").FieldsetHTMLAttributes<HTMLFieldSetElement>, HTMLFieldSetElement>>;
13
14
  export declare const form: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>>;
14
15
  export declare const hr: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHRElement>, HTMLHRElement>>;
15
16
  export declare const h1: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>>;
@@ -17,6 +18,7 @@ export declare const h2: import("@xh/hoist/core").ElementFactory<import("react")
17
18
  export declare const h3: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>>;
18
19
  export declare const h4: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>>;
19
20
  export declare const label: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>>;
21
+ export declare const legend: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLLegendElement>, HTMLLegendElement>>;
20
22
  export declare const li: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>>;
21
23
  export declare const nav: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLElement>, HTMLElement>>;
22
24
  export declare const ol: import("@xh/hoist/core").ElementFactory<import("react").DetailedHTMLProps<import("react").OlHTMLAttributes<HTMLOListElement>, HTMLOListElement>>;
@@ -70,7 +70,7 @@ export declare class TabContainerModel extends HoistModel {
70
70
  protected lastActiveTabId: string;
71
71
  /**
72
72
  * @param config - TabContainer configuration.
73
- * @param [depth] - Depth in hierarchy of nested TabContainerModels. Not for application use.
73
+ * @param depth - Depth in hierarchy of nested TabContainerModels. Not for application use.
74
74
  */
75
75
  constructor({ tabs, defaultTabId, route, track, renderMode, refreshMode, persistWith, emptyText, xhImpl, switcher }: TabContainerConfig, depth?: number);
76
76
  /** Set/replace all tabs within the container. */
@@ -73,8 +73,9 @@ export declare class XHApi {
73
73
  webSocketService: WebSocketService;
74
74
  /**
75
75
  * Tracks globally loading promises.
76
- * Apps should link any async operations that should mask the entire viewport to this model.
76
+ * Apps should link any async operations that should mask the entire viewport to this observer.
77
77
  */
78
+ get appLoadObserver(): TaskObserver;
78
79
  get appLoadModel(): TaskObserver;
79
80
  /** Root level application model. */
80
81
  get appModel(): HoistAppModel;
@@ -0,0 +1,10 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ import { HoistProps } from '@xh/hoist/core';
3
+ export interface FieldsetCollapseButtonProps extends HoistProps {
4
+ icon?: ReactElement;
5
+ text: ReactNode;
6
+ clickHandler?: (boolean: any) => void;
7
+ collapsed?: boolean;
8
+ disabled?: boolean;
9
+ }
10
+ export declare const FieldsetCollapseButton: import("react").FC<FieldsetCollapseButtonProps>, fieldsetCollapseButton: import("@xh/hoist/core").ElementFactory<FieldsetCollapseButtonProps>;
@@ -14,11 +14,12 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
14
14
  */
15
15
  rowHeight?: number;
16
16
  /**
17
- * Whether views should "compact" vertically or horizontally
17
+ * Whether views should "compact" vertically, horizontally or wrap
18
18
  * to condense space. Default `true` defaults to vertical compaction.
19
+ * Use `wrap` with caution. It only works well if all items are 1 row high.
19
20
  * See react-grid-layout docs for more information.
20
- * */
21
- compact?: boolean | 'vertical' | 'horizontal';
21
+ */
22
+ compact?: boolean | 'vertical' | 'horizontal' | 'wrap';
22
23
  /** Between items [x,y] in pixels. Default `[10, 10]`. */
23
24
  margin?: [number, number];
24
25
  /** Padding inside the container [x, y] in pixels. Defaults to same as `margin`. */
@@ -29,6 +30,31 @@ export interface DashCanvasConfig extends DashConfig<DashCanvasViewSpec, DashCan
29
30
  * Whether a grid background should be shown. Default false.
30
31
  */
31
32
  showGridBackground?: boolean;
33
+ /**
34
+ * Whether the canvas should accept drag-and-drop of views from outside
35
+ * the canvas. Default false.
36
+ */
37
+ allowsDrop?: boolean;
38
+ /**
39
+ * Optional callback to invoke after a view is successfully dropped onto the canvas.
40
+ */
41
+ onDropDone?: (viewModel: DashCanvasViewModel) => void;
42
+ /**
43
+ * Optional callback to invoke when an item is dragged over the canvas. This may be used to
44
+ * customize how the size of the dropping placeholder is calculated. The callback should
45
+ * return an object with optional properties indicating the desired width, height (in grid units),
46
+ * and offset (in pixels) of the dropping placeholder. The method's signature is the same as
47
+ * the `onDropDragOver` prop of ReactGridLayout.
48
+ * Returning `false` will prevent the dropping placeholder from being shown, and prevents a drop.
49
+ * Returning `void` will use the default behavior, which is to size the placeholder as per the
50
+ * `dropConfig.defaultItem` specification.
51
+ */
52
+ onDropDragOver?: (e: DragEvent) => OnDropDragOverResult;
53
+ /**
54
+ * Whether an overlay with an Add View button should be rendered
55
+ * when the canvas is empty. Default true.
56
+ */
57
+ showAddViewButtonWhenEmpty?: boolean;
32
58
  }
33
59
  export interface DashCanvasItemState {
34
60
  layout: DashCanvasItemLayout;
@@ -42,6 +68,12 @@ export interface DashCanvasItemLayout {
42
68
  w: number;
43
69
  h: number;
44
70
  }
71
+ export type OnDropDragOverResult = {
72
+ w?: number;
73
+ h?: number;
74
+ dragOffsetX?: number;
75
+ dragOffsetY?: number;
76
+ } | false | void;
45
77
  /**
46
78
  * Model for {@link DashCanvas}, managing all configurable options for the component and publishing
47
79
  * the observable state of its current widgets and their layout.
@@ -51,12 +83,17 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
51
83
  }> {
52
84
  columns: number;
53
85
  rowHeight: number;
54
- compact: 'vertical' | 'horizontal';
86
+ compact: 'vertical' | 'horizontal' | 'wrap';
55
87
  margin: [number, number];
56
88
  containerPadding: [number, number];
57
89
  showGridBackground: boolean;
58
90
  rglHeight: number;
91
+ showAddViewButtonWhenEmpty: boolean;
92
+ DROPPING_ELEM_ID: string;
59
93
  maxRows: number;
94
+ allowsDrop: boolean;
95
+ onDropDone: (viewModel: DashCanvasViewModel) => void;
96
+ draggedInView: DashCanvasItemState;
60
97
  /** Current number of rows in canvas */
61
98
  get rows(): number;
62
99
  get isEmpty(): boolean;
@@ -65,7 +102,7 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
65
102
  isResizing: boolean;
66
103
  private isLoadingState;
67
104
  get rglLayout(): any[];
68
- constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems, showGridBackground }: DashCanvasConfig);
105
+ constructor({ viewSpecs, viewSpecDefaults, initialState, layoutLocked, contentLocked, renameLocked, persistWith, emptyText, addViewButtonText, columns, rowHeight, compact, margin, maxRows, containerPadding, extraMenuItems, showGridBackground, showAddViewButtonWhenEmpty, allowsDrop, onDropDone, onDropDragOver }: DashCanvasConfig);
69
106
  /** Removes all views from the canvas */
70
107
  clear(): void;
71
108
  /**
@@ -103,6 +140,10 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
103
140
  renameView(id: string): void;
104
141
  /** Scrolls a DashCanvasView into view. */
105
142
  ensureViewVisible(id: string): void;
143
+ onDrop(rglLayout: LayoutItem[], layoutItem: LayoutItem, evt: Event): void;
144
+ setDraggedInView(view?: DashCanvasItemState): void;
145
+ onDropDragOver(evt: DragEvent): OnDropDragOverResult;
146
+ getViewsBySpecId(id: any): DashCanvasViewModel[];
106
147
  getPersistableState(): PersistableState<{
107
148
  state: DashCanvasItemState[];
108
149
  }>;
@@ -123,6 +164,5 @@ export declare class DashCanvasModel extends DashModel<DashCanvasViewSpec, DashC
123
164
  private setViewLayout;
124
165
  private getSpec;
125
166
  private hasSpec;
126
- private getViewsBySpecId;
127
167
  private getNextAvailablePosition;
128
168
  }
@@ -0,0 +1,17 @@
1
+ import { HoistProps } from '@xh/hoist/core';
2
+ import { DashCanvasModel } from '@xh/hoist/desktop/cmp/dash';
3
+ import './DashCanvasWidgetWell.scss';
4
+ export interface DashCanvasWidgetWellProps extends HoistProps {
5
+ /** DashCanvasModel for which this widget well should allow the user to add views from. */
6
+ dashCanvasModel?: DashCanvasModel;
7
+ }
8
+ /**
9
+ * Widget Well from which to add items to a DashCanvas by drag-and-drop.
10
+ *
11
+ * Available view specs are listed in their defined order,
12
+ * grouped by their 'groupName' property if present.
13
+ *
14
+ * Typically, an app developer would place this inside a collapsible panel to the side of
15
+ * a DashCanvas.
16
+ */
17
+ export declare const DashCanvasWidgetWell: import("react").FC<DashCanvasWidgetWellProps>, dashCanvasWidgetWell: import("@xh/hoist/core").ElementFactory<DashCanvasWidgetWellProps>;
@@ -0,0 +1,11 @@
1
+ import { DragEvent } from 'react';
2
+ import { DashCanvasModel } from '@xh/hoist/desktop/cmp/dash';
3
+ import { HoistModel } from '@xh/hoist/core';
4
+ import '@xh/hoist/desktop/register';
5
+ export declare class DashCanvasWidgetWellModel extends HoistModel {
6
+ dashCanvasModel: DashCanvasModel;
7
+ constructor();
8
+ onLinked(): void;
9
+ onDragStart(evt: DragEvent<HTMLDivElement>): void;
10
+ onDragEnd(evt: DragEvent<HTMLDivElement>): void;
11
+ }
@@ -0,0 +1,11 @@
1
+ import { type FieldsetHTMLAttributes, type ReactElement, type ReactNode } from 'react';
2
+ import { HoistProps } from '@xh/hoist/core';
3
+ import './CollapsibleFieldset.scss';
4
+ export interface CollapsibleFieldsetProps extends FieldsetHTMLAttributes<HTMLFieldSetElement>, HoistProps {
5
+ icon?: ReactElement;
6
+ label: ReactNode;
7
+ clickHandler?: () => void;
8
+ collapsed?: boolean;
9
+ hideItemCount?: boolean;
10
+ }
11
+ export declare const CollapsibleFieldset: import("react").FC<CollapsibleFieldsetProps>, collapsibleFieldset: import("@xh/hoist/core").ElementFactory<CollapsibleFieldsetProps>;
@@ -1442,9 +1442,9 @@ export class GridModel extends HoistModel {
1442
1442
 
1443
1443
  /**
1444
1444
  * Begin an inline editing session.
1445
- * @param record - StoreRecord/ID to edit. If unspecified, the first selected StoreRecord
1445
+ * @param opts.record - StoreRecord/ID to edit. If unspecified, the first selected StoreRecord
1446
1446
  * will be used, if any, or the first overall StoreRecord in the grid.
1447
- * @param colId - ID of column on which to start editing. If unspecified, the first
1447
+ * @param opts.colId - ID of column on which to start editing. If unspecified, the first
1448
1448
  * editable column will be used.
1449
1449
  */
1450
1450
  async beginEditAsync(opts: {record?: StoreRecordOrId; colId?: string} = {}) {
@@ -29,6 +29,7 @@ export const a = elementFactory('a');
29
29
  export const br = elementFactory('br');
30
30
  export const code = elementFactory('code');
31
31
  export const div = elementFactory('div');
32
+ export const fieldset = elementFactory('fieldset');
32
33
  export const form = elementFactory('form');
33
34
  export const hr = elementFactory('hr');
34
35
  export const h1 = elementFactory('h1');
@@ -36,6 +37,7 @@ export const h2 = elementFactory('h2');
36
37
  export const h3 = elementFactory('h3');
37
38
  export const h4 = elementFactory('h4');
38
39
  export const label = elementFactory('label');
40
+ export const legend = elementFactory('legend');
39
41
  export const li = elementFactory('li');
40
42
  export const nav = elementFactory('nav');
41
43
  export const ol = elementFactory('ol');
@@ -119,7 +119,7 @@ export class TabContainerModel extends HoistModel {
119
119
 
120
120
  /**
121
121
  * @param config - TabContainer configuration.
122
- * @param [depth] - Depth in hierarchy of nested TabContainerModels. Not for application use.
122
+ * @param depth - Depth in hierarchy of nested TabContainerModels. Not for application use.
123
123
  */
124
124
  constructor(
125
125
  {
@@ -281,8 +281,8 @@ export class ViewManagerModel<T = PlainObject> extends HoistModel {
281
281
 
282
282
  /** True if any async tasks are pending. */
283
283
  get isLoading(): boolean {
284
- const {loadModel, saveTask, selectTask} = this;
285
- return loadModel.isPending || saveTask.isPending || selectTask.isPending;
284
+ const {loadObserver, saveTask, selectTask} = this;
285
+ return loadObserver.isPending || saveTask.isPending || selectTask.isPending;
286
286
  }
287
287
 
288
288
  /**
package/core/XH.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  WebSocketService,
32
32
  ClientHealthService
33
33
  } from '@xh/hoist/svc';
34
- import {getLogLevel, setLogLevel, LogLevel} from '@xh/hoist/utils/js';
34
+ import {getLogLevel, setLogLevel, LogLevel, apiDeprecated} from '@xh/hoist/utils/js';
35
35
  import {camelCase, flatten, isString, uniqueId} from 'lodash';
36
36
  import {Router, State} from 'router5';
37
37
  import {CancelFn} from 'router5/types/types/base';
@@ -164,10 +164,18 @@ export class XHApi {
164
164
  //----------------------------
165
165
  /**
166
166
  * Tracks globally loading promises.
167
- * Apps should link any async operations that should mask the entire viewport to this model.
167
+ * Apps should link any async operations that should mask the entire viewport to this observer.
168
168
  */
169
+ get appLoadObserver(): TaskObserver {
170
+ return this.acm.appLoadObserver;
171
+ }
172
+
169
173
  get appLoadModel(): TaskObserver {
170
- return this.acm.appLoadModel;
174
+ apiDeprecated('XH.appLoadModel', {
175
+ v: 'v82',
176
+ msg: 'Use XH.appLoadObserver instead.'
177
+ });
178
+ return this.appLoadObserver;
171
179
  }
172
180
 
173
181
  /** Root level application model. */
@@ -439,7 +447,7 @@ export class XHApi {
439
447
  */
440
448
  @action
441
449
  reloadApp(opts?: ReloadAppOptions | string) {
442
- never().linkTo(this.appLoadModel);
450
+ never().linkTo(this.appLoadObserver);
443
451
 
444
452
  opts = isString(opts) ? {path: opts} : (opts ?? {});
445
453
 
@@ -171,7 +171,7 @@ const appContainerView = hoistCmp.factory({
171
171
  });
172
172
 
173
173
  const appLoadMask = hoistCmp.factory<AppContainerModel>(({model}) =>
174
- mask({bind: model.appLoadModel, spinner: true})
174
+ mask({bind: model.appLoadObserver, spinner: true})
175
175
  );
176
176
 
177
177
  const suspendedView = hoistCmp.factory(() => viewport(suspendPanel(), appLoadMask()));
@@ -25,7 +25,7 @@ export const loginPanel = hoistCmp.factory({
25
25
 
26
26
  render({model}) {
27
27
  const {loginMessage} = XH.appSpec,
28
- {loadModel, warning, isValid, loginInProgress} = model;
28
+ {loadObserver, warning, isValid, loginInProgress} = model;
29
29
 
30
30
  const onKeyDown = ev => {
31
31
  if (ev.key === 'Enter') model.submitAsync();
@@ -40,7 +40,7 @@ export const loginPanel = hoistCmp.factory({
40
40
  icon: Icon.login(),
41
41
  className: 'xh-login',
42
42
  width: 300,
43
- mask: loadModel,
43
+ mask: loadObserver,
44
44
  items: [
45
45
  vspacer(10),
46
46
  form(
@@ -0,0 +1,44 @@
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 {type ReactElement, type ReactNode, useState} from 'react';
9
+ import {hoistCmp, HoistProps} from '@xh/hoist/core';
10
+ import {button} from '@xh/hoist/desktop/cmp/button';
11
+ import {legend} from '@xh/hoist/cmp/layout';
12
+ import {Icon} from '@xh/hoist/icon/Icon';
13
+
14
+ export interface FieldsetCollapseButtonProps extends HoistProps {
15
+ icon?: ReactElement;
16
+ text: ReactNode;
17
+ clickHandler?: (boolean) => void;
18
+ collapsed?: boolean;
19
+ disabled?: boolean;
20
+ }
21
+
22
+ export const [FieldsetCollapseButton, fieldsetCollapseButton] =
23
+ hoistCmp.withFactory<FieldsetCollapseButtonProps>({
24
+ displayName: 'FieldsetCollapseButton',
25
+ model: false,
26
+ render({icon, text, clickHandler, collapsed, disabled}) {
27
+ const [isCollapsed, setIsCollapsed] = useState<boolean>(collapsed === true);
28
+
29
+ return legend(
30
+ button({
31
+ text,
32
+ icon,
33
+ rightIcon: isCollapsed ? Icon.angleDown() : Icon.angleUp(),
34
+ outlined: false,
35
+ disabled,
36
+ onClick: () => {
37
+ const val = !isCollapsed;
38
+ setIsCollapsed(val);
39
+ clickHandler?.(val);
40
+ }
41
+ })
42
+ );
43
+ }
44
+ });
@@ -10,7 +10,7 @@ import ReactGridLayout, {
10
10
  useContainerWidth,
11
11
  getCompactor
12
12
  } from 'react-grid-layout';
13
- import {GridBackground, type GridBackgroundProps} from 'react-grid-layout/extras';
13
+ import {GridBackground, type GridBackgroundProps, wrapCompactor} from 'react-grid-layout/extras';
14
14
  import composeRefs from '@seznam/compose-react-refs';
15
15
  import {div, vbox, vspacer} from '@xh/hoist/cmp/layout';
16
16
  import {
@@ -62,7 +62,11 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
62
62
  render({className, model, rglOptions, testId}, ref) {
63
63
  const isDraggable = !model.layoutLocked,
64
64
  isResizable = !model.layoutLocked,
65
- {width, containerRef, mounted} = useContainerWidth();
65
+ {width, containerRef, mounted} = useContainerWidth(),
66
+ defaultDroppedItemDims = {
67
+ w: Math.floor(model.columns / 3),
68
+ h: Math.floor(model.columns / 3)
69
+ };
66
70
 
67
71
  return refreshContextView({
68
72
  model: model.refreshContextModel,
@@ -98,7 +102,20 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
98
102
  resizeConfig: {
99
103
  enabled: isResizable
100
104
  },
101
- compactor: getCompactor(model.compact, false, false),
105
+ dropConfig: {
106
+ enabled: model.contentLocked ? false : model.allowsDrop,
107
+ defaultItem: defaultDroppedItemDims,
108
+ onDragOver: (evt: DragEvent) => model.onDropDragOver(evt)
109
+ },
110
+ onDrop: (
111
+ layout: LayoutItem[],
112
+ layoutItem: LayoutItem,
113
+ evt: Event
114
+ ) => model.onDrop(layout, layoutItem, evt),
115
+ compactor:
116
+ model.compact === 'wrap'
117
+ ? wrapCompactor
118
+ : getCompactor(model.compact, false, false),
102
119
  onLayoutChange: (layout: LayoutItem[]) =>
103
120
  model.onRglLayoutChange(layout),
104
121
  onResizeStart: () => (model.isResizing = true),
@@ -116,7 +133,7 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
116
133
  ),
117
134
  width
118
135
  }),
119
- emptyContainerOverlay({omit: !mounted})
136
+ emptyContainerOverlay({omit: !mounted || !model.showAddViewButtonWhenEmpty})
120
137
  ],
121
138
  [TEST_ID]: testId
122
139
  })