@antelopejs/dms 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/api-base-url.d.ts +1 -0
- package/dist/implementations/dms/api-base-url.js +6 -0
- package/dist/implementations/dms/api-base-url.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.js +7 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/page.js +8 -2
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/test/attachment-host/capabilities.d.ts +25 -0
- package/dist/test/attachment-host/capabilities.js +53 -1
- package/dist/test/attachment-host/capabilities.js.map +1 -1
- package/dist/test/integration/table-view-capabilities.test.js +13 -0
- package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
- package/dist/test/unit/api-base-url.test.d.ts +1 -0
- package/dist/test/unit/api-base-url.test.js +31 -0
- package/dist/test/unit/api-base-url.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js +106 -0
- package/dist/test/unit/interfaces/dms/page-layout-query.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js +1 -0
- package/dist/test/unit/interfaces/dms/page-teardown.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js +46 -0
- package/dist/test/unit/interfaces/dms-base/block-parity-options.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +26 -0
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js +10 -0
- package/dist/test/unit/interfaces/dms-base/data-types/field-types.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/guard.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js +53 -0
- package/dist/test/unit/interfaces/dms-base/guard.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js +23 -0
- package/dist/test/unit/interfaces/dms-base/layout-header-actions.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/page-live.test.js +55 -0
- package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +17 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +8 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +28 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/user/preferencesCookie.ts +149 -0
- package/frontend-vue/layers/dms-core/app/build/composables/user/tablePreferences.ts +321 -0
- package/frontend-vue/layers/dms-core/app/composables/period/compareResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/presetResolvers.ts +4 -3
- package/frontend-vue/layers/dms-core/app/composables/period/types.ts +3 -1
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +2 -2
- package/frontend-vue/layers/dms-core/app/composables/period/usePeriodScope.ts +9 -2
- package/frontend-vue/layers/dms-core/app/composables/realtime/usePageRealtime.ts +48 -16
- package/frontend-vue/layers/dms-core/app/composables/realtime/useUserRealtime.ts +21 -4
- package/frontend-vue/layers/dms-core/app/composables/user/usePreferences.ts +81 -49
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +35 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +10 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +32 -1
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +13 -3
- package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +27 -3
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +16 -0
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +75 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageLayout.ts +8 -1
- package/frontend-vue/layers/dms-layout/app/composables/page/useSiteLayout.ts +13 -2
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +7 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -4
- package/frontend-vue/layers/dms-layout/app/utils/pageQuery.ts +59 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockInlineStatus.vue +67 -0
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +60 -20
- package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +15 -8
- package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +14 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +29 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +76 -28
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useGuard.ts +115 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +58 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/secondaryTarget.ts +86 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveCadence.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveHolds.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/livePoller.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/liveStorage.ts +42 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/missedTally.ts +65 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +247 -88
- package/frontend-vue/layers/dms-ui/app/build/composables/live/useLivePeriodSpan.ts +33 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +79 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodComparisonControl.ts +70 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodDisplay.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +41 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodDraft.ts +22 -9
- package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +13 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTable.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableColumns.ts +27 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +18 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +19 -26
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useScrolledPastTop.ts +66 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +7 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +65 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTablePreferences.ts +149 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +39 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +16 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViews.ts +39 -16
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/pinnedRows.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +18 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/sortableColumns.ts +17 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +46 -1
- package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
- package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +4 -0
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +40 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +129 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +6 -0
- package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +33 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/ChoiceCards.vue +22 -4
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +74 -0
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +12 -17
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +15 -0
- package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +12 -15
- package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +104 -16
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +113 -99
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +11 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +15 -1
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +86 -10
- package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewShiftF.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +5 -0
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/useTableViewEmptyState.ts +55 -0
- package/frontend-vue/layers/dms-ui/app/types/live.ts +47 -1
- package/frontend-vue/layers/dms-ui/app/types/row-action.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +13 -4
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +12 -3
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +1 -0
- package/frontend-vue/tests/activity-feed-head.test.ts +140 -0
- package/frontend-vue/tests/block-not-found.test.ts +220 -0
- package/frontend-vue/tests/choice-cards-columns.test.ts +60 -0
- package/frontend-vue/tests/component-id.test.ts +18 -0
- package/frontend-vue/tests/confirm-secondary-action.test.ts +269 -0
- package/frontend-vue/tests/guard.test.ts +242 -0
- package/frontend-vue/tests/header-action-page-query.test.ts +286 -0
- package/frontend-vue/tests/live-toggle.test.ts +87 -1
- package/frontend-vue/tests/page-layout-registration.test.ts +1 -0
- package/frontend-vue/tests/page-live-parity.test.ts +367 -0
- package/frontend-vue/tests/page-realtime-remount.test.ts +185 -0
- package/frontend-vue/tests/period-selector-parity.test.ts +138 -0
- package/frontend-vue/tests/period-selector-url.test.ts +84 -6
- package/frontend-vue/tests/period-url-query.test.ts +52 -0
- package/frontend-vue/tests/preferences-cookie-size.test.ts +311 -0
- package/frontend-vue/tests/row-side-panel.test.ts +109 -10
- package/frontend-vue/tests/table-actions-search-label.test.ts +57 -1
- package/frontend-vue/tests/table-clear-filters.test.ts +99 -1
- package/frontend-vue/tests/table-density-menu.test.ts +4 -3
- package/frontend-vue/tests/table-live-hold.test.ts +95 -5
- package/frontend-vue/tests/table-row-action-labels.test.ts +181 -0
- package/frontend-vue/tests/table-row-hover.test.ts +32 -0
- package/frontend-vue/tests/table-source-period-scope.test.ts +31 -3
- package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +14 -17
- package/frontend-vue/tests/table-view-pinned-rows.test.ts +143 -0
- package/frontend-vue/tests/table-view-sortable-columns.test.ts +45 -0
- package/frontend-vue/tests/table-view-views.test.ts +56 -15
- package/frontend-vue/tests/top-list-card-value-suffix.test.ts +107 -0
- package/frontend-vue/tests/vue-inventory.test.ts +1 -1
- package/package.json +2 -2
- package/skills/dms/SKILL.md +9 -3
- package/skills/dms-module/SKILL.md +5 -0
- package/skills/dms-pages/REFERENCE.md +23 -2
|
@@ -1,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"
|
|
@@ -21,6 +21,17 @@ export const ROW_SIDE_PANEL_ID = "dms:row-side-panel";
|
|
|
21
21
|
/** Registered name of `RowSidePanel.vue` (the layer's `Dms` prefix applied). */
|
|
22
22
|
const ROW_SIDE_PANEL_COMPONENT = "DmsRowSidePanel";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The state the panel draws in place of the component on a row that is gone,
|
|
26
|
+
* its texts translated.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
export interface RowSidePanelNotFound {
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* What the row side panel shows: the header the DMS draws, the module's
|
|
26
37
|
* component and the row it receives.
|
|
@@ -32,7 +43,12 @@ export interface RowSidePanelContent {
|
|
|
32
43
|
session: symbol;
|
|
33
44
|
/** The table (or button bar) that opened the panel; see {@link rowSidePanelOwnerKey}. */
|
|
34
45
|
ownerKey: string;
|
|
46
|
+
/** The panel's title: its accessible name, and its header without a row. */
|
|
35
47
|
title: string;
|
|
48
|
+
/** The header's title while `row` is open (the target's `rowTitle`). */
|
|
49
|
+
rowTitle?: string;
|
|
50
|
+
/** The row the link names is gone: this state replaces the component. */
|
|
51
|
+
notFound?: RowSidePanelNotFound;
|
|
36
52
|
/** i18n key (`$`-prefixed) of the position, receiving `{ index, total }`. */
|
|
37
53
|
positionLabel?: string;
|
|
38
54
|
component: Component;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { computed, shallowRef, watch, type ComputedRef } from "vue";
|
|
2
|
+
import type { LayoutHeaderAction } from "../../build/components/layout/PageHeaderActionBar.vue";
|
|
3
|
+
import type { PageLayout } from "../../types/page";
|
|
4
|
+
import {
|
|
5
|
+
isSamePageQuery,
|
|
6
|
+
layoutUrlWithQuery,
|
|
7
|
+
pageQueryValues,
|
|
8
|
+
} from "../../utils/pageQuery";
|
|
9
|
+
|
|
10
|
+
interface ServedHeaderActionsOptions {
|
|
11
|
+
/** The buttons the page layout was served with. */
|
|
12
|
+
actions: () => LayoutHeaderAction[];
|
|
13
|
+
/**
|
|
14
|
+
* The page query their availability was resolved for, set when one of them
|
|
15
|
+
* declares an `availability` (`headerActionsQuery`).
|
|
16
|
+
*/
|
|
17
|
+
servedQuery: () => Record<string, string> | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The page header's buttons for the page query of the moment. A button whose
|
|
22
|
+
* `availability` depends on the query (`?workspace=` names the record the
|
|
23
|
+
* page acts on) was resolved for the query the layout was loaded with: when
|
|
24
|
+
* the URL's query moves away from it, the header asks the page layout again
|
|
25
|
+
* for that query and takes its buttons, keeping the last ones meanwhile.
|
|
26
|
+
* Buttons without an availability never cause a request.
|
|
27
|
+
*/
|
|
28
|
+
export function useServedHeaderActions({
|
|
29
|
+
actions,
|
|
30
|
+
servedQuery,
|
|
31
|
+
}: ServedHeaderActionsOptions): ComputedRef<LayoutHeaderAction[]> {
|
|
32
|
+
const route = useDmsRoute();
|
|
33
|
+
const siteLayout = useSiteLayout();
|
|
34
|
+
const refreshed = shallowRef<LayoutHeaderAction[] | null>(null);
|
|
35
|
+
let latestRequest = 0;
|
|
36
|
+
|
|
37
|
+
const layoutUrl = () =>
|
|
38
|
+
siteLayout.findMatchingRoute(route.path)?.metadata?.layoutUrl;
|
|
39
|
+
|
|
40
|
+
async function askFor(url: string, query: Record<string, string>) {
|
|
41
|
+
const request = ++latestRequest;
|
|
42
|
+
const { $authFetch } = useAuthFetch();
|
|
43
|
+
try {
|
|
44
|
+
const layout = await $authFetch<PageLayout>(
|
|
45
|
+
layoutUrlWithQuery(url, query),
|
|
46
|
+
);
|
|
47
|
+
if (request !== latestRequest) return;
|
|
48
|
+
const options = layout.layout.options as
|
|
49
|
+
| { headerActions?: LayoutHeaderAction[] }
|
|
50
|
+
| undefined;
|
|
51
|
+
refreshed.value = options?.headerActions ?? [];
|
|
52
|
+
} catch {
|
|
53
|
+
// The buttons resolved for the previous query stay: the operation
|
|
54
|
+
// behind each one still refuses on its own.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function follow() {
|
|
59
|
+
const served = servedQuery();
|
|
60
|
+
const current = pageQueryValues(route.query as Record<string, unknown>);
|
|
61
|
+
const url = layoutUrl();
|
|
62
|
+
if (!served || !url || isSamePageQuery(served, current)) {
|
|
63
|
+
latestRequest++;
|
|
64
|
+
refreshed.value = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
void askFor(url, current);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!import.meta.env.SSR) {
|
|
71
|
+
watch([() => route.query, servedQuery], follow, { immediate: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return computed(() => refreshed.value ?? actions());
|
|
75
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { pageQueryValues } from "../../utils/pageQuery";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Reads the same page cache that Inertia hydrates before navigation. Missing
|
|
3
5
|
* layouts (including newly registered dev pages) use the shared deduped loader.
|
|
@@ -17,7 +19,12 @@ export async function usePageLayout() {
|
|
|
17
19
|
);
|
|
18
20
|
const load = async () => {
|
|
19
21
|
if (layoutUrl.value && !pageLayout.value) {
|
|
20
|
-
|
|
22
|
+
// Asked with the page query, for the header buttons whose availability
|
|
23
|
+
// depends on it; cached for the page whatever its query.
|
|
24
|
+
await siteLayout.loadPageLayout(
|
|
25
|
+
layoutUrl.value,
|
|
26
|
+
pageQueryValues(route.query),
|
|
27
|
+
);
|
|
21
28
|
}
|
|
22
29
|
};
|
|
23
30
|
watch([layoutUrl, () => siteLayout.pageLayouts.value], load);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { layoutUrlWithQuery } from "../../utils/pageQuery";
|
|
1
2
|
import { withoutTrailingSlash } from "ufo";
|
|
2
3
|
|
|
3
4
|
const STALE_THRESHOLD_MS = 300_000;
|
|
@@ -282,7 +283,15 @@ export const useSiteLayout = () => {
|
|
|
282
283
|
await refresh();
|
|
283
284
|
};
|
|
284
285
|
|
|
285
|
-
|
|
286
|
+
/**
|
|
287
|
+
* The layout of a page, cached by its layout URL. `query` is the page
|
|
288
|
+
* URL's, which the first request carries for the header buttons whose
|
|
289
|
+
* availability reads it (the header asks again for another query).
|
|
290
|
+
*/
|
|
291
|
+
const loadPageLayout = async (
|
|
292
|
+
slug: string,
|
|
293
|
+
query?: Record<string, string>,
|
|
294
|
+
): Promise<PageLayout> => {
|
|
286
295
|
if (pageLayouts.value[slug]) {
|
|
287
296
|
return pageLayouts.value[slug];
|
|
288
297
|
}
|
|
@@ -294,7 +303,9 @@ export const useSiteLayout = () => {
|
|
|
294
303
|
}
|
|
295
304
|
|
|
296
305
|
const { $authFetch } = useAuthFetch();
|
|
297
|
-
const request = $authFetch<PageLayout>(
|
|
306
|
+
const request = $authFetch<PageLayout>(
|
|
307
|
+
layoutUrlWithQuery(slug, query ?? {}),
|
|
308
|
+
).then((result) => {
|
|
298
309
|
if (pageLayouts.value === cache) cache[slug] = result;
|
|
299
310
|
return result;
|
|
300
311
|
});
|
|
@@ -12,6 +12,11 @@ interface Props {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
14
14
|
headerActions?: LayoutHeaderAction[];
|
|
15
|
+
/**
|
|
16
|
+
* The page query the server resolved the buttons' availability for, set
|
|
17
|
+
* when one declares an `availability` (`headerActionsQuery`).
|
|
18
|
+
*/
|
|
19
|
+
headerActionsQuery?: Record<string, string>;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -22,6 +27,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
22
27
|
title: undefined,
|
|
23
28
|
description: undefined,
|
|
24
29
|
headerActions: () => [],
|
|
30
|
+
headerActionsQuery: undefined,
|
|
25
31
|
});
|
|
26
32
|
</script>
|
|
27
33
|
|
|
@@ -33,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
33
39
|
:title="props.title"
|
|
34
40
|
:description="props.description"
|
|
35
41
|
:header-actions="props.headerActions"
|
|
42
|
+
:header-actions-query="props.headerActionsQuery"
|
|
36
43
|
>
|
|
37
44
|
<slot />
|
|
38
45
|
</DashboardPageBody>
|
|
@@ -12,6 +12,11 @@ interface Props {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
14
14
|
headerActions?: LayoutHeaderAction[];
|
|
15
|
+
/**
|
|
16
|
+
* The page query the server resolved the buttons' availability for, set
|
|
17
|
+
* when one declares an `availability` (`headerActionsQuery`).
|
|
18
|
+
*/
|
|
19
|
+
headerActionsQuery?: Record<string, string>;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -21,6 +26,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
21
26
|
title: undefined,
|
|
22
27
|
description: undefined,
|
|
23
28
|
headerActions: () => [],
|
|
29
|
+
headerActionsQuery: undefined,
|
|
24
30
|
});
|
|
25
31
|
</script>
|
|
26
32
|
|
|
@@ -33,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
33
39
|
:title="props.title"
|
|
34
40
|
:description="props.description"
|
|
35
41
|
:header-actions="props.headerActions"
|
|
42
|
+
:header-actions-query="props.headerActionsQuery"
|
|
36
43
|
header-class="pb-5"
|
|
37
44
|
>
|
|
38
45
|
<slot />
|
|
@@ -218,10 +218,13 @@ const realtimePageId = computed(() => pagelayoutMetadata.value?.fullId ?? "");
|
|
|
218
218
|
const realtime = usePageRealtime(realtimePageId);
|
|
219
219
|
providePageRealtime(realtime);
|
|
220
220
|
// Before the blocks set up: their refetches go through the page's live gate.
|
|
221
|
-
usePageLiveHost(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
usePageLiveHost(
|
|
222
|
+
() => ({
|
|
223
|
+
pageId: pagelayoutMetadata.value?.fullId,
|
|
224
|
+
live: pagelayoutMetadata.value?.live,
|
|
225
|
+
}),
|
|
226
|
+
{ isStreamConnected: () => !!realtime.sessionId.value },
|
|
227
|
+
);
|
|
225
228
|
|
|
226
229
|
function buildPageSetupContext(
|
|
227
230
|
pageInfo: PageInfo,
|