@antelopejs/dms 0.7.8 → 0.7.10
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/CHANGELOG.md +45 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +25 -0
- package/dist/test/attachment-host/capabilities.js +53 -1
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +13 -0
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -1,23 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
readonly,
|
|
4
|
+
ref,
|
|
5
|
+
shallowRef,
|
|
6
|
+
type ComputedRef,
|
|
7
|
+
type Ref,
|
|
8
|
+
} from "vue";
|
|
9
|
+
import type {
|
|
10
|
+
PageLiveNote,
|
|
11
|
+
PageLiveNoteInput,
|
|
12
|
+
PageLiveOptions,
|
|
13
|
+
} from "../../../types/live";
|
|
14
|
+
import { refreshLivePageBlocks } from "../../../utils/blockRefresh";
|
|
15
|
+
import { liveIntervalMs } from "./liveCadence";
|
|
16
|
+
import { LiveHolds, toLiveNote } from "./liveHolds";
|
|
17
|
+
import { LivePoller } from "./livePoller";
|
|
18
|
+
import { liveStorageKey, readStoredPause, storePause } from "./liveStorage";
|
|
19
|
+
import { MissedTally } from "./missedTally";
|
|
3
20
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Why a page hidden in a background tab is paused.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export const HIDDEN_TAB_NOTE: PageLiveNote = {
|
|
27
|
+
text: "$dms.live.reason_hidden",
|
|
28
|
+
};
|
|
7
29
|
|
|
8
30
|
/**
|
|
9
31
|
* One block or table of a live page: what it reads again, and what it missed
|
|
10
|
-
* while the page was paused.
|
|
32
|
+
* while the page was paused or the block held.
|
|
11
33
|
*
|
|
12
34
|
* @internal
|
|
13
35
|
*/
|
|
14
36
|
export interface LiveSubscription {
|
|
15
37
|
/**
|
|
16
38
|
* An event behind the block arrived. `event` identifies it, so one event
|
|
17
|
-
* reaching several blocks counts once in the page's `missedCount
|
|
39
|
+
* reaching several blocks counts once in the page's `missedCount`; its
|
|
40
|
+
* `payload.ids` or `payload.count` say how many rows it stands for.
|
|
18
41
|
*/
|
|
19
42
|
trigger(event?: object): void;
|
|
20
|
-
/**
|
|
43
|
+
/** Rows and events the block received while paused or held, not read yet. */
|
|
21
44
|
readonly missed: Readonly<Ref<number>>;
|
|
22
45
|
/** When the block last read through the gate (ms since the epoch), 0 before. */
|
|
23
46
|
readonly lastReadAt: Readonly<Ref<number>>;
|
|
@@ -28,11 +51,29 @@ export interface LiveSubscription {
|
|
|
28
51
|
* forgets what was missed, and a waiting trailing read is dropped.
|
|
29
52
|
*/
|
|
30
53
|
acknowledge(): void;
|
|
54
|
+
/**
|
|
55
|
+
* Holds the block alone while the page stays live: its events are counted,
|
|
56
|
+
* not read. Released on a live page, it reads once if it missed any.
|
|
57
|
+
*/
|
|
58
|
+
setHeld(isHeld: boolean): void;
|
|
31
59
|
unsubscribe(): void;
|
|
32
60
|
}
|
|
33
61
|
|
|
62
|
+
/** How the page starts, beside what the browser remembers. @internal */
|
|
63
|
+
export interface PageLiveStart {
|
|
64
|
+
/** The URL asked for the page paused (`?live=0`): not remembered. */
|
|
65
|
+
isPausedByUrl?: boolean;
|
|
66
|
+
/** Reads the page's blocks while the stream is down; `refreshLivePageBlocks`. */
|
|
67
|
+
poll?: () => void;
|
|
68
|
+
/**
|
|
69
|
+
* Starts live whatever the browser remembers, until `restoreChoice()`: the
|
|
70
|
+
* page renderer restores it once mounted, so hydration matches the server.
|
|
71
|
+
*/
|
|
72
|
+
isRestoreDeferred?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
/**
|
|
35
|
-
* The
|
|
76
|
+
* The Live / Pause state of one page and the gate every refetch of its
|
|
36
77
|
* blocks goes through (see `PageLiveOptions` in interface-dms).
|
|
37
78
|
*
|
|
38
79
|
* @internal
|
|
@@ -40,97 +81,110 @@ export interface LiveSubscription {
|
|
|
40
81
|
export interface PageLiveController {
|
|
41
82
|
readonly pageId: string;
|
|
42
83
|
readonly isPaused: Readonly<Ref<boolean>>;
|
|
84
|
+
/** Why the page paused by itself; `null` when live or paused by the user. */
|
|
85
|
+
readonly pauseReason: ComputedRef<PageLiveNote | null>;
|
|
43
86
|
/** When the page last read its data (ms since the epoch). */
|
|
44
87
|
readonly lastUpdatedAt: Readonly<Ref<number>>;
|
|
45
|
-
/**
|
|
88
|
+
/** Rows and distinct events received since the pause. */
|
|
46
89
|
readonly missedCount: Readonly<Ref<number>>;
|
|
90
|
+
/** What a module's component says the missed events hold. */
|
|
91
|
+
readonly missedDetail: Readonly<Ref<PageLiveNote | null>>;
|
|
92
|
+
/** The stream is down and the page reads its blocks on a clock. */
|
|
93
|
+
readonly isPolling: Readonly<Ref<boolean>>;
|
|
94
|
+
/**
|
|
95
|
+
* Applies the remembered choice and `?live=0`, once: what the controller
|
|
96
|
+
* does on creation unless `isRestoreDeferred`.
|
|
97
|
+
*/
|
|
98
|
+
restoreChoice(): void;
|
|
47
99
|
pause(): void;
|
|
100
|
+
/** Leaves the user's pause and lifts the holds of the moment. */
|
|
48
101
|
resume(): void;
|
|
49
102
|
toggle(): void;
|
|
103
|
+
hold(owner: symbol, reason: PageLiveNoteInput): void;
|
|
104
|
+
release(owner: symbol): void;
|
|
105
|
+
setMissedDetail(detail: PageLiveNoteInput | null): void;
|
|
106
|
+
setHidden(isHidden: boolean): void;
|
|
107
|
+
setStreamConnected(isConnected: boolean): void;
|
|
108
|
+
/** The span of a period the page shows, `null` once it is gone. */
|
|
109
|
+
reportPeriodSpan(owner: symbol, spanMs: number | null): void;
|
|
50
110
|
subscribe(refresh: () => void): LiveSubscription;
|
|
51
111
|
dispose(): void;
|
|
52
112
|
}
|
|
53
113
|
|
|
54
114
|
interface SubscriberState {
|
|
55
115
|
refresh: () => void;
|
|
56
|
-
|
|
116
|
+
tally: MissedTally;
|
|
57
117
|
lastRunAt: Ref<number>;
|
|
58
118
|
timer: ReturnType<typeof setTimeout> | null;
|
|
59
|
-
/** The
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/** Browser storage of the paused pages; absent on the server or when denied. */
|
|
64
|
-
function liveStorage(): Storage | undefined {
|
|
65
|
-
try {
|
|
66
|
-
return typeof window === "undefined" ? undefined : window.localStorage;
|
|
67
|
-
} catch {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function readStoredPause(pageId: string): boolean {
|
|
73
|
-
return liveStorage()?.getItem(STORAGE_KEY_PREFIX + pageId) === PAUSED_VALUE;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function storePause(pageId: string, isPaused: boolean): void {
|
|
77
|
-
const storage = liveStorage();
|
|
78
|
-
if (!storage) return;
|
|
79
|
-
try {
|
|
80
|
-
if (isPaused) storage.setItem(STORAGE_KEY_PREFIX + pageId, PAUSED_VALUE);
|
|
81
|
-
else storage.removeItem(STORAGE_KEY_PREFIX + pageId);
|
|
82
|
-
} catch {
|
|
83
|
-
// A full or locked storage only loses the memory of the choice.
|
|
84
|
-
}
|
|
119
|
+
/** The events the trailing call stands for, counted if the page pauses first. */
|
|
120
|
+
pendingEvents: Array<object | undefined>;
|
|
121
|
+
isHeld: boolean;
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
class PageLive implements PageLiveController {
|
|
88
125
|
readonly isPaused: Readonly<Ref<boolean>>;
|
|
126
|
+
readonly pauseReason: ComputedRef<PageLiveNote | null>;
|
|
89
127
|
readonly lastUpdatedAt: Readonly<Ref<number>>;
|
|
90
128
|
readonly missedCount: Readonly<Ref<number>>;
|
|
91
|
-
|
|
129
|
+
readonly missedDetail: Readonly<Ref<PageLiveNote | null>>;
|
|
130
|
+
readonly isPolling: Readonly<Ref<boolean>>;
|
|
131
|
+
private readonly paused = ref(false);
|
|
132
|
+
private readonly isUserPaused = ref(false);
|
|
133
|
+
private readonly isPausedByUrl: boolean;
|
|
134
|
+
private isRestored = false;
|
|
135
|
+
private readonly isHidden = ref(false);
|
|
136
|
+
private readonly holds = new LiveHolds();
|
|
137
|
+
private readonly detail = shallowRef<PageLiveNote | null>(null);
|
|
92
138
|
private readonly updatedAt = ref(Date.now());
|
|
93
|
-
private readonly
|
|
139
|
+
private readonly tally = new MissedTally();
|
|
94
140
|
private readonly subscribers = new Set<SubscriberState>();
|
|
95
|
-
private
|
|
96
|
-
private readonly
|
|
141
|
+
private readonly periodSpans = new Map<symbol, number>();
|
|
142
|
+
private readonly poller: LivePoller;
|
|
143
|
+
private readonly storageKey: string;
|
|
144
|
+
private isStreamConnected = true;
|
|
97
145
|
|
|
98
146
|
constructor(
|
|
99
147
|
readonly pageId: string,
|
|
100
|
-
options: PageLiveOptions,
|
|
148
|
+
private readonly options: PageLiveOptions,
|
|
149
|
+
start: PageLiveStart = {},
|
|
101
150
|
) {
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
151
|
+
this.storageKey = liveStorageKey(pageId, options.rememberKey);
|
|
152
|
+
this.isPausedByUrl = !!start.isPausedByUrl;
|
|
153
|
+
this.poller = new LivePoller(
|
|
154
|
+
() => this.intervalMs(),
|
|
155
|
+
start.poll ?? refreshLivePageBlocks,
|
|
156
|
+
);
|
|
104
157
|
this.isPaused = readonly(this.paused);
|
|
158
|
+
this.pauseReason = computed(() => this.reason());
|
|
105
159
|
this.lastUpdatedAt = readonly(this.updatedAt);
|
|
106
|
-
this.missedCount =
|
|
160
|
+
this.missedCount = this.tally.total;
|
|
161
|
+
this.missedDetail = readonly(this.detail);
|
|
162
|
+
this.isPolling = this.poller.isPolling;
|
|
163
|
+
if (!start.isRestoreDeferred) this.restoreChoice();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
restoreChoice(): void {
|
|
167
|
+
if (this.isRestored) return;
|
|
168
|
+
this.isRestored = true;
|
|
169
|
+
if (!this.isPausedByUrl && !readStoredPause(this.storageKey)) return;
|
|
170
|
+
this.isUserPaused.value = true;
|
|
171
|
+
this.sync();
|
|
107
172
|
}
|
|
108
173
|
|
|
109
174
|
pause(): void {
|
|
110
|
-
if (this.
|
|
111
|
-
this.
|
|
112
|
-
storePause(this.
|
|
113
|
-
|
|
114
|
-
for (const state of this.subscribers) {
|
|
115
|
-
if (!state.timer) continue;
|
|
116
|
-
const event = state.pendingEvent;
|
|
117
|
-
clearTimer(state);
|
|
118
|
-
this.countMissed(state, event);
|
|
119
|
-
}
|
|
175
|
+
if (this.isUserPaused.value) return;
|
|
176
|
+
this.isUserPaused.value = true;
|
|
177
|
+
storePause(this.storageKey, true);
|
|
178
|
+
this.sync();
|
|
120
179
|
}
|
|
121
180
|
|
|
122
181
|
resume(): void {
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
this.missedTotal.value = 0;
|
|
127
|
-
this.countedEvents = new WeakSet();
|
|
128
|
-
this.updatedAt.value = Date.now();
|
|
129
|
-
for (const state of this.subscribers) {
|
|
130
|
-
if (state.missed.value === 0) continue;
|
|
131
|
-
state.missed.value = 0;
|
|
132
|
-
this.run(state);
|
|
182
|
+
if (this.isUserPaused.value) {
|
|
183
|
+
this.isUserPaused.value = false;
|
|
184
|
+
storePause(this.storageKey, false);
|
|
133
185
|
}
|
|
186
|
+
this.holds.liftAll();
|
|
187
|
+
this.sync();
|
|
134
188
|
}
|
|
135
189
|
|
|
136
190
|
toggle(): void {
|
|
@@ -138,27 +192,55 @@ class PageLive implements PageLiveController {
|
|
|
138
192
|
else this.pause();
|
|
139
193
|
}
|
|
140
194
|
|
|
195
|
+
hold(owner: symbol, reason: PageLiveNoteInput): void {
|
|
196
|
+
this.holds.set(owner, reason);
|
|
197
|
+
this.sync();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
release(owner: symbol): void {
|
|
201
|
+
this.holds.release(owner);
|
|
202
|
+
this.sync();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setMissedDetail(detail: PageLiveNoteInput | null): void {
|
|
206
|
+
this.detail.value = detail === null ? null : toLiveNote(detail);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setHidden(isHidden: boolean): void {
|
|
210
|
+
this.isHidden.value = isHidden;
|
|
211
|
+
this.sync();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
setStreamConnected(isConnected: boolean): void {
|
|
215
|
+
this.isStreamConnected = isConnected;
|
|
216
|
+
this.updatePolling();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
reportPeriodSpan(owner: symbol, spanMs: number | null): void {
|
|
220
|
+
if (spanMs === null) this.periodSpans.delete(owner);
|
|
221
|
+
else this.periodSpans.set(owner, spanMs);
|
|
222
|
+
}
|
|
223
|
+
|
|
141
224
|
subscribe(refresh: () => void): LiveSubscription {
|
|
142
225
|
const state: SubscriberState = {
|
|
143
226
|
refresh,
|
|
144
|
-
|
|
227
|
+
tally: new MissedTally(),
|
|
145
228
|
lastRunAt: ref(0),
|
|
146
229
|
timer: null,
|
|
147
|
-
|
|
230
|
+
pendingEvents: [],
|
|
231
|
+
isHeld: false,
|
|
148
232
|
};
|
|
149
233
|
this.subscribers.add(state);
|
|
150
234
|
return {
|
|
151
235
|
trigger: (event) => this.trigger(state, event),
|
|
152
|
-
missed:
|
|
236
|
+
missed: state.tally.total,
|
|
153
237
|
lastReadAt: readonly(state.lastRunAt),
|
|
154
|
-
catchUp: () =>
|
|
155
|
-
state.missed.value = 0;
|
|
156
|
-
this.run(state);
|
|
157
|
-
},
|
|
238
|
+
catchUp: () => this.catchUp(state),
|
|
158
239
|
acknowledge: () => {
|
|
159
|
-
state.
|
|
240
|
+
state.tally.reset();
|
|
160
241
|
this.markRead(state);
|
|
161
242
|
},
|
|
243
|
+
setHeld: (isHeld) => this.setHeld(state, isHeld),
|
|
162
244
|
unsubscribe: () => {
|
|
163
245
|
clearTimer(state);
|
|
164
246
|
this.subscribers.delete(state);
|
|
@@ -169,27 +251,100 @@ class PageLive implements PageLiveController {
|
|
|
169
251
|
dispose(): void {
|
|
170
252
|
for (const state of this.subscribers) clearTimer(state);
|
|
171
253
|
this.subscribers.clear();
|
|
254
|
+
this.poller.stop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// A hidden tab outranks everything: nobody sees the page. The user's pause
|
|
258
|
+
// comes next, and says nothing more; a hold names its reason.
|
|
259
|
+
private reason(): PageLiveNote | null {
|
|
260
|
+
if (this.isHidden.value) return HIDDEN_TAB_NOTE;
|
|
261
|
+
if (this.isUserPaused.value) return null;
|
|
262
|
+
return this.holds.activeNote.value;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private sync(): void {
|
|
266
|
+
const shouldPause =
|
|
267
|
+
this.isUserPaused.value || this.isHidden.value || this.holds.isActive;
|
|
268
|
+
if (shouldPause !== this.paused.value) {
|
|
269
|
+
if (shouldPause) this.enterPause();
|
|
270
|
+
else this.leavePause();
|
|
271
|
+
}
|
|
272
|
+
this.updatePolling();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private enterPause(): void {
|
|
276
|
+
this.paused.value = true;
|
|
277
|
+
// A trailing read still waiting stands for events not shown yet.
|
|
278
|
+
for (const state of this.subscribers) this.flushPending(state, true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private leavePause(): void {
|
|
282
|
+
this.paused.value = false;
|
|
283
|
+
this.tally.reset();
|
|
284
|
+
this.detail.value = null;
|
|
285
|
+
this.updatedAt.value = Date.now();
|
|
286
|
+
for (const state of this.subscribers) {
|
|
287
|
+
if (state.isHeld || state.tally.total.value === 0) continue;
|
|
288
|
+
this.catchUp(state);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private updatePolling(): void {
|
|
293
|
+
this.poller.update(!this.isStreamConnected && !this.paused.value);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private intervalMs(): number {
|
|
297
|
+
const spans = [...this.periodSpans.values()];
|
|
298
|
+
return liveIntervalMs(
|
|
299
|
+
this.options,
|
|
300
|
+
spans.length > 0 ? Math.max(...spans) : null,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private setHeld(state: SubscriberState, isHeld: boolean): void {
|
|
305
|
+
if (state.isHeld === isHeld) return;
|
|
306
|
+
state.isHeld = isHeld;
|
|
307
|
+
if (isHeld) this.flushPending(state, false);
|
|
308
|
+
else if (!this.paused.value && state.tally.total.value > 0) {
|
|
309
|
+
this.catchUp(state);
|
|
310
|
+
}
|
|
172
311
|
}
|
|
173
312
|
|
|
174
313
|
private trigger(state: SubscriberState, event: object | undefined): void {
|
|
175
|
-
if (this.paused.value) this.countMissed(state, event);
|
|
314
|
+
if (this.paused.value) this.countMissed(state, event, true);
|
|
315
|
+
else if (state.isHeld) this.countMissed(state, event, false);
|
|
176
316
|
else this.triggerDirect(state, event);
|
|
177
317
|
}
|
|
178
318
|
|
|
179
319
|
// Leading and trailing: the first event of a quiet block reads at once, the
|
|
180
|
-
// ones within
|
|
181
|
-
// the last event of a burst is never lost.
|
|
320
|
+
// ones within the interval of a read collapse into one read when it ends,
|
|
321
|
+
// so the last event of a burst is never lost.
|
|
182
322
|
private triggerDirect(state: SubscriberState, event: object | undefined) {
|
|
183
|
-
if (state.timer)
|
|
184
|
-
|
|
323
|
+
if (state.timer) {
|
|
324
|
+
state.pendingEvents.push(event);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const wait = state.lastRunAt.value + this.intervalMs() - Date.now();
|
|
185
328
|
if (wait <= 0) {
|
|
186
329
|
this.run(state);
|
|
187
330
|
return;
|
|
188
331
|
}
|
|
189
|
-
state.
|
|
332
|
+
state.pendingEvents = [event];
|
|
190
333
|
state.timer = setTimeout(() => this.run(state), wait);
|
|
191
334
|
}
|
|
192
335
|
|
|
336
|
+
private flushPending(state: SubscriberState, isPageMissed: boolean): void {
|
|
337
|
+
if (!state.timer) return;
|
|
338
|
+
const events = state.pendingEvents;
|
|
339
|
+
clearTimer(state);
|
|
340
|
+
for (const event of events) this.countMissed(state, event, isPageMissed);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private catchUp(state: SubscriberState): void {
|
|
344
|
+
state.tally.reset();
|
|
345
|
+
this.run(state);
|
|
346
|
+
}
|
|
347
|
+
|
|
193
348
|
private run(state: SubscriberState): void {
|
|
194
349
|
this.markRead(state);
|
|
195
350
|
state.refresh();
|
|
@@ -201,29 +356,33 @@ class PageLive implements PageLiveController {
|
|
|
201
356
|
this.updatedAt.value = state.lastRunAt.value;
|
|
202
357
|
}
|
|
203
358
|
|
|
204
|
-
private countMissed(
|
|
205
|
-
state
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
359
|
+
private countMissed(
|
|
360
|
+
state: SubscriberState,
|
|
361
|
+
event: object | undefined,
|
|
362
|
+
isPageMissed: boolean,
|
|
363
|
+
): void {
|
|
364
|
+
state.tally.add(event);
|
|
365
|
+
if (isPageMissed) this.tally.add(event);
|
|
209
366
|
}
|
|
210
367
|
}
|
|
211
368
|
|
|
212
369
|
function clearTimer(state: SubscriberState): void {
|
|
213
370
|
if (state.timer) clearTimeout(state.timer);
|
|
214
371
|
state.timer = null;
|
|
215
|
-
state.
|
|
372
|
+
state.pendingEvents = [];
|
|
216
373
|
}
|
|
217
374
|
|
|
218
375
|
/**
|
|
219
|
-
* The controller of a page declared with `live`. Starts
|
|
220
|
-
* browser remembers
|
|
376
|
+
* The controller of a page declared with `live`. Starts paused when the URL
|
|
377
|
+
* asks for it or the browser remembers the page (or its `rememberKey`) paused,
|
|
378
|
+
* live otherwise.
|
|
221
379
|
*
|
|
222
380
|
* @internal
|
|
223
381
|
*/
|
|
224
382
|
export function createPageLiveController(
|
|
225
383
|
pageId: string,
|
|
226
384
|
options: PageLiveOptions,
|
|
385
|
+
start?: PageLiveStart,
|
|
227
386
|
): PageLiveController {
|
|
228
|
-
return new PageLive(pageId, options);
|
|
387
|
+
return new PageLive(pageId, options, start);
|
|
229
388
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { onScopeDispose, watch } from "vue";
|
|
2
|
+
import { currentPageLive } from "./currentPageLive";
|
|
3
|
+
|
|
4
|
+
/** The range a period selector shows. */
|
|
5
|
+
interface ShownRange {
|
|
6
|
+
from: Date;
|
|
7
|
+
to: Date;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tells the live page on screen how long a period the selector shows, so a
|
|
12
|
+
* page declaring `live.slowRateAfter` slows down over a long one. Follows
|
|
13
|
+
* the page's controller as it is installed or replaced, and withdraws the
|
|
14
|
+
* span when the selector goes. A range without bounds (all time) spans more
|
|
15
|
+
* than any period.
|
|
16
|
+
*
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function useLivePeriodSpan(range: () => ShownRange | null): void {
|
|
20
|
+
if (typeof window === "undefined") return;
|
|
21
|
+
const owner = Symbol("live-period");
|
|
22
|
+
const spanOf = (shown: ShownRange | null) =>
|
|
23
|
+
shown ? shown.to.getTime() - shown.from.getTime() : Number.POSITIVE_INFINITY;
|
|
24
|
+
const stop = watch(
|
|
25
|
+
[currentPageLive, () => spanOf(range())],
|
|
26
|
+
([controller, span], _previous, onCleanup) => {
|
|
27
|
+
controller?.reportPeriodSpan(owner, span);
|
|
28
|
+
onCleanup(() => controller?.reportPeriodSpan(owner, null));
|
|
29
|
+
},
|
|
30
|
+
{ immediate: true },
|
|
31
|
+
);
|
|
32
|
+
onScopeDispose(stop);
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onScopeDispose, watch } from "vue";
|
|
1
|
+
import { onMounted, onScopeDispose, watch } from "vue";
|
|
2
2
|
import type { PageLiveOptions } from "../../../types/live";
|
|
3
3
|
import { setCurrentPageLive } from "./currentPageLive";
|
|
4
4
|
import {
|
|
@@ -6,25 +6,93 @@ import {
|
|
|
6
6
|
type PageLiveController,
|
|
7
7
|
} from "./pageLiveController";
|
|
8
8
|
|
|
9
|
+
/** The URL query key opening a live page paused: `?live=0`. */
|
|
10
|
+
const LIVE_URL_KEY = "live";
|
|
11
|
+
const LIVE_URL_PAUSED = "0";
|
|
12
|
+
|
|
9
13
|
/** The page the renderer shows: its id and its `live` option. */
|
|
10
14
|
export interface PageLiveSource {
|
|
11
15
|
pageId: string | undefined;
|
|
12
16
|
live: PageLiveOptions | undefined;
|
|
13
17
|
}
|
|
14
18
|
|
|
19
|
+
/** What the page's controller follows around it. @internal */
|
|
20
|
+
export interface PageLiveEnvironment {
|
|
21
|
+
/** The realtime stream is open; while it is not, the page polls. */
|
|
22
|
+
isStreamConnected?: () => boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The URL asks for the page paused (`?live=0`). @internal */
|
|
26
|
+
export function isPausedByUrl(): boolean {
|
|
27
|
+
return (
|
|
28
|
+
new URLSearchParams(window.location.search).get(LIVE_URL_KEY) ===
|
|
29
|
+
LIVE_URL_PAUSED
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Resumed, the page no longer opens paused: a reload keeps it live.
|
|
34
|
+
function dropUrlPause(): void {
|
|
35
|
+
if (!isPausedByUrl()) return;
|
|
36
|
+
const url = new URL(window.location.href);
|
|
37
|
+
url.searchParams.delete(LIVE_URL_KEY);
|
|
38
|
+
window.history.replaceState(window.history.state, "", url.href);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Binds a controller to the browser around it: a hidden tab pauses it, the
|
|
43
|
+
* stream state switches its polling, and resuming drops `?live=0` from the
|
|
44
|
+
* URL. Returns the unbinding.
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export function bindPageLiveEnvironment(
|
|
49
|
+
controller: PageLiveController,
|
|
50
|
+
environment: PageLiveEnvironment,
|
|
51
|
+
): () => void {
|
|
52
|
+
const onVisibility = () =>
|
|
53
|
+
controller.setHidden(document.visibilityState === "hidden");
|
|
54
|
+
onVisibility();
|
|
55
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
56
|
+
const stops = [
|
|
57
|
+
() => document.removeEventListener("visibilitychange", onVisibility),
|
|
58
|
+
watch(
|
|
59
|
+
() => environment.isStreamConnected?.() ?? true,
|
|
60
|
+
(isConnected) => controller.setStreamConnected(isConnected),
|
|
61
|
+
{ immediate: true },
|
|
62
|
+
),
|
|
63
|
+
watch(controller.isPaused, (isPaused) => {
|
|
64
|
+
if (!isPaused) dropUrlPause();
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
return () => stops.forEach((stop) => stop());
|
|
68
|
+
}
|
|
69
|
+
|
|
15
70
|
/**
|
|
16
71
|
* Gives the page on screen its live controller while it declares `live`, and
|
|
17
72
|
* a new one when the renderer moves to another page. Browser only.
|
|
18
73
|
*
|
|
19
74
|
* @internal
|
|
20
75
|
*/
|
|
21
|
-
export function usePageLiveHost(
|
|
76
|
+
export function usePageLiveHost(
|
|
77
|
+
source: () => PageLiveSource,
|
|
78
|
+
environment: PageLiveEnvironment = {},
|
|
79
|
+
): void {
|
|
22
80
|
if (typeof window === "undefined") return;
|
|
23
81
|
let controller: PageLiveController | null = null;
|
|
82
|
+
let unbind: (() => void) | null = null;
|
|
83
|
+
// The server renders every page live: the remembered pause applies once
|
|
84
|
+
// mounted, so the hydrating render draws the same rows.
|
|
85
|
+
let isMounted = false;
|
|
86
|
+
onMounted(() => {
|
|
87
|
+
isMounted = true;
|
|
88
|
+
controller?.restoreChoice();
|
|
89
|
+
});
|
|
24
90
|
|
|
25
91
|
const install = (next: PageLiveController | null) => {
|
|
92
|
+
unbind?.();
|
|
26
93
|
controller?.dispose();
|
|
27
94
|
controller = next;
|
|
95
|
+
unbind = next ? bindPageLiveEnvironment(next, environment) : null;
|
|
28
96
|
setCurrentPageLive(next);
|
|
29
97
|
};
|
|
30
98
|
|
|
@@ -35,9 +103,15 @@ export function usePageLiveHost(source: () => PageLiveSource): void {
|
|
|
35
103
|
},
|
|
36
104
|
(key) => {
|
|
37
105
|
const { pageId, live } = source();
|
|
38
|
-
|
|
39
|
-
key && pageId && live
|
|
40
|
-
|
|
106
|
+
const next =
|
|
107
|
+
key && pageId && live
|
|
108
|
+
? createPageLiveController(pageId, live, {
|
|
109
|
+
isPausedByUrl: isPausedByUrl(),
|
|
110
|
+
isRestoreDeferred: true,
|
|
111
|
+
})
|
|
112
|
+
: null;
|
|
113
|
+
install(next);
|
|
114
|
+
if (isMounted) next?.restoreChoice();
|
|
41
115
|
},
|
|
42
116
|
{ immediate: true },
|
|
43
117
|
);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_COMPARISONS,
|
|
3
|
+
type PeriodComparison,
|
|
4
|
+
} from "#dms-core/app/composables/period/types";
|
|
5
|
+
|
|
6
|
+
/** How a selector offers its comparisons. */
|
|
7
|
+
export type ComparisonControl = "menu" | "toggle" | "hidden";
|
|
8
|
+
|
|
9
|
+
/** What the selector was given about its comparisons. */
|
|
10
|
+
export interface ComparisonControlOptions {
|
|
11
|
+
comparisons?: PeriodComparison[];
|
|
12
|
+
defaultComparison?: PeriodComparison;
|
|
13
|
+
comparisonControl?: ComparisonControl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The comparison control a selector draws, and what its period offers. */
|
|
17
|
+
export interface ResolvedComparisonControl {
|
|
18
|
+
control: ComparisonControl;
|
|
19
|
+
/** The comparisons the period accepts: `none` too behind a toggle. */
|
|
20
|
+
comparisons: PeriodComparison[];
|
|
21
|
+
defaultComparison: PeriodComparison | undefined;
|
|
22
|
+
/** The comparison a toggle switches on, else `null`. */
|
|
23
|
+
toggled: PeriodComparison | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NO_COMPARISON: PeriodComparison = "none";
|
|
27
|
+
|
|
28
|
+
function defaultControl(activeCount: number): ComparisonControl {
|
|
29
|
+
return activeCount === 1 ? "toggle" : "menu";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function preferredComparison(
|
|
33
|
+
active: PeriodComparison[],
|
|
34
|
+
defaultComparison: PeriodComparison | undefined,
|
|
35
|
+
): PeriodComparison | null {
|
|
36
|
+
const isActiveDefault =
|
|
37
|
+
defaultComparison !== undefined && active.includes(defaultComparison);
|
|
38
|
+
return isActiveDefault ? defaultComparison : (active[0] ?? null);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The comparison control of a selector: a `menu` of its comparisons, an
|
|
43
|
+
* inline `toggle` switching one comparison on and off (the default when it
|
|
44
|
+
* offers a single one besides `none`), or `hidden`, the comparison kept on.
|
|
45
|
+
* A selector offering no comparison besides `none` draws nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveComparisonControl(
|
|
48
|
+
options: ComparisonControlOptions,
|
|
49
|
+
): ResolvedComparisonControl {
|
|
50
|
+
const offered = options.comparisons ?? DEFAULT_COMPARISONS;
|
|
51
|
+
const active = offered.filter((entry) => entry !== NO_COMPARISON);
|
|
52
|
+
const preferred = preferredComparison(active, options.defaultComparison);
|
|
53
|
+
const control: ComparisonControl = preferred
|
|
54
|
+
? (options.comparisonControl ?? defaultControl(active.length))
|
|
55
|
+
: "hidden";
|
|
56
|
+
const isToggle = control === "toggle";
|
|
57
|
+
const isHidden = control === "hidden";
|
|
58
|
+
return {
|
|
59
|
+
control,
|
|
60
|
+
comparisons:
|
|
61
|
+
isToggle && !offered.includes(NO_COMPARISON)
|
|
62
|
+
? [NO_COMPARISON, ...offered]
|
|
63
|
+
: [...offered],
|
|
64
|
+
defaultComparison:
|
|
65
|
+
isHidden && preferred
|
|
66
|
+
? (options.defaultComparison ?? preferred)
|
|
67
|
+
: options.defaultComparison,
|
|
68
|
+
toggled: isToggle ? preferred : null,
|
|
69
|
+
};
|
|
70
|
+
}
|