Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f4ede20
Handle RGL dropping item in get rglLayout
cnrudd Nov 4, 2025
0ed3722
DashCanvas: Support dragging widget into and dropping to add
cnrudd Nov 7, 2025
a642e7b
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Nov 17, 2025
ec3613d
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Nov 24, 2025
6837432
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 2, 2025
c788fc6
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 4, 2025
651eaa8
Add typings
cnrudd Dec 4, 2025
fb2ab47
Make droppable a first class option on DashCanvas, not something to b…
cnrudd Dec 5, 2025
e945fa8
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 5, 2025
5df8688
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 8, 2025
9b9bdb0
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 15, 2025
d7e18d4
Update react-grid-layout to v2.1.0
cnrudd Dec 17, 2025
f7daaeb
CHANGELOG.md entry
cnrudd Dec 17, 2025
7bdcf9b
`<Some>` not needed since ReactNode includes `Iterable<ReactNode>`
cnrudd Dec 17, 2025
485cea6
Remove unused argument `type` from `normalizeArgs` method.
cnrudd Dec 17, 2025
9d77f34
Make not droppable if contentLocked.
cnrudd Dec 18, 2025
fb80f8a
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 18, 2025
1e0c6c6
fix spreading of rglOptions
cnrudd Dec 18, 2025
d2a3610
fix typing of rglOptions
cnrudd Dec 18, 2025
e77515d
switch to "moduleResolution": "bundler", triggered by import of
cnrudd Dec 23, 2025
bf8cfab
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Dec 23, 2025
12a8c8b
support 2 compact strategies + support toggle grid background
cnrudd Dec 23, 2025
864b789
comment removed as per Lee's suggestion
cnrudd Dec 23, 2025
9d1427a
replace droppable with allowsDrop
cnrudd Dec 23, 2025
7b558ab
create type OnDropDragOverResult
cnrudd Dec 23, 2025
ea98f00
merge in latest from develop
cnrudd Jan 2, 2026
b8fbe5f
Update qs dep to resolve this code analysis warning:
cnrudd Jan 2, 2026
685b9ee
Update to "react-grid-layout": "2.2.2" to get latest fixes, and conse…
cnrudd Jan 2, 2026
e5a8f51
Merge branch 'refs/heads/develop' into drag-onto-dash-canvas
cnrudd Jan 6, 2026
4e7ad50
2 new html tags
cnrudd Jan 8, 2026
c504c91
Merge branch 'refs/heads/develop' into drag-onto-dash-canvas
cnrudd Jan 8, 2026
4d2243b
new hoist components:
cnrudd Jan 8, 2026
38208f1
add to docs.
cnrudd Jan 8, 2026
6d8fa28
Rename CollapsibleFieldset to CollapsibleBox,
cnrudd Jan 9, 2026
a013eec
Default to flex box rendering for collapsibleBox, not block.
cnrudd Jan 9, 2026
66b47b0
Support flexDirection prop on DashCanvasWidgetWell
cnrudd Jan 9, 2026
dee8742
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Jan 9, 2026
e4ef652
add testId support
cnrudd Jan 10, 2026
2b1d463
Merge branch 'develop' into drag-onto-dash-canvas
cnrudd Jan 12, 2026
233821d
support collapsibleSet on mobile by creating a dynamic button
cnrudd Jan 12, 2026
a577133
Add support for renderMode
cnrudd Jan 12, 2026
c93d120
Merge remote-tracking branch 'origin/develop' into drag-onto-dash-canvas
ghsolomon Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@
* Added new CSS variables `--xh-intent-danger-text-color` (and others). Consider using these when
styling text with Hoist intent colors to enhance legibility in dark mode.

### 🎁 New Features

* DashCanvas:
* supports dragging and dropping widgets in from an external container.
* supports new compacting strategy: 'wrap'
* new elementFactory tags: `fieldset`, `legend`

### 📚 Libraries

* react-grid-layout `2.1 → 2.2.2`

## 79.0.0 - 2026-01-05

### 💥 Breaking Changes
Expand Down
4 changes: 2 additions & 2 deletions cmp/grid/GridModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1442,9 +1442,9 @@ export class GridModel extends HoistModel {

/**
* Begin an inline editing session.
* @param record - StoreRecord/ID to edit. If unspecified, the first selected StoreRecord
* @param opts.record - StoreRecord/ID to edit. If unspecified, the first selected StoreRecord
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated linting change

* will be used, if any, or the first overall StoreRecord in the grid.
* @param colId - ID of column on which to start editing. If unspecified, the first
* @param opts.colId - ID of column on which to start editing. If unspecified, the first
* editable column will be used.
*/
async beginEditAsync(opts: {record?: StoreRecordOrId; colId?: string} = {}) {
Expand Down
49 changes: 49 additions & 0 deletions cmp/layout/CollapsibleSet.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/

.xh-collapsible-set {
&--collapsed {
border-bottom: none;
border-left: none;
border-right: none;

&--render-mode--always,
&--render-mode--lazy {
> *:not(:first-child) {
display: none;
}
}
}

&.xh-collapsible-set--intent-primary {
border-color: var(--xh-intent-primary-trans2);
&.xh-collapsible-set--enabled {
border-color: var(--xh-intent-primary);
}
}

&.xh-collapsible-set--intent-success {
border-color: var(--xh-intent-success-trans2);
&.xh-collapsible-set--enabled {
border-color: var(--xh-intent-success);
}
}

&.xh-collapsible-set--intent-warning {
border-color: var(--xh-intent-warning-trans2);
&.xh-collapsible-set--enabled {
border-color: var(--xh-intent-warning);
}
}

&.xh-collapsible-set--intent-danger {
border-color: var(--xh-intent-danger-trans2);
&.xh-collapsible-set--enabled {
border-color: var(--xh-intent-danger);
}
}
}
135 changes: 135 additions & 0 deletions cmp/layout/CollapsibleSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/

import classNames from 'classnames';
import {castArray} from 'lodash';
import {type FieldsetHTMLAttributes, type ReactElement, type ReactNode, useState} from 'react';
import {XH, hoistCmp} from '@xh/hoist/core';
import type {HoistProps, Intent, LayoutProps, RenderMode, TestSupportProps} from '@xh/hoist/core';
import {fieldset} from '@xh/hoist/cmp/layout';
import {TEST_ID, mergeDeep} from '@xh/hoist/utils/js';
import {splitLayoutProps} from '@xh/hoist/utils/react';
import {collapsibleSetButton as desktopCollapsibleSetButtonImpl} from '@xh/hoist/dynamics/desktop';
import {collapsibleSetButton as mobileCollapsibleSetButtonImpl} from '@xh/hoist/dynamics/mobile';

import './CollapsibleSet.scss';

export interface CollapsibleSetProps
extends FieldsetHTMLAttributes<HTMLFieldSetElement>, HoistProps, TestSupportProps, LayoutProps {
icon?: ReactElement;
label: ReactNode;
tooltip?: JSX.Element | string;
intent?: Intent;
clickHandler?: () => void;
collapsed?: boolean;
hideItemCount?: boolean;
renderMode?: RenderMode;
}

export const [CollapsibleSet, collapsibleSet] = hoistCmp.withFactory<CollapsibleSetProps>({
displayName: 'CollapsibleSet',
model: false,
className: 'xh-collapsible-set',
render({
icon,
label,
tooltip,
intent,
collapsed,
children,
hideItemCount,
className,
disabled,
display = 'flex',
flexDirection = 'column',
flexWrap = 'wrap',
renderMode = 'unmountOnHide',
...rest
}) {
// Note `model` destructured off of non-layout props to avoid setting
// model as a bogus DOM attribute. This low-level component may easily be passed one from
// a parent that has not properly managed its own props.
let [layoutProps, {model, testId, ...restProps}] = splitLayoutProps(rest);

restProps = mergeDeep(
{style: {display, flexDirection, flexWrap, ...layoutProps}},
{[TEST_ID]: testId},
restProps
);

const [isCollapsed, setIsCollapsed] = useState<boolean>(collapsed === true),
[expandCount, setExpandCount] = useState<number>(!collapsed ? 1 : 0),
items = castArray(children),
itemCount = hideItemCount === true ? '' : ` (${items.length})`,
classes = [];

if (isCollapsed) {
classes.push('xh-collapsible-set--collapsed');
} else {
classes.push('xh-collapsible-set--expanded');
}

if (disabled) {
classes.push('xh-collapsible-set--disabled');
} else {
classes.push('xh-collapsible-set--enabled');
}

if (intent) {
classes.push(`xh-collapsible-set--intent-${intent}`);
} else {
classes.push(`xh-collapsible-set--intent-none`);
}

const btn = XH.isMobileApp
? mobileCollapsibleSetButtonImpl
: desktopCollapsibleSetButtonImpl;

let content;
switch (renderMode) {
case 'always':
content = items;
if (isCollapsed) {
classes.push('xh-collapsible-set--collapsed--render-mode--always');
}
break;

case 'lazy':
content = isCollapsed && !expandCount ? [] : items;
if (isCollapsed) {
classes.push('xh-collapsible-set--collapsed--render-mode--lazy');
}
break;

// unmountOnHide
default:
content = isCollapsed ? [] : items;
break;
}

return fieldset({
className: classNames(className, classes),
items: [
btn({
icon,
text: `${label}${itemCount}`,
tooltip,
intent,
clickHandler: val => {
setIsCollapsed(val);
setExpandCount(expandCount + 1);
},
collapsed: isCollapsed,
disabled
}),
...content
],
disabled,
...restProps
});
}
});
2 changes: 2 additions & 0 deletions cmp/layout/Tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export const a = elementFactory('a');
export const br = elementFactory('br');
export const code = elementFactory('code');
export const div = elementFactory('div');
export const fieldset = elementFactory('fieldset');
export const form = elementFactory('form');
export const hr = elementFactory('hr');
export const h1 = elementFactory('h1');
export const h2 = elementFactory('h2');
export const h3 = elementFactory('h3');
export const h4 = elementFactory('h4');
export const label = elementFactory('label');
export const legend = elementFactory('legend');
export const li = elementFactory('li');
export const nav = elementFactory('nav');
export const ol = elementFactory('ol');
Expand Down
2 changes: 1 addition & 1 deletion cmp/tab/TabContainerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export class TabContainerModel extends HoistModel {

/**
* @param config - TabContainer configuration.
* @param [depth] - Depth in hierarchy of nested TabContainerModels. Not for application use.
* @param depth - Depth in hierarchy of nested TabContainerModels. Not for application use.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated linting change

*/
constructor(
{
Expand Down
2 changes: 1 addition & 1 deletion core/enums/RenderMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

/**
* Options for how contents should be rendered by their parent container.
* Used by {@link TabContainerModel}, {@link DashContainerModel}, and {@link PanelModel}.
* Used by {@link TabContainerModel}, {@link DashContainerModel}, {@link PanelModel}, and {@link CollapsibleSet}.
*/
export const RenderMode = Object.freeze({
/** Always render contents when the parent container is rendered, even if inactive. */
Expand Down
2 changes: 2 additions & 0 deletions desktop/appcontainer/AppContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {zoneMapperDialog as zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/imp
import {useContextMenu, useHotkeys} from '@xh/hoist/desktop/hooks';
import {DynamicTabSwitcherModel, installDesktopImpls} from '@xh/hoist/dynamics/desktop';
import {inspectorPanel} from '@xh/hoist/inspector/InspectorPanel';
import {collapsibleSetButton} from '@xh/hoist/desktop/cmp/button/CollapsibleSetButton';
import {blueprintProvider} from '@xh/hoist/kit/blueprint';
import {consumeEvent} from '@xh/hoist/utils/js';
import {elementFromContent, useOnMount} from '@xh/hoist/utils/react';
Expand All @@ -46,6 +47,7 @@ import {toastSource} from './ToastSource';
import {versionBar} from './VersionBar';

installDesktopImpls({
collapsibleSetButton,
tabContainerImpl,
dockContainerImpl,
storeFilterFieldImpl,
Expand Down
57 changes: 57 additions & 0 deletions desktop/cmp/button/CollapsibleSetButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/

import {type ReactElement, type ReactNode, type JSX, useState} from 'react';
import {tooltip as bpTooltip} from '@xh/hoist/kit/blueprint';
import {hoistCmp} from '@xh/hoist/core';
import type {Intent, HoistProps} from '@xh/hoist/core';
import {button} from '@xh/hoist/desktop/cmp/button';
import {legend} from '@xh/hoist/cmp/layout';
import {Icon} from '@xh/hoist/icon/Icon';

export interface CollapsibleSetButtonProps extends HoistProps {
icon?: ReactElement;
text: ReactNode;
tooltip?: JSX.Element | string;
clickHandler?: (boolean) => void;
intent?: Intent;
collapsed?: boolean;
disabled?: boolean;
}

export const [CollapsibleSetButton, collapsibleSetButton] =
hoistCmp.withFactory<CollapsibleSetButtonProps>({
displayName: 'CollapsibleSetButton',
model: false,
render({icon, text, tooltip, intent, clickHandler, collapsed, disabled}) {
const [isCollapsed, setIsCollapsed] = useState<boolean>(collapsed === true),
btn = button({
text,
icon,
rightIcon: isCollapsed ? Icon.angleDown() : Icon.angleUp(),
outlined: isCollapsed && !intent,
minimal: !intent || (intent && !isCollapsed),
intent,
disabled,
onClick: () => {
const val = !isCollapsed;
setIsCollapsed(val);
clickHandler?.(val);
}
});

return legend(
tooltip
? bpTooltip({
item: btn,
content: tooltip,
intent
})
: btn
);
}
});
25 changes: 21 additions & 4 deletions desktop/cmp/dash/canvas/DashCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import ReactGridLayout, {
useContainerWidth,
getCompactor
} from 'react-grid-layout';
import {GridBackground, type GridBackgroundProps} from 'react-grid-layout/extras';
import {GridBackground, type GridBackgroundProps, wrapCompactor} from 'react-grid-layout/extras';
import composeRefs from '@seznam/compose-react-refs';
import {div, vbox, vspacer} from '@xh/hoist/cmp/layout';
import {
Expand Down Expand Up @@ -62,7 +62,11 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
render({className, model, rglOptions, testId}, ref) {
const isDraggable = !model.layoutLocked,
isResizable = !model.layoutLocked,
{width, containerRef, mounted} = useContainerWidth();
{width, containerRef, mounted} = useContainerWidth(),
defaultDroppedItemDims = {
w: Math.floor(model.columns / 3),
h: Math.floor(model.columns / 3)
};

return refreshContextView({
model: model.refreshContextModel,
Expand Down Expand Up @@ -98,7 +102,20 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
resizeConfig: {
enabled: isResizable
},
compactor: getCompactor(model.compact, false, false),
dropConfig: {
enabled: model.contentLocked ? false : model.allowsDrop,
defaultItem: defaultDroppedItemDims,
onDragOver: (evt: DragEvent) => model.onDropDragOver(evt)
},
onDrop: (
layout: LayoutItem[],
layoutItem: LayoutItem,
evt: Event
) => model.onDrop(layout, layoutItem, evt),
compactor:
model.compact === 'wrap'
? wrapCompactor
: getCompactor(model.compact, false, false),
onLayoutChange: (layout: LayoutItem[]) =>
model.onRglLayoutChange(layout),
onResizeStart: () => (model.isResizing = true),
Expand All @@ -116,7 +133,7 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
),
width
}),
emptyContainerOverlay({omit: !mounted})
emptyContainerOverlay({omit: !mounted || !model.showAddViewButtonWhenEmpty})
],
[TEST_ID]: testId
})
Expand Down
Loading
Loading