@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
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { nextTick, reactive } from "vue";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type I18nTranslate,
|
|
4
|
+
resolveApiMessage,
|
|
5
|
+
} from "./translation/useTranslation";
|
|
3
6
|
import { resolveApiErrorMessage } from "./useApiError";
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -116,24 +119,23 @@ const FIELD_ERROR_KEYS = {
|
|
|
116
119
|
/** Issue types whose size is a count (characters, items), not a value. */
|
|
117
120
|
const COUNTED_TYPES = new Set(["string", "array", "set"]);
|
|
118
121
|
|
|
119
|
-
// The data API (`@antelopejs/interface-data-api`)
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
// The codes the data API (`@antelopejs/interface-data-api`) refuses a body
|
|
123
|
+
// with, `{ code, fields }`: the message each field shows.
|
|
124
|
+
const DATA_API_CODES: Readonly<Record<string, string>> = {
|
|
125
|
+
missing_mandatory_fields: FIELD_ERROR_KEYS.required,
|
|
126
|
+
invalid_field_types: FIELD_ERROR_KEYS.invalid,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// The same, naming the field: "Loss reason is required".
|
|
130
|
+
const NAMED_FIELD_ERROR_KEYS: Readonly<Record<string, string>> = {
|
|
131
|
+
[FIELD_ERROR_KEYS.required]: "dms.field_errors.named_required",
|
|
132
|
+
[FIELD_ERROR_KEYS.invalid]: "dms.field_errors.named_invalid",
|
|
133
|
+
};
|
|
131
134
|
|
|
132
135
|
// `name: String must contain…`: one validation issue prefixed with its field.
|
|
133
136
|
const FIELD_PREFIXED_MESSAGE = /^([\w.]+):\s+(.+)$/s;
|
|
134
137
|
// `error.invalid_2fa_code`, `page.settings.roles.error.name_taken`.
|
|
135
138
|
const BARE_I18N_KEY = /^[a-z0-9_]+(\.[a-z0-9_]+)+$/i;
|
|
136
|
-
const LIST_SEPARATOR = /\s*,\s*/;
|
|
137
139
|
|
|
138
140
|
/** The HTTP status of a fetch error; undefined for a network failure. */
|
|
139
141
|
export function apiErrorStatus(error: unknown): number | undefined {
|
|
@@ -265,16 +267,19 @@ function fromFieldObject(body: Record<string, unknown>): ApiFieldError[] {
|
|
|
265
267
|
];
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
/** `{ code, fields }`, the data API's refusal of a body; undefined otherwise. */
|
|
271
|
+
function fromCodedFields(
|
|
272
|
+
body: Record<string, unknown>,
|
|
273
|
+
): ApiFieldError[] | undefined {
|
|
274
|
+
const { code, fields } = body;
|
|
275
|
+
const message = typeof code === "string" ? DATA_API_CODES[code] : undefined;
|
|
276
|
+
if (!message || !Array.isArray(fields)) return undefined;
|
|
277
|
+
return fields
|
|
278
|
+
.filter((field): field is string => typeof field === "string")
|
|
279
|
+
.map((field) => ({ field, message }));
|
|
280
|
+
}
|
|
281
|
+
|
|
268
282
|
function fromText(text: string): ApiFieldError[] {
|
|
269
|
-
for (const { pattern, message } of DATA_API_MESSAGES) {
|
|
270
|
-
const match = pattern.exec(text);
|
|
271
|
-
if (match) {
|
|
272
|
-
return match[1]!
|
|
273
|
-
.split(LIST_SEPARATOR)
|
|
274
|
-
.filter(Boolean)
|
|
275
|
-
.map((field) => ({ field, message }));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
283
|
const prefixed = FIELD_PREFIXED_MESSAGE.exec(text);
|
|
279
284
|
if (prefixed) {
|
|
280
285
|
const [, field, message] = prefixed;
|
|
@@ -320,7 +325,7 @@ function fromBody(body: unknown): ApiFieldError[] {
|
|
|
320
325
|
const record = body as Record<string, unknown>;
|
|
321
326
|
if (isIssueList(record.issues)) return fromIssues(record.issues);
|
|
322
327
|
if (isIssueList(record.errors)) return fromIssues(record.errors);
|
|
323
|
-
return fromFieldObject(record);
|
|
328
|
+
return fromCodedFields(record) ?? fromFieldObject(record);
|
|
324
329
|
}
|
|
325
330
|
|
|
326
331
|
function isClientError(status: number | undefined): status is number {
|
|
@@ -337,8 +342,8 @@ function isClientError(status: number | undefined): status is number {
|
|
|
337
342
|
*
|
|
338
343
|
* Read, in order: a code the form maps to a field (`codes`, for any 4xx), a
|
|
339
344
|
* zod issue list (what `assertValidation` answers), a `{ field, message }`
|
|
340
|
-
* body (with optional `values` and `params`), the data API's
|
|
341
|
-
*
|
|
345
|
+
* body (with optional `values` and `params`), the data API's
|
|
346
|
+
* `{ code, fields }` and a `field: message` text. A network failure, a 403, a 429
|
|
342
347
|
* or a 5xx never resolves to a field.
|
|
343
348
|
*
|
|
344
349
|
* @returns The field errors, one per field (the first issue wins), and
|
|
@@ -371,6 +376,36 @@ export function resolveFieldErrors<F extends string>(
|
|
|
371
376
|
return { fields, hasUnmatched: hasUnmatched || fields.length === 0 };
|
|
372
377
|
}
|
|
373
378
|
|
|
379
|
+
/**
|
|
380
|
+
* The data API's refusal of a body (`{ code, fields }`) as one sentence per
|
|
381
|
+
* field naming it, translated: "Loss reason is required." Undefined for any
|
|
382
|
+
* other error. `labelOf` gives a field's label, translated; the field's id
|
|
383
|
+
* stands in for one it does not know.
|
|
384
|
+
*/
|
|
385
|
+
export function describeFieldErrors(
|
|
386
|
+
error: unknown,
|
|
387
|
+
translate: I18nTranslate,
|
|
388
|
+
labelOf: (field: string) => string | undefined,
|
|
389
|
+
): string | undefined {
|
|
390
|
+
const status = apiErrorStatus(error);
|
|
391
|
+
if (status === undefined || !VALIDATION_STATUSES.has(status)) {
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
const body = bodyOf(error);
|
|
395
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const entries = fromCodedFields(body as Record<string, unknown>);
|
|
399
|
+
if (!entries?.length) return undefined;
|
|
400
|
+
return entries
|
|
401
|
+
.map(({ field, message }) =>
|
|
402
|
+
translate(NAMED_FIELD_ERROR_KEYS[message]!, {
|
|
403
|
+
field: labelOf(field) ?? field,
|
|
404
|
+
}),
|
|
405
|
+
)
|
|
406
|
+
.join(" ");
|
|
407
|
+
}
|
|
408
|
+
|
|
374
409
|
/**
|
|
375
410
|
* The message of an error that belongs to no field, for its toast: the API's
|
|
376
411
|
* message key, the first validation issue's (or a generic one), else the
|
|
@@ -227,50 +227,18 @@ export function formatDateTime(
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
// The narrow no-break space French (and a few other locales) groups digits
|
|
231
|
-
// with: the interface's font draws it a fraction of a digit wide, so "1 234"
|
|
232
|
-
// read as "1234" beside a column's "12 345". Groups take a full no-break space.
|
|
233
|
-
const NARROW_GROUP_SEPARATOR = " ";
|
|
234
|
-
const GROUP_SEPARATOR = " ";
|
|
235
|
-
|
|
236
|
-
/** `Intl.NumberFormat`, its digit groups always shown and visible. */
|
|
237
|
-
export interface ReadableNumberFormat {
|
|
238
|
-
format(value: number): string;
|
|
239
|
-
formatToParts(value: number): Intl.NumberFormatPart[];
|
|
240
|
-
resolvedOptions(): Intl.ResolvedNumberFormatOptions;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function readablePart(part: Intl.NumberFormatPart): Intl.NumberFormatPart {
|
|
244
|
-
return part.type === "group" && part.value === NARROW_GROUP_SEPARATOR
|
|
245
|
-
? { ...part, value: GROUP_SEPARATOR }
|
|
246
|
-
: part;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
230
|
/**
|
|
250
231
|
* `new Intl.NumberFormat(locale, options)` the way the interface writes a
|
|
251
232
|
* figure: a four-digit one grouped like a longer one ("1 234" next to
|
|
252
|
-
* "12 345", where Spanish or Polish would leave "1234" by default)
|
|
253
|
-
*
|
|
254
|
-
*
|
|
233
|
+
* "12 345", where Spanish or Polish would leave "1234" by default).
|
|
234
|
+
* `options.useGrouping` still decides. Throws as `Intl.NumberFormat` does on
|
|
235
|
+
* options it refuses.
|
|
255
236
|
*/
|
|
256
237
|
export function readableNumberFormat(
|
|
257
238
|
locale?: string,
|
|
258
239
|
options?: Intl.NumberFormatOptions,
|
|
259
|
-
):
|
|
260
|
-
|
|
261
|
-
useGrouping: true,
|
|
262
|
-
...options,
|
|
263
|
-
});
|
|
264
|
-
const formatToParts = (value: number) =>
|
|
265
|
-
format.formatToParts(value).map(readablePart);
|
|
266
|
-
return {
|
|
267
|
-
format: (value) =>
|
|
268
|
-
formatToParts(value)
|
|
269
|
-
.map((part) => part.value)
|
|
270
|
-
.join(""),
|
|
271
|
-
formatToParts,
|
|
272
|
-
resolvedOptions: () => format.resolvedOptions(),
|
|
273
|
-
};
|
|
240
|
+
): Intl.NumberFormat {
|
|
241
|
+
return new Intl.NumberFormat(locale, { useGrouping: true, ...options });
|
|
274
242
|
}
|
|
275
243
|
|
|
276
244
|
/**
|
|
@@ -16,6 +16,14 @@ export const MENU_STATUS_TEXT_CLASSES: Record<MenuItemStatus, string> = {
|
|
|
16
16
|
neutral: "text-dimmed",
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/** Status colors for the chip on a parent entry's leading icon. */
|
|
20
|
+
export const MENU_STATUS_CHIP_COLORS = {
|
|
21
|
+
success: "success",
|
|
22
|
+
warning: "warning",
|
|
23
|
+
error: "error",
|
|
24
|
+
neutral: "neutral",
|
|
25
|
+
} as const satisfies Record<MenuItemStatus, string>;
|
|
26
|
+
|
|
19
27
|
export const translateMenuItem = (
|
|
20
28
|
item: NavigationMenuItem,
|
|
21
29
|
processI18n: (value: string) => string,
|
|
@@ -50,28 +58,6 @@ export const shouldExpandMenuItem = (
|
|
|
50
58
|
return matchedFullId === fullId || matchedFullId.startsWith(`${fullId}.`);
|
|
51
59
|
};
|
|
52
60
|
|
|
53
|
-
export const addDefaultOpenToMenuItem = (
|
|
54
|
-
item: NavigationMenuItem,
|
|
55
|
-
matchedFullId: string | null,
|
|
56
|
-
): NavigationMenuItem => ({
|
|
57
|
-
...item,
|
|
58
|
-
defaultOpen: item.children
|
|
59
|
-
? shouldExpandMenuItem(item, matchedFullId)
|
|
60
|
-
: undefined,
|
|
61
|
-
children: item.children?.map((child) =>
|
|
62
|
-
addDefaultOpenToMenuItem(child, matchedFullId),
|
|
63
|
-
),
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
export const addDefaultOpenToMenuItems = (
|
|
67
|
-
items: NavigationMenuItem[][],
|
|
68
|
-
matchedFullId: string | null,
|
|
69
|
-
): NavigationMenuItem[][] => {
|
|
70
|
-
return items.map((group) =>
|
|
71
|
-
group.map((item) => addDefaultOpenToMenuItem(item, matchedFullId)),
|
|
72
|
-
);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
61
|
const normalizeRoutePath = (path: string): string =>
|
|
76
62
|
path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
|
|
77
63
|
|
|
@@ -114,90 +100,100 @@ const queryMatches = (
|
|
|
114
100
|
);
|
|
115
101
|
};
|
|
116
102
|
|
|
117
|
-
|
|
118
|
-
// restricted to leaf links so a page stays highlighted on its auto-generated
|
|
119
|
-
// sub-routes (e.g. `/orders/detail/42`); a parent that carries its own menu
|
|
120
|
-
// children highlights on its exact route only, so it never lights up alongside
|
|
121
|
-
// an active descendant that already has its own entry. The fullId equality
|
|
122
|
-
// fallback covers param routes whose URL never string-matches the `to` pattern.
|
|
123
|
-
// Entries sharing one page through query parameters (`/project?project=<id>`)
|
|
124
|
-
// are told apart by those parameters, so only one of them lights up.
|
|
103
|
+
/** Highlights a parent on sub-routes only when no descendant is active. */
|
|
125
104
|
export const isMenuItemActive = (
|
|
126
105
|
item: NavigationMenuItem,
|
|
127
106
|
matchedFullId: string | null,
|
|
128
107
|
currentRoute: MenuRouteState,
|
|
108
|
+
isDescendantActive = false,
|
|
129
109
|
): boolean => {
|
|
130
110
|
const target = resolveItemTarget(item.to);
|
|
131
111
|
if (!target) return false;
|
|
132
112
|
if (!queryMatches(target.query, currentRoute.query)) return false;
|
|
133
|
-
const matchesRoute =
|
|
134
|
-
|
|
135
|
-
|
|
113
|
+
const matchesRoute =
|
|
114
|
+
item.children?.length && isDescendantActive
|
|
115
|
+
? normalizeRoutePath(currentRoute.path) ===
|
|
116
|
+
normalizeRoutePath(target.path)
|
|
117
|
+
: matchesRoutePrefix(currentRoute.path, target.path);
|
|
136
118
|
if (matchesRoute) return true;
|
|
137
119
|
const fullId = (item as DmsMenuItem).fullId;
|
|
138
120
|
return matchedFullId !== null && fullId === matchedFullId;
|
|
139
121
|
};
|
|
140
122
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// `?project=a` on `?project=ab`.
|
|
123
|
+
/**
|
|
124
|
+
* Query entries and parents override native prefix highlighting. Plain leaves
|
|
125
|
+
* keep the router's fallback for routes the DMS cannot string-match.
|
|
126
|
+
*/
|
|
146
127
|
const resolveActiveState = (
|
|
147
128
|
item: NavigationMenuItem,
|
|
148
|
-
|
|
149
|
-
currentRoute: MenuRouteState,
|
|
129
|
+
isActive: boolean,
|
|
150
130
|
): Partial<Pick<NavigationMenuItem, "active">> => {
|
|
151
|
-
if (
|
|
131
|
+
if (isActive) {
|
|
152
132
|
return { active: true };
|
|
153
133
|
}
|
|
154
|
-
return hasQuery(resolveItemTarget(item.to))
|
|
134
|
+
return hasQuery(resolveItemTarget(item.to)) || item.children?.length
|
|
135
|
+
? { active: false }
|
|
136
|
+
: {};
|
|
155
137
|
};
|
|
156
138
|
|
|
157
|
-
|
|
139
|
+
interface MenuRouteVerdict {
|
|
140
|
+
item: NavigationMenuItem;
|
|
141
|
+
holdsActive: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function judgeMenuRoute(
|
|
158
145
|
item: NavigationMenuItem,
|
|
159
146
|
matchedFullId: string | null,
|
|
160
147
|
currentRoute: MenuRouteState,
|
|
161
|
-
):
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
148
|
+
): MenuRouteVerdict {
|
|
149
|
+
const children = item.children?.map((child) =>
|
|
150
|
+
judgeMenuRoute(child, matchedFullId, currentRoute),
|
|
151
|
+
);
|
|
152
|
+
const descendantActive =
|
|
153
|
+
children?.some((child) => child.holdsActive) ?? false;
|
|
154
|
+
const active = isMenuItemActive(
|
|
155
|
+
item,
|
|
156
|
+
matchedFullId,
|
|
157
|
+
currentRoute,
|
|
158
|
+
descendantActive,
|
|
159
|
+
);
|
|
160
|
+
const holdsActive = active || descendantActive;
|
|
161
|
+
return {
|
|
162
|
+
holdsActive,
|
|
163
|
+
item: {
|
|
164
|
+
...item,
|
|
165
|
+
...resolveActiveState(item, active),
|
|
166
|
+
defaultOpen: children?.length
|
|
167
|
+
? holdsActive || shouldExpandMenuItem(item, matchedFullId)
|
|
168
|
+
: undefined,
|
|
169
|
+
children: children?.map((child) => child.item),
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Computes highlight and expansion together, from descendants to ancestors. */
|
|
175
|
+
export const applyMenuRouteState = (
|
|
170
176
|
items: NavigationMenuItem[][],
|
|
171
177
|
matchedFullId: string | null,
|
|
172
178
|
currentRoute: MenuRouteState,
|
|
173
179
|
): NavigationMenuItem[][] => {
|
|
174
180
|
return items.map((group) =>
|
|
175
|
-
group.map((item) =>
|
|
176
|
-
addActiveStateToMenuItem(item, matchedFullId, currentRoute),
|
|
177
|
-
),
|
|
181
|
+
group.map((item) => judgeMenuRoute(item, matchedFullId, currentRoute).item),
|
|
178
182
|
);
|
|
179
183
|
};
|
|
180
184
|
|
|
181
|
-
const collectExpandedIds = (
|
|
182
|
-
items: NavigationMenuItem[],
|
|
183
|
-
matchedFullId: string,
|
|
184
|
-
): string[] => {
|
|
185
|
+
const collectExpandedIds = (items: NavigationMenuItem[]): string[] => {
|
|
185
186
|
const ids: string[] = [];
|
|
186
187
|
for (const item of items) {
|
|
187
|
-
if (item.id &&
|
|
188
|
-
ids.push(item.id
|
|
189
|
-
if (item.children) {
|
|
190
|
-
ids.push(...collectExpandedIds(item.children, matchedFullId));
|
|
191
|
-
}
|
|
188
|
+
if (typeof item.id === "string" && item.defaultOpen) {
|
|
189
|
+
ids.push(item.id);
|
|
192
190
|
}
|
|
191
|
+
if (item.children) ids.push(...collectExpandedIds(item.children));
|
|
193
192
|
}
|
|
194
193
|
return ids;
|
|
195
194
|
};
|
|
196
195
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
): string[] => {
|
|
201
|
-
if (!matchedFullId) return [];
|
|
202
|
-
return items.flatMap((group) => collectExpandedIds(group, matchedFullId));
|
|
196
|
+
/** Reads the expansion verdict for controlled accordions. */
|
|
197
|
+
export const collectOpenItemIds = (items: NavigationMenuItem[][]): string[] => {
|
|
198
|
+
return items.flatMap((group) => collectExpandedIds(group));
|
|
203
199
|
};
|
|
@@ -408,6 +408,8 @@
|
|
|
408
408
|
"too_short": "This value is too short.",
|
|
409
409
|
"too_small": "This value is too small.",
|
|
410
410
|
"too_large": "This value is too large.",
|
|
411
|
+
"named_required": "{field} is required.",
|
|
412
|
+
"named_invalid": "{field} is not valid.",
|
|
411
413
|
"with_values": "{message}: {values}",
|
|
412
414
|
"code_incomplete": "Enter every digit of the code.",
|
|
413
415
|
"password_rules": "The password doesn’t meet every rule.",
|
|
@@ -408,6 +408,8 @@
|
|
|
408
408
|
"too_short": "Cette valeur est trop courte.",
|
|
409
409
|
"too_small": "Cette valeur est trop petite.",
|
|
410
410
|
"too_large": "Cette valeur est trop grande.",
|
|
411
|
+
"named_required": "{field} est obligatoire.",
|
|
412
|
+
"named_invalid": "{field} n’est pas valide.",
|
|
411
413
|
"with_values": "{message} : {values}",
|
|
412
414
|
"code_incomplete": "Saisissez tous les chiffres du code.",
|
|
413
415
|
"password_rules": "Le mot de passe ne respecte pas toutes les règles.",
|
|
@@ -31,7 +31,12 @@
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/* Self-hosted Geist (SIL OFL 1.1, files and LICENSE.txt in ../fonts).
|
|
34
|
-
Variable files cover weights 100–900 and keep builds deterministic offline.
|
|
34
|
+
Variable files cover weights 100–900 and keep builds deterministic offline.
|
|
35
|
+
Geist has no narrow no-break space (U+202F), which French and a few other
|
|
36
|
+
locales group digits with: a fallback face drew it a fraction of a digit
|
|
37
|
+
wide, so "1 234" read as "1234". Each file here maps U+202F to its no-break
|
|
38
|
+
space glyph (fontTools: cmap[0x202F] = cmap[0xA0]); keep that mapping when
|
|
39
|
+
updating them. */
|
|
35
40
|
@font-face {
|
|
36
41
|
font-family: "Geist";
|
|
37
42
|
font-style: normal;
|
|
@@ -155,6 +160,11 @@
|
|
|
155
160
|
items, ~10px menus and 16px modals. */
|
|
156
161
|
--ui-radius: calc(0.5rem / 1.5);
|
|
157
162
|
--dms-radius-card: 0.875rem;
|
|
163
|
+
/* Pill tabs keep their v2 10px track, 7px tab and 4px count at the
|
|
164
|
+
default radius, and follow a theme that changes it. */
|
|
165
|
+
--dms-radius-tab-list: calc(var(--ui-radius) * 1.875);
|
|
166
|
+
--dms-radius-tab: calc(var(--ui-radius) * 1.3125);
|
|
167
|
+
--dms-radius-tab-badge: calc(var(--ui-radius) * 0.75);
|
|
158
168
|
|
|
159
169
|
/* v2 shell metrics: 48px navbar, 56px sidebar brand row. */
|
|
160
170
|
--ui-header-height: 3rem;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5,7 +5,6 @@ import PageHeaderActionsOutlet from "./PageHeaderActionsOutlet.vue";
|
|
|
5
5
|
import PageSkeleton from "./PageSkeleton.vue";
|
|
6
6
|
import type { LayoutHeaderAction } from "./PageHeaderActionBar.vue";
|
|
7
7
|
import { providePageHeaderActions } from "../../../composables/layout/usePageHeaderActions";
|
|
8
|
-
import type { PageRecordSource } from "../../../composables/layout/usePageRecord";
|
|
9
8
|
|
|
10
9
|
interface Props {
|
|
11
10
|
hideHeader?: boolean;
|
|
@@ -15,14 +14,12 @@ interface Props {
|
|
|
15
14
|
/** Header buttons declared by the backend page (`headerActions`). */
|
|
16
15
|
headerActions?: LayoutHeaderAction[];
|
|
17
16
|
/**
|
|
18
|
-
* The page query the server
|
|
19
|
-
*
|
|
17
|
+
* The page query the server served the buttons for, set when one declares
|
|
18
|
+
* an `availability` or the page reads a `record` (`headerActionsQuery`).
|
|
20
19
|
*/
|
|
21
20
|
headerActionsQuery?: Record<string, string>;
|
|
22
21
|
/** The route parameters (`:id`) they were resolved for, likewise. */
|
|
23
22
|
headerActionsParams?: Record<string, string>;
|
|
24
|
-
/** The record the page shows, which header buttons may act on (`record`). */
|
|
25
|
-
record?: PageRecordSource;
|
|
26
23
|
/** Space between the page header and the page. */
|
|
27
24
|
headerClass?: string;
|
|
28
25
|
}
|
|
@@ -35,7 +32,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
35
32
|
headerActions: () => [],
|
|
36
33
|
headerActionsQuery: undefined,
|
|
37
34
|
headerActionsParams: undefined,
|
|
38
|
-
record: undefined,
|
|
39
35
|
headerClass: "pb-6",
|
|
40
36
|
});
|
|
41
37
|
|
|
@@ -75,7 +71,6 @@ const PAGE_SLOT_CLASS =
|
|
|
75
71
|
:actions="props.headerActions"
|
|
76
72
|
:served-query="props.headerActionsQuery"
|
|
77
73
|
:served-params="props.headerActionsParams"
|
|
78
|
-
:record="props.record"
|
|
79
74
|
/>
|
|
80
75
|
</template>
|
|
81
76
|
</PageHeader>
|
|
@@ -185,13 +185,9 @@ const items = computed((): NavigationMenuItem[][] => {
|
|
|
185
185
|
moduleRootNode.value,
|
|
186
186
|
);
|
|
187
187
|
const translatedItems = translateMenuItems(moduleMenuItems, processI18n);
|
|
188
|
-
|
|
188
|
+
return applyMenuRouteState(
|
|
189
189
|
translatedItems,
|
|
190
190
|
currentFullId.value,
|
|
191
|
-
);
|
|
192
|
-
return addActiveStateToMenuItems(
|
|
193
|
-
openItems,
|
|
194
|
-
currentFullId.value,
|
|
195
191
|
currentRoute.value,
|
|
196
192
|
);
|
|
197
193
|
}
|
|
@@ -204,13 +200,9 @@ const items = computed((): NavigationMenuItem[][] => {
|
|
|
204
200
|
siteLayout.siteLayoutTree.value,
|
|
205
201
|
);
|
|
206
202
|
const translatedItems = translateMenuItems(menuItems, processI18n);
|
|
207
|
-
|
|
203
|
+
return applyMenuRouteState(
|
|
208
204
|
translatedItems,
|
|
209
205
|
currentFullId.value,
|
|
210
|
-
);
|
|
211
|
-
return addActiveStateToMenuItems(
|
|
212
|
-
openItems,
|
|
213
|
-
currentFullId.value,
|
|
214
206
|
currentRoute.value,
|
|
215
207
|
);
|
|
216
208
|
});
|
|
@@ -220,7 +212,7 @@ const openCategories = ref<string[]>([]);
|
|
|
220
212
|
watch(
|
|
221
213
|
() => items.value,
|
|
222
214
|
(menuItems) => {
|
|
223
|
-
const requiredIds =
|
|
215
|
+
const requiredIds = collectOpenItemIds(menuItems);
|
|
224
216
|
const current = new Set(openCategories.value);
|
|
225
217
|
for (const id of requiredIds) {
|
|
226
218
|
current.add(id);
|