@xh/hoist 86.1.0 → 86.3.0
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/.gitattributes +4 -0
- package/CHANGELOG.md +104 -0
- package/admin/AdminJsonDisplay.ts +31 -0
- package/admin/App.scss +3 -11
- package/admin/AppModel.ts +3 -4
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +4 -4
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +56 -6
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +9 -26
- package/admin/tabs/clients/ClientsModel.ts +4 -1
- package/admin/tabs/clients/activity/ClientDetailModel.ts +5 -1
- package/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel.ts +3 -11
- package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -13
- package/admin/tabs/cluster/objects/DetailPanel.ts +3 -12
- package/admin/tabs/userData/roles/RolePanel.ts +2 -2
- package/appcontainer/RouterModel.ts +6 -6
- package/build/types/admin/AdminJsonDisplay.d.ts +7 -0
- package/build/types/admin/AppModel.d.ts +3 -4
- package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +2 -2
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
- package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
- package/build/types/appcontainer/RouterModel.d.ts +5 -5
- package/build/types/cmp/grid/Grid.d.ts +8 -4
- package/build/types/cmp/grid/GridModel.d.ts +13 -2
- package/build/types/cmp/grid/Types.d.ts +8 -1
- package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +24 -6
- package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
- package/build/types/core/HoistAppModel.d.ts +2 -3
- package/build/types/core/types/Types.d.ts +12 -0
- package/build/types/desktop/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
- package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
- package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
- package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
- package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
- package/build/types/desktop/cmp/input/CodeInput.d.ts +7 -0
- package/build/types/desktop/cmp/input/NumberInput.d.ts +1 -1
- package/build/types/desktop/cmp/input/Select.d.ts +8 -0
- package/build/types/desktop/cmp/input/impl/CalcWindowedMenuWidth.d.ts +20 -0
- package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
- package/build/types/format/FormatNumber.d.ts +11 -3
- package/build/types/icon/Icon.d.ts +4 -3
- package/build/types/kit/react-dropzone/index.d.ts +1 -0
- package/build/types/kit/swiper/index.d.ts +4 -3
- package/build/types/mobile/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
- package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
- package/build/types/mobile/cmp/input/NumberInput.d.ts +1 -1
- package/build/types/mobile/cmp/input/Select.d.ts +8 -0
- package/cmp/grid/Grid.ts +27 -2
- package/cmp/grid/GridModel.ts +21 -8
- package/cmp/grid/Types.ts +9 -1
- package/cmp/grid/filter/GridFilterFieldSpec.ts +28 -5
- package/cmp/grid/impl/MenuSupport.ts +3 -5
- package/cmp/input/SegmentedControlOption.ts +14 -0
- package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
- package/core/HoistAppModel.ts +2 -3
- package/core/types/Types.ts +14 -5
- package/desktop/cmp/button/grid/ExpandToLevelButton.ts +4 -5
- package/desktop/cmp/filechooser/FileChooser.ts +6 -3
- package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
- package/desktop/cmp/filter/FilterChooser.scss +41 -0
- package/desktop/cmp/filter/FilterChooser.ts +147 -7
- package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
- package/desktop/cmp/grid/editors/NumberEditor.ts +16 -17
- package/desktop/cmp/grid/editors/SelectEditor.ts +15 -10
- package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
- package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
- package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
- package/desktop/cmp/grid/impl/filter/GridFilterDialog.ts +2 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
- package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
- package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +46 -7
- package/desktop/cmp/input/CheckboxButton.ts +21 -1
- package/desktop/cmp/input/CodeInput.ts +20 -3
- package/desktop/cmp/input/NumberInput.ts +1 -1
- package/desktop/cmp/input/SegmentedControl.ts +7 -3
- package/desktop/cmp/input/Select.ts +37 -18
- package/desktop/cmp/input/impl/CalcWindowedMenuWidth.ts +106 -0
- package/desktop/cmp/rest/impl/RestFormField.ts +1 -1
- package/desktop/cmp/tab/Tabs.scss +72 -25
- package/docs/routing.md +4 -2
- package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
- package/format/FormatNumber.ts +69 -32
- package/icon/Icon.scss +13 -0
- package/icon/Icon.ts +4 -3
- package/icon/impl/IconHtml.ts +1 -1
- package/kit/react-dropzone/index.ts +1 -0
- package/mcp/README.md +12 -0
- package/mcp/data/ts-registry.spec.ts +156 -0
- package/mcp/data/ts-registry.ts +14 -7
- package/mcp/formatters/typescript.ts +7 -3
- package/mcp/util/paths.ts +21 -0
- package/mobile/cmp/button/grid/ExpandToLevelButton.ts +4 -5
- package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
- package/mobile/cmp/input/CheckboxButton.ts +18 -1
- package/mobile/cmp/input/NumberInput.ts +1 -1
- package/mobile/cmp/input/SegmentedControl.ts +7 -3
- package/mobile/cmp/input/Select.ts +21 -15
- package/mobx/README.md +2 -1
- package/package.json +10 -10
- package/styles/vars.scss +15 -0
- package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
|
@@ -26,7 +26,7 @@ export interface NumberInputProps extends HoistProps, HoistInputProps, StyleProp
|
|
|
26
26
|
modifier?: string;
|
|
27
27
|
/** Text to display when control is empty. */
|
|
28
28
|
placeholder?: string;
|
|
29
|
-
/** Max decimal precision of the value, defaults to 4. */
|
|
29
|
+
/** Max decimal precision of the value, defaults to 4. Set to null for full, unrestricted precision. */
|
|
30
30
|
precision?: NumericPrecision;
|
|
31
31
|
/**
|
|
32
32
|
* Scale factor to apply when converting between the internal and external value. Useful for
|
|
@@ -109,6 +109,14 @@ export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps {
|
|
|
109
109
|
title?: string;
|
|
110
110
|
/** Field on provided options for sourcing each option's value (default `value`). */
|
|
111
111
|
valueField?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Function to generate a `SelectOption` for a (non-null) selected value not present in the
|
|
114
|
+
* current options list. Return null to fall back to the default value-as-label behavior.
|
|
115
|
+
*
|
|
116
|
+
* Useful with queryFn-based selects, readonly forms, or any case where options may not be
|
|
117
|
+
* loaded when a value is set, ensuring the value renders with its proper label.
|
|
118
|
+
*/
|
|
119
|
+
generateOptionFn?: (value: any) => SelectOption;
|
|
112
120
|
}
|
|
113
121
|
/**
|
|
114
122
|
* A managed wrapper around the React-Select combobox/dropdown component.
|
package/cmp/grid/Grid.ts
CHANGED
|
@@ -48,6 +48,8 @@ import {consumeEvent, isDisplayed, logWithDebug} from '@xh/hoist/utils/js';
|
|
|
48
48
|
import {composeRefs, createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
|
|
49
49
|
import classNames from 'classnames';
|
|
50
50
|
import {compact, debounce, isBoolean, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
|
|
51
|
+
import {type MouseEvent} from 'react';
|
|
52
|
+
import {PartialDeep} from 'type-fest';
|
|
51
53
|
import './Grid.scss';
|
|
52
54
|
import {GridModel} from './GridModel';
|
|
53
55
|
import {columnGroupHeader} from './impl/ColumnGroupHeader';
|
|
@@ -65,7 +67,7 @@ export interface GridProps<M extends GridModel = GridModel>
|
|
|
65
67
|
*
|
|
66
68
|
* Note that changes to these options after the component's initial render will be ignored.
|
|
67
69
|
*/
|
|
68
|
-
agOptions?: GridOptions
|
|
70
|
+
agOptions?: PartialDeep<GridOptions>;
|
|
69
71
|
|
|
70
72
|
/**
|
|
71
73
|
* Callback when the grid has initialized. The component will call this with the ag-Grid
|
|
@@ -135,6 +137,7 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
|
|
|
135
137
|
],
|
|
136
138
|
testId,
|
|
137
139
|
onKeyDown: impl.onKeyDown,
|
|
140
|
+
onMouseDown: impl.onViewMouseDown,
|
|
138
141
|
ref: composeRefs(impl.viewRef, model.viewRef, ref)
|
|
139
142
|
}),
|
|
140
143
|
colChooserModel ? platformColChooser({model: colChooserModel}) : null,
|
|
@@ -151,6 +154,10 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
|
|
|
151
154
|
export class GridLocalModel extends HoistModel {
|
|
152
155
|
override xhImpl = true;
|
|
153
156
|
|
|
157
|
+
// Structural "empty" grid space.
|
|
158
|
+
private static EMPTY_SPACE_SELECTOR =
|
|
159
|
+
'.ag-body-viewport, .ag-center-cols-viewport, .ag-center-cols-container, .ag-row';
|
|
160
|
+
|
|
154
161
|
@lookup(GridModel)
|
|
155
162
|
private model: GridModel;
|
|
156
163
|
agOptions: GridOptions;
|
|
@@ -386,7 +393,14 @@ export class GridLocalModel extends HoistModel {
|
|
|
386
393
|
return {
|
|
387
394
|
track: () => [model.agApi, model.groupBy],
|
|
388
395
|
run: ([agApi, groupBy]) => {
|
|
389
|
-
if (agApi)
|
|
396
|
+
if (!agApi) return;
|
|
397
|
+
agApi.setRowGroupColumns(groupBy);
|
|
398
|
+
|
|
399
|
+
// Re-assert configured visibility - AG Grid re-shows a column when ungrouped (#4473).
|
|
400
|
+
const state = model.columnState
|
|
401
|
+
.filter(({colId}) => !groupBy.includes(colId))
|
|
402
|
+
.map(({colId, hidden}) => ({colId, hide: hidden}));
|
|
403
|
+
agApi.applyColumnState({state});
|
|
390
404
|
}
|
|
391
405
|
};
|
|
392
406
|
}
|
|
@@ -844,6 +858,17 @@ export class GridLocalModel extends HoistModel {
|
|
|
844
858
|
return this.rowKeyNavSupport?.navigateToNextCell(agParams);
|
|
845
859
|
};
|
|
846
860
|
|
|
861
|
+
// `stopEditingWhenCellsLoseFocus` doesn't fire on clicks in empty grid space (focus stays in
|
|
862
|
+
// the grid), so commit the active edit on those clicks ourselves. Require exact match on empty
|
|
863
|
+
// space to avoid interfering with cell editors.
|
|
864
|
+
onViewMouseDown = (evt: MouseEvent) => {
|
|
865
|
+
const {model} = this,
|
|
866
|
+
target = evt.target as HTMLElement;
|
|
867
|
+
if (model.isEditing && target.matches(GridLocalModel.EMPTY_SPACE_SELECTOR)) {
|
|
868
|
+
model.agApi?.stopEditing();
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
|
|
847
872
|
onCellMouseDown = evt => {
|
|
848
873
|
const {model} = this;
|
|
849
874
|
if (model.highlightRowOnClick) {
|
package/cmp/grid/GridModel.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
GridAutosizeMode,
|
|
17
17
|
GridFilterModelConfig,
|
|
18
18
|
GridGroupSortFn,
|
|
19
|
+
IColChooserModel,
|
|
19
20
|
isColumnSpec,
|
|
20
21
|
TreeStyle
|
|
21
22
|
} from '@xh/hoist/cmp/grid';
|
|
@@ -305,6 +306,10 @@ export interface GridConfig {
|
|
|
305
306
|
* expand/collapse options in the default context menu will be enhanced to allow users to
|
|
306
307
|
* expand/collapse to a specific level. See {@link GroupingChooserModel.valueDisplayNames}
|
|
307
308
|
* for a convenient getter that will satisfy this API when a GroupingChooser is in play.
|
|
309
|
+
*
|
|
310
|
+
* Labels are matched to levels top-down and need not cover the full depth of the grid - provide
|
|
311
|
+
* a partial array to label only the top levels (e.g. when deeper levels should not be
|
|
312
|
+
* expand-to targets). Deeper, unlabelled levels are omitted from the menu.
|
|
308
313
|
*/
|
|
309
314
|
levelLabels?: Thunkable<string[]>;
|
|
310
315
|
|
|
@@ -494,7 +499,7 @@ export class GridModel extends HoistModel {
|
|
|
494
499
|
store: Store;
|
|
495
500
|
selModel: StoreSelectionModel;
|
|
496
501
|
treeMode: boolean;
|
|
497
|
-
colChooserModel:
|
|
502
|
+
colChooserModel: IColChooserModel;
|
|
498
503
|
rowClassFn: RowClassFn;
|
|
499
504
|
rowClassRules: Record<string, RowClassRuleFn>;
|
|
500
505
|
contextMenu: GridContextMenuSpec;
|
|
@@ -1174,15 +1179,23 @@ export class GridModel extends HoistModel {
|
|
|
1174
1179
|
|
|
1175
1180
|
/**
|
|
1176
1181
|
* Get the resolved level labels for the current state of the grid.
|
|
1182
|
+
* An over-long array is truncated to the current `maxDepth`.
|
|
1177
1183
|
*/
|
|
1178
1184
|
get resolvedLevelLabels(): string[] {
|
|
1179
1185
|
const {maxDepth, levelLabels} = this,
|
|
1180
1186
|
ret = executeIfFunction(levelLabels);
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1187
|
+
return !isEmpty(ret) ? take(ret, maxDepth + 1) : null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* True if the given `resolvedLevelLabels` index is the grid's current expand level - used to
|
|
1192
|
+
* mark the active item in the "Expand to..." menu. The deepest labelled level counts as current
|
|
1193
|
+
* whenever the grid is expanded to or beyond it.
|
|
1194
|
+
*/
|
|
1195
|
+
isCurrentExpandLevel(idx: number): boolean {
|
|
1196
|
+
const {expandLevel, resolvedLevelLabels} = this,
|
|
1197
|
+
lastIdx = resolvedLevelLabels?.length - 1;
|
|
1198
|
+
return expandLevel === idx || (idx === lastIdx && expandLevel > lastIdx);
|
|
1186
1199
|
}
|
|
1187
1200
|
|
|
1188
1201
|
/**
|
|
@@ -1247,7 +1260,7 @@ export class GridModel extends HoistModel {
|
|
|
1247
1260
|
}
|
|
1248
1261
|
|
|
1249
1262
|
showColChooser() {
|
|
1250
|
-
|
|
1263
|
+
this.colChooserModel?.open();
|
|
1251
1264
|
}
|
|
1252
1265
|
|
|
1253
1266
|
noteAgColumnStateChanged(agColState: AgColumnState[]) {
|
|
@@ -1974,7 +1987,7 @@ export class GridModel extends HoistModel {
|
|
|
1974
1987
|
};
|
|
1975
1988
|
}
|
|
1976
1989
|
|
|
1977
|
-
private parseChooserModel(chooserModel: GridConfig['colChooserModel']):
|
|
1990
|
+
private parseChooserModel(chooserModel: GridConfig['colChooserModel']): IColChooserModel {
|
|
1978
1991
|
if (!chooserModel) return null;
|
|
1979
1992
|
|
|
1980
1993
|
const modelClass = XH.isMobileApp ? MobileColChooserModel : DesktopColChooserModel;
|
package/cmp/grid/Types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {HSide, PersistOptions, Some} from '@xh/hoist/core';
|
|
8
|
+
import type {HoistModel, HSide, PersistOptions, Some} from '@xh/hoist/core';
|
|
9
9
|
import type {
|
|
10
10
|
FilterBindTarget,
|
|
11
11
|
FilterMatchMode,
|
|
@@ -142,6 +142,14 @@ export interface GridFilterBindTarget extends FilterBindTarget, FilterValueSourc
|
|
|
142
142
|
*/
|
|
143
143
|
export type GroupRowRenderer = (context: ICellRendererParams) => ReactNode;
|
|
144
144
|
|
|
145
|
+
/** Cross-platform interface for desktop and mobile ColChooserModels. */
|
|
146
|
+
export interface IColChooserModel extends HoistModel {
|
|
147
|
+
readonly gridModel: GridModel;
|
|
148
|
+
readonly isOpen: boolean;
|
|
149
|
+
open(): void;
|
|
150
|
+
close(): void;
|
|
151
|
+
}
|
|
152
|
+
|
|
145
153
|
/**
|
|
146
154
|
* Configuration for a {@link ColChooserModel} - the model backing the grid column chooser UI.
|
|
147
155
|
* Passed via the `colChooserModel` config on {@link GridConfig}, or set app-wide via
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {ColumnRenderer} from '@xh/hoist/cmp/grid';
|
|
8
7
|
import {HoistInputProps} from '@xh/hoist/cmp/input';
|
|
9
8
|
import {PlainObject} from '@xh/hoist/core';
|
|
10
9
|
import {
|
|
@@ -19,17 +18,38 @@ import {
|
|
|
19
18
|
BaseFilterFieldSpecConfig
|
|
20
19
|
} from '@xh/hoist/data/filter/BaseFilterFieldSpec';
|
|
21
20
|
import {castArray, compact, flatMap, isDate, isEmpty, uniqBy} from 'lodash';
|
|
21
|
+
import {ReactNode} from 'react';
|
|
22
22
|
import {GridFilterModel} from './GridFilterModel';
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Produces the display content for an entry in the Values tab of a column filter. Must be a pure
|
|
26
|
+
* transform of the value - the values list carries no source record, and there is no column or
|
|
27
|
+
* grid context to reach for (unlike a Column's `renderer`, which runs against the source grid).
|
|
28
|
+
*/
|
|
29
|
+
export type GridFilterRenderer = (value: any) => ReactNode;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Produces the value to sort by for an entry in the Values tab of a column filter. Must be a pure
|
|
33
|
+
* transform of the value - the values list carries no source record, and there is no column or
|
|
34
|
+
* grid context to reach for (unlike a Column's `sortValue`, which runs against the source grid).
|
|
35
|
+
*/
|
|
36
|
+
export type GridFilterSortValueFn = (value: any) => any;
|
|
37
|
+
|
|
24
38
|
export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
|
|
25
39
|
/** GridFilterModel instance owning this fieldSpec. */
|
|
26
40
|
filterModel?: GridFilterModel;
|
|
27
41
|
|
|
28
42
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
43
|
+
* Pure function producing the display content for each entry in the values filter display. If
|
|
44
|
+
* not provided, the Column's renderer is used where it can be applied to a bare value.
|
|
45
|
+
*/
|
|
46
|
+
renderer?: GridFilterRenderer;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Pure function producing the value to sort each entry of the values filter display by. If not
|
|
50
|
+
* provided, the Column's sortValue is used where it can be applied to a bare value.
|
|
31
51
|
*/
|
|
32
|
-
|
|
52
|
+
sortValue?: GridFilterSortValueFn;
|
|
33
53
|
|
|
34
54
|
/**
|
|
35
55
|
* Props to pass through to the HoistInput components used on the custom filter tab.
|
|
@@ -47,7 +67,8 @@ export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
|
|
|
47
67
|
*/
|
|
48
68
|
export class GridFilterFieldSpec extends BaseFilterFieldSpec {
|
|
49
69
|
filterModel: GridFilterModel;
|
|
50
|
-
renderer:
|
|
70
|
+
renderer: GridFilterRenderer;
|
|
71
|
+
sortValue: GridFilterSortValueFn;
|
|
51
72
|
inputProps: PlainObject;
|
|
52
73
|
defaultOp: FieldFilterOperator;
|
|
53
74
|
|
|
@@ -57,6 +78,7 @@ export class GridFilterFieldSpec extends BaseFilterFieldSpec {
|
|
|
57
78
|
constructor({
|
|
58
79
|
filterModel,
|
|
59
80
|
renderer,
|
|
81
|
+
sortValue,
|
|
60
82
|
inputProps,
|
|
61
83
|
defaultOp,
|
|
62
84
|
...rest
|
|
@@ -65,6 +87,7 @@ export class GridFilterFieldSpec extends BaseFilterFieldSpec {
|
|
|
65
87
|
|
|
66
88
|
this.filterModel = filterModel;
|
|
67
89
|
this.renderer = renderer;
|
|
90
|
+
this.sortValue = sortValue;
|
|
68
91
|
this.inputProps = inputProps;
|
|
69
92
|
this.defaultOp = this.ops.includes(defaultOp) ? defaultOp : this.ops[0];
|
|
70
93
|
}
|
|
@@ -138,7 +138,7 @@ function replaceHoistToken(token: string, gridModel: GridModel): Some<RecordActi
|
|
|
138
138
|
text: 'Columns...',
|
|
139
139
|
icon: Icon.gridPanel(),
|
|
140
140
|
hidden: !gridModel?.colChooserModel,
|
|
141
|
-
actionFn: () =>
|
|
141
|
+
actionFn: () => gridModel.colChooserModel?.open()
|
|
142
142
|
});
|
|
143
143
|
case 'expandCollapseAll': // For backward compatibility
|
|
144
144
|
case 'expandCollapse':
|
|
@@ -280,15 +280,13 @@ function levelExpandAction(gridModel: GridModel): RecordAction {
|
|
|
280
280
|
return new RecordAction({
|
|
281
281
|
text: 'Expand to...',
|
|
282
282
|
displayFn: () => {
|
|
283
|
-
const {maxDepth,
|
|
283
|
+
const {maxDepth, resolvedLevelLabels} = gridModel;
|
|
284
284
|
|
|
285
285
|
// Don't show for flat grid models or if we don't have labels
|
|
286
286
|
if (!maxDepth || !resolvedLevelLabels) return {hidden: true};
|
|
287
287
|
|
|
288
288
|
const items = resolvedLevelLabels.map((label, idx) => {
|
|
289
|
-
const isCurrLevel =
|
|
290
|
-
expandLevel === idx ||
|
|
291
|
-
(expandLevel > maxDepth && idx === resolvedLevelLabels.length - 1);
|
|
289
|
+
const isCurrLevel = gridModel.isCurrentExpandLevel(idx);
|
|
292
290
|
|
|
293
291
|
return {
|
|
294
292
|
icon: isCurrLevel ? Icon.check() : null,
|
|
@@ -33,6 +33,13 @@ export interface SegmentedControlOption {
|
|
|
33
33
|
* `intent` default. Defaults to the control's `intent`.
|
|
34
34
|
*/
|
|
35
35
|
intent?: Intent;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional stable identifier emitted on this option's rendered button as `data-testid`, for
|
|
39
|
+
* use by E2E tests. If omitted and the control itself has a `testId`, one is auto-derived as
|
|
40
|
+
* `${controlTestId}-${value}`. If neither is set, no attribute is emitted.
|
|
41
|
+
*/
|
|
42
|
+
testId?: string;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
|
@@ -58,4 +65,11 @@ export interface SegmentedControlNullOption {
|
|
|
58
65
|
* `intent` default. Defaults to the control's `intent`.
|
|
59
66
|
*/
|
|
60
67
|
intent?: Intent;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Optional stable identifier emitted on this option's rendered button as `data-testid`, for
|
|
71
|
+
* use by E2E tests. If omitted and the control itself has a `testId`, one is auto-derived as
|
|
72
|
+
* `${controlTestId}-null`. If neither is set, no attribute is emitted.
|
|
73
|
+
*/
|
|
74
|
+
testId?: string;
|
|
61
75
|
}
|
|
@@ -66,7 +66,7 @@ export class StoreFilterFieldImplModel extends HoistModel {
|
|
|
66
66
|
|
|
67
67
|
this.addReaction(
|
|
68
68
|
{
|
|
69
|
-
track: () => [this.filterText, gridModel?.columns
|
|
69
|
+
track: () => [this.filterText, gridModel?.columns],
|
|
70
70
|
run: () => this.regenerateFilter(),
|
|
71
71
|
fireImmediately: true
|
|
72
72
|
},
|
|
@@ -161,8 +161,7 @@ export class StoreFilterFieldImplModel extends HoistModel {
|
|
|
161
161
|
if (excludeFields) ret = without(ret, ...excludeFields);
|
|
162
162
|
|
|
163
163
|
if (gridModel) {
|
|
164
|
-
const
|
|
165
|
-
visibleCols = gridModel.getVisibleLeafColumns();
|
|
164
|
+
const visibleCols = gridModel.getVisibleLeafColumns();
|
|
166
165
|
|
|
167
166
|
// Push on dot-delimited grid column fields. These are supported by Grid and traverse
|
|
168
167
|
// sub-objects in StoreRecord.data to display nested properties. Given that Grid treats these
|
|
@@ -182,12 +181,12 @@ export class StoreFilterFieldImplModel extends HoistModel {
|
|
|
182
181
|
// Run exclude once more to support explicitly excluding a dot-sep field added above.
|
|
183
182
|
if (excludeFields) ret = without(ret, ...excludeFields);
|
|
184
183
|
|
|
185
|
-
// Final filter for column visibility, or explicit request for inclusion.
|
|
184
|
+
// Final filter for column visibility, or explicit request for inclusion. Deliberately
|
|
185
|
+
// not keyed to groupBy, so filter results stay stable across regrouping (see #4070).
|
|
186
186
|
ret = ret.filter(f => {
|
|
187
187
|
return (
|
|
188
188
|
(includeFields && includeFields.includes(f)) ||
|
|
189
|
-
visibleCols.find(c => c.field === f)
|
|
190
|
-
groupBy.includes(f)
|
|
189
|
+
visibleCols.find(c => c.field === f)
|
|
191
190
|
);
|
|
192
191
|
});
|
|
193
192
|
}
|
package/core/HoistAppModel.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Copyright © 2026 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
import {webSocketIndicator} from '@xh/hoist/cmp/websocket';
|
|
8
|
-
import {AppOptionSpec, HoistModel, InitContext, Thunkable} from './';
|
|
9
|
-
import {Route} from 'router5';
|
|
8
|
+
import {AppOptionSpec, HoistModel, HoistRoute, InitContext, Thunkable} from './';
|
|
10
9
|
import {ReactNode} from 'react';
|
|
11
10
|
/**
|
|
12
11
|
* Specialized base class for defining the central model for a Hoist app as specified by its
|
|
@@ -52,7 +51,7 @@ export class HoistAppModel extends HoistModel {
|
|
|
52
51
|
/**
|
|
53
52
|
* Provide the initial set of Router5 Routes to be used by this application.
|
|
54
53
|
*/
|
|
55
|
-
getRoutes():
|
|
54
|
+
getRoutes(): HoistRoute[] {
|
|
56
55
|
return [];
|
|
57
56
|
}
|
|
58
57
|
|
package/core/types/Types.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
|
9
9
|
import {MomentInput} from 'moment';
|
|
10
10
|
import {Component, FunctionComponent, ReactElement} from 'react';
|
|
11
11
|
import {DebounceSettings} from 'lodash';
|
|
12
|
+
import {Route} from 'router5';
|
|
12
13
|
|
|
13
14
|
/** Values available for intents. */
|
|
14
15
|
export type Intent = 'primary' | 'success' | 'warning' | 'danger';
|
|
@@ -55,14 +56,22 @@ export type DebounceSpec = number | (DebounceSettings & {interval: number});
|
|
|
55
56
|
* function that returns a ReactElement. In either case, the function will be called with no arguments.
|
|
56
57
|
*/
|
|
57
58
|
export type Content =
|
|
58
|
-
| ReactElement
|
|
59
|
-
| FunctionComponent
|
|
60
|
-
| Component
|
|
61
|
-
| ElementFactory
|
|
62
|
-
| (() => ReactElement);
|
|
59
|
+
ReactElement | FunctionComponent | Component | ElementFactory | (() => ReactElement);
|
|
63
60
|
|
|
64
61
|
export type DateLike = Date | LocalDate | MomentInput;
|
|
65
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A Router5 {@link Route} spec, extended with Hoist's `omit` support for declarative exclusion of
|
|
65
|
+
* routes at registration time (e.g. role-gated sections). Note `omit` is evaluated once when routes
|
|
66
|
+
* are added to the router during app startup - it is not reactive.
|
|
67
|
+
*
|
|
68
|
+
* @see HoistAppModel.getRoutes
|
|
69
|
+
*/
|
|
70
|
+
export interface HoistRoute extends Omit<Route, 'children'> {
|
|
71
|
+
omit?: Thunkable<boolean>;
|
|
72
|
+
children?: HoistRoute[];
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
/** Valid units for the {@link LocalDate} adjustment methods. */
|
|
67
76
|
export type LocalDateUnit =
|
|
68
77
|
| 'year'
|
|
@@ -28,7 +28,8 @@ export interface ExpandToLevelButtonProps extends Omit<ButtonProps, 'title'> {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* A menu button to expand a multi-level grouped or tree grid out to a desired level.
|
|
31
|
-
* Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel
|
|
31
|
+
* Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel - the menu offers
|
|
32
|
+
* one entry per labelled level, so a partial array will limit the available expand-to targets.
|
|
32
33
|
*/
|
|
33
34
|
export const [ExpandToLevelButton, expandToLevelButton] =
|
|
34
35
|
hoistCmp.withFactory<ExpandToLevelButtonProps>({
|
|
@@ -53,13 +54,11 @@ export const [ExpandToLevelButton, expandToLevelButton] =
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
// Render a disabled button if requested, if we have a flat grid, or no level labels
|
|
56
|
-
const {maxDepth,
|
|
57
|
+
const {maxDepth, resolvedLevelLabels} = gridModel;
|
|
57
58
|
if (disabled || !maxDepth || !resolvedLevelLabels) return disabledButton();
|
|
58
59
|
|
|
59
60
|
const menuItems: MenuItemLike[] = resolvedLevelLabels.map((label, idx) => {
|
|
60
|
-
const isCurrLevel =
|
|
61
|
-
expandLevel === idx ||
|
|
62
|
-
(expandLevel > maxDepth && idx === resolvedLevelLabels.length - 1);
|
|
61
|
+
const isCurrLevel = gridModel.isCurrentExpandLevel(idx);
|
|
63
62
|
|
|
64
63
|
return {
|
|
65
64
|
icon: isCurrLevel ? Icon.check() : Icon.placeholder(),
|
|
@@ -12,7 +12,7 @@ import {dropzone} from '@xh/hoist/kit/react-dropzone';
|
|
|
12
12
|
import {elementFromContent, getLayoutProps} from '@xh/hoist/utils/react';
|
|
13
13
|
import {FileRejection} from 'react-dropzone';
|
|
14
14
|
import {FileChooserModel} from './FileChooserModel';
|
|
15
|
-
import {
|
|
15
|
+
import {isEmpty} from 'lodash';
|
|
16
16
|
import classNames from 'classnames';
|
|
17
17
|
import {defaultEmptyDisplay} from './impl/DefaultEmptyDisplay';
|
|
18
18
|
import {defaultFileDisplay} from './impl/DefaultFileDisplay';
|
|
@@ -88,8 +88,11 @@ export const [FileChooser, fileChooser] = hoistCmp.withFactory<FileChooserProps>
|
|
|
88
88
|
|
|
89
89
|
return dropzone({
|
|
90
90
|
ref: model.dropzoneRef,
|
|
91
|
-
// react-dropzone
|
|
92
|
-
|
|
91
|
+
// react-dropzone expects `accept` to be a {type: [extensions]} map. The FileChooserModel
|
|
92
|
+
// only accepts a list of extensions. Extensions alone are sufficient for validation and
|
|
93
|
+
// a MIME type is not required. Use `_/_` as a dummy MIME type to prevent console warnings
|
|
94
|
+
// from react-dropzone.
|
|
95
|
+
accept: accept ? {'_/_': accept} : null,
|
|
93
96
|
// Disable interaction (drag/click/drop) at the limit; the target shows a clear message.
|
|
94
97
|
disabled: disabled || atLimit,
|
|
95
98
|
maxFiles,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {em, li, span, ul, vbox} from '@xh/hoist/cmp/layout';
|
|
8
8
|
import {HoistModel, Some, ToastSpec, XH} from '@xh/hoist/core';
|
|
9
9
|
import '@xh/hoist/desktop/register';
|
|
10
|
-
import {FileRejection} from '@xh/hoist/kit/react-dropzone';
|
|
10
|
+
import {ErrorCode, FileRejection} from '@xh/hoist/kit/react-dropzone';
|
|
11
11
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
12
12
|
import {pluralize, withDefault} from '@xh/hoist/utils/js';
|
|
13
13
|
import {createObservableRef} from '@xh/hoist/utils/react';
|
|
@@ -187,10 +187,17 @@ export class FileChooserModel extends HoistModel {
|
|
|
187
187
|
return true;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
private defaultRejectMessage(rejections: FileRejection[]): ReactElement {
|
|
191
|
-
// 1) Map rejected files to error messages
|
|
190
|
+
private defaultRejectMessage = (rejections: FileRejection[]): ReactElement => {
|
|
191
|
+
// 1) Map rejected files to error messages. Use custom message for invalid types to avoid displaying dummy MIME type.
|
|
192
192
|
const errorsByFile = fromPairs(
|
|
193
|
-
map(rejections, ({file, errors}) => [
|
|
193
|
+
map(rejections, ({file, errors}) => [
|
|
194
|
+
file.name,
|
|
195
|
+
map(errors, e =>
|
|
196
|
+
e.code === ErrorCode.FileInvalidType
|
|
197
|
+
? `File type must be one of ${this.accept.join(', ')}`
|
|
198
|
+
: e.message
|
|
199
|
+
)
|
|
200
|
+
])
|
|
194
201
|
);
|
|
195
202
|
|
|
196
203
|
// 2) List files with bulleted error messages
|
|
@@ -207,7 +214,7 @@ export class FileChooserModel extends HoistModel {
|
|
|
207
214
|
});
|
|
208
215
|
|
|
209
216
|
return vbox(rejectItems);
|
|
210
|
-
}
|
|
217
|
+
};
|
|
211
218
|
|
|
212
219
|
private getRejectToastSpec(params: Partial<ToastSpec> | boolean): Partial<ToastSpec> {
|
|
213
220
|
if (params == false) return null;
|
|
@@ -15,6 +15,47 @@
|
|
|
15
15
|
margin: 7px 4px;
|
|
16
16
|
align-self: start;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
// Popover mode - collapsed trigger expands into an overlaid popover.
|
|
20
|
+
&--popover > .bp6-popover-target {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex: 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&__trigger {
|
|
26
|
+
.xh-select__value-container--is-multi {
|
|
27
|
+
overflow-y: hidden !important;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Extra class names required to override the default styles of the popover.
|
|
32
|
+
&__popover.bp6-popover.bp6-minimal {
|
|
33
|
+
box-shadow: none;
|
|
34
|
+
|
|
35
|
+
// Overlay the expanded input onto the collapsed 30px trigger. Offset follows Blueprint's
|
|
36
|
+
// resolved placement, which flips (bottom-anchored) near the viewport's bottom edge.
|
|
37
|
+
&.bp6-popover-placement-bottom {
|
|
38
|
+
margin-top: -30px !important;
|
|
39
|
+
}
|
|
40
|
+
&.bp6-popover-placement-top {
|
|
41
|
+
margin-bottom: -30px !important;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Content is portaled out of .xh-filter-chooser - restate the flex rule so it fills the width.
|
|
45
|
+
.bp6-popover-target {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.bp6-popover-content {
|
|
51
|
+
background: transparent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.xh-select__value-container--is-multi {
|
|
55
|
+
height: unset;
|
|
56
|
+
line-height: unset;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
18
59
|
}
|
|
19
60
|
|
|
20
61
|
.xh-filter-chooser-option {
|