@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.
Files changed (66) hide show
  1. package/.gitattributes +4 -0
  2. package/CHANGELOG.md +58 -0
  3. package/admin/AppModel.ts +3 -4
  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/RolePanel.ts +2 -2
  9. package/appcontainer/RouterModel.ts +6 -6
  10. package/build/types/admin/AppModel.d.ts +3 -4
  11. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +14 -3
  12. package/build/types/admin/tabs/clients/ClientsModel.d.ts +3 -0
  13. package/build/types/appcontainer/RouterModel.d.ts +5 -5
  14. package/build/types/cmp/grid/Grid.d.ts +2 -1
  15. package/build/types/cmp/grid/GridModel.d.ts +2 -2
  16. package/build/types/cmp/grid/Types.d.ts +8 -1
  17. package/build/types/cmp/input/SegmentedControlOption.d.ts +12 -0
  18. package/build/types/core/HoistAppModel.d.ts +2 -3
  19. package/build/types/core/types/Types.d.ts +12 -0
  20. package/build/types/desktop/cmp/filter/FilterChooser.d.ts +5 -0
  21. package/build/types/desktop/cmp/filter/PopoverFilterChooser.d.ts +3 -1
  22. package/build/types/desktop/cmp/grid/impl/colchooser/ColChooserModel.d.ts +2 -2
  23. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.d.ts +9 -1
  24. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.d.ts +8 -2
  25. package/build/types/desktop/cmp/input/Select.d.ts +8 -0
  26. package/build/types/kit/react-dropzone/index.d.ts +1 -0
  27. package/build/types/kit/swiper/index.d.ts +4 -3
  28. package/build/types/mobile/cmp/grid/impl/ColChooserModel.d.ts +2 -2
  29. package/build/types/mobile/cmp/input/Select.d.ts +8 -0
  30. package/cmp/grid/Grid.ts +10 -2
  31. package/cmp/grid/GridModel.ts +4 -3
  32. package/cmp/grid/Types.ts +9 -1
  33. package/cmp/grid/impl/MenuSupport.ts +1 -1
  34. package/cmp/input/SegmentedControlOption.ts +14 -0
  35. package/cmp/store/impl/StoreFilterFieldImplModel.ts +5 -6
  36. package/core/HoistAppModel.ts +2 -3
  37. package/core/types/Types.ts +14 -5
  38. package/desktop/cmp/filechooser/FileChooser.ts +6 -3
  39. package/desktop/cmp/filechooser/FileChooserModel.ts +12 -5
  40. package/desktop/cmp/filter/FilterChooser.scss +41 -0
  41. package/desktop/cmp/filter/FilterChooser.ts +147 -7
  42. package/desktop/cmp/filter/PopoverFilterChooser.ts +7 -84
  43. package/desktop/cmp/grid/editors/impl/InlineEditorModel.ts +6 -1
  44. package/desktop/cmp/grid/find/impl/GridFindFieldImplModel.ts +3 -5
  45. package/desktop/cmp/grid/impl/colchooser/ColChooserModel.ts +2 -2
  46. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRow.ts +12 -12
  47. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomRowModel.ts +18 -0
  48. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.scss +5 -0
  49. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTab.ts +13 -2
  50. package/desktop/cmp/grid/impl/filter/headerfilter/custom/CustomTabModel.ts +97 -14
  51. package/desktop/cmp/input/SegmentedControl.ts +7 -3
  52. package/desktop/cmp/input/Select.ts +24 -16
  53. package/docs/routing.md +4 -2
  54. package/docs/upgrade-notes/v86-upgrade-notes.md +73 -8
  55. package/kit/react-dropzone/index.ts +1 -0
  56. package/mcp/README.md +12 -0
  57. package/mcp/data/ts-registry.spec.ts +156 -0
  58. package/mcp/data/ts-registry.ts +13 -6
  59. package/mcp/formatters/typescript.ts +7 -3
  60. package/mcp/util/paths.ts +21 -0
  61. package/mobile/cmp/grid/impl/ColChooserModel.ts +2 -2
  62. package/mobile/cmp/input/SegmentedControl.ts +7 -3
  63. package/mobile/cmp/input/Select.ts +21 -15
  64. package/mobx/README.md +2 -1
  65. package/package.json +5 -5
  66. package/desktop/cmp/filter/PopoverFilterChooser.scss +0 -42
package/.gitattributes ADDED
@@ -0,0 +1,4 @@
1
+ # Launcher scripts run under node via a `#!/usr/bin/env node` shebang; a CRLF
2
+ # shebang line breaks execution on Linux/macOS. Force LF so Windows checkouts
3
+ # don't rewrite them to CRLF (which otherwise shows them as perpetually modified).
4
+ bin/*.mjs text eol=lf
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 86.3.0 - 2026-07-10
4
+
5
+ ### 🎁 New Features
6
+
7
+ * `Select` now accepts a `generateOptionFn` prop to resolve an option for a selected value that is
8
+ not present in the current options list (e.g. with `queryFn`-based selects or readonly forms),
9
+ ensuring such values render with their proper label rather than falling back to the raw value.
10
+ * `SegmentedControl` options (desktop and mobile) now accept a `testId`, emitted on the option's
11
+ rendered button as `data-testid` for E2E targeting. If an option omits its own `testId` but the
12
+ control has one, an id is auto-derived as `${controlTestId}-${value}` - restoring parity with
13
+ the legacy `ButtonGroupInput` test-hook pattern for apps migrating between the two.
14
+
15
+ ### 🐞 Bug Fixes
16
+
17
+ * Fixed grid columns configured as `hidden` becoming visible after being grouped and then
18
+ ungrouped. `GridModel` now re-asserts each column's configured visibility whenever `groupBy`
19
+ changes, keeping AG Grid's column state in sync with `columnState`.
20
+ * Fixed `StoreFilterField` and grid Find so an active quick-filter or find query no longer returns
21
+ different results when the grid's `groupBy` changes.
22
+ * Fixed inline grid cell editors to reliably commit their value when editing ends, including popup
23
+ editors (e.g. `textAreaEditor`) within a dialog, which previously dropped edits on Enter or
24
+ click-away.
25
+ * Fixed `Select` to correctly handle non-primitive (object) values: selected-option matching and
26
+ async query de-duplication now use deep equality, so object values no longer render as
27
+ `[object Object]` or collide with one another.
28
+ * Hardened the grid column filter's Custom tab against filters it previously mishandled -
29
+ multi-value clauses are now expanded into editable rows and recombined on commit, and filters it
30
+ cannot represent are left untouched rather than corrupted.
31
+ * Fixed `FilterChooser` popover mode (formerly `PopoverFilterChooser`) so its collapsed control no
32
+ longer disappears when opened - it now always occupies its place in the layout, so surrounding
33
+ elements no longer shift. Its clear and favorites controls also respond to a single click rather
34
+ than requiring the popover to be opened first. This mode is now enabled more naturally via
35
+ a new option `filterChooser({popover: true})`, deprecating `PopoverFilterChooser`, which remains
36
+ as a thin alias.
37
+ * Fixed "not a valid MIME type" console warnings from `FileChooser`. Accepted extensions are now
38
+ passed under a dummy MIME type key, silencing the warnings while continuing to filter selected
39
+ files by extension.
40
+
41
+ ### ⚙️ Typescript API Adjustments
42
+
43
+ * Retyped `GridModel.colChooserModel` as the new cross-platform `IColChooserModel` interface,
44
+ replacing the bare `HoistModel` type and exposing `isOpen`, `open()`, and `close()` directly.
45
+ * Added the exported `HoistRoute` type - Router5's `Route` extended with Hoist's `omit` key - and
46
+ retyped `HoistAppModel.getRoutes()` to return it, so declarative route exclusion (e.g.
47
+ `omit: !XH.getUser().isHoistAdmin`) now type-checks without a cast.
48
+
49
+ ### 🤖 AI Docs + Tooling
50
+
51
+ * Fixed the MCP server and `hoist-ts` CLI TypeScript symbol tools (`search`, `symbol`, `members`)
52
+ returning no results on Windows, where a path-separator mismatch left the symbol index empty.
53
+ Path handling is now normalized so the developer tools work on Windows as well as macOS/Linux.
54
+
55
+ ### 📚 Libraries
56
+
57
+ * @auth0/auth0-spa-js `2.21 → 2.23`
58
+ * @azure/msal-browser `5.14 → 5.16`
59
+ * swiper `12.1 -> 14.0`
60
+
3
61
  ## 86.2.0 - 2026-06-25
4
62
 
5
63
  ### 🎁 New Features
package/admin/AppModel.ts CHANGED
@@ -7,10 +7,9 @@
7
7
  import {GridModel} from '@xh/hoist/cmp/grid';
8
8
  import {TabConfig, TabContainerModel} from '@xh/hoist/cmp/tab';
9
9
  import {ViewManagerModel} from '@xh/hoist/cmp/viewmanager';
10
- import {HoistAppModel, InitContext, XH} from '@xh/hoist/core';
10
+ import {HoistAppModel, HoistRoute, InitContext, XH} from '@xh/hoist/core';
11
11
  import {Icon} from '@xh/hoist/icon';
12
12
  import {without} from 'lodash';
13
- import {Route} from 'router5';
14
13
  import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
15
14
  import {clientsPanel} from './tabs/clients/ClientsPanel';
16
15
  import {monitorTab} from './tabs/monitor/MonitorTab';
@@ -49,7 +48,7 @@ export class AppModel extends HoistAppModel {
49
48
  await super.initAsync(ctx);
50
49
  }
51
50
 
52
- override getRoutes(): Route[] {
51
+ override getRoutes(): HoistRoute[] {
53
52
  return [
54
53
  {
55
54
  name: 'default',
@@ -66,7 +65,7 @@ export class AppModel extends HoistAppModel {
66
65
  return [];
67
66
  }
68
67
 
69
- getTabRoutes(): Route[] {
68
+ getTabRoutes(): HoistRoute[] {
70
69
  return [
71
70
  {
72
71
  name: 'general',
@@ -15,7 +15,17 @@ import {FilterChooserModel} from '@xh/hoist/cmp/filter';
15
15
  import {FormModel} from '@xh/hoist/cmp/form';
16
16
  import {ColumnRenderer, ColumnSpec, GridModel, TreeStyle} from '@xh/hoist/cmp/grid';
17
17
  import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
18
- import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
18
+ import {
19
+ HoistModel,
20
+ LoadSpec,
21
+ LocalDateUnit,
22
+ managed,
23
+ PersistableState,
24
+ PersistenceProvider,
25
+ persistOptions,
26
+ PlainObject,
27
+ XH
28
+ } from '@xh/hoist/core';
19
29
  import {Cube, CubeFieldSpec, FieldSpec, ViewRowData} from '@xh/hoist/data';
20
30
  import {dateRenderer, dateTimeSecRenderer, numberRenderer} from '@xh/hoist/format';
21
31
  import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
@@ -24,6 +34,23 @@ import {compact, get, isEmpty, isEqual, round} from 'lodash';
24
34
  import moment from 'moment';
25
35
  import {ActivityDetailProvider} from './detail/ActivityDetailModel';
26
36
 
37
+ /**
38
+ * A relative time interval (e.g. 7 days) ending on the current app day. Drives the quick-select
39
+ * buttons and the persisted time period, which rolls forward to end on today on restore.
40
+ */
41
+ export interface ActivityInterval {
42
+ value: number;
43
+ unit: LocalDateUnit;
44
+ }
45
+
46
+ /** Quick-select intervals offered in the toolbar - labels are derived as `${value}${unit[0]}`. */
47
+ export const INTERVALS: ActivityInterval[] = [
48
+ {value: 6, unit: 'months'},
49
+ {value: 1, unit: 'months'},
50
+ {value: 7, unit: 'days'},
51
+ {value: 1, unit: 'days'}
52
+ ];
53
+
27
54
  export class ActivityTrackingModel extends HoistModel implements ActivityDetailProvider {
28
55
  override telemetryPrefix = 'xh.client.admin.tracking';
29
56
 
@@ -115,6 +142,7 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
115
142
  this.markPersist('showFilterChooser');
116
143
 
117
144
  this.formModel = this.createQueryFormModel();
145
+ this.setupPeriodPersistence();
118
146
 
119
147
  this.dataFieldsEditorModel = new DataFieldsEditorModel(this);
120
148
  this.markPersist('dataFields');
@@ -203,17 +231,17 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
203
231
  endDay.setValue(newEnd);
204
232
  }
205
233
 
206
- // Set the start date by taking the end date and pushing back [value] [units] - then pushing
234
+ // Set the start date by taking the end date and pushing back the interval - then pushing
207
235
  // forward one day as the day range query is inclusive.
208
- adjustStartDate(value, unit) {
236
+ adjustStartDate(interval: ActivityInterval) {
209
237
  this.formModel.setValues({
210
- startDay: this.endDay.subtract(value, unit).nextDay()
238
+ startDay: this.endDay.subtract(interval.value, interval.unit).nextDay()
211
239
  });
212
240
  }
213
241
 
214
- isInterval(value, unit) {
242
+ isInterval(interval: ActivityInterval) {
215
243
  const {startDay, endDay} = this.formModel.values;
216
- return startDay === endDay.subtract(value, unit).nextDay();
244
+ return startDay === endDay.subtract(interval.value, interval.unit).nextDay();
217
245
  }
218
246
 
219
247
  getDisplayName(fieldName: string) {
@@ -234,6 +262,28 @@ export class ActivityTrackingModel extends HoistModel implements ActivityDetailP
234
262
  });
235
263
  }
236
264
 
265
+ // Persist the selected time period as part of the active view, if endDay is currentDay
266
+ private setupPeriodPersistence() {
267
+ PersistenceProvider.create<ActivityInterval>({
268
+ persistOptions: persistOptions({path: 'queryPeriod'}, this.persistWith),
269
+ owner: this,
270
+ target: {
271
+ getPersistableState: () => {
272
+ const currDay = this.endDay === LocalDate.currentAppDay(),
273
+ interval = currDay ? INTERVALS.find(it => this.isInterval(it)) : null;
274
+ return new PersistableState(interval ?? null);
275
+ },
276
+ setPersistableState: ({value}) => {
277
+ if (value) {
278
+ const endDay = LocalDate.currentAppDay();
279
+ this.formModel.setValues({endDay});
280
+ this.adjustStartDate(value);
281
+ }
282
+ }
283
+ }
284
+ });
285
+ }
286
+
237
287
  private async loadGridAsync() {
238
288
  const {cube, gridModel, dimensions} = this,
239
289
  data = cube.executeQuery({
@@ -11,7 +11,7 @@ import {grid} from '@xh/hoist/cmp/grid';
11
11
  import {div, filler, hframe} from '@xh/hoist/cmp/layout';
12
12
  import {creates, hoistCmp} from '@xh/hoist/core';
13
13
  import {button, buttonGroup, colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
14
- import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
14
+ import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
15
15
  import {formField} from '@xh/hoist/desktop/cmp/form';
16
16
  import {groupingChooser} from '@xh/hoist/desktop/cmp/grouping';
17
17
  import {dateInput, DateInputProps, select} from '@xh/hoist/desktop/cmp/input';
@@ -20,7 +20,7 @@ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
20
20
  import {viewManager} from '@xh/hoist/desktop/cmp/viewmanager';
21
21
  import {Icon} from '@xh/hoist/icon';
22
22
  import {LocalDate} from '@xh/hoist/utils/datetime';
23
- import {ActivityTrackingModel} from './ActivityTrackingModel';
23
+ import {INTERVALS, ActivityTrackingModel} from './ActivityTrackingModel';
24
24
  import {aggChartPanel} from '@xh/hoist/admin/tabs/activity/tracking/chart/AggChartPanel';
25
25
  import {activityDetailView} from './detail/ActivityDetailView';
26
26
  import './ActivityTracking.scss';
@@ -75,32 +75,14 @@ const tbar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
75
75
  disabled: model.endDay >= LocalDate.currentAppDay()
76
76
  }),
77
77
  buttonGroup({
78
- items: [
78
+ items: INTERVALS.map(interval =>
79
79
  button({
80
- text: '6m',
81
- onClick: () => model.adjustStartDate(6, 'months'),
82
- active: model.isInterval(6, 'months'),
83
- ...dateBtn
84
- }),
85
- button({
86
- text: '1m',
87
- onClick: () => model.adjustStartDate(1, 'months'),
88
- active: model.isInterval(1, 'months'),
89
- ...dateBtn
90
- }),
91
- button({
92
- text: '7d',
93
- onClick: () => model.adjustStartDate(7, 'days'),
94
- active: model.isInterval(7, 'days'),
95
- ...dateBtn
96
- }),
97
- button({
98
- text: '1d',
99
- onClick: () => model.adjustStartDate(1, 'days'),
100
- active: model.isInterval(1, 'days'),
80
+ text: `${interval.value}${interval.unit[0]}`,
81
+ onClick: () => model.adjustStartDate(interval),
82
+ active: model.isInterval(interval),
101
83
  ...dateBtn
102
84
  })
103
- ]
85
+ )
104
86
  }),
105
87
  toolbarSep(),
106
88
  filterChooserToggleButton(),
@@ -137,7 +119,8 @@ const filterChooserToggleButton = hoistCmp.factory<ActivityTrackingModel>(({mode
137
119
  const filterBar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
138
120
  return model.showFilterChooser
139
121
  ? toolbar(
140
- popoverFilterChooser({
122
+ filterChooser({
123
+ popover: true,
141
124
  flex: 1,
142
125
  enableClear: true
143
126
  })
@@ -9,7 +9,7 @@ import {AppModel} from '@xh/hoist/admin/AppModel';
9
9
  import * as Col from '@xh/hoist/admin/columns';
10
10
  import {GridModel} from '@xh/hoist/cmp/grid';
11
11
  import {div, p} from '@xh/hoist/cmp/layout';
12
- import {LoadSpec, managed, XH} from '@xh/hoist/core';
12
+ import {LoadSpec, managed, persistOptions, XH} from '@xh/hoist/core';
13
13
  import {RecordActionSpec, StoreRecord} from '@xh/hoist/data';
14
14
  import {textInput} from '@xh/hoist/desktop/cmp/input';
15
15
  import {Icon} from '@xh/hoist/icon';
@@ -23,6 +23,8 @@ import {BaseAdminTabModel} from '@xh/hoist/admin/tabs/BaseAdminTabModel';
23
23
  export class ClientsModel extends BaseAdminTabModel {
24
24
  override telemetryPrefix = 'xh.client.admin.clients';
25
25
 
26
+ override persistWith = {localStorageKey: 'xhAdminClientsState'};
27
+
26
28
  @observable
27
29
  lastRefresh: number;
28
30
 
@@ -150,6 +152,7 @@ export class ClientsModel extends BaseAdminTabModel {
150
152
  return new GridModel({
151
153
  emptyText: 'No clients connected.',
152
154
  groupBy: this.groupBy,
155
+ persistWith: persistOptions(this.persistWith, {path: 'mainGrid'}),
153
156
  colChooserModel: true,
154
157
  enableExport: true,
155
158
  selModel: 'multiple',
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {ClientsModel} from '../ClientsModel';
8
8
  import {ColumnSpec} from '@xh/hoist/cmp/grid';
9
- import {HoistModel, LoadSpec, lookup, PlainObject, XH} from '@xh/hoist/core';
9
+ import {HoistModel, LoadSpec, lookup, persistOptions, PlainObject, XH} from '@xh/hoist/core';
10
10
  import {StoreRecord} from '@xh/hoist/data';
11
11
  import {bindable, computed, makeObservable} from '@xh/hoist/mobx';
12
12
  import {ReactNode} from 'react';
@@ -53,6 +53,10 @@ export class ClientDetailModel extends HoistModel implements ActivityDetailProvi
53
53
  override onLinked() {
54
54
  super.onLinked();
55
55
 
56
+ this.persistWith = persistOptions(this.clientsModel.persistWith, {
57
+ pathPrefix: 'activityGrid'
58
+ });
59
+
56
60
  this.addReaction(
57
61
  {
58
62
  track: () => this.clientsModel.gridModel.selectedRecord,
@@ -10,7 +10,7 @@ import {fragment, hframe, vframe} from '@xh/hoist/cmp/layout';
10
10
  import {creates, hoistCmp} from '@xh/hoist/core';
11
11
  import {button, colChooserButton} from '@xh/hoist/desktop/cmp/button';
12
12
  import {errorMessage} from '@xh/hoist/cmp/error';
13
- import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
13
+ import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
14
14
  import {switchInput} from '@xh/hoist/desktop/cmp/input';
15
15
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
16
  import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
@@ -44,7 +44,7 @@ export const rolePanel = hoistCmp.factory({
44
44
  selModel: gridModel.selModel
45
45
  }),
46
46
  '-',
47
- popoverFilterChooser({flex: 1}),
47
+ filterChooser({popover: true, flex: 1}),
48
48
  '-',
49
49
  switchInput({
50
50
  bind: 'showInGroups',
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistModel} from '../core';
7
+ import {HoistModel, HoistRoute} from '../core';
8
8
  import {action, observable, makeObservable} from '@xh/hoist/mobx';
9
9
  import {mergeDeep} from '@xh/hoist/utils/js';
10
10
  import {isOmitted} from '@xh/hoist/utils/impl';
@@ -38,11 +38,11 @@ export class RouterModel extends HoistModel {
38
38
  /**
39
39
  * Add routes to the router.
40
40
  *
41
- * @param routes - collection of router5 route spec.
42
- * This method supports an additional keyword 'omit' on each spec, in order to allow declarative
43
- * exclusion. Otherwise these are Router5 configs to be passed directly to the Router5 API.
41
+ * @param routes - collection of {@link HoistRoute} specs. In addition to the standard Router5
42
+ * route config, each spec supports an `omit` keyword to allow declarative exclusion.
43
+ * Otherwise these are Router5 configs to be passed directly to the Router5 API.
44
44
  */
45
- addRoutes(routes: object[]) {
45
+ addRoutes(routes: HoistRoute[]) {
46
46
  this.router.add(this.preprocessRoutes(routes));
47
47
  }
48
48
 
@@ -100,7 +100,7 @@ export class RouterModel extends HoistModel {
100
100
  return ret;
101
101
  }
102
102
 
103
- private preprocessRoutes(routes) {
103
+ private preprocessRoutes(routes: HoistRoute[]): HoistRoute[] {
104
104
  const ret = routes.filter(r => !isOmitted(r));
105
105
  ret.forEach(r => {
106
106
  if (r.children) r.children = this.preprocessRoutes(r.children);
@@ -1,16 +1,15 @@
1
1
  import { TabConfig, TabContainerModel } from '@xh/hoist/cmp/tab';
2
2
  import { ViewManagerModel } from '@xh/hoist/cmp/viewmanager';
3
- import { HoistAppModel, InitContext } from '@xh/hoist/core';
4
- import { Route } from 'router5';
3
+ import { HoistAppModel, HoistRoute, InitContext } from '@xh/hoist/core';
5
4
  export declare class AppModel extends HoistAppModel {
6
5
  tabModel: TabContainerModel;
7
6
  viewManagerModels: Record<string, ViewManagerModel>;
8
7
  static get readonly(): boolean;
9
8
  constructor();
10
9
  initAsync(ctx: InitContext): Promise<void>;
11
- getRoutes(): Route[];
10
+ getRoutes(): HoistRoute[];
12
11
  getAppMenuButtonExtraItems(): any[];
13
- getTabRoutes(): Route[];
12
+ getTabRoutes(): HoistRoute[];
14
13
  createTabs(): TabConfig[];
15
14
  /** Open the primary business-facing application, typically 'app'. */
16
15
  openPrimaryApp(): void;
@@ -3,10 +3,20 @@ import { FilterChooserModel } from '@xh/hoist/cmp/filter';
3
3
  import { FormModel } from '@xh/hoist/cmp/form';
4
4
  import { ColumnSpec, GridModel } from '@xh/hoist/cmp/grid';
5
5
  import { GroupingChooserModel } from '@xh/hoist/cmp/grouping';
6
- import { HoistModel, LoadSpec, PlainObject } from '@xh/hoist/core';
6
+ import { HoistModel, LoadSpec, LocalDateUnit, PlainObject } from '@xh/hoist/core';
7
7
  import { Cube } from '@xh/hoist/data';
8
8
  import { LocalDate } from '@xh/hoist/utils/datetime';
9
9
  import { ActivityDetailProvider } from './detail/ActivityDetailModel';
10
+ /**
11
+ * A relative time interval (e.g. 7 days) ending on the current app day. Drives the quick-select
12
+ * buttons and the persisted time period, which rolls forward to end on today on restore.
13
+ */
14
+ export interface ActivityInterval {
15
+ value: number;
16
+ unit: LocalDateUnit;
17
+ }
18
+ /** Quick-select intervals offered in the toolbar - labels are derived as `${value}${unit[0]}`. */
19
+ export declare const INTERVALS: ActivityInterval[];
10
20
  export declare class ActivityTrackingModel extends HoistModel implements ActivityDetailProvider {
11
21
  telemetryPrefix: string;
12
22
  /** FormModel for server-side querying controls. */
@@ -45,10 +55,11 @@ export declare class ActivityTrackingModel extends HoistModel implements Activit
45
55
  setDataFields(dataFields: ActivityTrackingDataFieldSpec[]): void;
46
56
  toggleFilterChooser(): void;
47
57
  adjustDates(dir: 'add' | 'subtract'): void;
48
- adjustStartDate(value: any, unit: any): void;
49
- isInterval(value: any, unit: any): boolean;
58
+ adjustStartDate(interval: ActivityInterval): void;
59
+ isInterval(interval: ActivityInterval): boolean;
50
60
  getDisplayName(fieldName: string): string;
51
61
  private createQueryFormModel;
62
+ private setupPeriodPersistence;
52
63
  private loadGridAsync;
53
64
  private cubeLabelComparator;
54
65
  private getComparableValForDim;
@@ -4,6 +4,9 @@ import { RecordActionSpec, StoreRecord } from '@xh/hoist/data';
4
4
  import { BaseAdminTabModel } from '@xh/hoist/admin/tabs/BaseAdminTabModel';
5
5
  export declare class ClientsModel extends BaseAdminTabModel {
6
6
  telemetryPrefix: string;
7
+ persistWith: {
8
+ localStorageKey: string;
9
+ };
7
10
  lastRefresh: number;
8
11
  gridModel: GridModel;
9
12
  groupBy: 'user' | 'instance';
@@ -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 =
@@ -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;