@xh/hoist 87.1.1 → 87.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 (270) hide show
  1. package/.npmignore +39 -0
  2. package/CHANGELOG.md +108 -20
  3. package/admin/AdminUtils.ts +64 -0
  4. package/admin/App.scss +88 -0
  5. package/admin/columns/UserData.ts +30 -3
  6. package/admin/detail/RestDetailModel.ts +64 -0
  7. package/admin/detail/RestDetailPanel.ts +106 -0
  8. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
  9. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
  10. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
  11. package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
  12. package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
  13. package/admin/tabs/general/config/ConfigPanel.ts +41 -53
  14. package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
  15. package/admin/tabs/general/config/ConfigValue.scss +14 -0
  16. package/admin/tabs/general/config/ConfigValue.ts +104 -65
  17. package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
  18. package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
  19. package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
  20. package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
  21. package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
  22. package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
  23. package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
  24. package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
  25. package/appcontainer/AboutDialogModel.ts +1 -0
  26. package/appcontainer/AppContainerModel.ts +3 -0
  27. package/appcontainer/AppStateModel.ts +1 -0
  28. package/appcontainer/BannerSourceModel.ts +1 -0
  29. package/appcontainer/ChangelogDialogModel.ts +1 -0
  30. package/appcontainer/ColChooserOptionsModel.ts +1 -0
  31. package/appcontainer/ExceptionDialogModel.ts +1 -0
  32. package/appcontainer/FeedbackDialogModel.ts +1 -0
  33. package/appcontainer/ImpersonationBarModel.ts +1 -0
  34. package/appcontainer/MessageSourceModel.ts +1 -0
  35. package/appcontainer/OptionsDialogModel.ts +1 -0
  36. package/appcontainer/PageStateModel.ts +1 -0
  37. package/appcontainer/RouterModel.ts +2 -0
  38. package/appcontainer/SizingModeModel.ts +1 -0
  39. package/appcontainer/ThemeModel.ts +1 -0
  40. package/appcontainer/ToastSourceModel.ts +1 -0
  41. package/appcontainer/UserAgentModel.ts +1 -0
  42. package/appcontainer/ViewportSizeModel.ts +1 -0
  43. package/build/types/admin/AdminUtils.d.ts +29 -0
  44. package/build/types/admin/columns/UserData.d.ts +3 -0
  45. package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
  46. package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
  47. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
  48. package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
  49. package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
  50. package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
  51. package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
  52. package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
  53. package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
  54. package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
  55. package/build/types/appcontainer/AppStateModel.d.ts +1 -0
  56. package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
  57. package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
  58. package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
  59. package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
  60. package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
  61. package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
  62. package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
  63. package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
  64. package/build/types/appcontainer/PageStateModel.d.ts +1 -0
  65. package/build/types/appcontainer/RouterModel.d.ts +1 -0
  66. package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
  67. package/build/types/appcontainer/ThemeModel.d.ts +1 -0
  68. package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
  69. package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
  70. package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
  71. package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
  72. package/build/types/cmp/card/Card.d.ts +2 -2
  73. package/build/types/cmp/chart/ChartModel.d.ts +2 -0
  74. package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
  75. package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
  76. package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
  77. package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
  78. package/build/types/cmp/daterange/Types.d.ts +152 -0
  79. package/build/types/cmp/daterange/index.d.ts +4 -0
  80. package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
  81. package/build/types/cmp/form/FormModel.d.ts +3 -1
  82. package/build/types/cmp/grid/GridModel.d.ts +2 -0
  83. package/build/types/cmp/grid/Types.d.ts +2 -0
  84. package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
  85. package/build/types/cmp/grid/impl/ColumnWidthCalculator.d.ts +1 -1
  86. package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
  87. package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
  88. package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
  89. package/build/types/cmp/tab/TabModel.d.ts +3 -1
  90. package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
  91. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
  92. package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
  93. package/build/types/core/HoistBase.d.ts +7 -0
  94. package/build/types/core/HoistProps.d.ts +26 -1
  95. package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
  96. package/build/types/core/types/Telemetry.d.ts +1 -1
  97. package/build/types/data/Store.d.ts +3 -1
  98. package/build/types/data/StoreSelectionModel.d.ts +3 -1
  99. package/build/types/data/cube/Cube.d.ts +5 -2
  100. package/build/types/data/cube/View.d.ts +2 -0
  101. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
  102. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
  103. package/build/types/desktop/cmp/button/Button.d.ts +2 -2
  104. package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
  105. package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
  106. package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
  107. package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
  108. package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
  109. package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
  110. package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
  111. package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
  112. package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
  113. package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
  114. package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
  115. package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
  116. package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
  117. package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
  118. package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
  119. package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
  120. package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
  121. package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
  122. package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
  123. package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
  124. package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
  125. package/build/types/desktop/cmp/input/index.d.ts +1 -0
  126. package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
  127. package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
  128. package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
  129. package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
  130. package/build/types/desktop/hooks/UseHotkeys.d.ts +4 -3
  131. package/build/types/icon/Icon.d.ts +3 -0
  132. package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
  133. package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
  134. package/build/types/kit/react-day-picker/index.d.ts +3 -3
  135. package/build/types/mobile/cmp/button/Button.d.ts +2 -2
  136. package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
  137. package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
  138. package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
  139. package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
  140. package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +8 -2
  141. package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
  142. package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
  143. package/build/types/svc/InspectorService.d.ts +1 -0
  144. package/build/types/utils/js/LangUtils.d.ts +24 -5
  145. package/cmp/README.md +1 -0
  146. package/cmp/ag-grid/AgGridModel.ts +5 -0
  147. package/cmp/badge/Badge.ts +5 -3
  148. package/cmp/card/Card.ts +4 -3
  149. package/cmp/chart/ChartModel.ts +11 -1
  150. package/cmp/clock/Clock.ts +1 -1
  151. package/cmp/dataview/DataViewModel.ts +7 -1
  152. package/cmp/daterange/DateRangePickerModel.ts +628 -0
  153. package/cmp/daterange/DateRangePresets.ts +274 -0
  154. package/cmp/daterange/DateRangeUtils.ts +557 -0
  155. package/cmp/daterange/README.md +434 -0
  156. package/cmp/daterange/Types.ts +203 -0
  157. package/cmp/daterange/index.ts +10 -0
  158. package/cmp/filter/FilterChooserModel.ts +7 -4
  159. package/cmp/form/FormModel.ts +6 -0
  160. package/cmp/form/README.md +1 -0
  161. package/cmp/grid/GridModel.ts +24 -4
  162. package/cmp/grid/Types.ts +3 -0
  163. package/cmp/grid/filter/GridFilterModel.ts +3 -1
  164. package/cmp/grid/impl/ColumnWidthCalculator.ts +5 -10
  165. package/cmp/grouping/GroupingChooserModel.ts +6 -1
  166. package/cmp/input/HoistInputProps.ts +50 -2
  167. package/cmp/input/README.md +42 -0
  168. package/cmp/layout/Box.ts +3 -2
  169. package/cmp/tab/TabContainerModel.ts +9 -1
  170. package/cmp/tab/TabModel.ts +5 -0
  171. package/cmp/treemap/SplitTreeMapModel.ts +12 -2
  172. package/cmp/treemap/TreeMapModel.ts +6 -1
  173. package/cmp/viewmanager/ViewManagerModel.ts +6 -1
  174. package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
  175. package/core/HoistBase.ts +11 -0
  176. package/core/HoistProps.ts +27 -0
  177. package/core/README.md +46 -0
  178. package/core/impl/InstallServices.ts +5 -1
  179. package/core/model/RootRefreshContextModel.ts +2 -0
  180. package/core/types/Telemetry.ts +1 -1
  181. package/data/README.md +3 -1
  182. package/data/Store.ts +7 -0
  183. package/data/StoreSelectionModel.ts +5 -1
  184. package/data/cube/Cube.ts +12 -3
  185. package/data/cube/View.ts +5 -1
  186. package/desktop/README.md +2 -0
  187. package/desktop/appcontainer/LoginPanel.ts +2 -0
  188. package/desktop/cmp/button/Button.scss +71 -42
  189. package/desktop/cmp/button/Button.ts +10 -1
  190. package/desktop/cmp/button/ButtonGroup.ts +4 -1
  191. package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
  192. package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
  193. package/desktop/cmp/dash/DashConfig.ts +3 -0
  194. package/desktop/cmp/dash/DashViewModel.ts +14 -1
  195. package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
  196. package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
  197. package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
  198. package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
  199. package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
  200. package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
  201. package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
  202. package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
  203. package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
  204. package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
  205. package/desktop/cmp/daterange/index.ts +8 -0
  206. package/desktop/cmp/dock/DockContainerModel.ts +6 -1
  207. package/desktop/cmp/dock/DockViewModel.ts +6 -1
  208. package/desktop/cmp/filechooser/FileChooser.ts +4 -0
  209. package/desktop/cmp/form/FormField.ts +1 -0
  210. package/desktop/cmp/grid/editors/DateEditor.ts +11 -2
  211. package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
  212. package/desktop/cmp/input/Checkbox.ts +1 -0
  213. package/desktop/cmp/input/CodeInput.ts +1 -0
  214. package/desktop/cmp/input/DateInput.scss +1 -1
  215. package/desktop/cmp/input/DateInput.ts +1 -0
  216. package/desktop/cmp/input/IntentInput.scss +202 -0
  217. package/desktop/cmp/input/IntentInput.ts +213 -0
  218. package/desktop/cmp/input/NumberInput.ts +11 -2
  219. package/desktop/cmp/input/Picker.ts +1 -0
  220. package/desktop/cmp/input/RadioInput.ts +2 -1
  221. package/desktop/cmp/input/SegmentedControl.scss +57 -5
  222. package/desktop/cmp/input/SegmentedControl.ts +66 -11
  223. package/desktop/cmp/input/Select.ts +9 -7
  224. package/desktop/cmp/input/Slider.ts +2 -0
  225. package/desktop/cmp/input/SwitchInput.ts +1 -0
  226. package/desktop/cmp/input/TextArea.ts +11 -2
  227. package/desktop/cmp/input/TextInput.ts +11 -2
  228. package/desktop/cmp/input/index.ts +1 -0
  229. package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
  230. package/desktop/cmp/panel/Panel.ts +8 -5
  231. package/desktop/cmp/panel/PanelModel.ts +8 -0
  232. package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
  233. package/desktop/cmp/rest/RestGridModel.ts +4 -0
  234. package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
  235. package/desktop/cmp/toolbar/Toolbar.scss +20 -6
  236. package/desktop/hooks/UseHotkeys.ts +41 -12
  237. package/docs/README.md +2 -0
  238. package/docs/build-and-publish.md +23 -7
  239. package/docs/doc-registry.json +8 -0
  240. package/docs/version-compatibility.md +2 -0
  241. package/icon/Icon.ts +9 -0
  242. package/icon/index.ts +24 -0
  243. package/inspector/README.md +8 -2
  244. package/inspector/impl/InspectorUtils.ts +12 -0
  245. package/inspector/instances/DiagnosticsModel.ts +3 -2
  246. package/inspector/instances/InstancesModel.ts +81 -14
  247. package/inspector/instances/InstancesPanel.ts +6 -0
  248. package/kit/react-day-picker/index.ts +3 -3
  249. package/mobile/appcontainer/LoginPanel.ts +2 -0
  250. package/mobile/cmp/button/Button.ts +16 -3
  251. package/mobile/cmp/form/FormField.ts +1 -0
  252. package/mobile/cmp/input/Checkbox.ts +1 -0
  253. package/mobile/cmp/input/DateInput.ts +1 -0
  254. package/mobile/cmp/input/Label.ts +13 -8
  255. package/mobile/cmp/input/NumberInput.ts +11 -2
  256. package/mobile/cmp/input/SearchInput.ts +1 -0
  257. package/mobile/cmp/input/SegmentedControl.scss +36 -6
  258. package/mobile/cmp/input/SegmentedControl.ts +37 -11
  259. package/mobile/cmp/input/Select.ts +2 -0
  260. package/mobile/cmp/input/SwitchInput.ts +1 -0
  261. package/mobile/cmp/input/TextArea.ts +11 -2
  262. package/mobile/cmp/input/TextInput.ts +11 -2
  263. package/mobile/cmp/navigator/NavigatorModel.ts +20 -8
  264. package/mobile/cmp/navigator/PageModel.ts +6 -1
  265. package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
  266. package/package.json +19 -19
  267. package/styles/vars.scss +32 -2
  268. package/svc/InspectorService.ts +2 -0
  269. package/svc/TraceService.ts +5 -2
  270. package/utils/js/LangUtils.ts +37 -5
@@ -125,6 +125,9 @@ export interface DataViewConfig {
125
125
  * with its usages of GridModel.
126
126
  */
127
127
  gridOptions?: Omit<GridConfig, keyof DataViewConfig>;
128
+
129
+ /** See {@link HoistBase.xhName}. */
130
+ xhName?: string;
128
131
  }
129
132
 
130
133
  export type ItemHeightFn = (params: {
@@ -177,8 +180,10 @@ export class DataViewModel extends HoistModel {
177
180
  rowClassRules,
178
181
  onRowClicked,
179
182
  onRowDoubleClicked,
180
- gridOptions
183
+ gridOptions,
184
+ xhName = null
181
185
  } = config;
186
+ this.xhName = xhName;
182
187
 
183
188
  throwIf(
184
189
  !isFunction(itemHeight) && !isNumber(itemHeight),
@@ -203,6 +208,7 @@ export class DataViewModel extends HoistModel {
203
208
  });
204
209
 
205
210
  this.gridModel = new GridModel({
211
+ xhName: this.childXhName('gridModel'),
206
212
  store,
207
213
  sortBy,
208
214
  selModel,
@@ -0,0 +1,628 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {
8
+ HoistModel,
9
+ managed,
10
+ PersistableState,
11
+ PersistenceProvider,
12
+ type PersistOptions,
13
+ persistOptions
14
+ } from '@xh/hoist/core';
15
+ import type {FieldFilterSpec} from '@xh/hoist/data';
16
+ import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
17
+ import {Timer} from '@xh/hoist/utils/async';
18
+ import {LocalDate, SECONDS} from '@xh/hoist/utils/datetime';
19
+ import {throwIf} from '@xh/hoist/utils/js';
20
+ import {isEmpty, isEqual, isFunction, isObject, isString, keyBy, uniq, uniqBy} from 'lodash';
21
+ import {dateRangePresets, DEFAULT_DATE_RANGE_PRESETS} from './DateRangePresets';
22
+ import {
23
+ businessDayOnOrBefore,
24
+ DATE_RANGE_PICKER_TABS,
25
+ fmtDate,
26
+ fmtDateRange,
27
+ getDateRangeLabel,
28
+ parseDateRangeSelection,
29
+ resolveDateRange,
30
+ stepDateRangeSelection
31
+ } from './DateRangeUtils';
32
+ import type {
33
+ DateRangeAnchorDay,
34
+ DateRangeContext,
35
+ DateRangeFormat,
36
+ DateRangePickerTab,
37
+ DateRangePreset,
38
+ DateRangePresetToken,
39
+ DateRangeSelection,
40
+ LocalDateRange,
41
+ ResolvedDateRange
42
+ } from './Types';
43
+
44
+ /**
45
+ * Configuration for a {@link DateRangePickerModel} - a control for selecting a period of time
46
+ * as a preset, relative lookback, calendar month or year, or custom range of dates.
47
+ */
48
+ export interface DateRangePickerConfig {
49
+ /**
50
+ * The day that relative and to-date selections resolve against, and (unless `maxDate` is set)
51
+ * the latest selectable date. Default `'localDay'` - the current day in the browser's time
52
+ * zone, kept current as the day rolls. See {@link DateRangeAnchorDay} for the app-day, pinned,
53
+ * and computed alternatives.
54
+ */
55
+ anchorDay?: DateRangeAnchorDay;
56
+
57
+ /**
58
+ * True to treat single days by business day: a live `anchorDay` (`'localDay'` or `'appDay'`)
59
+ * snaps back to the most recent business day, and single-day selections step by business day
60
+ * rather than calendar day. Multi-day ranges are unaffected - seven days is still seven days.
61
+ * A pinned or computed `anchorDay` is honored verbatim, never snapped. Default false.
62
+ */
63
+ businessDayMode?: boolean;
64
+
65
+ /**
66
+ * False (default) waits for the user to apply a relative or custom draft before updating the
67
+ * value. True commits those drafts as they change, and applies a tab's current draft as soon
68
+ * as the tab is activated, so the popover never shows a period other than the applied one. The
69
+ * Apply and Cancel buttons are omitted. Preset and month/year picks commit on click either way.
70
+ * Overridable app-wide via `DateRangePickerModel.defaults`.
71
+ */
72
+ commitOnChange?: boolean;
73
+
74
+ /**
75
+ * Format for the dates of a range - the two ends shown on the trigger, beside presets, and in
76
+ * `rangeLabel`. A moment.js format string, or a function of the date. Default `YYYY-MM-DD`,
77
+ * overridable app-wide via `DateRangePickerModel.defaults`. The Custom tab's date inputs use
78
+ * it when it is a string, else `YYYY-MM-DD`.
79
+ */
80
+ dateFormat?: DateRangeFormat;
81
+
82
+ /**
83
+ * Format for a single day - a one-day range, and the anchor date in the popover footer -
84
+ * where the weekday matters more than the year. A moment.js format string, or a function of
85
+ * the date. Default `ddd MMM D`, overridable app-wide via `DateRangePickerModel.defaults`.
86
+ */
87
+ singleDayFormat?: DateRangeFormat;
88
+
89
+ /**
90
+ * Name of the field to filter in the {@link FieldFilterSpec}s produced by
91
+ * {@link DateRangePickerModel.currentRangeFilter} and `priorRangeFilter`. Required to read
92
+ * those properties.
93
+ */
94
+ filterField?: string;
95
+
96
+ /**
97
+ * Initial value, and the fallback when a persisted value is missing or fails validation.
98
+ * Accepts a preset token string as shorthand for a preset selection. Default: the first
99
+ * configured preset, or a rolling 30 days if no presets are configured.
100
+ */
101
+ initialValue?: DateRangeSelection | DateRangePresetToken | string;
102
+
103
+ /**
104
+ * Test for whether a date is a business day, consulted by `businessDayMode` and available to
105
+ * app-defined presets via {@link DateRangeContext}. Default: weekdays. Supply to honor a
106
+ * holiday calendar.
107
+ */
108
+ isBusinessDay?: (date: LocalDate) => boolean;
109
+
110
+ /**
111
+ * Latest selectable date. Default: the anchor date, so nothing beyond it can be selected. Set
112
+ * later than the anchor to allow selection of future dates.
113
+ */
114
+ maxDate?: LocalDate;
115
+
116
+ /** Earliest selectable date. Default: none. */
117
+ minDate?: LocalDate;
118
+
119
+ /**
120
+ * Options governing persistence. The value persists under a `dateRangePicker.value` path by
121
+ * default - set `path` to disambiguate multiple pickers sharing one provider.
122
+ */
123
+ persistWith?: DateRangePickerPersistOptions;
124
+
125
+ /**
126
+ * Presets offered on the Presets tab, in display order - tokens of Hoist's built-in
127
+ * {@link dateRangePresets}, app-defined {@link DateRangePreset} objects, or a mix.
128
+ * Default: {@link DEFAULT_DATE_RANGE_PRESETS}.
129
+ */
130
+ presets?: Array<DateRangePresetToken | DateRangePreset>;
131
+
132
+ /**
133
+ * Tabs to offer within the picker popover, in display order. Default: all tabs, less the
134
+ * Presets tab when `presets` is empty. A single tab renders without a rail.
135
+ */
136
+ tabs?: DateRangePickerTab[];
137
+
138
+ /** See {@link HoistBase.xhName}. */
139
+ xhName?: string;
140
+ }
141
+
142
+ /** Default `isBusinessDay` - Monday through Friday. */
143
+ const isWeekday = (date: LocalDate): boolean => date.isWeekday;
144
+
145
+ /** How often a live `anchorDay` is re-evaluated. Cheap: a no-op until the day actually changes. */
146
+ const ANCHOR_REFRESH_INTERVAL = 10 * SECONDS;
147
+
148
+ export interface DateRangePickerPersistOptions extends PersistOptions {
149
+ /** True (default) to persist the value, or provide value-specific PersistOptions. */
150
+ persistValue?: boolean | PersistOptions;
151
+ }
152
+
153
+ /**
154
+ * App-wide overridable defaults for {@link DateRangePickerModel}. Instance config takes precedence.
155
+ */
156
+ export interface DateRangePickerModelDefaults {
157
+ anchorDay?: DateRangeAnchorDay;
158
+ businessDayMode?: boolean;
159
+ commitOnChange?: boolean;
160
+ dateFormat?: DateRangeFormat;
161
+ singleDayFormat?: DateRangeFormat;
162
+ }
163
+
164
+ /**
165
+ * Model for a control that allows users to select a period of time - a preset (e.g. MTD, Prev
166
+ * 30 Days), a relative lookback (e.g. Prev 6 Months), a calendar month or year, or a custom
167
+ * range of dates - and the API through which an app reads the applied period.
168
+ *
169
+ * The value is a single compound {@link DateRangeSelection}, which this model resolves to a
170
+ * concrete {@link LocalDateRange} anchored to `anchorDate`, alongside the comparable prior range.
171
+ * The value is plain JSON, so it persists via `persistWith` (including through saved views)
172
+ * without custom serialization, and re-resolves as the anchor date moves forward - a persisted
173
+ * `mtd` stays month-to-date.
174
+ *
175
+ * The anchor date is live by default: this model keeps it on the current day as midnight passes,
176
+ * so every derived range, label, and filter follows without app intervention. See `anchorDay`.
177
+ *
178
+ * Construct one within an app model and render a {@link DateRangePicker} bound to it to let users
179
+ * view and change the value. The value and its derived ranges and filters stay live whether or
180
+ * not a picker is mounted - a locked dashboard widget, for example, can hide its picker but still
181
+ * query by period.
182
+ *
183
+ * @see DateRangePicker
184
+ * @see dateRangePresets
185
+ */
186
+ export class DateRangePickerModel extends HoistModel {
187
+ /** App-level defaults for DateRangePickerModel. Instance config takes precedence. */
188
+ static defaults: DateRangePickerModelDefaults = {
189
+ anchorDay: 'localDay',
190
+ businessDayMode: false,
191
+ commitOnChange: false,
192
+ dateFormat: 'YYYY-MM-DD',
193
+ singleDayFormat: 'ddd MMM D'
194
+ };
195
+
196
+ /** The applied selection, always in normalized form. Set via `setValue()`. */
197
+ @observable.ref value: DateRangeSelection;
198
+
199
+ /** Tabs offered in the popover, in display order. Set via `setTabs()` - the picker follows. */
200
+ @observable.ref tabs: DateRangePickerTab[];
201
+
202
+ /** Presets offered on the Presets tab, in display order. Set via `setPresets()`. */
203
+ @observable.ref presets: DateRangePreset[];
204
+
205
+ /** How the anchor date is determined - see {@link DateRangeAnchorDay}. Set via `setAnchorDay()`. */
206
+ @observable.ref anchorDay: DateRangeAnchorDay;
207
+
208
+ /**
209
+ * Date that relative and to-date selections resolve against - `anchorDay` resolved, and (in
210
+ * `businessDayMode`) snapped to a business day when live. Kept current by this model.
211
+ */
212
+ @observable.ref anchorDate: LocalDate;
213
+
214
+ /** The current day in the browser's time zone - the reader's "today". Kept current. */
215
+ @observable.ref today: LocalDate;
216
+
217
+ /** Earliest selectable date, or null if unbounded. Set via `setMinDate()`. */
218
+ @observable.ref minDate: LocalDate | null;
219
+
220
+ /** Business-day test used by `businessDayMode` and presets. Set via `setIsBusinessDay()`. */
221
+ @observable.ref isBusinessDay: (date: LocalDate) => boolean;
222
+
223
+ @bindable businessDayMode: boolean;
224
+ @bindable commitOnChange: boolean;
225
+ @bindable.ref dateFormat: DateRangeFormat;
226
+ @bindable.ref singleDayFormat: DateRangeFormat;
227
+ @bindable filterField: string;
228
+
229
+ /** The initial value, and the fallback for a missing or invalid persisted value. */
230
+ readonly defaultValue: DateRangeSelection;
231
+
232
+ @observable.ref private explicitMaxDate: LocalDate | null;
233
+ @managed private anchorTimer: Timer;
234
+
235
+ /** Latest selectable date - the explicit `maxDate` config if set, otherwise `anchorDate`. */
236
+ get maxDate(): LocalDate {
237
+ return this.explicitMaxDate ?? this.anchorDate;
238
+ }
239
+
240
+ /**
241
+ * True if the anchor date is the reader's current day - when `anchorDay` reads "Today". False
242
+ * for any other day, however the anchor got there (pinned, business-day snapped, or an app
243
+ * time zone a day apart from the browser's) - the picker then reads "As Of" with the date.
244
+ */
245
+ get isAnchorToday(): boolean {
246
+ return this.anchorDate === this.today;
247
+ }
248
+
249
+ /** Configured presets, keyed by token. */
250
+ @computed
251
+ get presetMap(): Record<string, DateRangePreset> {
252
+ return keyBy(this.presets, 'token');
253
+ }
254
+
255
+ /** The live context that selections resolve against. */
256
+ @computed
257
+ get context(): DateRangeContext {
258
+ const {anchorDate, today, minDate, maxDate, isBusinessDay, businessDayMode} = this;
259
+ return {
260
+ anchorDate,
261
+ today,
262
+ minDate,
263
+ maxDate,
264
+ isBusinessDay,
265
+ businessDayMode,
266
+ presets: this.presetMap
267
+ };
268
+ }
269
+
270
+ /** Resolved date range for the applied value. */
271
+ get currentRange(): LocalDateRange {
272
+ return this.resolvedValue.current;
273
+ }
274
+
275
+ /**
276
+ * The immediately preceding, non-overlapping range of comparable shape - the same span one
277
+ * unit earlier for a period-to-date, `count` units earlier for a lookback in weeks or larger
278
+ * units, an equal number of days for day-based windows. Null when the current range is
279
+ * unbounded.
280
+ */
281
+ get priorRange(): LocalDateRange | null {
282
+ return this.resolvedValue.prior;
283
+ }
284
+
285
+ /** Short label for the applied value - e.g. `MTD`, `Prev 6 Months`, `Aug 2026`, `Custom`. */
286
+ get label(): string {
287
+ return this.getLabel(this.value);
288
+ }
289
+
290
+ /**
291
+ * Resolved range as `start ▸ end` per `dateFormat`, with `…` for an unbounded edge. A single
292
+ * day reads as that one date, per `singleDayFormat`.
293
+ */
294
+ get rangeLabel(): string {
295
+ return this.fmtRange(this.currentRange);
296
+ }
297
+
298
+ /**
299
+ * Longer-form name for the applied value, suitable for panel titles - the period's name
300
+ * rather than its dates, with months spelled out (e.g. `August 2026`). A custom range, and the
301
+ * anchor day when it is not today, have no name beyond their dates, so they read as those.
302
+ */
303
+ get displayName(): string {
304
+ const {value, currentRange} = this;
305
+ if (value.kind === 'custom' || this.labelNeedsDates) return this.rangeLabel;
306
+ if (value.kind === 'month') return currentRange.start.format('MMMM YYYY');
307
+ return this.label;
308
+ }
309
+
310
+ /**
311
+ * True when `label` alone does not identify the period - a custom range, or the anchor day
312
+ * when it is not today and so reads only as `As Of`. The picker shows the dates instead.
313
+ */
314
+ get labelNeedsDates(): boolean {
315
+ const {value} = this;
316
+ return (
317
+ value.kind === 'custom' ||
318
+ (value.kind === 'preset' && value.token === 'anchorDay' && !this.isAnchorToday)
319
+ );
320
+ }
321
+
322
+ /** True if `stepRange(-1)` would move the applied range - bounded and not yet at `minDate`. */
323
+ get canStepBack(): boolean {
324
+ return !!this.getSteppedValue(-1);
325
+ }
326
+
327
+ /** True if `stepRange(1)` would move the applied range - bounded, and not yet at `maxDate`. */
328
+ get canStepForward(): boolean {
329
+ return !!this.getSteppedValue(1);
330
+ }
331
+
332
+ /** Filters for `currentRange` on `filterField`, ready to apply to a Store, View, or query. */
333
+ get currentRangeFilter(): FieldFilterSpec[] {
334
+ return this.getRangeFilter(this.currentRange);
335
+ }
336
+
337
+ /** Filters for `priorRange` on `filterField`. Empty when there is no prior range. */
338
+ get priorRangeFilter(): FieldFilterSpec[] {
339
+ return this.getRangeFilter(this.priorRange);
340
+ }
341
+
342
+ constructor({
343
+ tabs,
344
+ presets = DEFAULT_DATE_RANGE_PRESETS,
345
+ initialValue,
346
+ anchorDay = DateRangePickerModel.defaults.anchorDay,
347
+ businessDayMode = DateRangePickerModel.defaults.businessDayMode,
348
+ commitOnChange = DateRangePickerModel.defaults.commitOnChange,
349
+ minDate = null,
350
+ maxDate = null,
351
+ isBusinessDay = isWeekday,
352
+ filterField = null,
353
+ dateFormat = DateRangePickerModel.defaults.dateFormat,
354
+ singleDayFormat = DateRangePickerModel.defaults.singleDayFormat,
355
+ persistWith = null,
356
+ xhName = null
357
+ }: DateRangePickerConfig = {}) {
358
+ super();
359
+ makeObservable(this);
360
+ this.xhName = xhName;
361
+
362
+ this.businessDayMode = businessDayMode;
363
+ this.commitOnChange = commitOnChange;
364
+ this.dateFormat = dateFormat;
365
+ this.singleDayFormat = singleDayFormat;
366
+ this.filterField = filterField;
367
+ this.minDate = minDate;
368
+ this.explicitMaxDate = maxDate;
369
+ this.isBusinessDay = isBusinessDay;
370
+ this.setAnchorDay(anchorDay);
371
+
372
+ this.setPresets(presets);
373
+ this.setTabs(
374
+ tabs ?? DATE_RANGE_PICKER_TABS.filter(it => it !== 'presets' || !isEmpty(this.presets))
375
+ );
376
+
377
+ // Fail loudly on a bad in-code default - a silent fallback would leave data unfiltered.
378
+ this.defaultValue = this.parseValue(
379
+ initialValue ?? this.presets[0]?.token ?? {kind: 'relative', count: 30, unit: 'days'}
380
+ );
381
+ throwIf(!this.defaultValue, 'Invalid DateRangePickerModel `initialValue` config.');
382
+ this.value = this.defaultValue;
383
+
384
+ if (persistWith) this.initPersist(persistWith);
385
+
386
+ // Keep a live anchor on the current day. Idle for a pinned date - nothing to track.
387
+ this.anchorTimer = Timer.create({
388
+ runFn: () => this.refreshAnchorDate(),
389
+ interval: () => (this.isLiveAnchor ? ANCHOR_REFRESH_INTERVAL : 0)
390
+ });
391
+
392
+ this.addReaction(
393
+ {
394
+ // A computed anchor may read observables - follow those immediately, not on the
395
+ // next tick of the timer.
396
+ track: () => (isFunction(this.anchorDay) ? this.anchorDay() : null),
397
+ run: () => this.refreshAnchorDate()
398
+ },
399
+ {
400
+ track: () => [this.businessDayMode, this.isBusinessDay],
401
+ run: () => this.refreshAnchorDate()
402
+ }
403
+ );
404
+ }
405
+
406
+ /**
407
+ * Set the applied value. Accepts a preset token string as shorthand for a preset selection.
408
+ * An invalid value (unknown preset, out-of-bounds count or year, malformed date) is logged
409
+ * and ignored.
410
+ */
411
+ @action
412
+ setValue(value: DateRangeSelection | DateRangePresetToken | string) {
413
+ const parsed = this.parseValue(value);
414
+ if (!parsed) {
415
+ this.logWarn('Attempted to set invalid value', value);
416
+ return;
417
+ }
418
+ if (!isEqual(parsed, this.value)) this.value = parsed;
419
+ }
420
+
421
+ @action
422
+ setTabs(tabs: DateRangePickerTab[]) {
423
+ throwIf(isEmpty(tabs), 'DateRangePickerModel requires at least one tab.');
424
+ throwIf(
425
+ tabs.some(it => !DATE_RANGE_PICKER_TABS.includes(it)),
426
+ `Unknown DateRangePickerModel tab in [${tabs}].`
427
+ );
428
+ this.tabs = uniq(tabs);
429
+ }
430
+
431
+ @action
432
+ setPresets(presets: Array<DateRangePresetToken | DateRangePreset>) {
433
+ const ret = (presets ?? []).map(it => {
434
+ if (isString(it)) {
435
+ const preset = dateRangePresets[it];
436
+ throwIf(!preset, `Unknown date range preset token: '${it}'.`);
437
+ return preset;
438
+ }
439
+ throwIf(
440
+ !it?.token || !it.label || !isFunction(it.resolve),
441
+ 'App-defined date range presets require a `token`, a `label`, and a `resolve` function.'
442
+ );
443
+ return it;
444
+ });
445
+ throwIf(
446
+ uniqBy(ret, 'token').length !== ret.length,
447
+ 'Date range preset tokens must be unique.'
448
+ );
449
+ this.presets = ret;
450
+
451
+ // The applied value may name a preset that is no longer offered.
452
+ if (this.value && !this.parseValue(this.value)) this.value = this.fallbackValue;
453
+ }
454
+
455
+ /** Set how the anchor date is determined - see {@link DateRangeAnchorDay}. */
456
+ @action
457
+ setAnchorDay(anchorDay: DateRangeAnchorDay) {
458
+ throwIf(
459
+ !anchorDay ||
460
+ (isString(anchorDay) && anchorDay !== 'localDay' && anchorDay !== 'appDay'),
461
+ `Invalid DateRangePickerModel anchorDay: '${anchorDay}'.`
462
+ );
463
+ this.anchorDay = anchorDay;
464
+ this.refreshAnchorDate();
465
+ }
466
+
467
+ @action
468
+ setMinDate(minDate: LocalDate | null) {
469
+ this.minDate = minDate;
470
+ }
471
+
472
+ /** Set the latest selectable date, or null to revert to `anchorDate`. */
473
+ @action
474
+ setMaxDate(maxDate: LocalDate | null) {
475
+ this.explicitMaxDate = maxDate;
476
+ }
477
+
478
+ @action
479
+ setIsBusinessDay(isBusinessDay: (date: LocalDate) => boolean) {
480
+ this.isBusinessDay = isBusinessDay ?? isWeekday;
481
+ }
482
+
483
+ /**
484
+ * Move the applied range by `steps` periods - e.g. `stepRange(-1)` for the previous period.
485
+ * No selection changes kind: presets and relative lookbacks step through their own prior- and
486
+ * next-range logic via `offset`, months and years by calendar unit, and custom ranges by their
487
+ * length. Stops at `minDate` and `maxDate`, and a no-op when the range cannot move - see
488
+ * {@link stepDateRangeSelection}.
489
+ */
490
+ @action
491
+ stepRange(steps: number) {
492
+ const next = this.getSteppedValue(steps);
493
+ if (next) this.setValue(next);
494
+ }
495
+
496
+ /** The selection `stepRange(steps)` would apply, or null if the range cannot move that way. */
497
+ getSteppedValue(steps: number): DateRangeSelection | null {
498
+ return stepDateRangeSelection(this.value, steps, this.context);
499
+ }
500
+
501
+ /**
502
+ * Validate and normalize a raw value against this model's presets. Returns null for anything
503
+ * unrecognized - see {@link parseDateRangeSelection}.
504
+ */
505
+ parseValue(raw: unknown): DateRangeSelection | null {
506
+ return parseDateRangeSelection(raw, this.presetMap);
507
+ }
508
+
509
+ validateValue(raw: unknown): boolean {
510
+ return !!this.parseValue(raw);
511
+ }
512
+
513
+ /** Resolve any selection against this model's current context. */
514
+ resolve(sel: DateRangeSelection): ResolvedDateRange {
515
+ return resolveDateRange(sel, this.context);
516
+ }
517
+
518
+ /** Short label for any selection, as it would read on the picker trigger. */
519
+ getLabel(sel: DateRangeSelection): string {
520
+ return getDateRangeLabel(sel, this.context);
521
+ }
522
+
523
+ /**
524
+ * Filters bounding `field` to the given range - a `>=` filter for a bounded start and a `<=`
525
+ * filter for a bounded end, so an unbounded range produces no filters at all.
526
+ */
527
+ getRangeFilter(range: LocalDateRange, field: string = this.filterField): FieldFilterSpec[] {
528
+ throwIf(!field, 'DateRangePickerModel requires a `filterField` config to build filters.');
529
+ const ret: FieldFilterSpec[] = [];
530
+ if (range?.start) ret.push({field, op: '>=', value: range.start});
531
+ if (range?.end) ret.push({field, op: '<=', value: range.end});
532
+ return ret;
533
+ }
534
+
535
+ /** Format a range as `start ▸ end` per this model's `dateFormat` - a single day per `singleDayFormat`. */
536
+ fmtRange(range: LocalDateRange): string {
537
+ return fmtDateRange(range, this.dateFormat, this.singleDayFormat);
538
+ }
539
+
540
+ /** Format a single day per this model's `singleDayFormat`. */
541
+ fmtSingleDay(date: LocalDate): string {
542
+ return fmtDate(date, this.singleDayFormat);
543
+ }
544
+
545
+ //------------------------
546
+ // Implementation
547
+ //------------------------
548
+ @computed.struct
549
+ private get resolvedValue(): ResolvedDateRange {
550
+ return this.resolve(this.value);
551
+ }
552
+
553
+ /** True unless `anchorDay` is a pinned LocalDate. */
554
+ private get isLiveAnchor(): boolean {
555
+ return !LocalDate.isLocalDate(this.anchorDay);
556
+ }
557
+
558
+ /**
559
+ * Re-evaluate `today` and `anchorDate` from `anchorDay`. Assignments are identity no-ops until
560
+ * the day actually changes, as LocalDate instances are memoized.
561
+ */
562
+ @action
563
+ private refreshAnchorDate() {
564
+ const {anchorDay, businessDayMode, isBusinessDay} = this,
565
+ today = LocalDate.today();
566
+
567
+ let anchorDate: LocalDate;
568
+ if (isString(anchorDay)) {
569
+ // Only a clock-derived anchor is snapped. A pinned or computed date is what the app
570
+ // asked for - e.g. a month-end that falls on a Sunday.
571
+ const day = anchorDay === 'appDay' ? LocalDate.currentAppDay() : today;
572
+ anchorDate = businessDayMode ? businessDayOnOrBefore(day, {isBusinessDay}) : day;
573
+ } else {
574
+ anchorDate = isFunction(anchorDay) ? anchorDay() : anchorDay;
575
+ }
576
+ throwIf(!anchorDate, 'DateRangePickerModel anchorDay function must return a LocalDate.');
577
+
578
+ if (this.today !== today) this.today = today;
579
+ if (this.anchorDate !== anchorDate) this.anchorDate = anchorDate;
580
+ }
581
+
582
+ private initPersist({
583
+ persistValue = true,
584
+ path = 'dateRangePicker',
585
+ ...rootPersistWith
586
+ }: DateRangePickerPersistOptions) {
587
+ if (!persistValue) return;
588
+
589
+ PersistenceProvider.create({
590
+ persistOptions: persistOptions(
591
+ {path: `${path}.value`},
592
+ rootPersistWith,
593
+ isObject(persistValue) ? persistValue : null
594
+ ),
595
+ target: {
596
+ getPersistableState: () => new PersistableState(this.value),
597
+ setPersistableState: ({value}) => this.restoreValue(value)
598
+ },
599
+ owner: this
600
+ });
601
+ }
602
+
603
+ /**
604
+ * Persisted state can be missing (a saved view with no value) or stale (a preset since
605
+ * removed) - either way, fall back to the default rather than carrying a prior value over.
606
+ */
607
+ @action
608
+ private restoreValue(raw: unknown) {
609
+ const next = this.parseValue(raw) ?? this.fallbackValue;
610
+ if (!isEqual(next, this.value)) this.value = next;
611
+ }
612
+
613
+ /**
614
+ * `defaultValue` while it still resolves, else the first configured preset, else a rolling 30
615
+ * days - `defaultValue` can itself name a preset that `setPresets()` has since removed.
616
+ */
617
+ private get fallbackValue(): DateRangeSelection {
618
+ return (
619
+ this.parseValue(this.defaultValue) ??
620
+ this.parseValue(this.presets[0]?.token) ?? {
621
+ kind: 'relative',
622
+ count: 30,
623
+ unit: 'days',
624
+ snap: false
625
+ }
626
+ );
627
+ }
628
+ }