@antelopejs/dms 0.10.0 → 0.11.0
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 +69 -0
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -4
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.js +17 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +6 -0
- package/dist/implementations/dms/manifest.js +10 -7
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/index.js +4 -7
- package/dist/index.js.map +1 -1
- package/dist/store/listing.js +1 -2
- package/dist/store/listing.js.map +1 -1
- package/dist/store/operations.js +1 -2
- package/dist/store/operations.js.map +1 -1
- package/dist/utils/ensure-default-tenant.d.ts +2 -1
- package/dist/utils/ensure-default-tenant.js +13 -1
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/dist/utils/export-jobs.js +2 -1
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +1 -2
- package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +3 -0
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +5 -37
- 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-core/shared/types/app-config.ts +0 -2
- package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
- package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +2 -106
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +0 -8
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +0 -10
- package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +24 -4
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +10 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -0
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +9 -9
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +9 -9
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
- package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +6 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +6 -4
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +2 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +26 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +22 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +17 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +30 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +13 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
- package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +27 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +14 -7
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +3 -9
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +16 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +47 -7
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +19 -9
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -1
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -1
- package/frontend-vue/package.json +6 -2
- package/package.json +7 -7
- package/skills/dms-module/SKILL.md +1 -1
- package/skills/dms-pages/REFERENCE.md +7 -3
- package/dist/hooks/tenant-instances.d.ts +0 -1
- package/dist/hooks/tenant-instances.js +0 -13
- package/dist/hooks/tenant-instances.js.map +0 -1
- package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +0 -29
- package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +0 -50
- package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +0 -71
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { onScopeDispose, type Ref, watch } from "vue";
|
|
2
|
+
import type { RealtimeTopicHandler } from "#dms-core/app/composables/realtime/useUserRealtime";
|
|
3
|
+
import type { PageLayout } from "../../types/page";
|
|
4
|
+
import {
|
|
5
|
+
fillRouteParams,
|
|
6
|
+
isSamePageQuery,
|
|
7
|
+
isSameRouteParams,
|
|
8
|
+
isViewStateKey,
|
|
9
|
+
layoutUrlWithQuery,
|
|
10
|
+
pageQueryValues,
|
|
11
|
+
} from "../../utils/pageQuery";
|
|
12
|
+
|
|
13
|
+
// One frame: the events of one burst (a bulk delete) read the record once.
|
|
14
|
+
const BURST_WINDOW_MS = 16;
|
|
15
|
+
|
|
16
|
+
/** How the server read the record a page shows (`DefaultLayout({ record })`). */
|
|
17
|
+
type PageRecordStatus = "found" | "missing" | "error";
|
|
18
|
+
|
|
19
|
+
interface RecordLayoutOptions {
|
|
20
|
+
recordStatus?: PageRecordStatus;
|
|
21
|
+
headerActionsQuery?: Record<string, string>;
|
|
22
|
+
headerActionsParams?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** What the page's record deletion listens to. */
|
|
26
|
+
export interface PageRecordDeletionSource {
|
|
27
|
+
/** The page's layout as it was first served. */
|
|
28
|
+
pageLayout: () => PageLayout | null;
|
|
29
|
+
/** The page's realtime topics, once its session is subscribed to it. */
|
|
30
|
+
pageTopics: Ref<string[]>;
|
|
31
|
+
subscribe(topic: string, handler: RealtimeTopicHandler): () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const optionsOf = (layout: PageLayout | null | undefined) =>
|
|
35
|
+
layout?.layout.options as RecordLayoutOptions | undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Leaves a detail page whose record was deleted, in every tab and for every
|
|
39
|
+
* member on it. The layout of a page reading a record (`DefaultLayout({
|
|
40
|
+
* record })`) says how the server read it (`recordStatus`); on each event of
|
|
41
|
+
* one of the page's realtime topics, the page asks its layout again, and a
|
|
42
|
+
* record it had `found` that is now `missing` was deleted: a toast says so,
|
|
43
|
+
* and the page opens its list (`MenuOptions.parent`), or stays on its
|
|
44
|
+
* not-found blocks without one. A page reading no record never asks.
|
|
45
|
+
*/
|
|
46
|
+
export function usePageRecordDeletion(source: PageRecordDeletionSource): void {
|
|
47
|
+
if (import.meta.env.SSR) return;
|
|
48
|
+
const route = useDmsRoute();
|
|
49
|
+
const siteLayout = useSiteLayout();
|
|
50
|
+
const toast = useToast();
|
|
51
|
+
const { t } = useI18n();
|
|
52
|
+
|
|
53
|
+
const pageRoute = () => siteLayout.findMatchingRoute(route.path);
|
|
54
|
+
const currentQuery = () =>
|
|
55
|
+
pageQueryValues(route.query as Record<string, unknown>);
|
|
56
|
+
/** The page URL the record is named by, leaving out the view's state. */
|
|
57
|
+
const recordKey = () =>
|
|
58
|
+
JSON.stringify([
|
|
59
|
+
route.path,
|
|
60
|
+
Object.entries(currentQuery()).filter(([name]) => !isViewStateKey(name)),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
let known: { key: string; status: PageRecordStatus } | undefined;
|
|
64
|
+
let latestRead = 0;
|
|
65
|
+
|
|
66
|
+
async function readStatus(): Promise<PageRecordStatus | undefined> {
|
|
67
|
+
const match = pageRoute();
|
|
68
|
+
const layoutUrl = match?.metadata?.layoutUrl;
|
|
69
|
+
if (!layoutUrl) return undefined;
|
|
70
|
+
const { $authFetch } = useAuthFetch();
|
|
71
|
+
try {
|
|
72
|
+
const layout = await $authFetch<PageLayout>(
|
|
73
|
+
layoutUrlWithQuery(
|
|
74
|
+
fillRouteParams(layoutUrl, match.params ?? {}),
|
|
75
|
+
currentQuery(),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
return optionsOf(layout)?.recordStatus;
|
|
79
|
+
} catch {
|
|
80
|
+
// Unknown: the record is not taken for deleted on a failed read.
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function leave(): Promise<void> {
|
|
86
|
+
toast.add({
|
|
87
|
+
title: t("page.record_deleted.title"),
|
|
88
|
+
description: t("page.record_deleted.description"),
|
|
89
|
+
color: "warning",
|
|
90
|
+
icon: "i-ph-trash",
|
|
91
|
+
});
|
|
92
|
+
const match = pageRoute();
|
|
93
|
+
const parentSlug = match?.metadata?.parentSlug;
|
|
94
|
+
if (!parentSlug) return;
|
|
95
|
+
const path = fillRouteParams(parentSlug, match.params ?? {});
|
|
96
|
+
if (path.includes(":")) return;
|
|
97
|
+
const parent = siteLayout.findMatchingRoute(path)?.metadata;
|
|
98
|
+
if (!parent || parent.hasAccess === false) return;
|
|
99
|
+
await navigateDms(path, { replace: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function check(): Promise<void> {
|
|
103
|
+
const key = recordKey();
|
|
104
|
+
const read = ++latestRead;
|
|
105
|
+
const status = await readStatus();
|
|
106
|
+
if (read !== latestRead || key !== recordKey() || !status) return;
|
|
107
|
+
const previous = known?.key === key ? known.status : undefined;
|
|
108
|
+
known = { key, status };
|
|
109
|
+
if (previous === "found" && status === "missing") await leave();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const declaresRecord = () =>
|
|
113
|
+
optionsOf(source.pageLayout())?.recordStatus !== undefined;
|
|
114
|
+
|
|
115
|
+
// What the page was served with stands for its URL when it was served for
|
|
116
|
+
// it; another record of the same page is read once to know where it stands.
|
|
117
|
+
function follow(): void {
|
|
118
|
+
if (known?.key === recordKey()) return;
|
|
119
|
+
latestRead++;
|
|
120
|
+
known = undefined;
|
|
121
|
+
const options = optionsOf(source.pageLayout());
|
|
122
|
+
if (!options?.recordStatus) return;
|
|
123
|
+
const served =
|
|
124
|
+
isSamePageQuery(options.headerActionsQuery ?? {}, currentQuery()) &&
|
|
125
|
+
isSameRouteParams(
|
|
126
|
+
options.headerActionsParams ?? {},
|
|
127
|
+
pageRoute()?.params ?? {},
|
|
128
|
+
);
|
|
129
|
+
if (served) known = { key: recordKey(), status: options.recordStatus };
|
|
130
|
+
else void check();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
134
|
+
// Presence snapshots ride the same topics: they carry no `type`.
|
|
135
|
+
const onEvent: RealtimeTopicHandler = (event) => {
|
|
136
|
+
if (!("type" in event) || !declaresRecord()) return;
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
timer = setTimeout(() => void check(), BURST_WINDOW_MS);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
let unsubscribes: Array<() => void> = [];
|
|
142
|
+
const unsubscribeAll = () => {
|
|
143
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
144
|
+
unsubscribes = [];
|
|
145
|
+
};
|
|
146
|
+
watch(
|
|
147
|
+
source.pageTopics,
|
|
148
|
+
(topics) => {
|
|
149
|
+
unsubscribeAll();
|
|
150
|
+
unsubscribes = topics.map((topic) => source.subscribe(topic, onEvent));
|
|
151
|
+
},
|
|
152
|
+
{ immediate: true },
|
|
153
|
+
);
|
|
154
|
+
watch([() => route.path, () => route.query, source.pageLayout], follow, {
|
|
155
|
+
immediate: true,
|
|
156
|
+
});
|
|
157
|
+
onScopeDispose(() => {
|
|
158
|
+
unsubscribeAll();
|
|
159
|
+
clearTimeout(timer);
|
|
160
|
+
latestRead++;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import DashboardFrame from "../build/components/layout/DashboardFrame.vue";
|
|
3
3
|
import DashboardPageBody from "../build/components/layout/DashboardPageBody.vue";
|
|
4
4
|
import type { LayoutHeaderAction } from "../build/components/layout/PageHeaderActionBar.vue";
|
|
5
|
-
import type { PageRecordSource } from "../composables/layout/usePageRecord";
|
|
6
5
|
|
|
7
6
|
interface Props {
|
|
8
7
|
fullWidth?: boolean;
|
|
@@ -14,14 +13,12 @@ interface Props {
|
|
|
14
13
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
15
14
|
headerActions?: LayoutHeaderAction[];
|
|
16
15
|
/**
|
|
17
|
-
* The page query the server
|
|
18
|
-
*
|
|
16
|
+
* The page query the server served the buttons for, set when one declares
|
|
17
|
+
* an `availability` or the page reads a `record` (`headerActionsQuery`).
|
|
19
18
|
*/
|
|
20
19
|
headerActionsQuery?: Record<string, string>;
|
|
21
20
|
/** The route parameters (`:id`) they were resolved for, likewise. */
|
|
22
21
|
headerActionsParams?: Record<string, string>;
|
|
23
|
-
/** The record the page shows, which header buttons may act on (`record`). */
|
|
24
|
-
record?: PageRecordSource;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -34,7 +31,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
34
31
|
headerActions: () => [],
|
|
35
32
|
headerActionsQuery: undefined,
|
|
36
33
|
headerActionsParams: undefined,
|
|
37
|
-
record: undefined,
|
|
38
34
|
});
|
|
39
35
|
</script>
|
|
40
36
|
|
|
@@ -48,7 +44,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
48
44
|
:header-actions="props.headerActions"
|
|
49
45
|
:header-actions-query="props.headerActionsQuery"
|
|
50
46
|
:header-actions-params="props.headerActionsParams"
|
|
51
|
-
:record="props.record"
|
|
52
47
|
>
|
|
53
48
|
<slot />
|
|
54
49
|
</DashboardPageBody>
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import DashboardFrame from "../build/components/layout/DashboardFrame.vue";
|
|
3
3
|
import DashboardPageBody from "../build/components/layout/DashboardPageBody.vue";
|
|
4
4
|
import type { LayoutHeaderAction } from "../build/components/layout/PageHeaderActionBar.vue";
|
|
5
|
-
import type { PageRecordSource } from "../composables/layout/usePageRecord";
|
|
6
5
|
import SettingsShell from "../build/components/pages/settings/shell/SettingsShell.vue";
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
@@ -14,14 +13,12 @@ interface Props {
|
|
|
14
13
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
15
14
|
headerActions?: LayoutHeaderAction[];
|
|
16
15
|
/**
|
|
17
|
-
* The page query the server
|
|
18
|
-
*
|
|
16
|
+
* The page query the server served the buttons for, set when one declares
|
|
17
|
+
* an `availability` or the page reads a `record` (`headerActionsQuery`).
|
|
19
18
|
*/
|
|
20
19
|
headerActionsQuery?: Record<string, string>;
|
|
21
20
|
/** The route parameters (`:id`) they were resolved for, likewise. */
|
|
22
21
|
headerActionsParams?: Record<string, string>;
|
|
23
|
-
/** The record the page shows, which header buttons may act on (`record`). */
|
|
24
|
-
record?: PageRecordSource;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -33,7 +30,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
33
30
|
headerActions: () => [],
|
|
34
31
|
headerActionsQuery: undefined,
|
|
35
32
|
headerActionsParams: undefined,
|
|
36
|
-
record: undefined,
|
|
37
33
|
});
|
|
38
34
|
</script>
|
|
39
35
|
|
|
@@ -48,7 +44,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
48
44
|
:header-actions="props.headerActions"
|
|
49
45
|
:header-actions-query="props.headerActionsQuery"
|
|
50
46
|
:header-actions-params="props.headerActionsParams"
|
|
51
|
-
:record="props.record"
|
|
52
47
|
header-class="pb-5"
|
|
53
48
|
>
|
|
54
49
|
<slot />
|
|
@@ -4,6 +4,7 @@ import { holdPageHeaderActions } from "../composables/layout/usePageHeaderAction
|
|
|
4
4
|
import { useDevReloading } from "../build/composables/dev-reload/useDevReloadHolder";
|
|
5
5
|
import { usePageLiveHost } from "#dms-ui/app/build/composables/live/usePageLiveHost";
|
|
6
6
|
import { usePageDataRefresh } from "#dms-ui/app/build/composables/live/usePageDataRefresh";
|
|
7
|
+
import { usePageRecordDeletion } from "../composables/page/usePageRecordDeletion";
|
|
7
8
|
import { pageValidationRedirect } from "../build/utils/pageValidationRedirect";
|
|
8
9
|
import { resolveComponentChildren } from "#dms-ui/app/utils/componentTree";
|
|
9
10
|
|
|
@@ -251,6 +252,12 @@ usePageLiveHost(
|
|
|
251
252
|
);
|
|
252
253
|
// The page's `useApiData` reads follow its refreshes and its realtime events.
|
|
253
254
|
usePageDataRefresh(realtime);
|
|
255
|
+
// A detail page leaves for its list once its record is deleted.
|
|
256
|
+
usePageRecordDeletion({
|
|
257
|
+
pageLayout: () => pageLayout.value,
|
|
258
|
+
pageTopics: realtime.pageTopics,
|
|
259
|
+
subscribe: realtime.subscribe,
|
|
260
|
+
});
|
|
254
261
|
|
|
255
262
|
function buildPageSetupContext(
|
|
256
263
|
pageInfo: PageInfo,
|
|
@@ -13,14 +13,14 @@ function isHiddenFromNavigation(node: SiteLayoutTree): boolean {
|
|
|
13
13
|
function collectNavigationItems(
|
|
14
14
|
node: SiteLayoutTree,
|
|
15
15
|
ancestors: string[],
|
|
16
|
-
|
|
16
|
+
translation: AppTranslation,
|
|
17
17
|
items: CommandPaletteItem[],
|
|
18
18
|
): void {
|
|
19
19
|
for (const childId of node.childrenOrders) {
|
|
20
20
|
const child = node.children[childId];
|
|
21
21
|
if (!child || isHiddenFromNavigation(child)) continue;
|
|
22
22
|
|
|
23
|
-
const label =
|
|
23
|
+
const label = translation.processI18n(child.displayName);
|
|
24
24
|
const to = buildMenuItemTarget(child);
|
|
25
25
|
// The account entries (Settings, Profile) are the session group's.
|
|
26
26
|
if (to && child.fullSlug && !isAccountMenuRoute(to)) {
|
|
@@ -33,40 +33,40 @@ function collectNavigationItems(
|
|
|
33
33
|
icon: child.icon ?? DEFAULT_PAGE_ICON,
|
|
34
34
|
to,
|
|
35
35
|
[COMMAND_PALETTE_SEARCH_TEXT_KEY]: resolveOptionalI18nKey(
|
|
36
|
-
|
|
36
|
+
translation.t,
|
|
37
37
|
child.description,
|
|
38
38
|
),
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
collectNavigationItems(child, [...ancestors, label],
|
|
41
|
+
collectNavigationItems(child, [...ancestors, label], translation, items);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function buildNavigationGroups(
|
|
46
46
|
tree: SiteLayoutTree | undefined,
|
|
47
|
-
|
|
47
|
+
translation: AppTranslation,
|
|
48
48
|
): CommandPaletteGroup[] {
|
|
49
49
|
if (!tree) return [];
|
|
50
50
|
|
|
51
51
|
const items: CommandPaletteItem[] = [];
|
|
52
|
-
collectNavigationItems(tree, [],
|
|
52
|
+
collectNavigationItems(tree, [], translation, items);
|
|
53
53
|
if (items.length === 0) return [];
|
|
54
54
|
|
|
55
55
|
return [
|
|
56
56
|
{
|
|
57
57
|
id: NAVIGATION_GROUP_ID,
|
|
58
|
-
label:
|
|
58
|
+
label: translation.t("commandPalette.groups.navigation"),
|
|
59
59
|
items,
|
|
60
60
|
},
|
|
61
61
|
];
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export default defineDmsPlugin(() => {
|
|
65
|
-
const
|
|
65
|
+
const translation = getAppTranslation();
|
|
66
66
|
const { siteLayoutTree } = useSiteLayout();
|
|
67
67
|
|
|
68
68
|
const groups = computed<CommandPaletteGroup[]>(() =>
|
|
69
|
-
buildNavigationGroups(siteLayoutTree.value,
|
|
69
|
+
buildNavigationGroups(siteLayoutTree.value, translation),
|
|
70
70
|
);
|
|
71
71
|
|
|
72
72
|
registerCommandPaletteSource({
|
|
@@ -11,15 +11,15 @@ const RECENT_MODULES_GROUP_ID = "recent-modules";
|
|
|
11
11
|
|
|
12
12
|
function buildFavoritesGroup(
|
|
13
13
|
favorites: FavoritePage[],
|
|
14
|
-
|
|
14
|
+
translation: AppTranslation,
|
|
15
15
|
): CommandPaletteGroup | null {
|
|
16
16
|
if (favorites.length === 0) return null;
|
|
17
17
|
|
|
18
18
|
return {
|
|
19
19
|
id: FAVORITES_GROUP_ID,
|
|
20
|
-
label:
|
|
20
|
+
label: translation.t("menu.favorites"),
|
|
21
21
|
items: favorites.map((favorite) => ({
|
|
22
|
-
label:
|
|
22
|
+
label: translation.processI18n(favorite.title),
|
|
23
23
|
icon: favorite.icon ?? DEFAULT_PAGE_ICON,
|
|
24
24
|
to: favorite.path,
|
|
25
25
|
})),
|
|
@@ -28,15 +28,15 @@ function buildFavoritesGroup(
|
|
|
28
28
|
|
|
29
29
|
function buildRecentModulesGroup(
|
|
30
30
|
modules: RecentModule[],
|
|
31
|
-
|
|
31
|
+
translation: AppTranslation,
|
|
32
32
|
): CommandPaletteGroup | null {
|
|
33
33
|
if (modules.length === 0) return null;
|
|
34
34
|
|
|
35
35
|
return {
|
|
36
36
|
id: RECENT_MODULES_GROUP_ID,
|
|
37
|
-
label:
|
|
37
|
+
label: translation.t("commandPalette.groups.recentModules"),
|
|
38
38
|
items: modules.map((module) => ({
|
|
39
|
-
label:
|
|
39
|
+
label: translation.processI18n(module.title),
|
|
40
40
|
icon: module.icon,
|
|
41
41
|
to: module.to,
|
|
42
42
|
})),
|
|
@@ -44,17 +44,17 @@ function buildRecentModulesGroup(
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export default defineDmsPlugin(() => {
|
|
47
|
-
const
|
|
47
|
+
const translation = getAppTranslation();
|
|
48
48
|
const { sortedFavorites } = useFavoritePages();
|
|
49
49
|
const { entries } = useModuleHistory();
|
|
50
50
|
const { modules, siteLayoutTree } = useSiteLayout();
|
|
51
51
|
|
|
52
52
|
const groups = computed<CommandPaletteGroup[]>(() =>
|
|
53
53
|
[
|
|
54
|
-
buildFavoritesGroup(sortedFavorites.value,
|
|
54
|
+
buildFavoritesGroup(sortedFavorites.value, translation),
|
|
55
55
|
buildRecentModulesGroup(
|
|
56
56
|
recentModules(entries.value, modules.value, siteLayoutTree.value),
|
|
57
|
-
|
|
57
|
+
translation,
|
|
58
58
|
),
|
|
59
59
|
].filter((group): group is CommandPaletteGroup => group !== null),
|
|
60
60
|
);
|
|
@@ -6,10 +6,10 @@ const QUICK_ACTIONS_GROUP_PREFIX = "quick-actions";
|
|
|
6
6
|
|
|
7
7
|
function buildActionItem(
|
|
8
8
|
action: QuickActionInfo,
|
|
9
|
-
|
|
9
|
+
translation: AppTranslation,
|
|
10
10
|
): CommandPaletteItem {
|
|
11
11
|
return {
|
|
12
|
-
label:
|
|
12
|
+
label: translation.processI18n(action.displayName),
|
|
13
13
|
icon: action.icon,
|
|
14
14
|
onSelect: () => {
|
|
15
15
|
void dispatchQuickActionTarget(action.target);
|
|
@@ -19,21 +19,21 @@ function buildActionItem(
|
|
|
19
19
|
|
|
20
20
|
function buildQuickActionGroups(
|
|
21
21
|
categoryGroups: QuickActionCategoryGroup[],
|
|
22
|
-
|
|
22
|
+
translation: AppTranslation,
|
|
23
23
|
): CommandPaletteGroup[] {
|
|
24
24
|
return categoryGroups.map((group) => ({
|
|
25
25
|
id: `${QUICK_ACTIONS_GROUP_PREFIX}-${group.category.id}`,
|
|
26
|
-
label:
|
|
27
|
-
items: group.actions.map((action) => buildActionItem(action,
|
|
26
|
+
label: translation.processI18n(group.category.displayName),
|
|
27
|
+
items: group.actions.map((action) => buildActionItem(action, translation)),
|
|
28
28
|
}));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export default defineDmsPlugin(() => {
|
|
32
|
-
const
|
|
32
|
+
const translation = getAppTranslation();
|
|
33
33
|
const { categoryGroups } = useQuickActions();
|
|
34
34
|
|
|
35
35
|
const groups = computed<CommandPaletteGroup[]>(() =>
|
|
36
|
-
buildQuickActionGroups(categoryGroups.value,
|
|
36
|
+
buildQuickActionGroups(categoryGroups.value, translation),
|
|
37
37
|
);
|
|
38
38
|
|
|
39
39
|
registerCommandPaletteSource({
|
|
@@ -22,16 +22,17 @@ function uniqueByCode<T extends LocaleOption>(locales: T[]): T[] {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function buildAccountItems(
|
|
25
|
+
function buildAccountItems(t: AppTranslation["t"]): CommandPaletteItem[] {
|
|
26
26
|
return ACCOUNT_MENU_ENTRIES.map((entry) => ({
|
|
27
|
-
label:
|
|
27
|
+
label: t(entry.labelKey),
|
|
28
28
|
icon: entry.icon,
|
|
29
29
|
to: entry.to,
|
|
30
30
|
}));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export default defineDmsPlugin(() => {
|
|
34
|
-
const { i18n
|
|
34
|
+
const { $i18n: i18n } = useDmsApp();
|
|
35
|
+
const { t } = getAppTranslation();
|
|
35
36
|
const toast = useToast();
|
|
36
37
|
const { logout } = useLogout();
|
|
37
38
|
const { changeLanguage } = useUserLanguage();
|
|
@@ -49,10 +50,10 @@ export default defineDmsPlugin(() => {
|
|
|
49
50
|
async function copyCurrentUrl(): Promise<void> {
|
|
50
51
|
try {
|
|
51
52
|
await copy(window.location.href);
|
|
52
|
-
toast.add({ title:
|
|
53
|
+
toast.add({ title: t("commandPalette.session.urlCopied") });
|
|
53
54
|
} catch {
|
|
54
55
|
toast.add({
|
|
55
|
-
title:
|
|
56
|
+
title: t("commandPalette.session.copyFailed"),
|
|
56
57
|
color: "error",
|
|
57
58
|
});
|
|
58
59
|
}
|
|
@@ -61,23 +62,23 @@ export default defineDmsPlugin(() => {
|
|
|
61
62
|
function buildSessionGroup(): CommandPaletteGroup {
|
|
62
63
|
return {
|
|
63
64
|
id: SESSION_GROUP_ID,
|
|
64
|
-
label:
|
|
65
|
+
label: t("commandPalette.groups.session"),
|
|
65
66
|
items: [
|
|
66
|
-
...buildAccountItems(
|
|
67
|
+
...buildAccountItems(t),
|
|
67
68
|
{
|
|
68
|
-
label:
|
|
69
|
+
label: t("commandPalette.session.changeLanguage"),
|
|
69
70
|
icon: LANGUAGE_ICON,
|
|
70
71
|
children: localeItems(),
|
|
71
72
|
},
|
|
72
73
|
{
|
|
73
|
-
label:
|
|
74
|
+
label: t("commandPalette.session.copyUrl"),
|
|
74
75
|
icon: COPY_URL_ICON,
|
|
75
76
|
onSelect: () => {
|
|
76
77
|
void copyCurrentUrl();
|
|
77
78
|
},
|
|
78
79
|
},
|
|
79
80
|
{
|
|
80
|
-
label:
|
|
81
|
+
label: t("button.logout"),
|
|
81
82
|
icon: LOGOUT_ICON,
|
|
82
83
|
onSelect: () => {
|
|
83
84
|
void logout();
|
|
@@ -330,14 +330,15 @@ const tabsTheme = {
|
|
|
330
330
|
trigger: "data-[state=active]:font-semibold",
|
|
331
331
|
// Counts as mono chips, tinted with the accent on the active tab.
|
|
332
332
|
trailingBadge:
|
|
333
|
-
"font-mono text-[10.5px] h-4 px-[5px] rounded-
|
|
333
|
+
"font-mono text-[10.5px] h-4 px-[5px] rounded-(--dms-radius-tab-badge) ring-0 bg-elevated text-dimmed group-data-[state=active]:bg-(--dms-accent-tint) group-data-[state=active]:text-primary",
|
|
334
334
|
},
|
|
335
335
|
variants: {
|
|
336
336
|
variant: {
|
|
337
337
|
pill: {
|
|
338
|
-
list: "bg-(--dms-bg-muted) ring ring-inset ring-default p-[3px] gap-0.5 rounded-
|
|
339
|
-
indicator:
|
|
340
|
-
|
|
338
|
+
list: "bg-(--dms-bg-muted) ring ring-inset ring-default p-[3px] gap-0.5 rounded-(--dms-radius-tab-list)",
|
|
339
|
+
indicator:
|
|
340
|
+
"ring ring-accented shadow-(--shadow-sm) rounded-(--dms-radius-tab)",
|
|
341
|
+
trigger: "rounded-(--dms-radius-tab) before:rounded-(--dms-radius-tab)",
|
|
341
342
|
},
|
|
342
343
|
},
|
|
343
344
|
// A vertical list sits at the top of its panel, not centred beside it.
|
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
"no_content_title": "No content available",
|
|
101
101
|
"no_content_message": "This page has not been configured yet. Please contact the administrator."
|
|
102
102
|
},
|
|
103
|
+
"record_deleted": {
|
|
104
|
+
"title": "Deleted",
|
|
105
|
+
"description": "What this page showed was deleted. It is no longer available."
|
|
106
|
+
},
|
|
103
107
|
"no_visible_blocks": {
|
|
104
108
|
"title": "Nothing to show for you here",
|
|
105
109
|
"description": "Your role doesn’t let you see any part of this page. Ask a workspace owner for access."
|
|
@@ -826,7 +830,7 @@
|
|
|
826
830
|
},
|
|
827
831
|
"disabled": {
|
|
828
832
|
"title": "The store is read-only here",
|
|
829
|
-
"description": "Modules are installed from development, which writes them to antelope.config.ts; this platform loads what that file declares.
|
|
833
|
+
"description": "Modules are installed from development, which writes them to antelope.config.ts; this platform loads what that file declares. Change them from development."
|
|
830
834
|
},
|
|
831
835
|
"load_error": "The module store could not be loaded",
|
|
832
836
|
"search_placeholder": "Search the store",
|
|
@@ -1040,9 +1044,7 @@
|
|
|
1040
1044
|
"footer": {
|
|
1041
1045
|
"documentation": "Documentation",
|
|
1042
1046
|
"website": "Website",
|
|
1043
|
-
"language": "Language"
|
|
1044
|
-
"github_star": "Star",
|
|
1045
|
-
"github_star_label": "Star AntelopeJS DMS on GitHub"
|
|
1047
|
+
"language": "Language"
|
|
1046
1048
|
}
|
|
1047
1049
|
},
|
|
1048
1050
|
"error": {
|
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
"no_content_title": "Aucun contenu disponible",
|
|
101
101
|
"no_content_message": "Cette page n'a pas encore été configurée. Veuillez contacter l'administrateur."
|
|
102
102
|
},
|
|
103
|
+
"record_deleted": {
|
|
104
|
+
"title": "Supprimé",
|
|
105
|
+
"description": "Ce que montrait cette page a été supprimé. Il n’est plus disponible."
|
|
106
|
+
},
|
|
103
107
|
"no_visible_blocks": {
|
|
104
108
|
"title": "Rien à afficher pour vous ici",
|
|
105
109
|
"description": "Votre rôle ne vous permet de voir aucune partie de cette page. Demandez l’accès à un propriétaire de l’espace."
|
|
@@ -830,7 +834,7 @@
|
|
|
830
834
|
},
|
|
831
835
|
"disabled": {
|
|
832
836
|
"title": "Le store est en lecture seule ici",
|
|
833
|
-
"description": "Les modules s’installent depuis le développement, qui les écrit dans antelope.config.ts ; cette plateforme charge ce que déclare ce fichier.
|
|
837
|
+
"description": "Les modules s’installent depuis le développement, qui les écrit dans antelope.config.ts ; cette plateforme charge ce que déclare ce fichier. Modifiez-les depuis le développement."
|
|
834
838
|
},
|
|
835
839
|
"load_error": "Le store de modules n’a pas pu être chargé",
|
|
836
840
|
"search_placeholder": "Rechercher dans le store",
|
|
@@ -1044,9 +1048,7 @@
|
|
|
1044
1048
|
"footer": {
|
|
1045
1049
|
"documentation": "Documentation",
|
|
1046
1050
|
"website": "Site web",
|
|
1047
|
-
"language": "Langue"
|
|
1048
|
-
"github_star": "Star",
|
|
1049
|
-
"github_star_label": "Mettre une étoile à AntelopeJS DMS sur GitHub"
|
|
1051
|
+
"language": "Langue"
|
|
1050
1052
|
}
|
|
1051
1053
|
},
|
|
1052
1054
|
"error": {
|
|
@@ -27,8 +27,12 @@ const { t } = useI18n();
|
|
|
27
27
|
const VARIANTS: Record<FormLoadFailure, EmptyStateVariant> = {
|
|
28
28
|
forbidden: "no-access",
|
|
29
29
|
missing: "no-data",
|
|
30
|
+
not_found: "no-result",
|
|
30
31
|
error: "error",
|
|
31
32
|
};
|
|
33
|
+
// A record the address names none of, or one that does not exist, is not
|
|
34
|
+
// read again by trying.
|
|
35
|
+
const RETRIES = new Set<FormLoadFailure>(["forbidden", "error"]);
|
|
32
36
|
</script>
|
|
33
37
|
|
|
34
38
|
<template>
|
|
@@ -38,7 +42,7 @@ const VARIANTS: Record<FormLoadFailure, EmptyStateVariant> = {
|
|
|
38
42
|
:title="t(`dms.form.load_error.${props.failure}_title`)"
|
|
39
43
|
:description="t(`dms.form.load_error.${props.failure}_description`)"
|
|
40
44
|
>
|
|
41
|
-
<template v-if="props.failure
|
|
45
|
+
<template v-if="RETRIES.has(props.failure)" #actions>
|
|
42
46
|
<UButton
|
|
43
47
|
:label="t('dms.form.load_error.retry')"
|
|
44
48
|
icon="i-ph-arrows-clockwise"
|
|
@@ -207,7 +207,7 @@ const { labelsOf: relationLabels } = useRelationFilterLabels(() =>
|
|
|
207
207
|
const displayValue = (filter: TableFilter): string => {
|
|
208
208
|
const source = relationSource(filter);
|
|
209
209
|
const names = source ? relationLabels(source) : [];
|
|
210
|
-
if (names.length) return names.join(", ");
|
|
210
|
+
if (names.length) return names.map((name) => name.label).join(", ");
|
|
211
211
|
const labels = itemLabels(filter);
|
|
212
212
|
if (labels) return labels;
|
|
213
213
|
const formatted = formatFilterValue(
|
|
@@ -219,12 +219,22 @@ const displayValue = (filter: TableFilter): string => {
|
|
|
219
219
|
return formatted || EMPTY_FILTER_VALUE;
|
|
220
220
|
};
|
|
221
221
|
|
|
222
|
+
// The ids of a relation filter its route did not answer, named on hover.
|
|
223
|
+
const notFoundIds = (filter: TableFilter): string => {
|
|
224
|
+
const source = relationSource(filter);
|
|
225
|
+
if (!source) return "";
|
|
226
|
+
return relationLabels(source)
|
|
227
|
+
.flatMap((name) => (name.notFoundId ? [name.notFoundId] : []))
|
|
228
|
+
.join(", ");
|
|
229
|
+
};
|
|
230
|
+
|
|
222
231
|
// The whole filter in words, for a chip shrunk to an ellipsis.
|
|
223
232
|
const chipTitle = (filter: TableFilter, label?: string): string =>
|
|
224
233
|
[
|
|
225
234
|
label ?? getColumn(filter.accessorKey)?.label,
|
|
226
235
|
t(`dms.table.compare_mode.${filter.mode}`),
|
|
227
236
|
displayValue(filter),
|
|
237
|
+
notFoundIds(filter),
|
|
228
238
|
]
|
|
229
239
|
.filter(Boolean)
|
|
230
240
|
.join(" · ");
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
readTranslationKey,
|
|
3
3
|
type I18nTranslate,
|
|
4
4
|
} from "#dms-core/app/composables/translation/useTranslation";
|
|
5
|
+
import { describeFieldErrors } from "#dms-core/app/composables/useFieldErrors";
|
|
5
6
|
|
|
6
7
|
/** Shown when the request never reached the server (offline, DNS, CORS…). */
|
|
7
8
|
export const ACTION_ERROR_NETWORK_KEY = "error.network.description";
|
|
@@ -90,11 +91,14 @@ const translateKey = (translate: I18nTranslate, key: string) =>
|
|
|
90
91
|
* threw: the server's own message when it is user-facing (an i18n key, or
|
|
91
92
|
* plain text on a 4xx), else a translated fallback: the request never got
|
|
92
93
|
* through, it was refused for lack of permission, or something went wrong.
|
|
93
|
-
* Never a stack trace, raw JSON or an ofetch error line.
|
|
94
|
+
* Never a stack trace, raw JSON or an ofetch error line. A body the data API
|
|
95
|
+
* refused for some fields names them by `labelOf` ("Loss reason is
|
|
96
|
+
* required."): a table view's write passes its column labels.
|
|
94
97
|
*/
|
|
95
98
|
export function resolveActionError(
|
|
96
99
|
error: unknown,
|
|
97
100
|
translate: I18nTranslate,
|
|
101
|
+
labelOf: (field: string) => string | undefined = () => undefined,
|
|
98
102
|
): string {
|
|
99
103
|
if (isFetchError(error)) {
|
|
100
104
|
const status = statusOf(error);
|
|
@@ -121,6 +125,7 @@ export function resolveActionError(
|
|
|
121
125
|
);
|
|
122
126
|
}
|
|
123
127
|
return (
|
|
128
|
+
describeFieldErrors(error, translate, labelOf) ??
|
|
124
129
|
userFacingText(raw, translate) ??
|
|
125
130
|
translateKey(translate, ACTION_ERROR_GENERIC_KEY)
|
|
126
131
|
);
|