@xh/hoist 86.2.0 → 86.4.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.
Files changed (75) hide show
  1. package/.gitattributes +4 -0
  2. package/CHANGELOG.md +75 -0
  3. package/admin/AppModel.ts +38 -11
  4. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +56 -6
  5. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +9 -26
  6. package/admin/tabs/clients/ClientsModel.ts +4 -1
  7. package/admin/tabs/clients/activity/ClientDetailModel.ts +5 -1
  8. package/admin/tabs/userData/roles/RoleModel.ts +11 -18
  9. package/admin/tabs/userData/roles/RolePanel.ts +2 -9
  10. package/appcontainer/RouterModel.ts +6 -6
  11. package/build/types/admin/AppModel.d.ts +7 -4
  12. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
  13. package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
  14. package/build/types/admin/tabs/userData/roles/RoleModel.d.ts +2 -2
  15. package/build/types/appcontainer/RouterModel.d.ts +5 -5
  16. package/build/types/cmp/grid/Grid.d.ts +2 -1
  17. package/build/types/cmp/grid/GridModel.d.ts +2 -2
  18. package/build/types/cmp/grid/Types.d.ts +8 -1
  19. package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
  20. package/build/types/core/HoistAppModel.d.ts +2 -3
  21. package/build/types/core/types/Types.d.ts +12 -0
  22. package/build/types/data/Store.d.ts +3 -2
  23. package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
  24. package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
  25. package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
  26. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
  27. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
  28. package/build/types/desktop/cmp/input/Select.d.ts +8 -0
  29. package/build/types/kit/react-dropzone/index.d.ts +1 -0
  30. package/build/types/kit/swiper/index.d.ts +4 -3
  31. package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
  32. package/build/types/mobile/cmp/input/Select.d.ts +8 -0
  33. package/build/types/svc/PrefService.d.ts +12 -1
  34. package/build/types/utils/datetime/LocalDate.d.ts +23 -1
  35. package/cmp/grid/Grid.ts +10 -2
  36. package/cmp/grid/GridModel.ts +4 -3
  37. package/cmp/grid/Types.ts +9 -1
  38. package/cmp/grid/impl/MenuSupport.ts +1 -1
  39. package/cmp/input/SegmentedControlOption.ts +14 -0
  40. package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
  41. package/core/HoistAppModel.ts +2 -3
  42. package/core/types/Types.ts +14 -5
  43. package/data/Store.ts +3 -2
  44. package/desktop/cmp/filechooser/FileChooser.ts +6 -3
  45. package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
  46. package/desktop/cmp/filter/FilterChooser.scss +42 -0
  47. package/desktop/cmp/filter/FilterChooser.ts +147 -7
  48. package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
  49. package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
  50. package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
  51. package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
  52. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
  53. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
  54. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
  55. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
  56. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
  57. package/desktop/cmp/input/SegmentedControl.ts +7 -3
  58. package/desktop/cmp/input/Select.ts +24 -16
  59. package/docs/routing.md +4 -2
  60. package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
  61. package/kit/react-dropzone/index.ts +1 -0
  62. package/mcp/README.md +12 -0
  63. package/mcp/data/ts-registry.spec.ts +156 -0
  64. package/mcp/data/ts-registry.ts +13 -6
  65. package/mcp/formatters/typescript.ts +7 -3
  66. package/mcp/util/paths.ts +21 -0
  67. package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
  68. package/mobile/cmp/input/SegmentedControl.ts +7 -3
  69. package/mobile/cmp/input/Select.ts +21 -15
  70. package/mobx/README.md +2 -1
  71. package/package.json +5 -5
  72. package/svc/PrefService.ts +80 -20
  73. package/svc/README.md +6 -0
  74. package/utils/datetime/LocalDate.ts +24 -1
  75. package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
@@ -1,4 +1,4 @@
1
- import { HoistModel } from '../core';
1
+ import { HoistModel, HoistRoute } from '../core';
2
2
  import { Router, State } from 'router5';
3
3
  import 'router5-plugin-browser';
4
4
  /**
@@ -19,11 +19,11 @@ export declare class RouterModel extends HoistModel {
19
19
  /**
20
20
  * Add routes to the router.
21
21
  *
22
- * @param routes - collection of router5 route spec.
23
- * This method supports an additional keyword 'omit' on each spec, in order to allow declarative
24
- * exclusion. Otherwise these are Router5 configs to be passed directly to the Router5 API.
22
+ * @param routes - collection of {@link HoistRoute} specs. In addition to the standard Router5
23
+ * route config, each spec supports an `omit` keyword to allow declarative exclusion.
24
+ * Otherwise these are Router5 configs to be passed directly to the Router5 API.
25
25
  */
26
- addRoutes(routes: object[]): void;
26
+ addRoutes(routes: HoistRoute[]): void;
27
27
  /**
28
28
  * Add a routeName to the current route, preserving params
29
29
  * @param routeName - the routeName to append
@@ -4,6 +4,7 @@ import { HoistModel, HoistProps, LayoutProps, PlainObject, ReactionSpec, TestSup
4
4
  import { RecordSet } from '@xh/hoist/data/impl/RecordSet';
5
5
  import type { ColDef, ColGroupDef, GetContextMenuItemsParams, GridOptions, GridReadyEvent, ProcessCellForExportParams } from '@xh/hoist/kit/ag-grid';
6
6
  import { type MouseEvent } from 'react';
7
+ import { PartialDeep } from 'type-fest';
7
8
  import './Grid.scss';
8
9
  import { GridModel } from './GridModel';
9
10
  export interface GridProps<M extends GridModel = GridModel> extends HoistProps<M>, LayoutProps, TestSupportProps {
@@ -16,7 +17,7 @@ export interface GridProps<M extends GridModel = GridModel> extends HoistProps<M
16
17
  *
17
18
  * Note that changes to these options after the component's initial render will be ignored.
18
19
  */
19
- agOptions?: GridOptions;
20
+ agOptions?: PartialDeep<GridOptions>;
20
21
  /**
21
22
  * Callback when the grid has initialized. The component will call this with the ag-Grid
22
23
  * event after running its internal handler to associate the ag-Grid APIs with its model.
@@ -1,5 +1,5 @@
1
1
  import { AgGridModel } from '@xh/hoist/cmp/ag-grid';
2
- import { Column, ColumnGroup, ColumnOrGroup, ColumnOrGroupSpec, ColumnSpec, GridAutosizeMode, GridFilterModelConfig, GridGroupSortFn, TreeStyle } from '@xh/hoist/cmp/grid';
2
+ import { Column, ColumnGroup, ColumnOrGroup, ColumnOrGroupSpec, ColumnSpec, GridAutosizeMode, GridFilterModelConfig, GridGroupSortFn, IColChooserModel, TreeStyle } from '@xh/hoist/cmp/grid';
3
3
  import { GridFilterModel } from '@xh/hoist/cmp/grid/filter/GridFilterModel';
4
4
  import { Awaitable, HoistModel, HSide, LoadSpec, PlainObject, SizingMode, Some, TaskObserver, Thunkable, VSide } from '@xh/hoist/core';
5
5
  import { Store, StoreConfig, StoreRecord, StoreRecordId, StoreRecordOrId, StoreSelectionConfig, StoreSelectionModel, StoreTransaction } from '@xh/hoist/data';
@@ -285,7 +285,7 @@ export declare class GridModel extends HoistModel {
285
285
  store: Store;
286
286
  selModel: StoreSelectionModel;
287
287
  treeMode: boolean;
288
- colChooserModel: HoistModel;
288
+ colChooserModel: IColChooserModel;
289
289
  rowClassFn: RowClassFn;
290
290
  rowClassRules: Record<string, RowClassRuleFn>;
291
291
  contextMenu: GridContextMenuSpec;
@@ -1,4 +1,4 @@
1
- import type { HSide, PersistOptions, Some } from '@xh/hoist/core';
1
+ import type { HoistModel, HSide, PersistOptions, Some } from '@xh/hoist/core';
2
2
  import type { FilterBindTarget, FilterMatchMode, FilterValueSource, Store, StoreRecord } from '@xh/hoist/data';
3
3
  import type { CellClassParams, CustomCellEditorProps, HeaderClassParams, HeaderValueGetterParams, ICellRendererParams, IRowNode, ITooltipParams, RowClassParams, ValueSetterParams } from '@xh/hoist/kit/ag-grid';
4
4
  import type { ReactElement, ReactNode } from 'react';
@@ -101,6 +101,13 @@ export interface GridFilterBindTarget extends FilterBindTarget, FilterValueSourc
101
101
  * @returns the formatted value for display.
102
102
  */
103
103
  export type GroupRowRenderer = (context: ICellRendererParams) => ReactNode;
104
+ /** Cross-platform interface for desktop and mobile ColChooserModels. */
105
+ export interface IColChooserModel extends HoistModel {
106
+ readonly gridModel: GridModel;
107
+ readonly isOpen: boolean;
108
+ open(): void;
109
+ close(): void;
110
+ }
104
111
  /**
105
112
  * Configuration for a {@link ColChooserModel} - the model backing the grid column chooser UI.
106
113
  * Passed via the `colChooserModel` config on {@link GridConfig}, or set app-wide via
@@ -21,6 +21,12 @@ export interface SegmentedControlOption {
21
21
  * `intent` default. Defaults to the control's `intent`.
22
22
  */
23
23
  intent?: Intent;
24
+ /**
25
+ * Optional stable identifier emitted on this option's rendered button as `data-testid`, for
26
+ * use by E2E tests. If omitted and the control itself has a `testId`, one is auto-derived as
27
+ * `${controlTestId}-${value}`. If neither is set, no attribute is emitted.
28
+ */
29
+ testId?: string;
24
30
  }
25
31
  /**
26
32
  * Variant of SegmentedControlOption for representing a null/"no value" selection.
@@ -41,4 +47,10 @@ export interface SegmentedControlNullOption {
41
47
  * `intent` default. Defaults to the control's `intent`.
42
48
  */
43
49
  intent?: Intent;
50
+ /**
51
+ * Optional stable identifier emitted on this option's rendered button as `data-testid`, for
52
+ * use by E2E tests. If omitted and the control itself has a `testId`, one is auto-derived as
53
+ * `${controlTestId}-null`. If neither is set, no attribute is emitted.
54
+ */
55
+ testId?: string;
44
56
  }
@@ -1,5 +1,4 @@
1
- import { AppOptionSpec, HoistModel, InitContext, Thunkable } from './';
2
- import { Route } from 'router5';
1
+ import { AppOptionSpec, HoistModel, HoistRoute, InitContext, Thunkable } from './';
3
2
  import { ReactNode } from 'react';
4
3
  /**
5
4
  * Specialized base class for defining the central model for a Hoist app as specified by its
@@ -40,7 +39,7 @@ export declare class HoistAppModel extends HoistModel {
40
39
  /**
41
40
  * Provide the initial set of Router5 Routes to be used by this application.
42
41
  */
43
- getRoutes(): Route[];
42
+ getRoutes(): HoistRoute[];
44
43
  /**
45
44
  * Provide a list of app-wide options to be displayed in the App's built-in Options
46
45
  * dialog, accessible from the default AppBar menu when this method returns non-empty.
@@ -3,6 +3,7 @@ import { LocalDate } from '@xh/hoist/utils/datetime';
3
3
  import { MomentInput } from 'moment';
4
4
  import { Component, FunctionComponent, ReactElement } from 'react';
5
5
  import { DebounceSettings } from 'lodash';
6
+ import { Route } from 'router5';
6
7
  /** Values available for intents. */
7
8
  export type Intent = 'primary' | 'success' | 'warning' | 'danger';
8
9
  /** Values available for App Theme. */
@@ -42,6 +43,17 @@ export type DebounceSpec = number | (DebounceSettings & {
42
43
  */
43
44
  export type Content = ReactElement | FunctionComponent | Component | ElementFactory | (() => ReactElement);
44
45
  export type DateLike = Date | LocalDate | MomentInput;
46
+ /**
47
+ * A Router5 {@link Route} spec, extended with Hoist's `omit` support for declarative exclusion of
48
+ * routes at registration time (e.g. role-gated sections). Note `omit` is evaluated once when routes
49
+ * are added to the router during app startup - it is not reactive.
50
+ *
51
+ * @see HoistAppModel.getRoutes
52
+ */
53
+ export interface HoistRoute extends Omit<Route, 'children'> {
54
+ omit?: Thunkable<boolean>;
55
+ children?: HoistRoute[];
56
+ }
45
57
  /** Valid units for the {@link LocalDate} adjustment methods. */
46
58
  export type LocalDateUnit = 'year' | 'years' | 'quarter' | 'quarters' | 'month' | 'months' | 'week' | 'weeks' | 'day' | 'days';
47
59
  export type PageState =
@@ -267,8 +267,9 @@ export declare class Store extends HoistBase implements FilterBindTarget, Filter
267
267
  * Add new Records to this Store in a local, uncommitted state - i.e. with data that has yet to
268
268
  * be persisted back to, or sourced from, the server or other data source of record.
269
269
  *
270
- * Note that data objects passed to this method must include a unique ID - callers can generate
271
- * one with `XH.genId()` if no natural ID can be produced locally on the client.
270
+ * Note that data objects passed to this method must include a literal `id` property - this
271
+ * method does *not* run the Store's `idSpec` function. Callers can generate an id with
272
+ * `XH.genId()` if no natural ID can be produced locally on the client.
272
273
  *
273
274
  * For StoreRecord additions that originate from the server, call `updateData()` instead.
274
275
  *
@@ -24,6 +24,11 @@ export interface FilterChooserProps extends HoistProps<FilterChooserModel>, Layo
24
24
  placeholder?: string;
25
25
  /** Icon clicked to launch favorites menu. (Defaults to Icon.favorite()) */
26
26
  favoritesIcon?: ReactElement;
27
+ /**
28
+ * True to render collapsed in-place, expanding into a popover when opened - useful in toolbars
29
+ * and other height-constrained containers. Opens in the direction set by `menuPlacement`.
30
+ */
31
+ popover?: boolean;
27
32
  }
28
33
  /**
29
34
  * A Select based control for searching and choosing filters.
@@ -1,9 +1,11 @@
1
1
  import '@xh/hoist/desktop/register';
2
- import './PopoverFilterChooser.scss';
3
2
  import { FilterChooserProps } from './FilterChooser';
4
3
  /**
5
4
  * A wrapper around a FilterChooser that renders in a popover when opened, allowing it to expand
6
5
  * vertically beyond the height of a toolbar.
6
+ *
7
+ * @deprecated Use `filterChooser({popover: true})` instead - the popover behavior is now a built-in
8
+ * mode of `FilterChooser`. This alias will be removed in a future major release.
7
9
  * @see FilterChooser
8
10
  */
9
11
  export declare const PopoverFilterChooser: import("react").FC<FilterChooserProps>, popoverFilterChooser: import("@xh/hoist/core").ElementFactory<FilterChooserProps>;
@@ -1,4 +1,4 @@
1
- import { ColChooserConfig, GridModel } from '@xh/hoist/cmp/grid';
1
+ import { ColChooserConfig, GridModel, IColChooserModel } from '@xh/hoist/cmp/grid';
2
2
  import { HoistModel } from '@xh/hoist/core';
3
3
  import type { FilterMatchMode } from '@xh/hoist/data';
4
4
  import { LeftRightChooserModel } from '@xh/hoist/desktop/cmp/leftrightchooser';
@@ -6,7 +6,7 @@ import { LeftRightChooserModel } from '@xh/hoist/desktop/cmp/leftrightchooser';
6
6
  * State management for the ColChooser component.
7
7
  * @internal
8
8
  */
9
- export declare class ColChooserModel extends HoistModel {
9
+ export declare class ColChooserModel extends HoistModel implements IColChooserModel {
10
10
  xhImpl: boolean;
11
11
  gridModel: GridModel;
12
12
  lrModel: LeftRightChooserModel;
@@ -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(): import("../../../../../../../cmp/grid").GridFilterFieldSpec;
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(): import("@xh/hoist/data").FieldFilter[];
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>>;
@@ -28,6 +28,7 @@ export declare const swiper: import("@xh/hoist/core").ElementFactory<import("rea
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
30
  onZoomChange?: (swiper: import("swiper/types").Swiper, scale: number, imageEl: HTMLElement, slideEl: HTMLElement) => void;
31
+ onVirtualUpdate?: (swiper: import("swiper/types").Swiper) => void;
31
32
  onInit?: (swiper: import("swiper/types").Swiper) => any;
32
33
  onBeforeDestroy?: (swiper: import("swiper/types").Swiper) => void;
33
34
  onSlidesUpdated?: (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: any) => void;
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.
@@ -25,6 +25,13 @@ export declare class PrefService extends HoistService {
25
25
  * Check to see if a given preference has been *defined*.
26
26
  */
27
27
  hasKey(key: string): boolean;
28
+ /**
29
+ * Check whether the current user has an explicit value on file for the given preference, vs.
30
+ * receiving the preference's server-side default value.
31
+ *
32
+ * @param key - unique key used to identify the pref.
33
+ */
34
+ isSet(key: string): boolean;
28
35
  /**
29
36
  * Get the value for a given key, either the user-specific value (if set) or the default.
30
37
  * Typically accessed via the convenience alias {@link XH.getPref}.
@@ -47,7 +54,10 @@ export declare class PrefService extends HoistService {
47
54
  */
48
55
  set(key: string, value: any): void;
49
56
  /**
50
- * Restore a preference to its default value.
57
+ * Restore a preference to its default value, clearing the user's explicit value on the server.
58
+ *
59
+ * Unlike `set()`, this clears the user's explicit value rather than persisting the default as
60
+ * one - so {@link isSet} will report `false` afterwards. Saved asynchronously (see `set()`).
51
61
  */
52
62
  unset(key: string): void;
53
63
  /**
@@ -66,6 +76,7 @@ export declare class PrefService extends HoistService {
66
76
  pushPendingAsync(): Promise<void>;
67
77
  private pushPendingBuffered;
68
78
  private loadPrefsAsync;
79
+ private ensureKeyExists;
69
80
  private validateBeforeSet;
70
81
  private valueIsOfType;
71
82
  }
@@ -9,6 +9,15 @@ import { Moment, MomentInput } from 'moment';
9
9
  * For efficiency and to enable strict equality checks, instances of this class are memoized:
10
10
  * only a single version of the object will be created and returned for each calendar day,
11
11
  * as long as the caller uses one of the *public factory methods*, which they always should!
12
+ *
13
+ * Instances serialize directly to their ISO date string (e.g. '2024-01-15') via built-in
14
+ * `toString()`, `valueOf()`, and `toJSON()` overrides. This means a LocalDate can be passed
15
+ * as-is within the params or body of a `FetchService` request (or any `JSON.stringify()` call)
16
+ * and will serialize as expected - prefer this over calling `toString()` or `format()` yourself.
17
+ *
18
+ * Instances also support natural comparison: because they are memoized, `===` tests whether two
19
+ * references are the same calendar day, while `valueOf()` returning the (lexically sortable) ISO
20
+ * string means the relational operators `<`, `>`, `<=`, `>=` order instances chronologically.
12
21
  */
13
22
  export declare class LocalDate {
14
23
  static readonly VALID_UNITS: Set<LocalDateUnit>;
@@ -33,7 +42,11 @@ export declare class LocalDate {
33
42
  * @param val - any string, timestamp, or date parsable by moment.js.
34
43
  */
35
44
  static from(val: MomentInput | LocalDate): LocalDate;
36
- /** LocalDate representing the current day. */
45
+ /**
46
+ * LocalDate representing the current day in the browser's local time zone.
47
+ * See `currentAppDay()` / `currentServerDay()` to resolve "today" in the app or server zone,
48
+ * which can differ from the browser for users in another region.
49
+ */
37
50
  static today(): LocalDate;
38
51
  /** LocalDate representing the current day in the App TimeZone */
39
52
  static currentAppDay(): LocalDate;
@@ -46,9 +59,17 @@ export declare class LocalDate {
46
59
  /** Is the input value a local Date? */
47
60
  static isLocalDate(val: any): boolean;
48
61
  get isoString(): string;
62
+ /** JS `Date` for this day at midnight in the browser's local time zone. Fresh instance per call. */
49
63
  get date(): Date;
64
+ /** A mutable moment.js clone - safe to modify without affecting this (immutable) instance. */
50
65
  get moment(): Moment;
66
+ /** Epoch millis for this day at midnight in the browser's local time zone. */
51
67
  get timestamp(): number;
68
+ /**
69
+ * Format this date using moment.js format tokens, primarily for display.
70
+ * Note: to send a LocalDate to the server, pass the instance directly rather than a formatted
71
+ * string - it serializes to an ISO date on its own (see class-level docs).
72
+ */
52
73
  format(...args: any[]): string;
53
74
  dayOfWeek(): string;
54
75
  get isToday(): boolean;
@@ -83,6 +104,7 @@ export declare class LocalDate {
83
104
  currentOrNextWeekday(): LocalDate;
84
105
  /** The same date if already a weekday, or the previous weekday. */
85
106
  currentOrPreviousWeekday(): LocalDate;
107
+ /** Difference between this date and `other` in the given unit; positive when this is later. */
86
108
  diff(other: LocalDate, unit?: LocalDateUnit): number;
87
109
  /** @internal - use one of the static factory methods instead. */
88
110
  private constructor();
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) agApi.setRowGroupColumns(groupBy);
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
  }
@@ -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: HoistModel;
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
- (this.colChooserModel as any)?.open();
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']): HoistModel {
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: () => (gridModel.colChooserModel as any)?.open()
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, gridModel?.groupBy],
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 groupBy = gridModel.groupBy,
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
  }
@@ -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(): Route[] {
54
+ getRoutes(): HoistRoute[] {
56
55
  return [];
57
56
  }
58
57