@xh/hoist 86.2.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 +58 -0
- package/admin/AppModel.ts +3 -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/userData/roles/RolePanel.ts +2 -2
- package/appcontainer/RouterModel.ts +6 -6
- package/build/types/admin/AppModel.d.ts +3 -4
- 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 +2 -1
- package/build/types/cmp/grid/GridModel.d.ts +2 -2
- package/build/types/cmp/grid/Types.d.ts +8 -1
- 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/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/Select.d.ts +8 -0
- 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/grid/impl/ColChooserModel.d.ts +2 -2
- package/build/types/mobile/cmp/input/Select.d.ts +8 -0
- package/cmp/grid/Grid.ts +10 -2
- package/cmp/grid/GridModel.ts +4 -3
- package/cmp/grid/Types.ts +9 -1
- package/cmp/grid/impl/MenuSupport.ts +1 -1
- 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/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/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/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/input/SegmentedControl.ts +7 -3
- package/desktop/cmp/input/Select.ts +24 -16
- package/docs/routing.md +4 -2
- package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
- 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 +13 -6
- package/mcp/formatters/typescript.ts +7 -3
- package/mcp/util/paths.ts +21 -0
- package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
- 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 +5 -5
- package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import type { GridFilterFieldSpec } from '@xh/hoist/cmp/grid';
|
|
1
2
|
import { HoistModel } from '@xh/hoist/core';
|
|
2
3
|
import { FieldFilterOperator, FieldFilterSpec } from '@xh/hoist/data';
|
|
3
4
|
import { HeaderFilterModel } from '../HeaderFilterModel';
|
|
4
5
|
import { CustomTabModel } from './CustomTabModel';
|
|
5
6
|
type OperatorOptionValue = 'blank' | 'not blank' | FieldFilterOperator;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a row for the given op renders the multi-value `select` input - the only custom-tab input
|
|
9
|
+
* that holds an array of values directly. The single-value inputs (number, date, text) are used
|
|
10
|
+
* otherwise, so array filter values bound for them must be expanded into one row each (see
|
|
11
|
+
* `CustomTabModel.doSyncWithFilter`). Mirrors the input choice made in `CustomRow`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function usesMultiValueInput(fieldSpec: GridFilterFieldSpec, op: FieldFilterOperator): boolean;
|
|
6
14
|
/**
|
|
7
15
|
* @internal
|
|
8
16
|
*/
|
|
@@ -14,7 +22,7 @@ export declare class CustomRowModel extends HoistModel {
|
|
|
14
22
|
inputVal: any;
|
|
15
23
|
/** FieldFilter config output of this row. */
|
|
16
24
|
get value(): FieldFilterSpec;
|
|
17
|
-
get fieldSpec():
|
|
25
|
+
get fieldSpec(): GridFilterFieldSpec;
|
|
18
26
|
get options(): {
|
|
19
27
|
label: string;
|
|
20
28
|
value: OperatorOptionValue;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HoistModel } from '@xh/hoist/core';
|
|
2
|
-
import { CompoundFilterOperator, FilterLike } from '@xh/hoist/data';
|
|
2
|
+
import { CompoundFilterOperator, FieldFilter, FilterLike } from '@xh/hoist/data';
|
|
3
3
|
import { HeaderFilterModel } from '../HeaderFilterModel';
|
|
4
4
|
import { CustomRowModel } from './CustomRowModel';
|
|
5
5
|
export declare class CustomTabModel extends HoistModel {
|
|
@@ -11,11 +11,17 @@ export declare class CustomTabModel extends HoistModel {
|
|
|
11
11
|
get filter(): FilterLike;
|
|
12
12
|
get fieldSpec(): import("../../../../../../../cmp/grid").GridFilterFieldSpec;
|
|
13
13
|
get currentGridFilter(): import("@xh/hoist/data").Filter;
|
|
14
|
-
get columnFilters():
|
|
14
|
+
get columnFilters(): FieldFilter[];
|
|
15
|
+
get columnCompoundFilter(): import("@xh/hoist/data").CompoundFilter;
|
|
15
16
|
constructor(headerFilterModel: HeaderFilterModel);
|
|
16
17
|
syncWithFilter(): void;
|
|
17
18
|
reset(): void;
|
|
18
19
|
addEmptyRow(): void;
|
|
19
20
|
removeRow(model: any): void;
|
|
20
21
|
private doSyncWithFilter;
|
|
22
|
+
private needsExpansion;
|
|
23
|
+
private mergeOpFor;
|
|
24
|
+
private deriveOp;
|
|
25
|
+
private get isRepresentable();
|
|
26
|
+
private collapseToArrayFilters;
|
|
21
27
|
}
|
|
@@ -127,6 +127,14 @@ export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps {
|
|
|
127
127
|
selectOnFocus?: boolean;
|
|
128
128
|
/** Field on provided options for sourcing each option's value (default `value`). */
|
|
129
129
|
valueField?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Function to generate a `SelectOption` for a (non-null) selected value not present in the
|
|
132
|
+
* current options list. Return null to fall back to the default value-as-label behavior.
|
|
133
|
+
*
|
|
134
|
+
* Useful with queryFn-based selects, readonly forms, or any case where options may not be
|
|
135
|
+
* loaded when a value is set, ensuring the value renders with its proper label.
|
|
136
|
+
*/
|
|
137
|
+
generateOptionFn?: (value: any) => SelectOption;
|
|
130
138
|
}
|
|
131
139
|
/**
|
|
132
140
|
* A managed wrapper around the React-Select combobox/dropdown component.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Dropzone from 'react-dropzone';
|
|
2
2
|
export { Dropzone };
|
|
3
|
+
export { ErrorCode } from 'react-dropzone';
|
|
3
4
|
export type { Accept, FileRejection, FileWithPath } from 'react-dropzone';
|
|
4
5
|
export declare const dropzone: import("@xh/hoist/core").ElementFactory<import("react-dropzone").DropzoneProps & import("react").RefAttributes<import("react-dropzone").DropzoneRef>>;
|
|
@@ -27,6 +27,7 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
27
27
|
onScrollbarDragStart?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
28
28
|
onScrollbarDragMove?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
29
29
|
onScrollbarDragEnd?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
30
|
+
onVirtualUpdate?: (swiper: import("swiper/types").Swiper) => void;
|
|
30
31
|
onZoomChange?: (swiper: import("swiper/types").Swiper, scale: number, imageEl: HTMLElement, slideEl: HTMLElement) => void;
|
|
31
32
|
onInit?: (swiper: import("swiper/types").Swiper) => any;
|
|
32
33
|
onBeforeDestroy?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -53,8 +54,8 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
53
54
|
onReachEnd?: (swiper: import("swiper/types").Swiper) => void;
|
|
54
55
|
onToEdge?: (swiper: import("swiper/types").Swiper) => void;
|
|
55
56
|
onFromEdge?: (swiper: import("swiper/types").Swiper) => void;
|
|
56
|
-
onSetTranslate?: (swiper: import("swiper/types").Swiper, translate: number) => void;
|
|
57
|
-
onSetTransition?: (swiper: import("swiper/types").Swiper, transition: number) => void;
|
|
57
|
+
onSetTranslate?: (swiper: import("swiper/types").Swiper, translate: number, byController?: boolean | Swiper) => void;
|
|
58
|
+
onSetTransition?: (swiper: import("swiper/types").Swiper, transition: number, byController?: boolean | Swiper) => void;
|
|
58
59
|
onResize?: (swiper: import("swiper/types").Swiper) => void;
|
|
59
60
|
onObserverUpdate?: (swiper: import("swiper/types").Swiper) => void;
|
|
60
61
|
onBeforeLoopFix?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -77,7 +78,7 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
|
|
|
77
78
|
onBeforeInit?: (swiper: import("swiper/types").Swiper) => void;
|
|
78
79
|
onBeforeResize?: (swiper: import("swiper/types").Swiper) => void;
|
|
79
80
|
onBeforeSlideChangeStart?: (swiper: import("swiper/types").Swiper) => void;
|
|
80
|
-
onBeforeTransitionStart?: (swiper: import("swiper/types").Swiper, speed: number, internal:
|
|
81
|
+
onBeforeTransitionStart?: (swiper: import("swiper/types").Swiper, speed: number, internal: unknown) => void;
|
|
81
82
|
onChangeDirection?: (swiper: import("swiper/types").Swiper) => void;
|
|
82
83
|
onDoubleClick?: (swiper: import("swiper/types").Swiper, event: MouseEvent | TouchEvent | PointerEvent) => void;
|
|
83
84
|
onDestroy?: (swiper: import("swiper/types").Swiper) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GridModel } from '@xh/hoist/cmp/grid';
|
|
1
|
+
import { GridModel, IColChooserModel } from '@xh/hoist/cmp/grid';
|
|
2
2
|
import { HoistModel, HSide } from '@xh/hoist/core';
|
|
3
3
|
import '@xh/hoist/mobile/register';
|
|
4
4
|
/**
|
|
@@ -7,7 +7,7 @@ import '@xh/hoist/mobile/register';
|
|
|
7
7
|
* It is not necessary to manually create instances of this class within an application.
|
|
8
8
|
* @internal
|
|
9
9
|
*/
|
|
10
|
-
export declare class ColChooserModel extends HoistModel {
|
|
10
|
+
export declare class ColChooserModel extends HoistModel implements IColChooserModel {
|
|
11
11
|
xhImpl: boolean;
|
|
12
12
|
gridModel: GridModel;
|
|
13
13
|
showRestoreDefaults: boolean;
|
|
@@ -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
|
@@ -49,6 +49,7 @@ import {composeRefs, createObservableRef, getLayoutProps} from '@xh/hoist/utils/
|
|
|
49
49
|
import classNames from 'classnames';
|
|
50
50
|
import {compact, debounce, isBoolean, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
|
|
51
51
|
import {type MouseEvent} from 'react';
|
|
52
|
+
import {PartialDeep} from 'type-fest';
|
|
52
53
|
import './Grid.scss';
|
|
53
54
|
import {GridModel} from './GridModel';
|
|
54
55
|
import {columnGroupHeader} from './impl/ColumnGroupHeader';
|
|
@@ -66,7 +67,7 @@ export interface GridProps<M extends GridModel = GridModel>
|
|
|
66
67
|
*
|
|
67
68
|
* Note that changes to these options after the component's initial render will be ignored.
|
|
68
69
|
*/
|
|
69
|
-
agOptions?: GridOptions
|
|
70
|
+
agOptions?: PartialDeep<GridOptions>;
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* Callback when the grid has initialized. The component will call this with the ag-Grid
|
|
@@ -392,7 +393,14 @@ export class GridLocalModel extends HoistModel {
|
|
|
392
393
|
return {
|
|
393
394
|
track: () => [model.agApi, model.groupBy],
|
|
394
395
|
run: ([agApi, groupBy]) => {
|
|
395
|
-
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});
|
|
396
404
|
}
|
|
397
405
|
};
|
|
398
406
|
}
|
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';
|
|
@@ -498,7 +499,7 @@ export class GridModel extends HoistModel {
|
|
|
498
499
|
store: Store;
|
|
499
500
|
selModel: StoreSelectionModel;
|
|
500
501
|
treeMode: boolean;
|
|
501
|
-
colChooserModel:
|
|
502
|
+
colChooserModel: IColChooserModel;
|
|
502
503
|
rowClassFn: RowClassFn;
|
|
503
504
|
rowClassRules: Record<string, RowClassRuleFn>;
|
|
504
505
|
contextMenu: GridContextMenuSpec;
|
|
@@ -1259,7 +1260,7 @@ export class GridModel extends HoistModel {
|
|
|
1259
1260
|
}
|
|
1260
1261
|
|
|
1261
1262
|
showColChooser() {
|
|
1262
|
-
|
|
1263
|
+
this.colChooserModel?.open();
|
|
1263
1264
|
}
|
|
1264
1265
|
|
|
1265
1266
|
noteAgColumnStateChanged(agColState: AgColumnState[]) {
|
|
@@ -1986,7 +1987,7 @@ export class GridModel extends HoistModel {
|
|
|
1986
1987
|
};
|
|
1987
1988
|
}
|
|
1988
1989
|
|
|
1989
|
-
private parseChooserModel(chooserModel: GridConfig['colChooserModel']):
|
|
1990
|
+
private parseChooserModel(chooserModel: GridConfig['colChooserModel']): IColChooserModel {
|
|
1990
1991
|
if (!chooserModel) return null;
|
|
1991
1992
|
|
|
1992
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
|
|
@@ -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':
|
|
@@ -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'
|
|
@@ -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 {
|