@antelopejs/dms 0.7.9 → 0.7.11
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 +41 -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/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 +2 -0
- 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/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 +75 -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 +104 -98
- 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/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- 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/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/tab-slot-spacing.test.ts +8 -0
- 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-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
|
@@ -152,3 +152,58 @@ export function periodDuration(range: PeriodRange): PeriodDuration {
|
|
|
152
152
|
}
|
|
153
153
|
return { unit: "days", count: Math.max(Math.round(span / MS_PER_DAY), 1) };
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
const DAYS_PER_WEEK = 7;
|
|
157
|
+
const YESTERDAY_OFFSET = -1;
|
|
158
|
+
const WEEKDAY_FORMAT: FormatOptions = { weekday: "short" };
|
|
159
|
+
|
|
160
|
+
function localDayStart(date: Date): number {
|
|
161
|
+
return new Date(
|
|
162
|
+
date.getFullYear(),
|
|
163
|
+
date.getMonth(),
|
|
164
|
+
date.getDate(),
|
|
165
|
+
).getTime();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function daysBefore(date: Date, now: Date): number {
|
|
169
|
+
return Math.round((localDayStart(now) - localDayStart(date)) / MS_PER_DAY);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatWith(date: Date, locale: string, options: FormatOptions) {
|
|
173
|
+
const formatter = regionalDateTimeFormat(locale, options, {
|
|
174
|
+
keepLocalZone: true,
|
|
175
|
+
});
|
|
176
|
+
return normalizeSpaces(formatter.format(date));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function relativeDayPrefix(date: Date, now: Date, locale: string): string {
|
|
180
|
+
const days = daysBefore(date, now);
|
|
181
|
+
if (days <= 0) return "";
|
|
182
|
+
if (days === 1) {
|
|
183
|
+
const format = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
184
|
+
return `${format.format(YESTERDAY_OFFSET, "day")} `;
|
|
185
|
+
}
|
|
186
|
+
if (days < DAYS_PER_WEEK) {
|
|
187
|
+
return `${formatWith(date, locale, WEEKDAY_FORMAT)} `;
|
|
188
|
+
}
|
|
189
|
+
const sameYear = date.getFullYear() === now.getFullYear();
|
|
190
|
+
const day: FormatOptions = sameYear
|
|
191
|
+
? DAY_FORMAT
|
|
192
|
+
: { ...DAY_FORMAT, year: "numeric" };
|
|
193
|
+
return `${formatWith(date, locale, day)}, `;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* An instant as seen from `now`, in the reader's locale: the time alone
|
|
198
|
+
* today ("11:52"), then "yesterday 11:52", the weekday within a week
|
|
199
|
+
* ("Mon 11:52"), and the date beyond ("Oct 4, 11:52", with its year when
|
|
200
|
+
* not this one's). The start of a window labelled relative to now.
|
|
201
|
+
*/
|
|
202
|
+
export function formatRelativeInstant(
|
|
203
|
+
date: Date,
|
|
204
|
+
now: Date,
|
|
205
|
+
locale: string,
|
|
206
|
+
): string {
|
|
207
|
+
const time = formatWith(date, locale, TIME_FORMAT);
|
|
208
|
+
return `${relativeDayPrefix(date, now, locale)}${time}`;
|
|
209
|
+
}
|
|
@@ -48,6 +48,11 @@ export interface PeriodQueryKeys {
|
|
|
48
48
|
export interface PeriodSource {
|
|
49
49
|
query: PeriodQuery;
|
|
50
50
|
keys: PeriodQueryKeys;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a range it names is taken even when `custom` is not offered: a
|
|
53
|
+
* link framing a window (an incident's) opens on it, no preset selected.
|
|
54
|
+
*/
|
|
55
|
+
isRangeAlwaysAccepted?: boolean;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
const CUSTOM_PRESET: PeriodPreset = "custom";
|
|
@@ -160,7 +165,9 @@ function readRange(
|
|
|
160
165
|
|
|
161
166
|
/**
|
|
162
167
|
* The range a source names, when the selector offers it: a custom range
|
|
163
|
-
* (`from` and `to`) first, then a preset (`period`).
|
|
168
|
+
* (`from` and `to`) first, then a preset (`period`). A source whose ranges
|
|
169
|
+
* are always accepted gives its custom range even when `custom` is not
|
|
170
|
+
* offered.
|
|
164
171
|
*/
|
|
165
172
|
export function readRangeChoice(
|
|
166
173
|
source: PeriodSource,
|
|
@@ -168,7 +175,9 @@ export function readRangeChoice(
|
|
|
168
175
|
): PeriodRangeChoice | null {
|
|
169
176
|
const { query, keys } = source;
|
|
170
177
|
const customRange = readRange(query, keys.from, keys.to);
|
|
171
|
-
|
|
178
|
+
const isCustomAccepted =
|
|
179
|
+
source.isRangeAlwaysAccepted || choices.presets.includes(CUSTOM_PRESET);
|
|
180
|
+
if (customRange && isCustomAccepted) {
|
|
172
181
|
return { preset: CUSTOM_PRESET, customRange };
|
|
173
182
|
}
|
|
174
183
|
const preset = choices.presets.find(
|
|
@@ -307,9 +316,36 @@ export type RememberedPeriods = Record<string, Record<string, string>>;
|
|
|
307
316
|
/** How many selectors' last choices the browser keeps. */
|
|
308
317
|
export const REMEMBERED_PERIODS_LIMIT = 20;
|
|
309
318
|
|
|
310
|
-
/**
|
|
311
|
-
export
|
|
312
|
-
|
|
319
|
+
/** What a remembered choice is kept under: a page's selector, or a shared key. */
|
|
320
|
+
export interface RememberedPeriodTarget {
|
|
321
|
+
pageId: string;
|
|
322
|
+
id: string;
|
|
323
|
+
/** Shared by every selector carrying it, on any page. */
|
|
324
|
+
rememberKey?: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// A page key always holds `#`, so a shared key cannot collide with one.
|
|
328
|
+
const SHARED_KEY_PREFIX = "@";
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The entry of a selector among the last choices: its `rememberKey`, shared
|
|
332
|
+
* by every selector carrying it, else selector `id` of page `pageId`.
|
|
333
|
+
*/
|
|
334
|
+
export function rememberedPeriodKey(target: RememberedPeriodTarget): string {
|
|
335
|
+
if (target.rememberKey) return `${SHARED_KEY_PREFIX}${target.rememberKey}`;
|
|
336
|
+
return `${target.pageId}#${target.id}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Whether the selector offers `selection`'s range: a range only a link could
|
|
341
|
+
* bring (a custom one without `custom` offered) is not remembered, so the
|
|
342
|
+
* last choice made in the menu stays.
|
|
343
|
+
*/
|
|
344
|
+
export function isRangeOffered(
|
|
345
|
+
selection: PeriodRangeChoice,
|
|
346
|
+
choices: PeriodChoices,
|
|
347
|
+
): boolean {
|
|
348
|
+
return choices.presets.includes(selection.preset);
|
|
313
349
|
}
|
|
314
350
|
|
|
315
351
|
/**
|
|
@@ -11,12 +11,23 @@ import { calendarToRange, rangeToCalendar } from "./periodDisplay";
|
|
|
11
11
|
|
|
12
12
|
const CUSTOM_PRESET: PeriodPreset = "custom";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Where a custom range starts when nothing on screen has bounds (every
|
|
16
|
+
* period): today.
|
|
17
|
+
*/
|
|
18
|
+
export function startingRange(now: Date): PeriodRange {
|
|
19
|
+
return calendarToRange(rangeToCalendar({ from: now, to: now }));
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export interface UsePeriodDraftReturn {
|
|
15
23
|
preset: Ref<PeriodPreset>;
|
|
16
24
|
comparison: Ref<PeriodComparison>;
|
|
17
25
|
customRange: Ref<PeriodRange | null>;
|
|
18
|
-
/**
|
|
19
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The range the staged preset (or custom range) resolves to; `null` for
|
|
28
|
+
* every period (`all`).
|
|
29
|
+
*/
|
|
30
|
+
range: ComputedRef<PeriodRange | null>;
|
|
20
31
|
compareRange: ComputedRef<PeriodRange | null>;
|
|
21
32
|
isDirty: ComputedRef<boolean>;
|
|
22
33
|
/** Start over from the committed period (on open, on cancel). */
|
|
@@ -59,13 +70,14 @@ export function usePeriodDraft(period: UsePeriodReturn): UsePeriodDraftReturn {
|
|
|
59
70
|
customRange.value = committedCustomRange();
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
const range = computed<PeriodRange>(() =>
|
|
63
|
-
|
|
73
|
+
const range = computed<PeriodRange | null>(() => {
|
|
74
|
+
const now = new Date();
|
|
75
|
+
return resolvePresetRange(
|
|
64
76
|
preset.value,
|
|
65
|
-
|
|
66
|
-
customRange.value ?? period.state.value.range,
|
|
67
|
-
)
|
|
68
|
-
);
|
|
77
|
+
now,
|
|
78
|
+
customRange.value ?? period.state.value.range ?? startingRange(now),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
69
81
|
|
|
70
82
|
const compareRange = computed<PeriodRange | null>(() =>
|
|
71
83
|
resolveComparisonRange(
|
|
@@ -86,7 +98,8 @@ export function usePeriodDraft(period: UsePeriodReturn): UsePeriodDraftReturn {
|
|
|
86
98
|
function selectPreset(next: PeriodPreset): void {
|
|
87
99
|
if (next === CUSTOM_PRESET && !customRange.value) {
|
|
88
100
|
// Custom starts from the days on screen, ready to be adjusted.
|
|
89
|
-
|
|
101
|
+
const shown = range.value ?? startingRange(new Date());
|
|
102
|
+
customRange.value = calendarToRange(rangeToCalendar(shown));
|
|
90
103
|
}
|
|
91
104
|
preset.value = next;
|
|
92
105
|
}
|
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
isPeriodQueryCurrent,
|
|
6
6
|
mergePeriodSearch,
|
|
7
7
|
periodQueryKeys,
|
|
8
|
+
isRangeOffered,
|
|
8
9
|
rememberedPeriodKey,
|
|
9
10
|
rememberPeriod,
|
|
10
11
|
type RememberedPeriods,
|
|
12
|
+
type RememberedPeriodTarget,
|
|
11
13
|
type PeriodQuery,
|
|
12
14
|
type PeriodQueryKeys,
|
|
13
15
|
type PeriodSelection,
|
|
@@ -21,6 +23,8 @@ export interface PeriodLocationOptions {
|
|
|
21
23
|
id: string;
|
|
22
24
|
/** The page it is on: its last choice is kept per page and per id. */
|
|
23
25
|
pageId: string;
|
|
26
|
+
/** Shares the last choice with every selector carrying it, on any page. */
|
|
27
|
+
rememberKey?: string;
|
|
24
28
|
/** What the period falls back to when no source names a valid one. */
|
|
25
29
|
fallback: PeriodSelection;
|
|
26
30
|
}
|
|
@@ -87,9 +91,9 @@ function replacePeriodInUrl(
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
/** The user's last choice of one selector, kept in the preferences. */
|
|
90
|
-
function useRememberedPeriod(
|
|
94
|
+
function useRememberedPeriod(target: RememberedPeriodTarget) {
|
|
91
95
|
const { getPreference, setPreference } = usePreferences();
|
|
92
|
-
const key = rememberedPeriodKey(
|
|
96
|
+
const key = rememberedPeriodKey(target);
|
|
93
97
|
const all = () => getPreference<RememberedPeriods>(PERIOD_PREFERENCE_KEY, {});
|
|
94
98
|
return {
|
|
95
99
|
source: (): PeriodSource => ({
|
|
@@ -127,8 +131,10 @@ function usePeriodUrlWriter(keys: PeriodQueryKeys) {
|
|
|
127
131
|
|
|
128
132
|
/**
|
|
129
133
|
* Keeps a selector's period in the page URL (`?<id>.period=`, see
|
|
130
|
-
* `periodQuery`) and its last choice in the user's preferences
|
|
131
|
-
* starts from the URL, then from that
|
|
134
|
+
* `periodQuery`) and its last choice in the user's preferences, per page or
|
|
135
|
+
* under its `rememberKey`. The period starts from the URL, then from that
|
|
136
|
+
* last choice, then from `fallback`. A URL range is taken even when `custom`
|
|
137
|
+
* is not offered, and is then not remembered. Both
|
|
132
138
|
* are read in setup, on the server too: the preferences are a cookie, so the
|
|
133
139
|
* server renders the period the browser will show.
|
|
134
140
|
*
|
|
@@ -145,7 +151,7 @@ export function usePeriodLocation(
|
|
|
145
151
|
): void {
|
|
146
152
|
const route = useDmsRoute();
|
|
147
153
|
const urlKeys = periodQueryKeys(options.id);
|
|
148
|
-
const remembered = useRememberedPeriod(options
|
|
154
|
+
const remembered = useRememberedPeriod(options);
|
|
149
155
|
const writer = usePeriodUrlWriter(urlKeys);
|
|
150
156
|
const choices = {
|
|
151
157
|
presets: period.presets.value,
|
|
@@ -154,6 +160,7 @@ export function usePeriodLocation(
|
|
|
154
160
|
const urlSource = (): PeriodSource => ({
|
|
155
161
|
query: route.query as PeriodQuery,
|
|
156
162
|
keys: urlKeys,
|
|
163
|
+
isRangeAlwaysAccepted: true,
|
|
157
164
|
});
|
|
158
165
|
|
|
159
166
|
applySelection(
|
|
@@ -170,7 +177,7 @@ export function usePeriodLocation(
|
|
|
170
177
|
() => selectionText(readSelection(period)),
|
|
171
178
|
() => {
|
|
172
179
|
const selection = readSelection(period);
|
|
173
|
-
remembered.remember(selection);
|
|
180
|
+
if (isRangeOffered(selection, choices)) remembered.remember(selection);
|
|
174
181
|
writer.write(selection);
|
|
175
182
|
},
|
|
176
183
|
);
|
|
@@ -109,6 +109,7 @@ export const useTable = <T extends Data>(props: UseTableProps<T>) => {
|
|
|
109
109
|
showArchived: props.showArchived,
|
|
110
110
|
columnMenus: props.columnMenus,
|
|
111
111
|
defaultSort: props.tableProps.defaultSort,
|
|
112
|
+
labelKey: props.tableProps.labelKey,
|
|
112
113
|
});
|
|
113
114
|
|
|
114
115
|
// A saved order (the user's, a view's) is kept, completed with the
|
|
@@ -41,6 +41,10 @@ import {
|
|
|
41
41
|
import type { DmsAppConfig } from "#dms-core/shared/types/app-config";
|
|
42
42
|
import { useColumnValueRenderer } from "../data-types/useColumnValueRenderer";
|
|
43
43
|
import { useActionConfirm } from "../confirm/useActionConfirm";
|
|
44
|
+
import {
|
|
45
|
+
resolveConfirmText,
|
|
46
|
+
type ConfirmTranslate,
|
|
47
|
+
} from "../confirm/confirmDialogTexts";
|
|
44
48
|
import {
|
|
45
49
|
type DefaultSortConfig,
|
|
46
50
|
type HeaderSortCue,
|
|
@@ -160,6 +164,8 @@ interface ColumnConfig<T> {
|
|
|
160
164
|
columnMenus?: boolean;
|
|
161
165
|
/** The table's default sort: its column shows a neutral arrow. */
|
|
162
166
|
defaultSort?: DefaultSortConfig;
|
|
167
|
+
/** Field naming a row: the default name of its ⋯ menu button. */
|
|
168
|
+
labelKey?: string;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
interface ExtendedColumnMeta<T> extends ColumnMeta<T, unknown> {
|
|
@@ -406,6 +412,7 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
|
|
|
406
412
|
const renderActionsDropdown = (
|
|
407
413
|
items: DropdownMenuItem[] | DropdownMenuItem[][],
|
|
408
414
|
rowId: string,
|
|
415
|
+
label: string,
|
|
409
416
|
) =>
|
|
410
417
|
h(
|
|
411
418
|
DropdownMenu as Component,
|
|
@@ -418,6 +425,7 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
|
|
|
418
425
|
default: () =>
|
|
419
426
|
h(Button, {
|
|
420
427
|
id: `row-actions-${rowId}`,
|
|
428
|
+
"aria-label": label,
|
|
421
429
|
icon: appConfig.ui.icons.ellipsis,
|
|
422
430
|
color: Color.neutral,
|
|
423
431
|
variant: ButtonVariant.ghost,
|
|
@@ -521,7 +529,9 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
|
|
|
521
529
|
children.unshift(renderRestoreButton(restoreDescriptor, row.id));
|
|
522
530
|
}
|
|
523
531
|
if (dropdownItems.length > 0) {
|
|
524
|
-
children.push(
|
|
532
|
+
children.push(
|
|
533
|
+
renderActionsDropdown(dropdownItems, row.id, menuLabel(row.original)),
|
|
534
|
+
);
|
|
525
535
|
}
|
|
526
536
|
|
|
527
537
|
return h(
|
|
@@ -580,6 +590,21 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
|
|
|
580
590
|
|
|
581
591
|
const { processI18n } = useTranslation();
|
|
582
592
|
|
|
593
|
+
// A row's own text, resolved as a confirmation's texts are: a `$` key
|
|
594
|
+
// translated with the row's fields as parameters, or a literal whose
|
|
595
|
+
// `{field}` braces they fill.
|
|
596
|
+
const rowText = (text: string, rowData: T): string =>
|
|
597
|
+
resolveConfirmText(text, rowData, t as ConfirmTranslate);
|
|
598
|
+
|
|
599
|
+
const menuLabel = (rowData: T): string => {
|
|
600
|
+
const declared = config.rowActions?.menuLabel;
|
|
601
|
+
if (declared) return rowText(declared, rowData);
|
|
602
|
+
const label = config.labelKey ? get(rowData, config.labelKey) : undefined;
|
|
603
|
+
return typeof label === "string" || typeof label === "number"
|
|
604
|
+
? t("dms.table.row_actions_for", { label })
|
|
605
|
+
: t("dms.table.row_actions");
|
|
606
|
+
};
|
|
607
|
+
|
|
583
608
|
const isCustomActionDisabled = (
|
|
584
609
|
action: CustomRowAction,
|
|
585
610
|
rowData: T,
|
|
@@ -638,7 +663,7 @@ export const useTableColumns = <T extends Data>(config: ColumnConfig<T>) => {
|
|
|
638
663
|
|
|
639
664
|
const buildCustomActionDescriptors = (rowData: T): RowActionDescriptor[] =>
|
|
640
665
|
(config.rowActions?.custom || []).map((action) => ({
|
|
641
|
-
label:
|
|
666
|
+
label: rowText(action.label, rowData),
|
|
642
667
|
// A labelled button may go without an icon.
|
|
643
668
|
icon: action.icon || (action.showLabel ? "" : "i-ph-lightning"),
|
|
644
669
|
onSelect: () => config.onCustomRowAction?.(action, rowData),
|
|
@@ -79,10 +79,25 @@ export const clearTableFiltersLabelKey = (
|
|
|
79
79
|
? "dms.table.clear_search"
|
|
80
80
|
: "dms.table.clear_filters";
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
state.columnFilters.value = resetColumnFilters(state.columnFilters.value);
|
|
84
|
-
state.globalFilter.value = "";
|
|
82
|
+
const backToFirstPage = (state: TableNarrowingState): void => {
|
|
85
83
|
if (state.pagination && state.pagination.value.pageIndex !== 0) {
|
|
86
84
|
state.pagination.value = { ...state.pagination.value, pageIndex: 0 };
|
|
87
85
|
}
|
|
88
86
|
};
|
|
87
|
+
|
|
88
|
+
/** Empties the toolbar search alone, back to the first page. */
|
|
89
|
+
export const clearTableSearch = (state: TableNarrowingState): void => {
|
|
90
|
+
state.globalFilter.value = "";
|
|
91
|
+
backToFirstPage(state);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Clears the filter chips alone (pinned ones reset), back to the first page. */
|
|
95
|
+
export const clearTableColumnFilters = (state: TableNarrowingState): void => {
|
|
96
|
+
state.columnFilters.value = resetColumnFilters(state.columnFilters.value);
|
|
97
|
+
backToFirstPage(state);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const clearTableFilters = (state: TableNarrowingState): void => {
|
|
101
|
+
state.columnFilters.value = resetColumnFilters(state.columnFilters.value);
|
|
102
|
+
clearTableSearch(state);
|
|
103
|
+
};
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import type { PaginationState } from "@tanstack/vue-table";
|
|
2
2
|
import { computed, ref, watch, type Ref } from "vue";
|
|
3
|
+
import { hashText } from "#dms-core/app/build/composables/user/tablePreferences";
|
|
4
|
+
import type { TablePreferences } from "./useTablePreferences";
|
|
3
5
|
|
|
4
6
|
const FIRST_PAGE_INDEX = 0;
|
|
5
|
-
const PAGINATION_PREFERENCE = "pagination";
|
|
6
|
-
// The hidden filters the stored page was reached under, next to it.
|
|
7
|
-
const PAGINATION_SCOPE_PREFERENCE = "paginationScope";
|
|
8
|
-
|
|
9
|
-
/** The preference store the page is kept in (`usePreferences`). */
|
|
10
|
-
export interface PaginationPreferences {
|
|
11
|
-
getPreference: <V>(keyPath: string, defaultValue?: V) => V;
|
|
12
|
-
setPreference: (keyPath: string, value: unknown) => void;
|
|
13
|
-
}
|
|
14
7
|
|
|
15
8
|
export interface PersistedPaginationOptions {
|
|
16
|
-
preferences
|
|
17
|
-
|
|
18
|
-
preferenceKey: (suffix: string) => string;
|
|
9
|
+
/** The table's preferences (`useTablePreferences`). */
|
|
10
|
+
preferences: TablePreferences;
|
|
19
11
|
defaults: PaginationState;
|
|
20
12
|
/**
|
|
21
13
|
* What lists other rows without the user picking it: the hidden filters
|
|
@@ -30,8 +22,8 @@ const firstPage = (pagination: PaginationState): PaginationState => ({
|
|
|
30
22
|
});
|
|
31
23
|
|
|
32
24
|
/**
|
|
33
|
-
* The table's page, kept in the user's preferences with the hidden
|
|
34
|
-
* it was reached under. A page restored under other hidden filters (a link
|
|
25
|
+
* The table's page, kept in the user's preferences with a hash of the hidden
|
|
26
|
+
* filters it was reached under (only past the first page, where it matters). A page restored under other hidden filters (a link
|
|
35
27
|
* that remounted the page with another `?status=`) starts at the first
|
|
36
28
|
* page, as does a change of their value while the table is shown; compared
|
|
37
29
|
* by value, so another query parameter changing keeps the page.
|
|
@@ -39,15 +31,14 @@ const firstPage = (pagination: PaginationState): PaginationState => ({
|
|
|
39
31
|
export function usePersistedPagination(
|
|
40
32
|
options: PersistedPaginationOptions,
|
|
41
33
|
): Ref<PaginationState> {
|
|
42
|
-
const { preferences,
|
|
43
|
-
const scopeKey = computed(() =>
|
|
44
|
-
|
|
45
|
-
preferenceKey(PAGINATION_PREFERENCE),
|
|
46
|
-
options.defaults,
|
|
34
|
+
const { preferences, defaults } = options;
|
|
35
|
+
const scopeKey = computed(() =>
|
|
36
|
+
hashText(JSON.stringify(options.scope() ?? null)),
|
|
47
37
|
);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
const stored = preferences.get<PaginationState>("pagination", defaults);
|
|
39
|
+
// A page kept without its scope (the first one): kept as it is.
|
|
40
|
+
const storedScope = preferences.get<string>(
|
|
41
|
+
"paginationScope",
|
|
51
42
|
scopeKey.value,
|
|
52
43
|
);
|
|
53
44
|
const pagination = ref<PaginationState>(
|
|
@@ -66,10 +57,12 @@ export function usePersistedPagination(
|
|
|
66
57
|
watch(
|
|
67
58
|
[pagination, scopeKey],
|
|
68
59
|
([value, scope]) => {
|
|
69
|
-
preferences.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
preferences.set("pagination", value, defaults);
|
|
61
|
+
const isFirstPage = value.pageIndex === FIRST_PAGE_INDEX;
|
|
62
|
+
preferences.set<string | undefined>(
|
|
63
|
+
"paginationScope",
|
|
64
|
+
isFirstPage ? undefined : scope,
|
|
65
|
+
undefined,
|
|
73
66
|
);
|
|
74
67
|
},
|
|
75
68
|
{ deep: true },
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { onBeforeUnmount, onMounted, ref, type Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
const FIRST_ROW_SELECTOR = "tbody tr";
|
|
4
|
+
const SCROLLABLE_OVERFLOW = /(auto|scroll)/;
|
|
5
|
+
|
|
6
|
+
function scrollParent(element: HTMLElement): HTMLElement | null {
|
|
7
|
+
for (let node = element.parentElement; node; node = node.parentElement) {
|
|
8
|
+
const { overflowY } = getComputedStyle(node);
|
|
9
|
+
if (
|
|
10
|
+
SCROLLABLE_OVERFLOW.test(overflowY) &&
|
|
11
|
+
node.scrollHeight > node.clientHeight
|
|
12
|
+
) {
|
|
13
|
+
return node;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether the first row of the table in `element` is out of view above the
|
|
21
|
+
* area it scrolls in (its scrolling parent, else the window).
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function isFirstRowScrolledPast(element: HTMLElement): boolean {
|
|
26
|
+
const row = element.querySelector(FIRST_ROW_SELECTOR) ?? element;
|
|
27
|
+
const scroller = scrollParent(element);
|
|
28
|
+
const viewTop = scroller ? scroller.getBoundingClientRect().top : 0;
|
|
29
|
+
return row.getBoundingClientRect().bottom <= viewTop;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Follows whether the user scrolled the table's first row out of view, on
|
|
34
|
+
* any scroll of the page (read once per frame). Inert when `isEnabled` is
|
|
35
|
+
* false, and on the server.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function useScrolledPastTop(
|
|
40
|
+
element: () => HTMLElement | null,
|
|
41
|
+
isEnabled: boolean,
|
|
42
|
+
): Ref<boolean> {
|
|
43
|
+
const isScrolled = ref(false);
|
|
44
|
+
if (!isEnabled || typeof window === "undefined") return isScrolled;
|
|
45
|
+
let frame: number | null = null;
|
|
46
|
+
const measure = () => {
|
|
47
|
+
frame = null;
|
|
48
|
+
const target = element();
|
|
49
|
+
isScrolled.value = !!target && isFirstRowScrolledPast(target);
|
|
50
|
+
};
|
|
51
|
+
const onScroll = () => {
|
|
52
|
+
frame ??= requestAnimationFrame(measure);
|
|
53
|
+
};
|
|
54
|
+
onMounted(() => {
|
|
55
|
+
window.addEventListener("scroll", onScroll, {
|
|
56
|
+
capture: true,
|
|
57
|
+
passive: true,
|
|
58
|
+
});
|
|
59
|
+
measure();
|
|
60
|
+
});
|
|
61
|
+
onBeforeUnmount(() => {
|
|
62
|
+
window.removeEventListener("scroll", onScroll, { capture: true });
|
|
63
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
64
|
+
});
|
|
65
|
+
return isScrolled;
|
|
66
|
+
}
|
|
@@ -83,6 +83,11 @@ export function useTableListData<T extends Record<string, unknown>>(
|
|
|
83
83
|
/**
|
|
84
84
|
* The counters of a table view's tabs and views, keyed by id; `null` until
|
|
85
85
|
* they arrive. Keyed by source URL like the list (see useTableListData).
|
|
86
|
+
*
|
|
87
|
+
* A source waiting on its period scope answers `null` too, like its list:
|
|
88
|
+
* the scope never registers during the server render, so the server and the
|
|
89
|
+
* browser (whose key carries the period, and has no payload yet) both draw
|
|
90
|
+
* the counters' skeleton, and hydration matches.
|
|
86
91
|
*/
|
|
87
92
|
export function useTableCountsData<T extends Record<string, unknown>>(
|
|
88
93
|
options: TableCountsDataOptions<T>,
|
|
@@ -90,9 +95,10 @@ export function useTableCountsData<T extends Record<string, unknown>>(
|
|
|
90
95
|
const { rows, queries, archiveQuery } = options;
|
|
91
96
|
const heldQuery = options.heldQuery ?? NO_HELD_QUERY;
|
|
92
97
|
const sourceSuffix = rows.sourceUrl.value ? `-${rows.sourceUrl.value}` : "";
|
|
93
|
-
return useServerRenderedAsyncData<Record<string, number
|
|
98
|
+
return useServerRenderedAsyncData<Record<string, number> | null>(
|
|
94
99
|
`table-view-${options.componentId}-${options.pageId}-tab-counts${sourceSuffix}`,
|
|
95
100
|
async () => {
|
|
101
|
+
if (rows.isPeriodPending.value) return null;
|
|
96
102
|
if (queries.value.length === 0) return {};
|
|
97
103
|
return await rows.countBatch(
|
|
98
104
|
queries.value.map(({ id, query }) => ({
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { computed, ref, watch, type ComputedRef } from "vue";
|
|
2
2
|
import { usePageLive } from "../../../composables/live/usePageLive";
|
|
3
|
+
import type { TableViewLiveHoldOptions } from "../../../types/live";
|
|
3
4
|
import type { LiveGate } from "../live/currentPageLive";
|
|
5
|
+
import { useScrolledPastTop } from "./useScrolledPastTop";
|
|
4
6
|
import { LIVE_HELD_AT_QUERY_KEY } from "./useTableRows";
|
|
5
7
|
|
|
6
8
|
type Query = Record<string, unknown>;
|
|
7
9
|
|
|
10
|
+
/** What the table tells its hold about the user's reading. */
|
|
11
|
+
export interface TableLiveHoldSignals {
|
|
12
|
+
/** One of its rows is open in the side panel. */
|
|
13
|
+
isRowOpen?: () => boolean;
|
|
14
|
+
/** The table's element, whose first row `whileScrolled` follows. */
|
|
15
|
+
tableElement?: () => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
export interface TableLiveHoldOptions {
|
|
9
|
-
/** The table
|
|
10
|
-
|
|
19
|
+
/** The table's `liveHold`: `true` or an object enables it. */
|
|
20
|
+
liveHold: boolean | TableViewLiveHoldOptions | undefined;
|
|
11
21
|
/** A `TableView.fromSource` table: its route receives the hold's bound. */
|
|
12
22
|
isSource: boolean;
|
|
13
23
|
/**
|
|
@@ -15,35 +25,71 @@ export interface TableLiveHoldOptions {
|
|
|
15
25
|
* sets it up after the list this hold bounds.
|
|
16
26
|
*/
|
|
17
27
|
gate: () => LiveGate;
|
|
28
|
+
signals?: TableLiveHoldSignals;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export interface TableLiveHold {
|
|
21
|
-
/** The rows are held: the
|
|
32
|
+
/** The rows are held: the page is paused, or the table holds by itself. */
|
|
22
33
|
isHolding: ComputedRef<boolean>;
|
|
23
34
|
/** Events the table received since its rows were held or last shown. */
|
|
24
35
|
newCount: ComputedRef<number>;
|
|
25
|
-
/** Reads the rows once, staying
|
|
36
|
+
/** Reads the rows once, staying held, and holds them from now. */
|
|
26
37
|
showNew: () => void;
|
|
27
38
|
/** The bound a source route receives with each request while held. */
|
|
28
39
|
heldQuery: () => Query;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
const NO_QUERY: Query = {};
|
|
43
|
+
const NEVER = () => false;
|
|
44
|
+
const NO_ELEMENT = () => null;
|
|
45
|
+
|
|
46
|
+
function holdOptionsOf(
|
|
47
|
+
liveHold: TableLiveHoldOptions["liveHold"],
|
|
48
|
+
): TableViewLiveHoldOptions | null {
|
|
49
|
+
if (!liveHold) return null;
|
|
50
|
+
return liveHold === true ? {} : liveHold;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The table alone holds while the page is live: the user reads a row in the
|
|
54
|
+
// side panel, or reads further down than the rows that would move.
|
|
55
|
+
function useLocalHold(
|
|
56
|
+
holdOptions: TableViewLiveHoldOptions | null,
|
|
57
|
+
signals: TableLiveHoldSignals,
|
|
58
|
+
): ComputedRef<boolean> {
|
|
59
|
+
const isRowOpen = signals.isRowOpen ?? NEVER;
|
|
60
|
+
const tableElement = signals.tableElement ?? NO_ELEMENT;
|
|
61
|
+
const isScrolled = useScrolledPastTop(() => {
|
|
62
|
+
const element = tableElement();
|
|
63
|
+
return element instanceof HTMLElement ? element : null;
|
|
64
|
+
}, !!holdOptions?.whileScrolled);
|
|
65
|
+
return computed(
|
|
66
|
+
() =>
|
|
67
|
+
(!!holdOptions?.whileRowOpen && isRowOpen()) ||
|
|
68
|
+
(!!holdOptions?.whileScrolled && isScrolled.value),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
32
71
|
|
|
33
72
|
/**
|
|
34
|
-
* The `liveHold` of a table view: while its live page is paused,
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
73
|
+
* The `liveHold` of a table view: while its live page is paused — or, with
|
|
74
|
+
* `whileRowOpen` / `whileScrolled`, while the user reads a row or scrolled
|
|
75
|
+
* down — the table keeps its rows: the gate requests nothing on its events
|
|
76
|
+
* and counts them, and a source route is told the moment they were frozen, so
|
|
77
|
+
* the user's own search, sort and pagination list the rows as they stood.
|
|
78
|
+
* Show reads them once more without leaving the hold; the end of a hold of
|
|
79
|
+
* the table's own reads them if it missed any.
|
|
39
80
|
*
|
|
40
81
|
* @internal
|
|
41
82
|
*/
|
|
42
83
|
export function useTableLiveHold(options: TableLiveHoldOptions): TableLiveHold {
|
|
43
84
|
const { isEnabled, isPaused } = usePageLive();
|
|
85
|
+
const holdOptions = holdOptionsOf(options.liveHold);
|
|
86
|
+
const isLocallyHeld = useLocalHold(holdOptions, options.signals ?? {});
|
|
44
87
|
const pausedAt = ref<number | null>(null);
|
|
45
88
|
const isHolding = computed(
|
|
46
|
-
() =>
|
|
89
|
+
() =>
|
|
90
|
+
!!holdOptions &&
|
|
91
|
+
isEnabled.value &&
|
|
92
|
+
(isPaused.value || isLocallyHeld.value),
|
|
47
93
|
);
|
|
48
94
|
|
|
49
95
|
watch(
|
|
@@ -53,8 +99,16 @@ export function useTableLiveHold(options: TableLiveHoldOptions): TableLiveHold {
|
|
|
53
99
|
},
|
|
54
100
|
{ immediate: true },
|
|
55
101
|
);
|
|
102
|
+
// Again on a new controller: the gate binds to the page on screen.
|
|
103
|
+
watch(
|
|
104
|
+
[isLocallyHeld, isEnabled],
|
|
105
|
+
([held, enabled]) => {
|
|
106
|
+
if (enabled) options.gate().setHeld(held);
|
|
107
|
+
},
|
|
108
|
+
{ flush: "sync" },
|
|
109
|
+
);
|
|
56
110
|
|
|
57
|
-
// The rows are as fresh as the
|
|
111
|
+
// The rows are as fresh as the hold, or as the last read the user asked
|
|
58
112
|
// for since (Show, an action of theirs).
|
|
59
113
|
const heldAt = () =>
|
|
60
114
|
pausedAt.value === null
|