@antelopejs/dms 0.7.7 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/test/unit/interfaces/dms-base/column-display.test.js +15 -0
  3. package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
  4. package/dist/test/unit/interfaces/dms-base/page-live.test.d.ts +1 -0
  5. package/dist/test/unit/interfaces/dms-base/page-live.test.js +66 -0
  6. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -0
  7. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +16 -0
  8. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  9. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +23 -3
  10. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -1
  11. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -1
  12. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +134 -0
  13. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +129 -0
  14. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -0
  15. package/frontend-vue/layers/dms-layout/app/types/page.ts +3 -0
  16. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  17. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  18. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +44 -8
  19. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +35 -0
  20. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +17 -0
  21. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +107 -12
  22. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +26 -11
  23. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/cellHelpers.ts +24 -0
  24. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +58 -16
  25. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +30 -3
  26. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +107 -0
  27. package/frontend-vue/layers/dms-ui/app/build/composables/live/formatElapsed.ts +25 -0
  28. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +229 -0
  29. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +8 -0
  30. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLiveToggleShortcut.ts +25 -0
  31. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +46 -0
  32. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +330 -0
  33. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +189 -0
  34. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +15 -3
  35. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +77 -0
  36. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +53 -18
  37. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  38. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +61 -2
  39. package/frontend-vue/layers/dms-ui/app/build/utils/rowClickTrigger.ts +73 -0
  40. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +92 -0
  41. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +10 -4
  42. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +19 -0
  43. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +90 -12
  44. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +52 -0
  45. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/index.ts +8 -0
  46. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewRowPanel.ts +30 -0
  47. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +15 -0
  48. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  49. package/frontend-vue/layers/dms-ui/app/config/shortcuts-registry.ts +5 -0
  50. package/frontend-vue/layers/dms-ui/app/types/live.ts +8 -0
  51. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +5 -2
  52. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +44 -6
  53. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +28 -0
  54. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +28 -0
  55. package/frontend-vue/tests/chart-live-pause.test.ts +74 -0
  56. package/frontend-vue/tests/field-row.test.ts +14 -0
  57. package/frontend-vue/tests/live-hold-banner.test.ts +37 -0
  58. package/frontend-vue/tests/live-toggle.test.ts +162 -0
  59. package/frontend-vue/tests/page-live-gate.test.ts +241 -0
  60. package/frontend-vue/tests/period-live-pause.test.ts +120 -0
  61. package/frontend-vue/tests/period-selector-url.test.ts +268 -0
  62. package/frontend-vue/tests/period-url-query.test.ts +351 -0
  63. package/frontend-vue/tests/row-side-panel.test.ts +653 -0
  64. package/frontend-vue/tests/table-cell-tones.test.ts +176 -0
  65. package/frontend-vue/tests/table-live-hold.test.ts +204 -0
  66. package/frontend-vue/tests/table-source-period-scope.test.ts +144 -0
  67. package/frontend-vue/tests/table-view-refresh.test.ts +2 -2
  68. package/frontend-vue/tests/table-view-sources.test.ts +40 -0
  69. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  70. package/package.json +2 -2
  71. package/skills/dms/SKILL.md +9 -2
  72. package/skills/dms-pages/REFERENCE.md +5 -0
@@ -0,0 +1,107 @@
1
+ import {
2
+ computed,
3
+ getCurrentScope,
4
+ onScopeDispose,
5
+ shallowRef,
6
+ type ComputedRef,
7
+ } from "vue";
8
+ import type {
9
+ LiveSubscription,
10
+ PageLiveController,
11
+ } from "./pageLiveController";
12
+
13
+ // Client-only state: the page renderer sets it in the browser, where one app
14
+ // shows one page at a time. The server renders no event and no refetch, so it
15
+ // never needs one, and a module-level value there would be shared between
16
+ // requests.
17
+ const currentController = shallowRef<PageLiveController | null>(null);
18
+
19
+ /**
20
+ * The live controller of the page on screen, `null` when the page has no
21
+ * `live` option (or while server-rendering).
22
+ *
23
+ * @internal
24
+ */
25
+ export function currentPageLive(): PageLiveController | null {
26
+ return currentController.value;
27
+ }
28
+
29
+ /**
30
+ * Installs the controller of the page on screen. The page renderer calls it.
31
+ *
32
+ * @internal
33
+ */
34
+ export function setCurrentPageLive(controller: PageLiveController | null) {
35
+ if (typeof window === "undefined") return;
36
+ currentController.value = controller;
37
+ }
38
+
39
+ /** @internal */
40
+ export interface LiveGate {
41
+ /** An event behind the block arrived: refetch, now or later, or count it. */
42
+ trigger(event?: object): void;
43
+ /** Events the block missed while the page is paused. */
44
+ missed: ComputedRef<number>;
45
+ /** When the block last read through the gate (ms), 0 before or without `live`. */
46
+ lastReadAt: ComputedRef<number>;
47
+ /** Refetches now and forgets what was missed. */
48
+ catchUp(): void;
49
+ /** Records a read the caller makes itself (see LiveSubscription). */
50
+ acknowledge(): void;
51
+ }
52
+
53
+ interface BoundSubscription {
54
+ controller: PageLiveController;
55
+ subscription: LiveSubscription;
56
+ }
57
+
58
+ /**
59
+ * The gate a block's refetch goes through. On a page without `live` it calls
60
+ * `refresh` at once, as before. Bound lazily, on the first event, to the
61
+ * controller of the page on screen: a block of the page layout sets up before
62
+ * the page installs its controller.
63
+ *
64
+ * @internal
65
+ */
66
+ export function useLiveGate(refresh: () => void): LiveGate {
67
+ const bound = shallowRef<BoundSubscription | null>(null);
68
+
69
+ const unbind = () => {
70
+ bound.value?.subscription.unsubscribe();
71
+ bound.value = null;
72
+ };
73
+
74
+ const resolve = (): LiveSubscription | null => {
75
+ const controller = currentController.value;
76
+ if (bound.value?.controller === controller) {
77
+ return bound.value?.subscription ?? null;
78
+ }
79
+ unbind();
80
+ if (!controller) return null;
81
+ bound.value = { controller, subscription: controller.subscribe(refresh) };
82
+ return bound.value.subscription;
83
+ };
84
+
85
+ if (getCurrentScope()) onScopeDispose(unbind);
86
+
87
+ const current = () =>
88
+ bound.value?.controller === currentController.value
89
+ ? (bound.value?.subscription ?? null)
90
+ : null;
91
+
92
+ return {
93
+ trigger: (event) => {
94
+ const subscription = resolve();
95
+ if (subscription) subscription.trigger(event);
96
+ else refresh();
97
+ },
98
+ missed: computed(() => current()?.missed.value ?? 0),
99
+ lastReadAt: computed(() => current()?.lastReadAt.value ?? 0),
100
+ catchUp: () => {
101
+ const subscription = resolve();
102
+ if (subscription) subscription.catchUp();
103
+ else refresh();
104
+ },
105
+ acknowledge: () => resolve()?.acknowledge(),
106
+ };
107
+ }
@@ -0,0 +1,25 @@
1
+ const MS_PER_SECOND = 1000;
2
+ const SECONDS_PER_MINUTE = 60;
3
+ const MINUTES_PER_HOUR = 60;
4
+
5
+ /** The unit an elapsed time reads in: seconds, then minutes, then hours. */
6
+ export type ElapsedUnit = "seconds" | "minutes" | "hours";
7
+
8
+ export interface Elapsed {
9
+ unit: ElapsedUnit;
10
+ count: number;
11
+ }
12
+
13
+ /**
14
+ * A duration as "N s", "N min" or "N h": the largest unit it fills once,
15
+ * rounded down. A negative duration (a clock set back) reads as 0 s.
16
+ *
17
+ * @internal
18
+ */
19
+ export function formatElapsed(elapsedMs: number): Elapsed {
20
+ const seconds = Math.max(0, Math.floor(elapsedMs / MS_PER_SECOND));
21
+ if (seconds < SECONDS_PER_MINUTE) return { unit: "seconds", count: seconds };
22
+ const minutes = Math.floor(seconds / SECONDS_PER_MINUTE);
23
+ if (minutes < MINUTES_PER_HOUR) return { unit: "minutes", count: minutes };
24
+ return { unit: "hours", count: Math.floor(minutes / MINUTES_PER_HOUR) };
25
+ }
@@ -0,0 +1,229 @@
1
+ import { readonly, ref, type Ref } from "vue";
2
+ import type { PageLiveOptions } from "../../../types/live";
3
+
4
+ const MS_PER_SECOND = 1000;
5
+ const STORAGE_KEY_PREFIX = "dms.live.paused:";
6
+ const PAUSED_VALUE = "1";
7
+
8
+ /**
9
+ * One block or table of a live page: what it reads again, and what it missed
10
+ * while the page was paused.
11
+ *
12
+ * @internal
13
+ */
14
+ export interface LiveSubscription {
15
+ /**
16
+ * An event behind the block arrived. `event` identifies it, so one event
17
+ * reaching several blocks counts once in the page's `missedCount`.
18
+ */
19
+ trigger(event?: object): void;
20
+ /** Events the block received while paused and has not read yet. */
21
+ readonly missed: Readonly<Ref<number>>;
22
+ /** When the block last read through the gate (ms since the epoch), 0 before. */
23
+ readonly lastReadAt: Readonly<Ref<number>>;
24
+ /** Reads now, whatever the state, and forgets what was missed. */
25
+ catchUp(): void;
26
+ /**
27
+ * Records a read the block made by itself (one the user asked for): it
28
+ * forgets what was missed, and a waiting trailing read is dropped.
29
+ */
30
+ acknowledge(): void;
31
+ unsubscribe(): void;
32
+ }
33
+
34
+ /**
35
+ * The Direct / Pause state of one page and the gate every refetch of its
36
+ * blocks goes through (see `PageLiveOptions` in interface-dms).
37
+ *
38
+ * @internal
39
+ */
40
+ export interface PageLiveController {
41
+ readonly pageId: string;
42
+ readonly isPaused: Readonly<Ref<boolean>>;
43
+ /** When the page last read its data (ms since the epoch). */
44
+ readonly lastUpdatedAt: Readonly<Ref<number>>;
45
+ /** Distinct events received since the pause. */
46
+ readonly missedCount: Readonly<Ref<number>>;
47
+ pause(): void;
48
+ resume(): void;
49
+ toggle(): void;
50
+ subscribe(refresh: () => void): LiveSubscription;
51
+ dispose(): void;
52
+ }
53
+
54
+ interface SubscriberState {
55
+ refresh: () => void;
56
+ missed: Ref<number>;
57
+ lastRunAt: Ref<number>;
58
+ timer: ReturnType<typeof setTimeout> | null;
59
+ /** The event the trailing call stands for, counted if the page pauses first. */
60
+ pendingEvent: object | undefined;
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
+ }
85
+ }
86
+
87
+ class PageLive implements PageLiveController {
88
+ readonly isPaused: Readonly<Ref<boolean>>;
89
+ readonly lastUpdatedAt: Readonly<Ref<number>>;
90
+ readonly missedCount: Readonly<Ref<number>>;
91
+ private readonly paused: Ref<boolean>;
92
+ private readonly updatedAt = ref(Date.now());
93
+ private readonly missedTotal = ref(0);
94
+ private readonly subscribers = new Set<SubscriberState>();
95
+ private countedEvents = new WeakSet<object>();
96
+ private readonly minIntervalMs: number;
97
+
98
+ constructor(
99
+ readonly pageId: string,
100
+ options: PageLiveOptions,
101
+ ) {
102
+ this.minIntervalMs = (options.maxRate ?? 0) * MS_PER_SECOND;
103
+ this.paused = ref(readStoredPause(pageId));
104
+ this.isPaused = readonly(this.paused);
105
+ this.lastUpdatedAt = readonly(this.updatedAt);
106
+ this.missedCount = readonly(this.missedTotal);
107
+ }
108
+
109
+ pause(): void {
110
+ if (this.paused.value) return;
111
+ this.paused.value = true;
112
+ storePause(this.pageId, true);
113
+ // A trailing read still waiting stands for an event not shown yet.
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
+ }
120
+ }
121
+
122
+ resume(): void {
123
+ if (!this.paused.value) return;
124
+ this.paused.value = false;
125
+ storePause(this.pageId, false);
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);
133
+ }
134
+ }
135
+
136
+ toggle(): void {
137
+ if (this.paused.value) this.resume();
138
+ else this.pause();
139
+ }
140
+
141
+ subscribe(refresh: () => void): LiveSubscription {
142
+ const state: SubscriberState = {
143
+ refresh,
144
+ missed: ref(0),
145
+ lastRunAt: ref(0),
146
+ timer: null,
147
+ pendingEvent: undefined,
148
+ };
149
+ this.subscribers.add(state);
150
+ return {
151
+ trigger: (event) => this.trigger(state, event),
152
+ missed: readonly(state.missed),
153
+ lastReadAt: readonly(state.lastRunAt),
154
+ catchUp: () => {
155
+ state.missed.value = 0;
156
+ this.run(state);
157
+ },
158
+ acknowledge: () => {
159
+ state.missed.value = 0;
160
+ this.markRead(state);
161
+ },
162
+ unsubscribe: () => {
163
+ clearTimer(state);
164
+ this.subscribers.delete(state);
165
+ },
166
+ };
167
+ }
168
+
169
+ dispose(): void {
170
+ for (const state of this.subscribers) clearTimer(state);
171
+ this.subscribers.clear();
172
+ }
173
+
174
+ private trigger(state: SubscriberState, event: object | undefined): void {
175
+ if (this.paused.value) this.countMissed(state, event);
176
+ else this.triggerDirect(state, event);
177
+ }
178
+
179
+ // Leading and trailing: the first event of a quiet block reads at once, the
180
+ // ones within `maxRate` of a read collapse into one read when it ends, so
181
+ // the last event of a burst is never lost.
182
+ private triggerDirect(state: SubscriberState, event: object | undefined) {
183
+ if (state.timer) return;
184
+ const wait = state.lastRunAt.value + this.minIntervalMs - Date.now();
185
+ if (wait <= 0) {
186
+ this.run(state);
187
+ return;
188
+ }
189
+ state.pendingEvent = event;
190
+ state.timer = setTimeout(() => this.run(state), wait);
191
+ }
192
+
193
+ private run(state: SubscriberState): void {
194
+ this.markRead(state);
195
+ state.refresh();
196
+ }
197
+
198
+ private markRead(state: SubscriberState): void {
199
+ clearTimer(state);
200
+ state.lastRunAt.value = Date.now();
201
+ this.updatedAt.value = state.lastRunAt.value;
202
+ }
203
+
204
+ private countMissed(state: SubscriberState, event: object | undefined) {
205
+ state.missed.value += 1;
206
+ if (event && this.countedEvents.has(event)) return;
207
+ if (event) this.countedEvents.add(event);
208
+ this.missedTotal.value += 1;
209
+ }
210
+ }
211
+
212
+ function clearTimer(state: SubscriberState): void {
213
+ if (state.timer) clearTimeout(state.timer);
214
+ state.timer = null;
215
+ state.pendingEvent = undefined;
216
+ }
217
+
218
+ /**
219
+ * The controller of a page declared with `live`. Starts in the state the
220
+ * browser remembers for `pageId`, Direct otherwise.
221
+ *
222
+ * @internal
223
+ */
224
+ export function createPageLiveController(
225
+ pageId: string,
226
+ options: PageLiveOptions,
227
+ ): PageLiveController {
228
+ return new PageLive(pageId, options);
229
+ }
@@ -0,0 +1,8 @@
1
+ /** Switches the page between Direct and Pause, while a LiveToggle is on it. */
2
+ export const LIVE_TOGGLE_L_METADATA: ShortcutMetadata = {
3
+ key: ["l"],
4
+ descriptionKey: "$dms.shortcuts.livetoggle.l.description",
5
+ component: "$dms.components.livetoggle",
6
+ };
7
+
8
+ export const LIVE_TOGGLE_SHORTCUTS_METADATA = [LIVE_TOGGLE_L_METADATA];
@@ -0,0 +1,25 @@
1
+ import { onBeforeUnmount, onMounted } from "vue";
2
+
3
+ // The toggles mounted on the page, in mount order: the first one owns the L
4
+ // key, so a page showing two toggles still switches once per press.
5
+ const claims: symbol[] = [];
6
+
7
+ /**
8
+ * Binds L to `toggle` for the first LiveToggle mounted on the page; the next
9
+ * one takes over when it goes.
10
+ *
11
+ * @internal
12
+ */
13
+ export function useLiveToggleShortcut(toggle: () => void): void {
14
+ const claim = Symbol("live-toggle");
15
+ onMounted(() => claims.push(claim));
16
+ onBeforeUnmount(() => {
17
+ const index = claims.indexOf(claim);
18
+ if (index >= 0) claims.splice(index, 1);
19
+ });
20
+ defineShortcuts({
21
+ l: () => {
22
+ if (claims[0] === claim) toggle();
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,46 @@
1
+ import { onScopeDispose, watch } from "vue";
2
+ import type { PageLiveOptions } from "../../../types/live";
3
+ import { setCurrentPageLive } from "./currentPageLive";
4
+ import {
5
+ createPageLiveController,
6
+ type PageLiveController,
7
+ } from "./pageLiveController";
8
+
9
+ /** The page the renderer shows: its id and its `live` option. */
10
+ export interface PageLiveSource {
11
+ pageId: string | undefined;
12
+ live: PageLiveOptions | undefined;
13
+ }
14
+
15
+ /**
16
+ * Gives the page on screen its live controller while it declares `live`, and
17
+ * a new one when the renderer moves to another page. Browser only.
18
+ *
19
+ * @internal
20
+ */
21
+ export function usePageLiveHost(source: () => PageLiveSource): void {
22
+ if (typeof window === "undefined") return;
23
+ let controller: PageLiveController | null = null;
24
+
25
+ const install = (next: PageLiveController | null) => {
26
+ controller?.dispose();
27
+ controller = next;
28
+ setCurrentPageLive(next);
29
+ };
30
+
31
+ watch(
32
+ () => {
33
+ const { pageId, live } = source();
34
+ return pageId && live ? JSON.stringify([pageId, live]) : "";
35
+ },
36
+ (key) => {
37
+ const { pageId, live } = source();
38
+ install(
39
+ key && pageId && live ? createPageLiveController(pageId, live) : null,
40
+ );
41
+ },
42
+ { immediate: true },
43
+ );
44
+
45
+ onScopeDispose(() => install(null));
46
+ }