@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
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,80 @@
1
1
  # Changelog
2
2
 
3
+ ## 86.4.0 - 2026-07-15
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Added `PrefService.isSet()` to report whether the current user has an explicit value on file for a
8
+ preference vs. receiving its server-side default - a distinction that cannot be reliably inferred
9
+ by comparing the value to the default. Requires a hoist-core version that emits the backing
10
+ `isSet` flag; against older servers all prefs report as unset.
11
+
12
+ ### 🐞 Bug Fixes
13
+
14
+ * `PrefService.unset()` now performs a true server-side unset, clearing the user's stored value so
15
+ the preference reverts to its (possibly changing) default and `isSet()` reports `false`.
16
+ Previously it persisted the current default as an explicit user value. Falls back to the legacy
17
+ behavior against hoist-core versions that predate the `xh/unsetPrefs` endpoint.
18
+ * Fixed `FilterChooser` popover mode to render an opaque background when expanded.
19
+
20
+ ## 86.3.0 - 2026-07-10
21
+
22
+ ### 🎁 New Features
23
+
24
+ * `Select` now accepts a `generateOptionFn` prop to resolve an option for a selected value that is
25
+ not present in the current options list (e.g. with `queryFn`-based selects or readonly forms),
26
+ ensuring such values render with their proper label rather than falling back to the raw value.
27
+ * `SegmentedControl` options (desktop and mobile) now accept a `testId`, emitted on the option's
28
+ rendered button as `data-testid` for E2E targeting. If an option omits its own `testId` but the
29
+ control has one, an id is auto-derived as `${controlTestId}-${value}` - restoring parity with
30
+ the legacy `ButtonGroupInput` test-hook pattern for apps migrating between the two.
31
+
32
+ ### 🐞 Bug Fixes
33
+
34
+ * Fixed grid columns configured as `hidden` becoming visible after being grouped and then
35
+ ungrouped. `GridModel` now re-asserts each column's configured visibility whenever `groupBy`
36
+ changes, keeping AG Grid's column state in sync with `columnState`.
37
+ * Fixed `StoreFilterField` and grid Find so an active quick-filter or find query no longer returns
38
+ different results when the grid's `groupBy` changes.
39
+ * Fixed inline grid cell editors to reliably commit their value when editing ends, including popup
40
+ editors (e.g. `textAreaEditor`) within a dialog, which previously dropped edits on Enter or
41
+ click-away.
42
+ * Fixed `Select` to correctly handle non-primitive (object) values: selected-option matching and
43
+ async query de-duplication now use deep equality, so object values no longer render as
44
+ `[object Object]` or collide with one another.
45
+ * Hardened the grid column filter's Custom tab against filters it previously mishandled -
46
+ multi-value clauses are now expanded into editable rows and recombined on commit, and filters it
47
+ cannot represent are left untouched rather than corrupted.
48
+ * Fixed `FilterChooser` popover mode (formerly `PopoverFilterChooser`) so its collapsed control no
49
+ longer disappears when opened - it now always occupies its place in the layout, so surrounding
50
+ elements no longer shift. Its clear and favorites controls also respond to a single click rather
51
+ than requiring the popover to be opened first. This mode is now enabled more naturally via
52
+ a new option `filterChooser({popover: true})`, deprecating `PopoverFilterChooser`, which remains
53
+ as a thin alias.
54
+ * Fixed "not a valid MIME type" console warnings from `FileChooser`. Accepted extensions are now
55
+ passed under a dummy MIME type key, silencing the warnings while continuing to filter selected
56
+ files by extension.
57
+
58
+ ### ⚙️ Typescript API Adjustments
59
+
60
+ * Retyped `GridModel.colChooserModel` as the new cross-platform `IColChooserModel` interface,
61
+ replacing the bare `HoistModel` type and exposing `isOpen`, `open()`, and `close()` directly.
62
+ * Added the exported `HoistRoute` type - Router5's `Route` extended with Hoist's `omit` key - and
63
+ retyped `HoistAppModel.getRoutes()` to return it, so declarative route exclusion (e.g.
64
+ `omit: !XH.getUser().isHoistAdmin`) now type-checks without a cast.
65
+
66
+ ### 🤖 AI Docs + Tooling
67
+
68
+ * Fixed the MCP server and `hoist-ts` CLI TypeScript symbol tools (`search`, `symbol`, `members`)
69
+ returning no results on Windows, where a path-separator mismatch left the symbol index empty.
70
+ Path handling is now normalized so the developer tools work on Windows as well as macOS/Linux.
71
+
72
+ ### 📚 Libraries
73
+
74
+ * @auth0/auth0-spa-js `2.21 → 2.23`
75
+ * @azure/msal-browser `5.14 → 5.16`
76
+ * swiper `12.1 -> 14.0`
77
+
3
78
  ## 86.2.0 - 2026-06-25
4
79
 
5
80
  ### 🎁 New Features
package/admin/AppModel.ts CHANGED
@@ -7,10 +7,11 @@
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
+ import {SECONDS} from '@xh/hoist/utils/datetime';
12
13
  import {without} from 'lodash';
13
- import {Route} from 'router5';
14
+ import {RoleModuleConfig} from './tabs/userData/roles/Types';
14
15
  import {activityTrackingPanel} from './tabs/activity/tracking/ActivityTrackingPanel';
15
16
  import {clientsPanel} from './tabs/clients/ClientsPanel';
16
17
  import {monitorTab} from './tabs/monitor/MonitorTab';
@@ -28,6 +29,9 @@ export class AppModel extends HoistAppModel {
28
29
 
29
30
  viewManagerModels: Record<string, ViewManagerModel> = {};
30
31
 
32
+ /** Role-module config, loaded once at init and shared with the Roles tab. */
33
+ roleModuleConfig: RoleModuleConfig = null;
34
+
31
35
  static get readonly() {
32
36
  return !XH.getUser().isHoistAdmin;
33
37
  }
@@ -35,21 +39,23 @@ export class AppModel extends HoistAppModel {
35
39
  constructor() {
36
40
  super();
37
41
 
38
- this.tabModel = new TabContainerModel({
39
- route: 'default',
40
- tabs: this.createTabs()
41
- });
42
-
43
42
  // Enable managed autosize mode across Hoist Admin console grids.
44
43
  GridModel.defaults.autosizeMode = 'managed';
45
44
  }
46
45
 
47
46
  override async initAsync(ctx: InitContext) {
47
+ await this.loadRoleModuleConfigAsync(ctx);
48
+
49
+ this.tabModel = new TabContainerModel({
50
+ route: 'default',
51
+ tabs: this.createTabs()
52
+ });
53
+
48
54
  await this.initViewManagerModelsAsync(ctx);
49
55
  await super.initAsync(ctx);
50
56
  }
51
57
 
52
- override getRoutes(): Route[] {
58
+ override getRoutes(): HoistRoute[] {
53
59
  return [
54
60
  {
55
61
  name: 'default',
@@ -66,7 +72,7 @@ export class AppModel extends HoistAppModel {
66
72
  return [];
67
73
  }
68
74
 
69
- getTabRoutes(): Route[] {
75
+ getTabRoutes(): HoistRoute[] {
70
76
  return [
71
77
  {
72
78
  name: 'general',
@@ -123,7 +129,8 @@ export class AppModel extends HoistAppModel {
123
129
  }
124
130
 
125
131
  createTabs(): TabConfig[] {
126
- const conf = XH.getConf('xhAdminAppConfig', {});
132
+ const conf = XH.getConf('xhAdminAppConfig', {}),
133
+ rolesEnabled = this.roleModuleConfig?.enabled ?? false;
127
134
 
128
135
  return [
129
136
  {
@@ -160,6 +167,7 @@ export class AppModel extends HoistAppModel {
160
167
  },
161
168
  {
162
169
  id: 'userData',
170
+ title: rolesEnabled ? 'User Data & Roles' : 'User Data',
163
171
  icon: Icon.users(),
164
172
  content: {
165
173
  refreshMode: 'onShowAlways',
@@ -173,7 +181,8 @@ export class AppModel extends HoistAppModel {
173
181
  {
174
182
  id: 'roles',
175
183
  icon: Icon.idBadge(),
176
- content: rolePanel
184
+ content: rolePanel,
185
+ omit: !rolesEnabled
177
186
  },
178
187
  {
179
188
  id: 'prefs',
@@ -220,4 +229,22 @@ export class AppModel extends HoistAppModel {
220
229
  ctx
221
230
  );
222
231
  }
232
+
233
+ //----------------
234
+ // Implementation
235
+ //----------------
236
+ private async loadRoleModuleConfigAsync(ctx: InitContext) {
237
+ // Load role config up-front to title/show the Roles tab (see createTabs).
238
+ // Never block startup if it can't load - the tab defaults to hidden.
239
+ try {
240
+ this.roleModuleConfig = await this.runner(ctx)
241
+ .span('loadRoleModuleConfig')
242
+ .fetchJson({url: 'roleAdmin/config', timeout: 10 * SECONDS});
243
+ } catch (e) {
244
+ XH.handleException(e, {
245
+ message: 'Unable to load roles configuration',
246
+ alertType: 'toast'
247
+ });
248
+ }
249
+ }
223
250
  }
@@ -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,
@@ -4,12 +4,13 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {getAppModel} from '@xh/hoist/admin/AdminUtils';
7
8
  import {RecategorizeDialogModel} from '@xh/hoist/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel';
8
9
  import {FilterChooserModel} from '@xh/hoist/cmp/filter';
9
10
  import {GridModel, tagsRenderer, TreeStyle} from '@xh/hoist/cmp/grid';
10
11
  import * as Col from '@xh/hoist/cmp/grid/columns';
11
12
  import {fragment, p} from '@xh/hoist/cmp/layout';
12
- import {CallContext, HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
13
+ import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
13
14
  import {RecordActionSpec} from '@xh/hoist/data';
14
15
  import {actionCol, calcActionColWidth} from '@xh/hoist/desktop/cmp/grid';
15
16
  import {Icon} from '@xh/hoist/icon';
@@ -39,10 +40,14 @@ export class RoleModel extends HoistModel {
39
40
  @managed recategorizeDialogModel = new RecategorizeDialogModel(this);
40
41
 
41
42
  @observable.ref allRoles: HoistRole[] = [];
42
- @observable.ref moduleConfig: RoleModuleConfig;
43
43
 
44
44
  @bindable showInGroups = true;
45
45
 
46
+ /** Role-module config - loaded at init. */
47
+ get moduleConfig(): RoleModuleConfig {
48
+ return getAppModel().roleModuleConfig;
49
+ }
50
+
46
51
  get readonly() {
47
52
  return !XH.getUser().isHoistRoleManager;
48
53
  }
@@ -56,6 +61,10 @@ export class RoleModel extends HoistModel {
56
61
  constructor() {
57
62
  super();
58
63
  makeObservable(this);
64
+
65
+ this.gridModel = this.createGridModel();
66
+ this.filterChooserModel = this.createFilterChooserModel();
67
+
59
68
  this.addReaction({
60
69
  track: () => this.showInGroups,
61
70
  run: showInGroups => {
@@ -74,9 +83,6 @@ export class RoleModel extends HoistModel {
74
83
  return this.runner({loadSpec})
75
84
  .span('list')
76
85
  .run(async ctx => {
77
- await this.ensureInitializedAsync(ctx);
78
- if (!this.moduleConfig.enabled) return;
79
-
80
86
  const {data} = await XH.fetchJson({url: 'roleAdmin/list'}, ctx);
81
87
  if (loadSpec.isStale) return;
82
88
 
@@ -233,19 +239,6 @@ export class RoleModel extends HoistModel {
233
239
  gridModel.autosizeAsync({includeCollapsedChildren: true});
234
240
  }
235
241
 
236
- private async ensureInitializedAsync(ctx: CallContext) {
237
- if (this.moduleConfig) return;
238
-
239
- const config = await this.runner(ctx).fetchJson({url: 'roleAdmin/config'});
240
- runInAction(() => {
241
- this.moduleConfig = config;
242
- if (config.enabled) {
243
- this.gridModel = this.createGridModel();
244
- this.filterChooserModel = this.createFilterChooserModel();
245
- }
246
- });
247
- }
248
-
249
242
  private processRolesFromServer(roles: Partial<HoistRole>[]): HoistRole[] {
250
243
  return roles.map(role => {
251
244
  const membersByType = mapValues(groupBy(role.members, 'type'), members =>
@@ -9,8 +9,7 @@ import {grid} from '@xh/hoist/cmp/grid';
9
9
  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
- import {errorMessage} from '@xh/hoist/cmp/error';
13
- import {popoverFilterChooser} from '@xh/hoist/desktop/cmp/filter';
12
+ import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
14
13
  import {switchInput} from '@xh/hoist/desktop/cmp/input';
15
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
16
15
  import {recordActionBar} from '@xh/hoist/desktop/cmp/record';
@@ -25,12 +24,6 @@ export const rolePanel = hoistCmp.factory({
25
24
  model: creates(RoleModel),
26
25
 
27
26
  render({className, model}) {
28
- const {moduleConfig} = model;
29
- if (!moduleConfig) return null;
30
- if (!moduleConfig.enabled) {
31
- return errorMessage({error: 'Default Role Module not enabled.'});
32
- }
33
-
34
27
  const {gridModel, readonly} = model;
35
28
  return fragment(
36
29
  panel({
@@ -44,7 +37,7 @@ export const rolePanel = hoistCmp.factory({
44
37
  selModel: gridModel.selModel
45
38
  }),
46
39
  '-',
47
- popoverFilterChooser({flex: 1}),
40
+ filterChooser({popover: true, flex: 1}),
48
41
  '-',
49
42
  switchInput({
50
43
  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,19 +1,22 @@
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';
4
+ import { RoleModuleConfig } from './tabs/userData/roles/Types';
5
5
  export declare class AppModel extends HoistAppModel {
6
6
  tabModel: TabContainerModel;
7
7
  viewManagerModels: Record<string, ViewManagerModel>;
8
+ /** Role-module config, loaded once at init and shared with the Roles tab. */
9
+ roleModuleConfig: RoleModuleConfig;
8
10
  static get readonly(): boolean;
9
11
  constructor();
10
12
  initAsync(ctx: InitContext): Promise<void>;
11
- getRoutes(): Route[];
13
+ getRoutes(): HoistRoute[];
12
14
  getAppMenuButtonExtraItems(): any[];
13
- getTabRoutes(): Route[];
15
+ getTabRoutes(): HoistRoute[];
14
16
  createTabs(): TabConfig[];
15
17
  /** Open the primary business-facing application, typically 'app'. */
16
18
  openPrimaryApp(): void;
17
19
  getPrimaryAppCode(): string;
18
20
  initViewManagerModelsAsync(ctx: InitContext): Promise<void>;
21
+ private loadRoleModuleConfigAsync;
19
22
  }
@@ -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';
@@ -19,8 +19,9 @@ export declare class RoleModel extends HoistModel {
19
19
  readonly roleEditorModel: RoleEditorModel;
20
20
  recategorizeDialogModel: RecategorizeDialogModel;
21
21
  allRoles: HoistRole[];
22
- moduleConfig: RoleModuleConfig;
23
22
  showInGroups: boolean;
23
+ /** Role-module config - loaded at init. */
24
+ get moduleConfig(): RoleModuleConfig;
24
25
  get readonly(): boolean;
25
26
  get selectedRole(): HoistRole;
26
27
  constructor();
@@ -36,7 +37,6 @@ export declare class RoleModel extends HoistModel {
36
37
  private deleteAction;
37
38
  private groupByAction;
38
39
  private displayRoles;
39
- private ensureInitializedAsync;
40
40
  private processRolesFromServer;
41
41
  private processRolesForTreeGrid;
42
42
  private createGridModel;