@xh/hoist 87.1.1 → 87.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +39 -0
- package/CHANGELOG.md +92 -20
- package/admin/AdminUtils.ts +64 -0
- package/admin/App.scss +88 -0
- package/admin/columns/UserData.ts +30 -3
- package/admin/detail/RestDetailModel.ts +64 -0
- package/admin/detail/RestDetailPanel.ts +106 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +27 -94
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +23 -60
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +1 -2
- package/admin/tabs/general/alertBanner/AlertBannerPanel.ts +2 -13
- package/admin/tabs/general/config/ConfigDetailPanel.ts +72 -0
- package/admin/tabs/general/config/ConfigPanel.ts +41 -53
- package/admin/tabs/general/config/ConfigPanelModel.ts +3 -0
- package/admin/tabs/general/config/ConfigValue.scss +14 -0
- package/admin/tabs/general/config/ConfigValue.ts +104 -65
- package/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.ts +93 -0
- package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +4 -0
- package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +2 -0
- package/admin/tabs/userData/prefs/UserPreferenceDetailPanel.ts +62 -0
- package/admin/tabs/userData/prefs/UserPreferenceModel.ts +10 -3
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -1
- package/admin/tabs/userData/roles/details/RoleDetails.scss +2 -19
- package/admin/tabs/userData/roles/details/RoleDetails.ts +13 -13
- package/appcontainer/AboutDialogModel.ts +1 -0
- package/appcontainer/AppContainerModel.ts +3 -0
- package/appcontainer/AppStateModel.ts +1 -0
- package/appcontainer/BannerSourceModel.ts +1 -0
- package/appcontainer/ChangelogDialogModel.ts +1 -0
- package/appcontainer/ColChooserOptionsModel.ts +1 -0
- package/appcontainer/ExceptionDialogModel.ts +1 -0
- package/appcontainer/FeedbackDialogModel.ts +1 -0
- package/appcontainer/ImpersonationBarModel.ts +1 -0
- package/appcontainer/MessageSourceModel.ts +1 -0
- package/appcontainer/OptionsDialogModel.ts +1 -0
- package/appcontainer/PageStateModel.ts +1 -0
- package/appcontainer/RouterModel.ts +2 -0
- package/appcontainer/SizingModeModel.ts +1 -0
- package/appcontainer/ThemeModel.ts +1 -0
- package/appcontainer/ToastSourceModel.ts +1 -0
- package/appcontainer/UserAgentModel.ts +1 -0
- package/appcontainer/ViewportSizeModel.ts +1 -0
- package/build/types/admin/AdminUtils.d.ts +29 -0
- package/build/types/admin/columns/UserData.d.ts +3 -0
- package/build/types/admin/detail/RestDetailModel.d.ts +20 -0
- package/build/types/admin/detail/RestDetailPanel.d.ts +34 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +5 -17
- package/build/types/admin/tabs/general/config/ConfigDetailPanel.d.ts +7 -0
- package/build/types/admin/tabs/general/config/ConfigValue.d.ts +10 -4
- package/build/types/admin/tabs/userData/jsonblob/JsonBlobDetailPanel.d.ts +3 -0
- package/build/types/admin/tabs/userData/prefs/UserPreferenceDetailPanel.d.ts +3 -0
- package/build/types/admin/tabs/userData/prefs/UserPreferenceModel.d.ts +3 -0
- package/build/types/appcontainer/AboutDialogModel.d.ts +1 -0
- package/build/types/appcontainer/AppContainerModel.d.ts +1 -0
- package/build/types/appcontainer/AppStateModel.d.ts +1 -0
- package/build/types/appcontainer/BannerSourceModel.d.ts +1 -0
- package/build/types/appcontainer/ChangelogDialogModel.d.ts +1 -0
- package/build/types/appcontainer/ColChooserOptionsModel.d.ts +1 -0
- package/build/types/appcontainer/ExceptionDialogModel.d.ts +1 -0
- package/build/types/appcontainer/FeedbackDialogModel.d.ts +1 -0
- package/build/types/appcontainer/ImpersonationBarModel.d.ts +1 -0
- package/build/types/appcontainer/MessageSourceModel.d.ts +1 -0
- package/build/types/appcontainer/OptionsDialogModel.d.ts +1 -0
- package/build/types/appcontainer/PageStateModel.d.ts +1 -0
- package/build/types/appcontainer/RouterModel.d.ts +1 -0
- package/build/types/appcontainer/SizingModeModel.d.ts +1 -0
- package/build/types/appcontainer/ThemeModel.d.ts +1 -0
- package/build/types/appcontainer/ToastSourceModel.d.ts +1 -0
- package/build/types/appcontainer/UserAgentModel.d.ts +1 -0
- package/build/types/appcontainer/ViewportSizeModel.d.ts +1 -0
- package/build/types/cmp/ag-grid/AgGridModel.d.ts +3 -1
- package/build/types/cmp/card/Card.d.ts +2 -2
- package/build/types/cmp/chart/ChartModel.d.ts +2 -0
- package/build/types/cmp/dataview/DataViewModel.d.ts +2 -0
- package/build/types/cmp/daterange/DateRangePickerModel.d.ts +266 -0
- package/build/types/cmp/daterange/DateRangePresets.d.ts +26 -0
- package/build/types/cmp/daterange/DateRangeUtils.d.ts +96 -0
- package/build/types/cmp/daterange/Types.d.ts +152 -0
- package/build/types/cmp/daterange/index.d.ts +4 -0
- package/build/types/cmp/filter/FilterChooserModel.d.ts +3 -1
- package/build/types/cmp/form/FormModel.d.ts +3 -1
- package/build/types/cmp/grid/GridModel.d.ts +2 -0
- package/build/types/cmp/grid/Types.d.ts +2 -0
- package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
- package/build/types/cmp/grouping/GroupingChooserModel.d.ts +3 -1
- package/build/types/cmp/input/HoistInputProps.d.ts +49 -2
- package/build/types/cmp/tab/TabContainerModel.d.ts +3 -1
- package/build/types/cmp/tab/TabModel.d.ts +3 -1
- package/build/types/cmp/treemap/TreeMapModel.d.ts +2 -0
- package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +2 -0
- package/build/types/cmp/zoneGrid/ZoneGridModel.d.ts +2 -0
- package/build/types/core/HoistBase.d.ts +7 -0
- package/build/types/core/HoistProps.d.ts +26 -1
- package/build/types/core/model/RootRefreshContextModel.d.ts +1 -0
- package/build/types/core/types/Telemetry.d.ts +1 -1
- package/build/types/data/Store.d.ts +3 -1
- package/build/types/data/StoreSelectionModel.d.ts +3 -1
- package/build/types/data/cube/Cube.d.ts +5 -2
- package/build/types/data/cube/View.d.ts +2 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -0
- package/build/types/desktop/cmp/button/Button.d.ts +2 -2
- package/build/types/desktop/cmp/button/ButtonGroup.d.ts +2 -2
- package/build/types/desktop/cmp/dash/DashConfig.d.ts +2 -0
- package/build/types/desktop/cmp/dash/DashViewModel.d.ts +3 -1
- package/build/types/desktop/cmp/dash/canvas/DashCanvasModel.d.ts +1 -1
- package/build/types/desktop/cmp/dash/container/DashContainerModel.d.ts +1 -1
- package/build/types/desktop/cmp/daterange/DateRangePicker.d.ts +66 -0
- package/build/types/desktop/cmp/daterange/impl/CustomTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/DateRangePickerLocalModel.d.ts +121 -0
- package/build/types/desktop/cmp/daterange/impl/PeriodTab.d.ts +2 -0
- package/build/types/desktop/cmp/daterange/impl/PresetsTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/RelativeTab.d.ts +3 -0
- package/build/types/desktop/cmp/daterange/impl/TabUtils.d.ts +21 -0
- package/build/types/desktop/cmp/daterange/index.d.ts +2 -0
- package/build/types/desktop/cmp/dock/DockContainerModel.d.ts +3 -1
- package/build/types/desktop/cmp/dock/DockViewModel.d.ts +3 -1
- package/build/types/desktop/cmp/grouping/GroupingChooser.d.ts +4 -1
- package/build/types/desktop/cmp/input/IntentInput.d.ts +38 -0
- package/build/types/desktop/cmp/input/NumberInput.d.ts +2 -2
- package/build/types/desktop/cmp/input/SegmentedControl.d.ts +25 -4
- package/build/types/desktop/cmp/input/TextArea.d.ts +2 -2
- package/build/types/desktop/cmp/input/TextInput.d.ts +2 -2
- package/build/types/desktop/cmp/input/index.d.ts +1 -0
- package/build/types/desktop/cmp/leftrightchooser/LeftRightChooserModel.d.ts +3 -1
- package/build/types/desktop/cmp/panel/PanelModel.d.ts +3 -1
- package/build/types/desktop/cmp/rest/RestGridModel.d.ts +1 -1
- package/build/types/desktop/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/icon/Icon.d.ts +3 -0
- package/build/types/inspector/impl/InspectorUtils.d.ts +3 -0
- package/build/types/inspector/instances/InstancesModel.d.ts +4 -0
- package/build/types/kit/react-day-picker/index.d.ts +3 -3
- package/build/types/mobile/cmp/button/Button.d.ts +2 -2
- package/build/types/mobile/cmp/input/NumberInput.d.ts +2 -2
- package/build/types/mobile/cmp/input/SegmentedControl.d.ts +18 -4
- package/build/types/mobile/cmp/input/TextArea.d.ts +2 -2
- package/build/types/mobile/cmp/input/TextInput.d.ts +2 -2
- package/build/types/mobile/cmp/navigator/NavigatorModel.d.ts +3 -1
- package/build/types/mobile/cmp/navigator/PageModel.d.ts +3 -1
- package/build/types/mobile/cmp/tab/impl/TabContainer.d.ts +1 -1
- package/build/types/svc/InspectorService.d.ts +1 -0
- package/build/types/utils/js/LangUtils.d.ts +24 -5
- package/cmp/README.md +1 -0
- package/cmp/ag-grid/AgGridModel.ts +5 -0
- package/cmp/badge/Badge.ts +5 -3
- package/cmp/card/Card.ts +4 -3
- package/cmp/chart/ChartModel.ts +11 -1
- package/cmp/dataview/DataViewModel.ts +7 -1
- package/cmp/daterange/DateRangePickerModel.ts +628 -0
- package/cmp/daterange/DateRangePresets.ts +274 -0
- package/cmp/daterange/DateRangeUtils.ts +557 -0
- package/cmp/daterange/README.md +434 -0
- package/cmp/daterange/Types.ts +203 -0
- package/cmp/daterange/index.ts +10 -0
- package/cmp/filter/FilterChooserModel.ts +7 -4
- package/cmp/form/FormModel.ts +6 -0
- package/cmp/form/README.md +1 -0
- package/cmp/grid/GridModel.ts +24 -4
- package/cmp/grid/Types.ts +3 -0
- package/cmp/grid/filter/GridFilterModel.ts +3 -1
- package/cmp/grouping/GroupingChooserModel.ts +6 -1
- package/cmp/input/HoistInputProps.ts +50 -2
- package/cmp/input/README.md +42 -0
- package/cmp/layout/Box.ts +3 -2
- package/cmp/tab/TabContainerModel.ts +9 -1
- package/cmp/tab/TabModel.ts +5 -0
- package/cmp/treemap/SplitTreeMapModel.ts +12 -2
- package/cmp/treemap/TreeMapModel.ts +6 -1
- package/cmp/viewmanager/ViewManagerModel.ts +6 -1
- package/cmp/zoneGrid/ZoneGridModel.ts +7 -0
- package/core/HoistBase.ts +11 -0
- package/core/HoistProps.ts +27 -0
- package/core/README.md +46 -0
- package/core/impl/InstallServices.ts +5 -1
- package/core/model/RootRefreshContextModel.ts +2 -0
- package/core/types/Telemetry.ts +1 -1
- package/data/README.md +3 -1
- package/data/Store.ts +7 -0
- package/data/StoreSelectionModel.ts +5 -1
- package/data/cube/Cube.ts +12 -3
- package/data/cube/View.ts +5 -1
- package/desktop/README.md +2 -0
- package/desktop/appcontainer/LoginPanel.ts +2 -0
- package/desktop/cmp/button/Button.scss +71 -42
- package/desktop/cmp/button/Button.ts +10 -1
- package/desktop/cmp/button/ButtonGroup.ts +4 -1
- package/desktop/cmp/button/grid/ColChooserButton.ts +9 -1
- package/desktop/cmp/button/zoneGrid/ZoneMapperButton.ts +1 -0
- package/desktop/cmp/dash/DashConfig.ts +3 -0
- package/desktop/cmp/dash/DashViewModel.ts +14 -1
- package/desktop/cmp/dash/canvas/DashCanvasModel.ts +3 -1
- package/desktop/cmp/dash/container/DashContainerModel.ts +3 -1
- package/desktop/cmp/daterange/DateRangePicker.scss +577 -0
- package/desktop/cmp/daterange/DateRangePicker.ts +357 -0
- package/desktop/cmp/daterange/impl/CustomTab.ts +165 -0
- package/desktop/cmp/daterange/impl/DateRangePickerLocalModel.ts +482 -0
- package/desktop/cmp/daterange/impl/PeriodTab.ts +138 -0
- package/desktop/cmp/daterange/impl/PresetsTab.ts +58 -0
- package/desktop/cmp/daterange/impl/RelativeTab.ts +108 -0
- package/desktop/cmp/daterange/impl/TabUtils.ts +34 -0
- package/desktop/cmp/daterange/index.ts +8 -0
- package/desktop/cmp/dock/DockContainerModel.ts +6 -1
- package/desktop/cmp/dock/DockViewModel.ts +6 -1
- package/desktop/cmp/filechooser/FileChooser.ts +4 -0
- package/desktop/cmp/form/FormField.ts +1 -0
- package/desktop/cmp/grouping/GroupingChooser.ts +8 -2
- package/desktop/cmp/input/Checkbox.ts +1 -0
- package/desktop/cmp/input/CodeInput.ts +1 -0
- package/desktop/cmp/input/DateInput.scss +1 -1
- package/desktop/cmp/input/DateInput.ts +1 -0
- package/desktop/cmp/input/IntentInput.scss +202 -0
- package/desktop/cmp/input/IntentInput.ts +213 -0
- package/desktop/cmp/input/NumberInput.ts +11 -2
- package/desktop/cmp/input/Picker.ts +1 -0
- package/desktop/cmp/input/RadioInput.ts +2 -1
- package/desktop/cmp/input/SegmentedControl.scss +45 -2
- package/desktop/cmp/input/SegmentedControl.ts +66 -11
- package/desktop/cmp/input/Select.ts +9 -7
- package/desktop/cmp/input/Slider.ts +2 -0
- package/desktop/cmp/input/SwitchInput.ts +1 -0
- package/desktop/cmp/input/TextArea.ts +11 -2
- package/desktop/cmp/input/TextInput.ts +11 -2
- package/desktop/cmp/input/index.ts +1 -0
- package/desktop/cmp/leftrightchooser/LeftRightChooserModel.ts +5 -0
- package/desktop/cmp/panel/Panel.ts +8 -5
- package/desktop/cmp/panel/PanelModel.ts +8 -0
- package/desktop/cmp/panel/impl/ResizeContainer.ts +2 -1
- package/desktop/cmp/rest/RestGridModel.ts +4 -0
- package/desktop/cmp/tab/impl/TabContainer.ts +2 -0
- package/docs/README.md +2 -0
- package/docs/build-and-publish.md +23 -7
- package/docs/doc-registry.json +8 -0
- package/docs/version-compatibility.md +2 -0
- package/icon/Icon.ts +9 -0
- package/icon/index.ts +24 -0
- package/inspector/README.md +8 -2
- package/inspector/impl/InspectorUtils.ts +12 -0
- package/inspector/instances/DiagnosticsModel.ts +3 -2
- package/inspector/instances/InstancesModel.ts +81 -14
- package/inspector/instances/InstancesPanel.ts +6 -0
- package/kit/react-day-picker/index.ts +3 -3
- package/mobile/appcontainer/LoginPanel.ts +2 -0
- package/mobile/cmp/button/Button.ts +16 -3
- package/mobile/cmp/form/FormField.ts +1 -0
- package/mobile/cmp/input/Checkbox.ts +1 -0
- package/mobile/cmp/input/DateInput.ts +1 -0
- package/mobile/cmp/input/Label.ts +13 -8
- package/mobile/cmp/input/NumberInput.ts +11 -2
- package/mobile/cmp/input/SearchInput.ts +1 -0
- package/mobile/cmp/input/SegmentedControl.scss +36 -6
- package/mobile/cmp/input/SegmentedControl.ts +37 -11
- package/mobile/cmp/input/Select.ts +2 -0
- package/mobile/cmp/input/SwitchInput.ts +1 -0
- package/mobile/cmp/input/TextArea.ts +11 -2
- package/mobile/cmp/input/TextInput.ts +11 -2
- package/mobile/cmp/navigator/NavigatorModel.ts +6 -1
- package/mobile/cmp/navigator/PageModel.ts +6 -1
- package/mobile/cmp/tab/impl/TabContainer.ts +9 -1
- package/package.json +19 -19
- package/styles/vars.scss +32 -2
- package/svc/InspectorService.ts +2 -0
- package/svc/TraceService.ts +5 -2
- package/utils/js/LangUtils.ts +37 -5
|
@@ -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
|
+
}
|