@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
|
@@ -67,7 +67,7 @@ function endOfYear(date: Date, yearOffset = 0): Date {
|
|
|
67
67
|
|
|
68
68
|
const PRESET_RANGE_RESOLVERS: Record<
|
|
69
69
|
Exclude<PeriodPreset, "custom">,
|
|
70
|
-
(now: Date) => PeriodRange
|
|
70
|
+
(now: Date) => PeriodRange | null
|
|
71
71
|
> = {
|
|
72
72
|
"last-hour": (now) => ({ from: addHours(now, -1), to: now }),
|
|
73
73
|
"last-24h": (now) => ({ from: addDays(now, -1), to: now }),
|
|
@@ -112,6 +112,7 @@ const PRESET_RANGE_RESOLVERS: Record<
|
|
|
112
112
|
from: startOfYear(now, -1),
|
|
113
113
|
to: endOfYear(now, -1),
|
|
114
114
|
}),
|
|
115
|
+
all: () => null,
|
|
115
116
|
};
|
|
116
117
|
|
|
117
118
|
const RELATIVE_PRESETS = new Set<PeriodPreset>(["last-hour", "last-24h"]);
|
|
@@ -130,13 +131,13 @@ export function isRelativePreset(preset: PeriodPreset): boolean {
|
|
|
130
131
|
*
|
|
131
132
|
* Calendar presets snap to day boundaries (start-of-day to end-of-day), while
|
|
132
133
|
* the relative ones (`last-hour`, `last-24h`) keep instant bounds so an ops
|
|
133
|
-
* dashboard can read sub-day granularity.
|
|
134
|
+
* dashboard can read sub-day granularity. `all` has no bounds: `null`.
|
|
134
135
|
*/
|
|
135
136
|
export function resolvePresetRange(
|
|
136
137
|
preset: PeriodPreset,
|
|
137
138
|
now: Date,
|
|
138
139
|
fallbackRange: PeriodRange,
|
|
139
|
-
): PeriodRange {
|
|
140
|
+
): PeriodRange | null {
|
|
140
141
|
if (preset === "custom") return fallbackRange;
|
|
141
142
|
return PRESET_RANGE_RESOLVERS[preset](now);
|
|
142
143
|
}
|
|
@@ -12,6 +12,7 @@ export type PeriodPreset =
|
|
|
12
12
|
| "last-quarter"
|
|
13
13
|
| "ytd"
|
|
14
14
|
| "last-year"
|
|
15
|
+
| "all"
|
|
15
16
|
| "custom";
|
|
16
17
|
|
|
17
18
|
export type PeriodComparison =
|
|
@@ -28,7 +29,8 @@ export interface PeriodRange {
|
|
|
28
29
|
export interface PeriodState {
|
|
29
30
|
preset: PeriodPreset;
|
|
30
31
|
comparison: PeriodComparison;
|
|
31
|
-
|
|
32
|
+
/** The window, or `null` for `all`: every period, with no bounds. */
|
|
33
|
+
range: PeriodRange | null;
|
|
32
34
|
compareRange: PeriodRange | null;
|
|
33
35
|
key: string;
|
|
34
36
|
}
|
|
@@ -100,8 +100,8 @@ function buildKey(state: Omit<PeriodState, "key">): string {
|
|
|
100
100
|
return [
|
|
101
101
|
state.preset,
|
|
102
102
|
state.comparison,
|
|
103
|
-
state.range.from.toISOString(),
|
|
104
|
-
state.range.to.toISOString(),
|
|
103
|
+
state.range ? state.range.from.toISOString() : "-",
|
|
104
|
+
state.range ? state.range.to.toISOString() : "-",
|
|
105
105
|
state.compareRange ? state.compareRange.from.toISOString() : "-",
|
|
106
106
|
state.compareRange ? state.compareRange.to.toISOString() : "-",
|
|
107
107
|
].join("|");
|
|
@@ -67,14 +67,21 @@ export function usePeriodScope(
|
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* `url` with `state` as query parameters: `from` / `to`, `compareFrom` /
|
|
72
|
+
* `compareTo` when a comparison range is set, `preset` and `comparison`.
|
|
73
|
+
* Every period (`all`) has no bounds, so it sends no `from` / `to`.
|
|
74
|
+
*/
|
|
70
75
|
export function appendPeriodToUrl(
|
|
71
76
|
url: string | undefined,
|
|
72
77
|
state: PeriodState | null,
|
|
73
78
|
): string | undefined {
|
|
74
79
|
if (!url || !state) return url;
|
|
75
80
|
const params = new URLSearchParams();
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
if (state.range) {
|
|
82
|
+
params.set("from", state.range.from.toISOString());
|
|
83
|
+
params.set("to", state.range.to.toISOString());
|
|
84
|
+
}
|
|
78
85
|
if (state.compareRange) {
|
|
79
86
|
params.set("compareFrom", state.compareRange.from.toISOString());
|
|
80
87
|
params.set("compareTo", state.compareRange.to.toISOString());
|
|
@@ -12,27 +12,59 @@ export interface PageRealtimeApi {
|
|
|
12
12
|
pageId: Ref<string | undefined>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
interface PageClaim {
|
|
16
|
+
pageId: string | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Every mounted page instance holding a page id, in the order each last set it.
|
|
21
|
+
* A page remounted at the same path (only its query changed) sets up its new
|
|
22
|
+
* instance before or after the old one is disposed, depending on the
|
|
23
|
+
* transition; the session follows the latest live claim, so neither order
|
|
24
|
+
* drops the page.
|
|
25
|
+
*/
|
|
26
|
+
const pageClaims: PageClaim[] = [];
|
|
27
|
+
|
|
28
|
+
const applyLatestClaim = (user: UserRealtimeApi): void => {
|
|
29
|
+
void user.setPage(pageClaims.at(-1)?.pageId ?? null);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const raiseClaim = (claim: PageClaim): void => {
|
|
33
|
+
const index = pageClaims.indexOf(claim);
|
|
34
|
+
if (index !== -1) pageClaims.splice(index, 1);
|
|
35
|
+
pageClaims.push(claim);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const dropClaim = (claim: PageClaim): void => {
|
|
39
|
+
const index = pageClaims.indexOf(claim);
|
|
40
|
+
if (index !== -1) pageClaims.splice(index, 1);
|
|
41
|
+
};
|
|
42
|
+
|
|
15
43
|
export function usePageRealtime(
|
|
16
44
|
pageId: MaybeRefOrGetter<string>,
|
|
17
45
|
): PageRealtimeApi {
|
|
18
46
|
const user: UserRealtimeApi = useUserRealtime();
|
|
19
47
|
const resolvedPageId = computed(() => toValue(pageId) || undefined);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
resolvedPageId
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
|
|
49
|
+
if (import.meta.env.SSR) {
|
|
50
|
+
// No dispose runs on the server, so a claim there would never be released.
|
|
51
|
+
void user.setPage(resolvedPageId.value ?? null);
|
|
52
|
+
} else {
|
|
53
|
+
const claim: PageClaim = { pageId: undefined };
|
|
54
|
+
watch(
|
|
55
|
+
resolvedPageId,
|
|
56
|
+
(next) => {
|
|
57
|
+
claim.pageId = next;
|
|
58
|
+
raiseClaim(claim);
|
|
59
|
+
applyLatestClaim(user);
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true },
|
|
62
|
+
);
|
|
63
|
+
onScopeDispose(() => {
|
|
64
|
+
dropClaim(claim);
|
|
65
|
+
applyLatestClaim(user);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
36
68
|
|
|
37
69
|
return {
|
|
38
70
|
subscribe: user.subscribe,
|
|
@@ -60,6 +60,7 @@ interface UserRealtimeState {
|
|
|
60
60
|
appliedPageId: Ref<string | null>;
|
|
61
61
|
handlers: Map<string, Set<RealtimeTopicHandler>>;
|
|
62
62
|
snapshots: Map<string, RealtimeSnapshotEvent>;
|
|
63
|
+
reconciling: Promise<void>;
|
|
63
64
|
cancelConnect?: () => void;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -247,7 +248,7 @@ const callPageEndpoint = async (
|
|
|
247
248
|
});
|
|
248
249
|
};
|
|
249
250
|
|
|
250
|
-
const
|
|
251
|
+
const applyDesiredPage = async (
|
|
251
252
|
state: UserRealtimeState,
|
|
252
253
|
baseUrl: string,
|
|
253
254
|
): Promise<void> => {
|
|
@@ -259,9 +260,24 @@ const reconcilePage = async (
|
|
|
259
260
|
// A reconnect during the call replaced the session: the page is applied to
|
|
260
261
|
// the dead one, and the new one still needs it.
|
|
261
262
|
if (state.sessionId.value !== sessionId) return;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
state.appliedPageId.value = desired;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Runs one page call at a time. Two overlapping calls would each compare the
|
|
268
|
+
* desired page with what the server held before either landed: an unsubscribe
|
|
269
|
+
* in flight followed by a subscribe to the same page would skip the
|
|
270
|
+
* subscribe, and the session would end up with no page at all.
|
|
271
|
+
*/
|
|
272
|
+
const reconcilePage = (
|
|
273
|
+
state: UserRealtimeState,
|
|
274
|
+
baseUrl: string,
|
|
275
|
+
): Promise<void> => {
|
|
276
|
+
const run = state.reconciling
|
|
277
|
+
.catch(() => undefined)
|
|
278
|
+
.then(() => applyDesiredPage(state, baseUrl));
|
|
279
|
+
state.reconciling = run;
|
|
280
|
+
return run;
|
|
265
281
|
};
|
|
266
282
|
|
|
267
283
|
/**
|
|
@@ -290,6 +306,7 @@ const buildApi = (): UserRealtimeApi => {
|
|
|
290
306
|
appliedPageId,
|
|
291
307
|
handlers: new Map(),
|
|
292
308
|
snapshots: new Map(),
|
|
309
|
+
reconciling: Promise.resolve(),
|
|
293
310
|
};
|
|
294
311
|
|
|
295
312
|
const config = useDmsRuntimeConfig();
|
|
@@ -1,74 +1,106 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
decodePreferencesTree,
|
|
3
|
+
encodePreferencesTree,
|
|
4
|
+
fitPreferencesBudget,
|
|
5
|
+
isBranch,
|
|
6
|
+
markWritten,
|
|
7
|
+
PREFERENCES_COOKIE_NAME,
|
|
8
|
+
readAt,
|
|
9
|
+
writeAt,
|
|
10
|
+
} from "../../build/composables/user/preferencesCookie";
|
|
11
|
+
import { clearLocalTableEntries } from "../../build/composables/user/tablePreferences";
|
|
2
12
|
|
|
3
13
|
export interface UserPreferences {
|
|
4
14
|
[key: string]: unknown;
|
|
5
15
|
}
|
|
6
16
|
|
|
7
|
-
const COOKIE_NAME = "user-preferences";
|
|
8
17
|
const COOKIE_MAX_AGE_ONE_YEAR = 60 * 60 * 24 * 365;
|
|
9
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The cookie the preferences were kept in up to `@antelopejs/dms` 0.7.9, as
|
|
21
|
+
* URI-encoded JSON that grew with every table the user opened. It is not
|
|
22
|
+
* read: the browser deletes it, so that it stops weighing on every request.
|
|
23
|
+
*/
|
|
24
|
+
const LEGACY_COOKIE_NAME = "user-preferences";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The most the preferences cookie may weigh, name and value together.
|
|
28
|
+
* Browsers drop a cookie over 4 KB, and every request carries it next to the
|
|
29
|
+
* session cookies under the server's 16 KB header limit.
|
|
30
|
+
*/
|
|
31
|
+
export const MAX_PREFERENCES_COOKIE_BYTES = 3 * 1024;
|
|
32
|
+
|
|
33
|
+
// A plain copy, as the cookie will read it back: the refs hand out reactive
|
|
34
|
+
// proxies, which a caller may keep mutating after the write.
|
|
35
|
+
const toPlain = <T>(value: T): T =>
|
|
36
|
+
value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T);
|
|
37
|
+
|
|
38
|
+
const expireLegacyCookie = (): void => {
|
|
39
|
+
if (typeof document === "undefined") return;
|
|
40
|
+
const isPresent = document.cookie
|
|
41
|
+
.split(/;\s*/)
|
|
42
|
+
.some((entry) => entry.startsWith(`${LEGACY_COOKIE_NAME}=`));
|
|
43
|
+
if (isPresent) document.cookie = `${LEGACY_COOKIE_NAME}=; path=/; max-age=0`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The user's preferences, kept in the `dms-preferences` cookie so that a
|
|
48
|
+
* server render reads them too: a tree addressed by dotted paths, stored as
|
|
49
|
+
* base64url JSON. The cookie is bounded by `MAX_PREFERENCES_COOKIE_BYTES`:
|
|
50
|
+
* past it, the table view states, then the remembered periods, written least
|
|
51
|
+
* recently are dropped, and a write that would not fit even then is refused.
|
|
52
|
+
*/
|
|
10
53
|
export const usePreferences = () => {
|
|
11
|
-
const preferenceCookie = useDmsCookie<
|
|
12
|
-
default: () =>
|
|
54
|
+
const preferenceCookie = useDmsCookie<string>(PREFERENCES_COOKIE_NAME, {
|
|
55
|
+
default: () => "",
|
|
13
56
|
maxAge: COOKIE_MAX_AGE_ONE_YEAR,
|
|
14
57
|
sameSite: "lax",
|
|
15
58
|
secure: true,
|
|
16
59
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
expireLegacyCookie();
|
|
61
|
+
|
|
62
|
+
// The cookie ref is shared by name, so every caller decodes the same value.
|
|
63
|
+
const tree = computed<UserPreferences>(() =>
|
|
64
|
+
typeof preferenceCookie.value === "string"
|
|
65
|
+
? decodePreferencesTree(preferenceCookie.value)
|
|
66
|
+
: {},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const commit = (next: UserPreferences): boolean => {
|
|
70
|
+
if (!fitPreferencesBudget(next, MAX_PREFERENCES_COOKIE_BYTES)) {
|
|
71
|
+
if (import.meta.env.DEV) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`[DMS] A preference was not saved: the "${PREFERENCES_COOKIE_NAME}" cookie would exceed ${MAX_PREFERENCES_COOKIE_BYTES} bytes.`,
|
|
74
|
+
);
|
|
29
75
|
}
|
|
76
|
+
return false;
|
|
30
77
|
}
|
|
78
|
+
preferenceCookie.value = encodePreferencesTree(next);
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
31
81
|
|
|
32
|
-
|
|
82
|
+
const getPreference = <T = unknown>(keyPath: string, defaultValue?: T): T => {
|
|
83
|
+
const value = readAt(tree.value, keyPath.split("."));
|
|
84
|
+
return (value === undefined ? defaultValue : value) as T;
|
|
33
85
|
};
|
|
34
86
|
|
|
35
87
|
const setPreference = (keyPath: string, value: unknown): void => {
|
|
36
88
|
const keys = keyPath.split(".");
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const key of keys) {
|
|
42
|
-
if (!current[key] || !isObject(current[key])) {
|
|
43
|
-
current[key] = {};
|
|
44
|
-
}
|
|
45
|
-
current = current[key] as UserPreferences;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
(current as Record<string, unknown>)[lastKey] = value;
|
|
49
|
-
|
|
50
|
-
preferenceCookie.value = preferences.value;
|
|
89
|
+
const next = toPlain(tree.value);
|
|
90
|
+
writeAt(next, keys, toPlain(value));
|
|
91
|
+
markWritten(next, keys);
|
|
92
|
+
commit(next);
|
|
51
93
|
};
|
|
52
94
|
|
|
53
95
|
const removePreference = (keyPath: string): void => {
|
|
54
96
|
const keys = keyPath.split(".");
|
|
55
97
|
const lastKey = keys.pop()!;
|
|
98
|
+
const next = toPlain(tree.value);
|
|
99
|
+
const parent = keys.length === 0 ? next : readAt(next, keys);
|
|
100
|
+
if (!isBranch(parent) || !(lastKey in parent)) return;
|
|
56
101
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (const key of keys) {
|
|
60
|
-
if (!current[key] || !isObject(current[key])) {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
current = current[key] as UserPreferences;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (lastKey in current) {
|
|
67
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
68
|
-
delete (current as Record<string, unknown>)[lastKey];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
preferenceCookie.value = preferences.value;
|
|
102
|
+
Reflect.deleteProperty(parent, lastKey);
|
|
103
|
+
commit(next);
|
|
72
104
|
};
|
|
73
105
|
|
|
74
106
|
const updatePreference = <T = unknown>(
|
|
@@ -81,12 +113,12 @@ export const usePreferences = () => {
|
|
|
81
113
|
};
|
|
82
114
|
|
|
83
115
|
const clearAllPreferences = (): void => {
|
|
84
|
-
|
|
85
|
-
|
|
116
|
+
preferenceCookie.value = "";
|
|
117
|
+
clearLocalTableEntries();
|
|
86
118
|
};
|
|
87
119
|
|
|
88
120
|
return {
|
|
89
|
-
preferences: readonly(
|
|
121
|
+
preferences: readonly(tree),
|
|
90
122
|
|
|
91
123
|
getPreference,
|
|
92
124
|
setPreference,
|
|
@@ -30,6 +30,39 @@ export type ConfirmDialogColor =
|
|
|
30
30
|
| "info"
|
|
31
31
|
| "neutral";
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A file a dialog's secondary action downloads (backend
|
|
35
|
+
* `ConfirmDialogDownloadTarget`), fetched with the user's session.
|
|
36
|
+
*/
|
|
37
|
+
export interface ConfirmDialogDownloadTarget {
|
|
38
|
+
type: "download";
|
|
39
|
+
url: string;
|
|
40
|
+
/** Saved under; the response's `Content-Disposition` name, else the URL's. */
|
|
41
|
+
filename?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A link opened by a dialog's secondary action. */
|
|
45
|
+
export interface ConfirmDialogLinkTarget {
|
|
46
|
+
type: "external";
|
|
47
|
+
url: string;
|
|
48
|
+
newTab?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Where a dialog's secondary action leads (backend `ConfirmDialogSecondaryTarget`). */
|
|
52
|
+
export type ConfirmDialogSecondaryTarget =
|
|
53
|
+
| ConfirmDialogLinkTarget
|
|
54
|
+
| ConfirmDialogDownloadTarget;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A button next to cancel and confirm, running its target with the dialog
|
|
58
|
+
* left open (backend `ConfirmDialogSecondaryAction`).
|
|
59
|
+
*/
|
|
60
|
+
export interface ConfirmDialogSecondaryAction {
|
|
61
|
+
label: string;
|
|
62
|
+
icon?: string;
|
|
63
|
+
target: ConfirmDialogSecondaryTarget;
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
/**
|
|
34
67
|
* The dialog an action asks in before it runs (backend `ConfirmDialog`),
|
|
35
68
|
* the same for every action. Texts are i18n keys (with `$`) or literals,
|
|
@@ -44,6 +77,8 @@ export interface ConfirmDialog {
|
|
|
44
77
|
confirmLabel?: string;
|
|
45
78
|
cancelLabel?: string;
|
|
46
79
|
impact?: ConfirmDialogImpact[];
|
|
80
|
+
/** A button next to cancel and confirm, leaving the dialog open. */
|
|
81
|
+
secondaryAction?: ConfirmDialogSecondaryAction;
|
|
47
82
|
/** The action cannot run: the dialog only explains why. */
|
|
48
83
|
blocked?: boolean;
|
|
49
84
|
/** Text the user must type to confirm. */
|
|
@@ -445,6 +445,7 @@
|
|
|
445
445
|
"preset_aria": "Preset",
|
|
446
446
|
"comparison_aria": "Comparison",
|
|
447
447
|
"custom_range_aria": "Custom range",
|
|
448
|
+
"since": "since {from}",
|
|
448
449
|
"presets": {
|
|
449
450
|
"last-hour": "Last hour",
|
|
450
451
|
"last-24h": "Last 24 hours",
|
|
@@ -459,6 +460,7 @@
|
|
|
459
460
|
"last-quarter": "Last quarter",
|
|
460
461
|
"ytd": "Year to date",
|
|
461
462
|
"last-year": "Last year",
|
|
463
|
+
"all": "All time",
|
|
462
464
|
"custom": "Custom"
|
|
463
465
|
},
|
|
464
466
|
"comparisons": {
|
|
@@ -445,6 +445,7 @@
|
|
|
445
445
|
"preset_aria": "Préréglage",
|
|
446
446
|
"comparison_aria": "Comparaison",
|
|
447
447
|
"custom_range_aria": "Plage personnalisée",
|
|
448
|
+
"since": "depuis {from}",
|
|
448
449
|
"presets": {
|
|
449
450
|
"last-hour": "Dernière heure",
|
|
450
451
|
"last-24h": "24 dernières heures",
|
|
@@ -459,6 +460,7 @@
|
|
|
459
460
|
"last-quarter": "Trimestre dernier",
|
|
460
461
|
"ytd": "Année en cours",
|
|
461
462
|
"last-year": "Année dernière",
|
|
463
|
+
"all": "Toute la période",
|
|
462
464
|
"custom": "Personnalisé"
|
|
463
465
|
},
|
|
464
466
|
"comparisons": {
|
|
@@ -13,6 +13,11 @@ interface Props {
|
|
|
13
13
|
description?: string;
|
|
14
14
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
15
15
|
headerActions?: LayoutHeaderAction[];
|
|
16
|
+
/**
|
|
17
|
+
* The page query the server resolved the buttons' availability for, set
|
|
18
|
+
* when one declares an `availability` (`headerActionsQuery`).
|
|
19
|
+
*/
|
|
20
|
+
headerActionsQuery?: Record<string, string>;
|
|
16
21
|
/** Space between the page header and the page. */
|
|
17
22
|
headerClass?: string;
|
|
18
23
|
}
|
|
@@ -23,6 +28,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
23
28
|
title: undefined,
|
|
24
29
|
description: undefined,
|
|
25
30
|
headerActions: () => [],
|
|
31
|
+
headerActionsQuery: undefined,
|
|
26
32
|
headerClass: "pb-6",
|
|
27
33
|
});
|
|
28
34
|
|
|
@@ -58,7 +64,10 @@ const PAGE_SLOT_CLASS =
|
|
|
58
64
|
:class="props.headerClass"
|
|
59
65
|
>
|
|
60
66
|
<template #actions>
|
|
61
|
-
<PageHeaderActionsOutlet
|
|
67
|
+
<PageHeaderActionsOutlet
|
|
68
|
+
:actions="props.headerActions"
|
|
69
|
+
:served-query="props.headerActionsQuery"
|
|
70
|
+
/>
|
|
62
71
|
</template>
|
|
63
72
|
</PageHeader>
|
|
64
73
|
<div :class="isPageLoading ? 'hidden' : PAGE_SLOT_CLASS">
|
|
@@ -15,6 +15,10 @@ import { PREVIEW_LOCK_ICON } from "#dms-ui/app/build/utils/permissionPreview";
|
|
|
15
15
|
import { runMountedQuickAction } from "#dms-ui/app/build/utils/quickActionTargets";
|
|
16
16
|
import { useActionTargets } from "#dms-ui/app/build/composables/actions/useActionTargets";
|
|
17
17
|
import { refreshPageBlocks } from "#dms-ui/app/utils/blockRefresh";
|
|
18
|
+
import {
|
|
19
|
+
hasUrlVariables,
|
|
20
|
+
resolveLinkVariables,
|
|
21
|
+
} from "#dms-ui/app/build/utils/urlVariables";
|
|
18
22
|
import {
|
|
19
23
|
dispatchQuickActionTarget,
|
|
20
24
|
findServedQuickAction,
|
|
@@ -111,6 +115,32 @@ function baseButton(action: LayoutHeaderAction): ButtonProps {
|
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
type LinkTarget = Extract<ActionTarget, { type: "page" | "external" }>;
|
|
118
|
+
type UrlTarget = Extract<ActionTarget, { url: string }>;
|
|
119
|
+
|
|
120
|
+
const hasTargetUrl = (target: ActionTarget): target is UrlTarget =>
|
|
121
|
+
typeof (target as Partial<UrlTarget>).url === "string";
|
|
122
|
+
|
|
123
|
+
// The values of the page a target's URL may name: a button declared once on
|
|
124
|
+
// the page leads to the record the page shows (`{{params.id}}`) or keeps its
|
|
125
|
+
// scope (`?workspace={{query.workspace}}`), following the URL as it changes.
|
|
126
|
+
const pageUrlValues = computed(() => ({
|
|
127
|
+
routeParams: siteLayout.findMatchingRoute(route.path)?.params,
|
|
128
|
+
routeQuery: route.query as Record<string, unknown>,
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
function withPageValues(action: LayoutHeaderAction): LayoutHeaderAction {
|
|
132
|
+
const { target } = action;
|
|
133
|
+
if (!target || !hasTargetUrl(target) || !hasUrlVariables(target.url)) {
|
|
134
|
+
return action;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
...action,
|
|
138
|
+
target: {
|
|
139
|
+
...target,
|
|
140
|
+
url: resolveLinkVariables(target.url, pageUrlValues.value),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
114
144
|
|
|
115
145
|
// A link target (without a confirmation) renders as a link: middle click and
|
|
116
146
|
// "open in a new tab" keep working.
|
|
@@ -157,8 +187,9 @@ function resolveQuickActionButton(
|
|
|
157
187
|
}
|
|
158
188
|
|
|
159
189
|
function resolveAction(
|
|
160
|
-
|
|
190
|
+
declared: LayoutHeaderAction,
|
|
161
191
|
): ResolvedHeaderAction | null {
|
|
192
|
+
const action = withPageValues(declared);
|
|
162
193
|
const base = baseButton(action);
|
|
163
194
|
const disabledReason =
|
|
164
195
|
action.disabled && action.disabledReason
|
package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue
CHANGED
|
@@ -4,13 +4,23 @@ import PageHeaderActionBar, {
|
|
|
4
4
|
type LayoutHeaderAction,
|
|
5
5
|
} from "./PageHeaderActionBar.vue";
|
|
6
6
|
import { injectPageHeaderActionsHost } from "../../../composables/layout/usePageHeaderActions";
|
|
7
|
+
import { useServedHeaderActions } from "../../../composables/layout/useServedHeaderActions";
|
|
7
8
|
|
|
8
9
|
interface PageHeaderActionsOutletProps {
|
|
9
10
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
10
11
|
actions: LayoutHeaderAction[];
|
|
12
|
+
/** The page query the server resolved their availability for. */
|
|
13
|
+
servedQuery?: Record<string, string>;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
const props = defineProps<PageHeaderActionsOutletProps>()
|
|
16
|
+
const props = withDefaults(defineProps<PageHeaderActionsOutletProps>(), {
|
|
17
|
+
servedQuery: undefined,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const actions = useServedHeaderActions({
|
|
21
|
+
actions: () => props.actions,
|
|
22
|
+
servedQuery: () => props.servedQuery,
|
|
23
|
+
});
|
|
14
24
|
|
|
15
25
|
const ROOT_SELECTOR = "[data-dms-page-header-actions]";
|
|
16
26
|
// The actions take their own full-width row below `md`, the end of the
|
|
@@ -26,7 +36,7 @@ onServerPrefetch(() => host?.whenReady());
|
|
|
26
36
|
|
|
27
37
|
const pageActions = computed(() => host?.actions.value ?? null);
|
|
28
38
|
const hasActions = computed(
|
|
29
|
-
() =>
|
|
39
|
+
() => actions.value.length > 0 || !!pageActions.value,
|
|
30
40
|
);
|
|
31
41
|
|
|
32
42
|
// Hydrating before the page has set up: its actions are not known yet, but
|
|
@@ -56,7 +66,7 @@ const showsServerMarkup = computed(
|
|
|
56
66
|
data-dms-page-header-actions
|
|
57
67
|
:class="ROOT_CLASS"
|
|
58
68
|
>
|
|
59
|
-
<PageHeaderActionBar :actions="
|
|
69
|
+
<PageHeaderActionBar :actions="actions" />
|
|
60
70
|
<component :is="pageActions" v-if="pageActions" />
|
|
61
71
|
</div>
|
|
62
72
|
</template>
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* among the rows the table shows, previous and next (J and K too) and close
|
|
6
6
|
* (Escape too). Below it, the module's component receives the row as a
|
|
7
7
|
* drawer's component does, mounted anew on each row. The position follows the
|
|
8
|
-
* table: a refresh that moves or drops the row updates it.
|
|
8
|
+
* table: a refresh that moves or drops the row updates it. A row the link
|
|
9
|
+
* names but its route answered 404 for shows the "not found" state instead,
|
|
10
|
+
* with a close button.
|
|
9
11
|
*/
|
|
10
12
|
import { computed, onBeforeUnmount, onMounted } from "vue";
|
|
13
|
+
import DmsEmptyState from "#dms-ui/app/components/empty-state/EmptyState.vue";
|
|
11
14
|
import {
|
|
12
15
|
listenRowPanelClose,
|
|
13
16
|
listenRowSteps,
|
|
@@ -46,8 +49,20 @@ const componentProps = computed(() => ({
|
|
|
46
49
|
navigation: navigation.value,
|
|
47
50
|
}));
|
|
48
51
|
|
|
52
|
+
const heading = computed(() => content.value?.rowTitle || content.value?.title);
|
|
53
|
+
|
|
49
54
|
const close = () => closeRowSidePanel();
|
|
50
55
|
|
|
56
|
+
const notFoundActions = computed(() => [
|
|
57
|
+
{
|
|
58
|
+
label: t("side_panel.close"),
|
|
59
|
+
icon: "i-ph-x",
|
|
60
|
+
color: "neutral" as const,
|
|
61
|
+
variant: "outline" as const,
|
|
62
|
+
onClick: close,
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
|
|
51
66
|
let stopKeys: Array<() => void> = [];
|
|
52
67
|
|
|
53
68
|
onMounted(() => {
|
|
@@ -74,7 +89,7 @@ onBeforeUnmount(() => {
|
|
|
74
89
|
<h2
|
|
75
90
|
class="text-highlighted truncate text-[15px] leading-[1.3] font-[650] tracking-[-0.015em]"
|
|
76
91
|
>
|
|
77
|
-
{{
|
|
92
|
+
{{ heading }}
|
|
78
93
|
</h2>
|
|
79
94
|
<p
|
|
80
95
|
v-if="position"
|
|
@@ -122,7 +137,16 @@ onBeforeUnmount(() => {
|
|
|
122
137
|
</div>
|
|
123
138
|
</header>
|
|
124
139
|
<div class="min-h-0 flex-1 overflow-y-auto p-4">
|
|
125
|
-
<
|
|
140
|
+
<DmsEmptyState
|
|
141
|
+
v-if="content.notFound"
|
|
142
|
+
data-dms-row-side-panel-not-found
|
|
143
|
+
variant="no-result"
|
|
144
|
+
:title="content.notFound.title"
|
|
145
|
+
:description="content.notFound.description"
|
|
146
|
+
:actions="notFoundActions"
|
|
147
|
+
class="py-10"
|
|
148
|
+
/>
|
|
149
|
+
<Suspense v-else>
|
|
126
150
|
<component
|
|
127
151
|
:is="content.component"
|
|
128
152
|
:key="content.rowKey"
|