@antelopejs/dms 0.10.0 → 0.11.1
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 +113 -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/implementations/dms/page.js +45 -16
- package/dist/implementations/dms/page.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/app/utils/menu.ts +66 -70
- 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/DashboardSidebar.vue +3 -11
- 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/components/navigation/NavigationMenu.vue +18 -3
- 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 +8 -11
- 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
|
@@ -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
|
);
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { DataTypeFormatter } from "#dms-core/app/composables/data-types/useDataType";
|
|
2
|
-
import {
|
|
3
|
-
getPluginI18n,
|
|
4
|
-
resolveI18nKey,
|
|
5
|
-
} from "../../../../../dms-core/app/composables/translation/useTranslation";
|
|
2
|
+
import { getAppTranslation } from "../../../../../dms-core/app/composables/translation/useTranslation";
|
|
6
3
|
|
|
7
4
|
/** A formatter writing a number for a locale, with Intl's options. */
|
|
8
5
|
type NumberFormatter = (
|
|
@@ -41,7 +38,7 @@ export function withNumberSuffix(format: NumberFormatter): DataTypeFormatter {
|
|
|
41
38
|
if (!suffix || typeof value !== "number" || typeof formatted !== "string") {
|
|
42
39
|
return formatted;
|
|
43
40
|
}
|
|
44
|
-
const unit =
|
|
41
|
+
const unit = getAppTranslation().processI18n(suffix);
|
|
45
42
|
return `${formatted}${UNIT_SEPARATOR}${unit}`;
|
|
46
43
|
};
|
|
47
44
|
}
|
|
@@ -7,7 +7,7 @@ import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
|
7
7
|
import ULink from "@nuxt/ui/components/Link.vue";
|
|
8
8
|
import { buildRelationBadges } from "./relationBadges";
|
|
9
9
|
import { mapRelationBeforeState, sameRelationValue } from "./relationValue";
|
|
10
|
-
import { relationSelectSource } from "./relationRows";
|
|
10
|
+
import { NotFoundRelation, relationSelectSource } from "./relationRows";
|
|
11
11
|
import {
|
|
12
12
|
isSet,
|
|
13
13
|
readRowField,
|
|
@@ -338,12 +338,31 @@ function renderRelationBadge(label: string, title?: string) {
|
|
|
338
338
|
});
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// An id its `select` route did not answer: "Not found", the id on hover.
|
|
342
|
+
function renderNotFoundRelation(relation: NotFoundRelation) {
|
|
343
|
+
const { t } = getAppTranslation();
|
|
344
|
+
return h(
|
|
345
|
+
"span",
|
|
346
|
+
{ class: "text-muted", title: relation.id },
|
|
347
|
+
t("dms.form.relation.not_found"),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
341
351
|
function renderRelationBadges(values: unknown[], labelKey: string) {
|
|
342
|
-
const { processI18n } = getAppTranslation();
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
352
|
+
const { processI18n, t } = getAppTranslation();
|
|
353
|
+
const notFound = values.filter(
|
|
354
|
+
(item): item is NotFoundRelation => item instanceof NotFoundRelation,
|
|
355
|
+
);
|
|
356
|
+
const { visible, hidden } = buildRelationBadges(
|
|
357
|
+
values.filter((item) => !(item instanceof NotFoundRelation)),
|
|
358
|
+
labelKey,
|
|
346
359
|
);
|
|
360
|
+
const badges = [
|
|
361
|
+
...visible.map((label) => renderRelationBadge(processI18n(label))),
|
|
362
|
+
...notFound.map((relation) =>
|
|
363
|
+
renderRelationBadge(t("dms.form.relation.not_found"), relation.id),
|
|
364
|
+
),
|
|
365
|
+
];
|
|
347
366
|
if (hidden.length > 0) {
|
|
348
367
|
const hiddenLabels = hidden.map((label) => processI18n(label)).join(", ");
|
|
349
368
|
badges.push(renderRelationBadge(`+${hidden.length}`, hiddenLabels));
|
|
@@ -359,6 +378,7 @@ function renderRelation(value: unknown, options: unknown) {
|
|
|
359
378
|
return renderRelationBadges(value, labelKey);
|
|
360
379
|
}
|
|
361
380
|
|
|
381
|
+
if (value instanceof NotFoundRelation) return renderNotFoundRelation(value);
|
|
362
382
|
if (!value || !isObject(value)) return value;
|
|
363
383
|
|
|
364
384
|
const valueObject = value as Record<string, string>;
|
|
@@ -981,6 +1001,7 @@ const registerCascaderRelationType = (
|
|
|
981
1001
|
},
|
|
982
1002
|
beforeStateMapper: mapRelationBeforeState,
|
|
983
1003
|
isSameValue: sameRelationValue,
|
|
1004
|
+
selectSource: relationSelectSource,
|
|
984
1005
|
});
|
|
985
1006
|
};
|
|
986
1007
|
|
|
@@ -45,12 +45,33 @@ export function relationRowLabel(
|
|
|
45
45
|
return row[source.keyMapping?.label || RELATION_LABEL_KEY];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* An id a relation's `select` route did not answer: a deleted row, or one out
|
|
50
|
+
* of the reader's reach — it cannot tell which, so it is shown as "Not
|
|
51
|
+
* found", never as "Deleted", with the id itself in its tooltip.
|
|
52
|
+
*/
|
|
53
|
+
export class NotFoundRelation {
|
|
54
|
+
constructor(readonly id: string) {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The ids of a read that its rows do not answer. */
|
|
58
|
+
export function notFoundRelationIds(
|
|
59
|
+
source: DataTypeSelectSource,
|
|
60
|
+
ids: string[],
|
|
61
|
+
rows: RelationRow[],
|
|
62
|
+
): string[] {
|
|
63
|
+
const answered = new Set(
|
|
64
|
+
rows.map((row) => String(relationRowValue(source, row))),
|
|
65
|
+
);
|
|
66
|
+
return ids.filter((id) => !answered.has(id));
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
/**
|
|
49
70
|
* Reads the rows of a relation's ids from its `select` route, narrowed with
|
|
50
71
|
* `?ids=` to those rows alone (matched on the mapped value key), whatever the
|
|
51
72
|
* route's page size. The one way the picker, the filter chips and the
|
|
52
73
|
* read-only lists name the ids they hold. A failed read answers no rows: the
|
|
53
|
-
* ids then show as
|
|
74
|
+
* ids then show as not found, as a row out of the reader's reach does.
|
|
54
75
|
*/
|
|
55
76
|
export function useRelationRows() {
|
|
56
77
|
const { $authFetch } = useAuthFetch();
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { type Ref, ref, watch } from "vue";
|
|
2
2
|
import { apiErrorStatus } from "#dms-core/app/composables/useFieldErrors";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
HTTP_FORBIDDEN,
|
|
5
|
+
HTTP_NOT_FOUND,
|
|
6
|
+
} from "#dms-core/app/utils/http-status";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Why a form could not load the record it edits: `forbidden` when the server
|
|
7
10
|
* refused the caller the read (a role that may edit rows but not view them),
|
|
8
11
|
* `missing` when its address lacks the record's id (a page reached without
|
|
9
|
-
* its `?id=`), `
|
|
10
|
-
* a
|
|
12
|
+
* its `?id=`), `not_found` when the server answered 404 (the record was
|
|
13
|
+
* deleted, or never existed), `error` for anything else (a network failure,
|
|
14
|
+
* a server error).
|
|
11
15
|
*/
|
|
12
|
-
export type FormLoadFailure = "forbidden" | "missing" | "error";
|
|
16
|
+
export type FormLoadFailure = "forbidden" | "missing" | "not_found" | "error";
|
|
13
17
|
|
|
14
18
|
/** What a form knows of the record it is about, as far as `missing` goes. */
|
|
15
19
|
export interface FormRecordAddress {
|
|
@@ -51,7 +55,9 @@ export interface FormRecordLoad {
|
|
|
51
55
|
/** The failure a load error stands for; `null` when there is none. */
|
|
52
56
|
export function formLoadFailure(error: unknown): FormLoadFailure | null {
|
|
53
57
|
if (!error) return null;
|
|
54
|
-
|
|
58
|
+
const status = apiErrorStatus(error);
|
|
59
|
+
if (status === HTTP_FORBIDDEN) return "forbidden";
|
|
60
|
+
return status === HTTP_NOT_FOUND ? "not_found" : "error";
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
/** How a form takes the answers of its record's later reads. */
|
|
@@ -22,6 +22,11 @@ export interface FormValidationField {
|
|
|
22
22
|
type?: string;
|
|
23
23
|
/** Holds one value per content language. */
|
|
24
24
|
localized?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The language a localized field falls back to: required as soon as the
|
|
27
|
+
* field holds any language, and where its required error goes.
|
|
28
|
+
*/
|
|
29
|
+
fallbackLocale?: string;
|
|
25
30
|
/** A box to tick: unticked, it is missing. */
|
|
26
31
|
acceptance?: boolean;
|
|
27
32
|
/**
|
|
@@ -63,6 +68,32 @@ function issueField(issue: { path: PropertyKey[] }): PropertyKey | undefined {
|
|
|
63
68
|
return issue.path[0];
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/** Whether a localized value fills some language but not its fallback one. */
|
|
72
|
+
function lacksFallbackLocale(
|
|
73
|
+
field: FormValidationField,
|
|
74
|
+
value: unknown,
|
|
75
|
+
): boolean {
|
|
76
|
+
if (!field.localized || !field.fallbackLocale) return false;
|
|
77
|
+
if (typeof value !== "object" || value === null) return false;
|
|
78
|
+
const languages = value as Record<string, unknown>;
|
|
79
|
+
const isAnyFilled = Object.values(languages).some(
|
|
80
|
+
(text) => !isBlankValue(text, field.type),
|
|
81
|
+
);
|
|
82
|
+
return (
|
|
83
|
+
isAnyFilled && isBlankValue(languages[field.fallbackLocale], field.type)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Where a localized field's required error goes: the language to fill. */
|
|
88
|
+
function requiredPath(
|
|
89
|
+
field: FormValidationField,
|
|
90
|
+
locale: string | undefined,
|
|
91
|
+
): PropertyKey[] {
|
|
92
|
+
if (!field.localized) return [field.id];
|
|
93
|
+
const language = field.fallbackLocale ?? locale;
|
|
94
|
+
return language ? [field.id, language] : [field.id];
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
/** An issue saying the value is not of a branch's type at all. */
|
|
67
98
|
function isTypeMismatch(issue: z.ZodIssue, path: PropertyKey[]): boolean {
|
|
68
99
|
return issue.code === "invalid_type" && issue.path.length === path.length;
|
|
@@ -117,6 +148,10 @@ export async function validateFormState(
|
|
|
117
148
|
for (const field of context.fields) {
|
|
118
149
|
const value = state[field.id];
|
|
119
150
|
if (reported.has(field.id)) continue;
|
|
151
|
+
if (lacksFallbackLocale(field, value)) {
|
|
152
|
+
missing.add(field.id);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
120
155
|
const isBlank = field.acceptance
|
|
121
156
|
? value !== true
|
|
122
157
|
: isBlankValue(value, field.type);
|
|
@@ -159,10 +194,7 @@ export async function validateFormState(
|
|
|
159
194
|
if (!missing.has(field.id)) continue;
|
|
160
195
|
issues.push({
|
|
161
196
|
message: context.requiredMessage,
|
|
162
|
-
path:
|
|
163
|
-
field.localized && context.locale
|
|
164
|
-
? [field.id, context.locale]
|
|
165
|
-
: [field.id],
|
|
197
|
+
path: requiredPath(field, context.locale),
|
|
166
198
|
});
|
|
167
199
|
}
|
|
168
200
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
DataTypeSelectSource,
|
|
4
4
|
} from "#dms-core/app/composables/data-types/useDataType";
|
|
5
5
|
import type { RenderableColumn } from "../data-types/useColumnValueRenderer";
|
|
6
|
+
import { NotFoundRelation } from "../data-types/relationRows";
|
|
6
7
|
|
|
7
8
|
/** A column of a read-only list: a field of its rows. */
|
|
8
9
|
export interface RepeaterTableColumn {
|
|
@@ -49,17 +50,29 @@ export function relationLabelSource(
|
|
|
49
50
|
return source ? { ...source, multiple: !!options?.multiple } : undefined;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** The rows a relation column's read answered, by value, and the ids it asked. */
|
|
54
|
+
export interface RelationColumnRows {
|
|
55
|
+
rowsByValue: ReadonlyMap<unknown, Record<string, unknown>>;
|
|
56
|
+
readIds: ReadonlySet<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
/**
|
|
53
60
|
* A relation value as its formatter draws it: the related row in place of
|
|
54
|
-
* each id it holds. An id
|
|
55
|
-
*
|
|
61
|
+
* each id it holds. An id the read asked for and no row answers (deleted, out
|
|
62
|
+
* of reach) is not found; one not asked yet stays as it is, as does a value
|
|
63
|
+
* the server already joined.
|
|
56
64
|
*/
|
|
57
65
|
export function joinRelationValue(
|
|
58
66
|
value: unknown,
|
|
59
|
-
rowsByValue:
|
|
67
|
+
{ rowsByValue, readIds }: RelationColumnRows,
|
|
60
68
|
multiple: boolean,
|
|
61
69
|
): unknown {
|
|
62
|
-
const join = (item: unknown) =>
|
|
70
|
+
const join = (item: unknown) => {
|
|
71
|
+
const row = rowsByValue.get(item);
|
|
72
|
+
if (row) return row;
|
|
73
|
+
const id = String(item);
|
|
74
|
+
return readIds.has(id) ? new NotFoundRelation(id) : item;
|
|
75
|
+
};
|
|
63
76
|
if (multiple && Array.isArray(value)) return value.map(join);
|
|
64
77
|
return join(value);
|
|
65
78
|
}
|