@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.
- package/.npmignore +39 -0
- package/CHANGELOG.md +108 -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/grid/impl/ColumnWidthCalculator.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/desktop/hooks/UseHotkeys.d.ts +4 -3
- 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 +8 -2
- 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/clock/Clock.ts +1 -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/grid/impl/ColumnWidthCalculator.ts +5 -10
- 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/grid/editors/DateEditor.ts +11 -2
- 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 +57 -5
- 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/desktop/cmp/toolbar/Toolbar.scss +20 -6
- package/desktop/hooks/UseHotkeys.ts +41 -12
- 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 +20 -8
- 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,482 @@
|
|
|
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 {type TabConfig, TabContainerModel} from '@xh/hoist/cmp/tab';
|
|
8
|
+
import {HoistModel, type Intent, lookup, managed, XH} from '@xh/hoist/core';
|
|
9
|
+
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
10
|
+
import type {LocalDate} from '@xh/hoist/utils/datetime';
|
|
11
|
+
import {clamp, isEqual} from 'lodash';
|
|
12
|
+
import {createRef, type ReactElement} from 'react';
|
|
13
|
+
import {
|
|
14
|
+
DateRangePickerModel,
|
|
15
|
+
type DateRangePickerTab,
|
|
16
|
+
type DateRangeSelection,
|
|
17
|
+
type DateRangeUnit,
|
|
18
|
+
getDateRangeUnitLabel,
|
|
19
|
+
getMonthStart,
|
|
20
|
+
getQuarterStart,
|
|
21
|
+
type LocalDateRange,
|
|
22
|
+
MAX_RELATIVE_COUNT,
|
|
23
|
+
MIN_SELECTION_YEAR
|
|
24
|
+
} from '@xh/hoist/cmp/daterange';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A tab within the picker popover - its rail entry and its content component. Declared by the
|
|
28
|
+
* platform component alongside its tab components and passed in, keeping this model free of any
|
|
29
|
+
* dependency on them.
|
|
30
|
+
*
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
export interface DateRangePickerTabSpec {
|
|
34
|
+
id: DateRangePickerTab;
|
|
35
|
+
title: string;
|
|
36
|
+
icon: ReactElement;
|
|
37
|
+
content: (props: {model: DateRangePickerLocalModel; testId: string}) => ReactElement;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Local model for DateRangePicker components. Holds popover-local view state - open state, the
|
|
42
|
+
* active tab, and per-tab drafts - that must be per-component-instance, even when multiple
|
|
43
|
+
* pickers share one DateRangePickerModel. Drafts never touch the parent model's applied value
|
|
44
|
+
* until committed, so Cancel / click-outside / Escape discard them for free.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export class DateRangePickerLocalModel extends HoistModel {
|
|
49
|
+
override xhImpl = true;
|
|
50
|
+
|
|
51
|
+
@lookup(DateRangePickerModel) parentModel: DateRangePickerModel;
|
|
52
|
+
|
|
53
|
+
@bindable isOpen: boolean = false;
|
|
54
|
+
|
|
55
|
+
/** Drives the popover's tab rail + content. Tab ids are {@link DateRangePickerTab} values. */
|
|
56
|
+
@managed tabModel: TabContainerModel;
|
|
57
|
+
|
|
58
|
+
/** testId of the host picker - tabs derive their own testIds from it. */
|
|
59
|
+
testId: string;
|
|
60
|
+
|
|
61
|
+
/** Measured width of the whole control - drives the compact variant in a narrow host. */
|
|
62
|
+
@bindable measuredWidth: number = null;
|
|
63
|
+
|
|
64
|
+
/** Intent of the host picker as of its last render - accents selected months and years. */
|
|
65
|
+
intent: Intent = null;
|
|
66
|
+
|
|
67
|
+
/** Root element of this instance - scopes popover outside-click handling to this picker. */
|
|
68
|
+
viewRef = createRef<HTMLDivElement>();
|
|
69
|
+
|
|
70
|
+
// Relative tab draft.
|
|
71
|
+
@bindable relativeCount: number = 30;
|
|
72
|
+
@bindable relativeUnit: DateRangeUnit = 'days';
|
|
73
|
+
@bindable relativeSnap: boolean = false;
|
|
74
|
+
|
|
75
|
+
// Months & Years tab - year currently shown in the grid.
|
|
76
|
+
@bindable gridYear: number;
|
|
77
|
+
|
|
78
|
+
// Custom tab draft.
|
|
79
|
+
@observable.ref customStart: LocalDate;
|
|
80
|
+
@observable.ref customEnd: LocalDate;
|
|
81
|
+
@bindable nextEdge: 'start' | 'end' = 'start';
|
|
82
|
+
/** Start-of-month for the left-hand calendar. Right-hand shows the following month. */
|
|
83
|
+
@observable.ref leftMonth: LocalDate;
|
|
84
|
+
|
|
85
|
+
private tabSpecs: DateRangePickerTabSpec[];
|
|
86
|
+
|
|
87
|
+
// State as of the last open - lets `commitOnChange` tell a user change from the seeded draft.
|
|
88
|
+
private openTabId: DateRangePickerTab;
|
|
89
|
+
private seededRelativeDraft: DateRangeSelection;
|
|
90
|
+
private seededCustomDraft: DateRangeSelection;
|
|
91
|
+
|
|
92
|
+
get anchorDate(): LocalDate {
|
|
93
|
+
return this.parentModel.anchorDate;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get minDate(): LocalDate | null {
|
|
97
|
+
return this.parentModel.minDate;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get maxDate(): LocalDate {
|
|
101
|
+
return this.parentModel.maxDate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get singleTab(): boolean {
|
|
105
|
+
return this.parentModel.tabs.length === 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get activeTabId(): DateRangePickerTab {
|
|
109
|
+
return this.tabModel.activeTabId as DateRangePickerTab;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
//------------------
|
|
113
|
+
// Relative tab
|
|
114
|
+
//------------------
|
|
115
|
+
get clampedCount(): number {
|
|
116
|
+
return clamp(Math.round(this.relativeCount ?? 1), 1, MAX_RELATIVE_COUNT);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@computed.struct
|
|
120
|
+
get relativeDraft(): DateRangeSelection {
|
|
121
|
+
return {
|
|
122
|
+
kind: 'relative',
|
|
123
|
+
count: this.clampedCount,
|
|
124
|
+
unit: this.relativeUnit,
|
|
125
|
+
snap: this.relativeSnap
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get relativeRange(): LocalDateRange {
|
|
130
|
+
return this.parentModel.resolve(this.relativeDraft).current;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
get relativeDayCount(): number {
|
|
134
|
+
const {start, end} = this.relativeRange;
|
|
135
|
+
return end.diff(start, 'days') + 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* False for days, where a day is already a calendar boundary - snapping has nothing to round
|
|
140
|
+
* out, so days resolve identically either way.
|
|
141
|
+
*/
|
|
142
|
+
get snapApplies(): boolean {
|
|
143
|
+
return this.relativeUnit !== 'days';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Explains the Relative tab's snap choice, or the day window when snap does not apply. */
|
|
147
|
+
get relativeModeHelpText(): string {
|
|
148
|
+
const {relativeUnit: unit, anchorDate, anchorNoun} = this,
|
|
149
|
+
n = this.clampedCount,
|
|
150
|
+
singular = getDateRangeUnitLabel(unit, 1).toLowerCase(),
|
|
151
|
+
units = getDateRangeUnitLabel(unit, n).toLowerCase();
|
|
152
|
+
|
|
153
|
+
if (unit === 'days') {
|
|
154
|
+
return `Rolling window of exactly ${n} calendar ${units} ending ${anchorNoun}.`;
|
|
155
|
+
}
|
|
156
|
+
if (this.relativeSnap) {
|
|
157
|
+
// The window stops at the anchor date, so the last unit is normally incomplete -
|
|
158
|
+
// unless the anchor happens to fall on its final day.
|
|
159
|
+
const partial = anchorDate.endOf(unit) === anchorDate ? '' : ' (partial)',
|
|
160
|
+
containing = this.parentModel.isAnchorToday ? 'today' : 'the anchor date';
|
|
161
|
+
return n === 1
|
|
162
|
+
? `The${partial} calendar ${singular} containing ${containing}.`
|
|
163
|
+
: `${n} calendar ${units}, ending with the${partial} one containing ${containing}.`;
|
|
164
|
+
}
|
|
165
|
+
return `Rolling window of exactly ${n} ${units} ending ${anchorNoun}.`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Secondary line for the Relative tab's preview - the window's length. */
|
|
169
|
+
get relativePreviewCountText(): string {
|
|
170
|
+
const days = this.relativeDayCount;
|
|
171
|
+
return `${days} ${days === 1 ? 'day' : 'days'}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//------------------
|
|
175
|
+
// Months & Years tab
|
|
176
|
+
//------------------
|
|
177
|
+
get canYearBack(): boolean {
|
|
178
|
+
return this.gridYear > Math.max(MIN_SELECTION_YEAR, yearOf(this.minDate) ?? 0);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
get canYearForward(): boolean {
|
|
182
|
+
return this.gridYear < yearOf(this.maxDate);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
isMonthDisabled(month: number): boolean {
|
|
186
|
+
const {minDate, maxDate} = this,
|
|
187
|
+
start = getMonthStart(this.gridYear, month);
|
|
188
|
+
return start > maxDate || (minDate && start.endOfMonth() < minDate);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
isQuarterDisabled(quarter: number): boolean {
|
|
192
|
+
const {minDate, maxDate} = this,
|
|
193
|
+
start = getQuarterStart(this.gridYear, quarter);
|
|
194
|
+
return start > maxDate || (minDate && start.endOfQuarter() < minDate);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get yearSelectable(): boolean {
|
|
198
|
+
const {gridYear, minDate, maxDate} = this;
|
|
199
|
+
return gridYear <= yearOf(maxDate) && (!minDate || gridYear >= yearOf(minDate));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//------------------
|
|
203
|
+
// Custom tab
|
|
204
|
+
//------------------
|
|
205
|
+
/** The Custom tab's draft, or null before the first open seeds it. */
|
|
206
|
+
@computed.struct
|
|
207
|
+
get customDraft(): DateRangeSelection {
|
|
208
|
+
const {customStart, customEnd} = this;
|
|
209
|
+
if (!customStart || !customEnd) return null;
|
|
210
|
+
return {kind: 'custom', start: customStart.isoString, end: customEnd.isoString};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
get rightMonth(): LocalDate {
|
|
214
|
+
return this.leftMonth.add(1, 'months');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
get canCalBack(): boolean {
|
|
218
|
+
const {minDate, leftMonth} = this;
|
|
219
|
+
return minDate
|
|
220
|
+
? leftMonth > minDate.startOfMonth()
|
|
221
|
+
: yearOf(leftMonth) > MIN_SELECTION_YEAR;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
get canCalForward(): boolean {
|
|
225
|
+
return this.rightMonth < this.maxDate.startOfMonth();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
isDayDisabled(day: LocalDate): boolean {
|
|
229
|
+
const {minDate, maxDate} = this;
|
|
230
|
+
return day > maxDate || (minDate && day < minDate);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//------------------
|
|
234
|
+
// Footer
|
|
235
|
+
//------------------
|
|
236
|
+
/** True when the active tab holds a draft to apply - preset and period picks commit on click. */
|
|
237
|
+
get applyEnabled(): boolean {
|
|
238
|
+
return this.activeTabId === 'relative' || this.activeTabId === 'custom';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** True to offer Apply and Cancel - drafts wait for Apply rather than committing on change. */
|
|
242
|
+
get showApplyControls(): boolean {
|
|
243
|
+
return !this.parentModel.commitOnChange;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Default footer note, as a prose prefix plus the date for the component to set apart. A live
|
|
248
|
+
* anchor names its clock, so a user whose day differs from the app's sees why. A pinned or
|
|
249
|
+
* computed anchor is just its date.
|
|
250
|
+
*/
|
|
251
|
+
get anchorNote(): {prefix: string; date: string} {
|
|
252
|
+
const {anchorDate, parentModel} = this,
|
|
253
|
+
{anchorDay, businessDayMode} = parentModel,
|
|
254
|
+
date = parentModel.fmtSingleDay(anchorDate),
|
|
255
|
+
day = businessDayMode ? 'business day' : 'day';
|
|
256
|
+
|
|
257
|
+
let source = '';
|
|
258
|
+
if (anchorDay === 'localDay') {
|
|
259
|
+
source = ` your current ${day}`;
|
|
260
|
+
} else if (anchorDay === 'appDay') {
|
|
261
|
+
source = ` the current ${day} in ${XH.environmentService.get('appTimeZone')}`;
|
|
262
|
+
}
|
|
263
|
+
return {prefix: `Relative to${source}: `, date};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private get anchorNoun(): string {
|
|
267
|
+
return this.parentModel.isAnchorToday ? 'today' : 'on the anchor date';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
constructor(testId: string, tabSpecs: DateRangePickerTabSpec[]) {
|
|
271
|
+
super();
|
|
272
|
+
makeObservable(this);
|
|
273
|
+
this.testId = testId;
|
|
274
|
+
this.tabSpecs = tabSpecs;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
override onLinked() {
|
|
278
|
+
this.tabModel = new TabContainerModel({
|
|
279
|
+
// Tabs hold no state of their own - every draft lives on this model - so unmount
|
|
280
|
+
// on hide rather than retaining hidden content.
|
|
281
|
+
renderMode: 'unmountOnHide',
|
|
282
|
+
tabs: this.buildTabs(),
|
|
283
|
+
xhImpl: true
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
this.addReaction({
|
|
287
|
+
track: () => this.parentModel.tabs,
|
|
288
|
+
run: () => this.tabModel.setTabs(this.buildTabs())
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Never hold a snap that does nothing - it would persist into the committed value.
|
|
292
|
+
this.addReaction({
|
|
293
|
+
track: () => [this.snapApplies, this.relativeSnap],
|
|
294
|
+
run: ([applies, snap]) => {
|
|
295
|
+
if (!applies && snap) this.relativeSnap = false;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// With `commitOnChange`, a relative or custom draft applies as it changes, and activating
|
|
300
|
+
// either tab applies its current draft - so the popover never shows a period other than
|
|
301
|
+
// the applied one. Opening the popover itself commits nothing.
|
|
302
|
+
this.addReaction({
|
|
303
|
+
track: () => [this.isOpen, this.activeTabId, this.relativeDraft, this.customDraft],
|
|
304
|
+
run: () => this.commitDraftIfLive()
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
//------------------
|
|
309
|
+
// Open / close / commit
|
|
310
|
+
//------------------
|
|
311
|
+
toggleOpen() {
|
|
312
|
+
this.isOpen ? this.close() : this.open();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@action
|
|
316
|
+
open() {
|
|
317
|
+
const {parentModel, anchorDate, maxDate} = this,
|
|
318
|
+
{value, tabs} = parentModel,
|
|
319
|
+
valueTab = tabForKind(value.kind),
|
|
320
|
+
openTabId = tabs.includes(valueTab) ? valueTab : tabs[0];
|
|
321
|
+
|
|
322
|
+
this.tabModel.activateTab(openTabId);
|
|
323
|
+
|
|
324
|
+
// A draft starts at offset zero - stepping is done from the trigger, not the tab.
|
|
325
|
+
if (value.kind === 'relative') {
|
|
326
|
+
this.relativeCount = value.count;
|
|
327
|
+
this.relativeUnit = value.unit;
|
|
328
|
+
this.relativeSnap = value.snap;
|
|
329
|
+
} else {
|
|
330
|
+
this.relativeCount = 30;
|
|
331
|
+
this.relativeUnit = 'days';
|
|
332
|
+
this.relativeSnap = false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.gridYear = 'year' in value ? value.year : yearOf(anchorDate);
|
|
336
|
+
|
|
337
|
+
// Seed the custom draft from the applied resolved range, clamped to selectable bounds.
|
|
338
|
+
const {start: curStart, end: curEnd} = parentModel.currentRange;
|
|
339
|
+
let end = curEnd ?? maxDate;
|
|
340
|
+
if (end > maxDate) end = maxDate;
|
|
341
|
+
let start = curStart ?? end;
|
|
342
|
+
if (start > end) start = end;
|
|
343
|
+
if (this.minDate && start < this.minDate) start = this.minDate;
|
|
344
|
+
if (start > end) end = start;
|
|
345
|
+
this.customStart = start;
|
|
346
|
+
this.customEnd = end;
|
|
347
|
+
this.nextEdge = 'start';
|
|
348
|
+
|
|
349
|
+
this.showMonthOnRight(end);
|
|
350
|
+
|
|
351
|
+
this.openTabId = openTabId;
|
|
352
|
+
this.seededRelativeDraft = this.relativeDraft;
|
|
353
|
+
this.seededCustomDraft = this.customDraft;
|
|
354
|
+
|
|
355
|
+
this.isOpen = true;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@action
|
|
359
|
+
close() {
|
|
360
|
+
this.isOpen = false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@action
|
|
364
|
+
commit(value: DateRangeSelection) {
|
|
365
|
+
this.parentModel.setValue(value);
|
|
366
|
+
this.close();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Commit the active tab's draft - a no-op on tabs whose picks commit on click. */
|
|
370
|
+
apply() {
|
|
371
|
+
const {activeTabId} = this;
|
|
372
|
+
if (activeTabId === 'relative') this.commit(this.relativeDraft);
|
|
373
|
+
else if (activeTabId === 'custom') this.commit(this.customDraft);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
//------------------
|
|
377
|
+
// Tab interactions
|
|
378
|
+
//------------------
|
|
379
|
+
@action
|
|
380
|
+
stepCount(delta: number) {
|
|
381
|
+
this.relativeCount = clamp(this.clampedCount + delta, 1, MAX_RELATIVE_COUNT);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@action
|
|
385
|
+
stepYear(delta: number) {
|
|
386
|
+
this.gridYear += delta;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
@action
|
|
390
|
+
stepCalendar(delta: number) {
|
|
391
|
+
this.leftMonth = this.leftMonth.add(delta, 'months');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@action
|
|
395
|
+
pickDay(day: LocalDate) {
|
|
396
|
+
if (this.nextEdge === 'start' || day < this.customStart) {
|
|
397
|
+
this.customStart = day;
|
|
398
|
+
this.customEnd = day;
|
|
399
|
+
this.nextEdge = 'end';
|
|
400
|
+
} else {
|
|
401
|
+
this.customEnd = day;
|
|
402
|
+
this.nextEdge = 'start';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@action
|
|
407
|
+
commitStartInput(val: LocalDate) {
|
|
408
|
+
if (!val) return;
|
|
409
|
+
this.customStart = val;
|
|
410
|
+
if (val > this.customEnd) this.customEnd = val;
|
|
411
|
+
this.showMonthOnLeft(val);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
@action
|
|
415
|
+
commitEndInput(val: LocalDate) {
|
|
416
|
+
if (!val) return;
|
|
417
|
+
this.customEnd = val;
|
|
418
|
+
if (val < this.customStart) this.customStart = val;
|
|
419
|
+
this.showMonthOnRight(val);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
//------------------
|
|
423
|
+
// Implementation
|
|
424
|
+
//------------------
|
|
425
|
+
/** Tab configs in the order the app configured them - the model's `tabs` sets the order. */
|
|
426
|
+
private buildTabs(): TabConfig[] {
|
|
427
|
+
return this.parentModel.tabs.map(id => {
|
|
428
|
+
const {title, icon, content} = this.tabSpecs.find(spec => spec.id === id);
|
|
429
|
+
return {id, title, icon, content: () => content({model: this, testId: this.testId})};
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private commitDraftIfLive() {
|
|
434
|
+
const {parentModel, isOpen, activeTabId, openTabId} = this;
|
|
435
|
+
if (!isOpen || !parentModel.commitOnChange) return;
|
|
436
|
+
|
|
437
|
+
const tabChanged = activeTabId !== openTabId;
|
|
438
|
+
if (activeTabId === 'relative') {
|
|
439
|
+
if (tabChanged || !isEqual(this.relativeDraft, this.seededRelativeDraft)) {
|
|
440
|
+
parentModel.setValue(this.relativeDraft);
|
|
441
|
+
}
|
|
442
|
+
} else if (activeTabId === 'custom' && this.customDraft) {
|
|
443
|
+
if (tabChanged || !isEqual(this.customDraft, this.seededCustomDraft)) {
|
|
444
|
+
parentModel.setValue(this.customDraft);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Position the calendars so `date`'s month shows on the left, right within maxDate. */
|
|
450
|
+
@action
|
|
451
|
+
private showMonthOnLeft(date: LocalDate) {
|
|
452
|
+
const maxLeft = this.maxDate.startOfMonth().subtract(1, 'months'),
|
|
453
|
+
month = date.startOfMonth();
|
|
454
|
+
this.leftMonth = month < maxLeft ? month : maxLeft;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Position the calendars so `date`'s month shows on the right, clamped to maxDate. */
|
|
458
|
+
@action
|
|
459
|
+
private showMonthOnRight(date: LocalDate) {
|
|
460
|
+
const maxRight = this.maxDate.startOfMonth(),
|
|
461
|
+
month = date.startOfMonth();
|
|
462
|
+
this.leftMonth = (month < maxRight ? month : maxRight).subtract(1, 'months');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** The tab on which a selection of the given kind is made. */
|
|
467
|
+
function tabForKind(kind: DateRangeSelection['kind']): DateRangePickerTab {
|
|
468
|
+
switch (kind) {
|
|
469
|
+
case 'preset':
|
|
470
|
+
return 'presets';
|
|
471
|
+
case 'month':
|
|
472
|
+
case 'quarter':
|
|
473
|
+
case 'year':
|
|
474
|
+
return 'period';
|
|
475
|
+
default:
|
|
476
|
+
return kind;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function yearOf(date: LocalDate | null): number | null {
|
|
481
|
+
return date ? Number(date.format('YYYY')) : null;
|
|
482
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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 type {DateRangePickerLocalModel} from './DateRangePickerLocalModel';
|
|
8
|
+
import {div, filler, hbox, span, vbox} from '@xh/hoist/cmp/layout';
|
|
9
|
+
import {hoistCmp, type Intent} from '@xh/hoist/core';
|
|
10
|
+
import {button, type ButtonProps} from '@xh/hoist/desktop/cmp/button';
|
|
11
|
+
import {Icon} from '@xh/hoist/icon';
|
|
12
|
+
import {getTestId} from '@xh/hoist/utils/js';
|
|
13
|
+
import {sectionHeader} from './TabUtils';
|
|
14
|
+
|
|
15
|
+
/** Period tab - a year of months, each quarter alongside its months, and the year itself. */
|
|
16
|
+
const MONTH_LABELS = [
|
|
17
|
+
'Jan',
|
|
18
|
+
'Feb',
|
|
19
|
+
'Mar',
|
|
20
|
+
'Apr',
|
|
21
|
+
'May',
|
|
22
|
+
'Jun',
|
|
23
|
+
'Jul',
|
|
24
|
+
'Aug',
|
|
25
|
+
'Sep',
|
|
26
|
+
'Oct',
|
|
27
|
+
'Nov',
|
|
28
|
+
'Dec'
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const periodTab = hoistCmp.factory<DateRangePickerLocalModel>(({model, testId}) => {
|
|
32
|
+
const {parentModel, gridYear} = model,
|
|
33
|
+
{value} = parentModel,
|
|
34
|
+
yearRange = parentModel.resolve({kind: 'year', year: gridYear}).current,
|
|
35
|
+
yearSelected = value.kind === 'year' && value.year === gridYear;
|
|
36
|
+
|
|
37
|
+
return vbox({
|
|
38
|
+
className: 'xh-date-range-picker-popover__tab',
|
|
39
|
+
items: [
|
|
40
|
+
hbox({
|
|
41
|
+
className: 'xh-date-range-picker-popover__month-hdr',
|
|
42
|
+
items: [
|
|
43
|
+
sectionHeader('Months & Years'),
|
|
44
|
+
filler(),
|
|
45
|
+
button({
|
|
46
|
+
className: 'xh-date-range-picker-popover__nav-btn',
|
|
47
|
+
icon: Icon.angleLeft(),
|
|
48
|
+
minimal: true,
|
|
49
|
+
disabled: !model.canYearBack,
|
|
50
|
+
testId: getTestId(testId, 'year-back'),
|
|
51
|
+
onClick: () => model.stepYear(-1)
|
|
52
|
+
}),
|
|
53
|
+
span({
|
|
54
|
+
className: 'xh-date-range-picker-popover__year-val',
|
|
55
|
+
item: String(gridYear)
|
|
56
|
+
}),
|
|
57
|
+
button({
|
|
58
|
+
className: 'xh-date-range-picker-popover__nav-btn',
|
|
59
|
+
icon: Icon.angleRight(),
|
|
60
|
+
minimal: true,
|
|
61
|
+
disabled: !model.canYearForward,
|
|
62
|
+
testId: getTestId(testId, 'year-forward'),
|
|
63
|
+
onClick: () => model.stepYear(1)
|
|
64
|
+
})
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
67
|
+
div({
|
|
68
|
+
// One row per quarter: its three months, then the quarter itself - so the quarter
|
|
69
|
+
// labels the months it contains, and both are a click away.
|
|
70
|
+
className: 'xh-date-range-picker-popover__month-grid',
|
|
71
|
+
items: [1, 2, 3, 4].flatMap(quarter => {
|
|
72
|
+
const quarterActive =
|
|
73
|
+
value.kind === 'quarter' &&
|
|
74
|
+
value.quarter === quarter &&
|
|
75
|
+
value.year === gridYear,
|
|
76
|
+
months = [quarter * 3 - 2, quarter * 3 - 1, quarter * 3];
|
|
77
|
+
return [
|
|
78
|
+
...months.map(month => {
|
|
79
|
+
const active =
|
|
80
|
+
value.kind === 'month' &&
|
|
81
|
+
value.month === month &&
|
|
82
|
+
value.year === gridYear;
|
|
83
|
+
return button({
|
|
84
|
+
key: month,
|
|
85
|
+
className: 'xh-date-range-picker-popover__month-btn',
|
|
86
|
+
text: MONTH_LABELS[month - 1],
|
|
87
|
+
...selectableProps(active, model.intent),
|
|
88
|
+
disabled: model.isMonthDisabled(month),
|
|
89
|
+
testId: getTestId(testId, `month-${month}`),
|
|
90
|
+
onClick: () => model.commit({kind: 'month', year: gridYear, month})
|
|
91
|
+
});
|
|
92
|
+
}),
|
|
93
|
+
button({
|
|
94
|
+
key: `q${quarter}`,
|
|
95
|
+
className: 'xh-date-range-picker-popover__quarter-btn',
|
|
96
|
+
text: `Q${quarter}`,
|
|
97
|
+
...selectableProps(quarterActive, model.intent),
|
|
98
|
+
disabled: model.isQuarterDisabled(quarter),
|
|
99
|
+
testId: getTestId(testId, `quarter-${quarter}`),
|
|
100
|
+
onClick: () => model.commit({kind: 'quarter', year: gridYear, quarter})
|
|
101
|
+
})
|
|
102
|
+
];
|
|
103
|
+
})
|
|
104
|
+
}),
|
|
105
|
+
button({
|
|
106
|
+
className: 'xh-date-range-picker-popover__year-row',
|
|
107
|
+
...selectableProps(yearSelected, model.intent),
|
|
108
|
+
disabled: !model.yearSelectable,
|
|
109
|
+
testId: getTestId(testId, 'year-row'),
|
|
110
|
+
onClick: () => model.commit({kind: 'year', year: gridYear}),
|
|
111
|
+
items: [
|
|
112
|
+
Icon.calendar(),
|
|
113
|
+
span({
|
|
114
|
+
className: 'xh-date-range-picker-popover__year-row-label',
|
|
115
|
+
item: String(gridYear)
|
|
116
|
+
}),
|
|
117
|
+
span({
|
|
118
|
+
// As on the Presets tab - the single-tab popover is too narrow for dates.
|
|
119
|
+
omit: model.singleTab,
|
|
120
|
+
className: 'xh-date-range-picker-popover__row-range',
|
|
121
|
+
item: parentModel.fmtRange(yearRange)
|
|
122
|
+
})
|
|
123
|
+
]
|
|
124
|
+
})
|
|
125
|
+
]
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Button props for a selectable period: a solid accent fill when selected (Hoist buttons are
|
|
131
|
+
* minimal by default, so that needs `minimal: false` too), outlined otherwise.
|
|
132
|
+
*/
|
|
133
|
+
const selectableProps = (active: boolean, intent: Intent): Partial<ButtonProps> => ({
|
|
134
|
+
outlined: !active,
|
|
135
|
+
minimal: !active,
|
|
136
|
+
active,
|
|
137
|
+
intent: active ? (intent ?? 'primary') : undefined
|
|
138
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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 {getDateRangePresetName} from '@xh/hoist/cmp/daterange';
|
|
8
|
+
import type {DateRangePickerLocalModel} from './DateRangePickerLocalModel';
|
|
9
|
+
import {div, span, vbox} from '@xh/hoist/cmp/layout';
|
|
10
|
+
import {hoistCmp} from '@xh/hoist/core';
|
|
11
|
+
import {Icon} from '@xh/hoist/icon';
|
|
12
|
+
import {getTestId, TEST_ID} from '@xh/hoist/utils/js';
|
|
13
|
+
import classNames from 'classnames';
|
|
14
|
+
import {clickable, sectionHeader} from './TabUtils';
|
|
15
|
+
|
|
16
|
+
/** Presets tab - one row per configured preset, committing on click. */
|
|
17
|
+
export const presetsTab = hoistCmp.factory<DateRangePickerLocalModel>(({model, testId}) => {
|
|
18
|
+
const {parentModel, singleTab} = model,
|
|
19
|
+
{value, context} = parentModel;
|
|
20
|
+
|
|
21
|
+
return vbox({
|
|
22
|
+
className: 'xh-date-range-picker-popover__tab',
|
|
23
|
+
items: [
|
|
24
|
+
sectionHeader('Presets'),
|
|
25
|
+
...parentModel.presets.map(preset => {
|
|
26
|
+
const {token} = preset,
|
|
27
|
+
selected = value.kind === 'preset' && value.token === token,
|
|
28
|
+
range = parentModel.resolve({kind: 'preset', token}).current;
|
|
29
|
+
return div({
|
|
30
|
+
key: token,
|
|
31
|
+
className: classNames(
|
|
32
|
+
'xh-date-range-picker-popover__preset-row',
|
|
33
|
+
selected && 'xh-date-range-picker-popover__preset-row--selected'
|
|
34
|
+
),
|
|
35
|
+
[TEST_ID]: getTestId(testId, `preset-${token}`),
|
|
36
|
+
...clickable(() => model.commit({kind: 'preset', token})),
|
|
37
|
+
items: [
|
|
38
|
+
Icon.check({
|
|
39
|
+
className: classNames(
|
|
40
|
+
'xh-date-range-picker-popover__row-check',
|
|
41
|
+
!selected && 'xh-date-range-picker-popover__row-check--hidden'
|
|
42
|
+
)
|
|
43
|
+
}),
|
|
44
|
+
span({
|
|
45
|
+
className: 'xh-date-range-picker-popover__row-name',
|
|
46
|
+
item: getDateRangePresetName(preset, context)
|
|
47
|
+
}),
|
|
48
|
+
span({
|
|
49
|
+
omit: singleTab,
|
|
50
|
+
className: 'xh-date-range-picker-popover__row-range',
|
|
51
|
+
item: parentModel.fmtRange(range)
|
|
52
|
+
})
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
})
|
|
56
|
+
]
|
|
57
|
+
});
|
|
58
|
+
});
|