@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/config.d.ts +0 -2
  3. package/dist/config.js +0 -4
  4. package/dist/config.js.map +1 -1
  5. package/dist/implementations/dms/frontend-modules.js +17 -6
  6. package/dist/implementations/dms/frontend-modules.js.map +1 -1
  7. package/dist/implementations/dms/manifest.d.ts +6 -0
  8. package/dist/implementations/dms/manifest.js +10 -7
  9. package/dist/implementations/dms/manifest.js.map +1 -1
  10. package/dist/implementations/dms/page.js +45 -16
  11. package/dist/implementations/dms/page.js.map +1 -1
  12. package/dist/index.js +4 -7
  13. package/dist/index.js.map +1 -1
  14. package/dist/store/listing.js +1 -2
  15. package/dist/store/listing.js.map +1 -1
  16. package/dist/store/operations.js +1 -2
  17. package/dist/store/operations.js.map +1 -1
  18. package/dist/utils/ensure-default-tenant.d.ts +2 -1
  19. package/dist/utils/ensure-default-tenant.js +13 -1
  20. package/dist/utils/ensure-default-tenant.js.map +1 -1
  21. package/dist/utils/export-jobs.js +2 -1
  22. package/dist/utils/export-jobs.js.map +1 -1
  23. package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +1 -2
  24. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +3 -0
  25. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
  26. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
  27. package/frontend-vue/layers/dms-core/app/utils/formatter.ts +5 -37
  28. package/frontend-vue/layers/dms-core/app/utils/menu.ts +66 -70
  29. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
  30. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
  31. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
  32. package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
  33. package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
  34. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
  35. package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
  36. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
  37. package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
  38. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +2 -7
  39. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +3 -11
  40. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +2 -106
  41. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +0 -8
  42. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +0 -10
  43. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
  44. package/frontend-vue/layers/dms-layout/app/components/navigation/NavigationMenu.vue +18 -3
  45. package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +24 -4
  46. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +10 -2
  47. package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
  48. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -7
  49. package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +2 -7
  50. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -0
  51. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +9 -9
  52. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +9 -9
  53. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
  54. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
  55. package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
  56. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +6 -4
  57. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +6 -4
  58. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
  59. package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +11 -1
  60. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
  61. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +2 -5
  62. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +26 -5
  63. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +22 -1
  64. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
  65. package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
  66. package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +17 -4
  67. package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +30 -6
  68. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +13 -3
  69. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
  70. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
  71. package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
  72. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
  73. package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
  74. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
  75. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +1 -0
  76. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
  77. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +27 -6
  78. package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +14 -7
  79. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
  80. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +3 -9
  81. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +16 -1
  82. package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +12 -0
  83. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +47 -7
  84. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +19 -9
  85. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
  86. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -1
  87. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -1
  88. package/frontend-vue/package.json +6 -2
  89. package/package.json +8 -11
  90. package/skills/dms-module/SKILL.md +1 -1
  91. package/skills/dms-pages/REFERENCE.md +7 -3
  92. package/dist/hooks/tenant-instances.d.ts +0 -1
  93. package/dist/hooks/tenant-instances.js +0 -13
  94. package/dist/hooks/tenant-instances.js.map +0 -1
  95. package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +0 -29
  96. package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +0 -50
  97. 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 { resolveApiMessage } from "./translation/useTranslation";
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`) words its refusals in
120
- // English and lists the fields after a colon.
121
- const DATA_API_MESSAGES: ReadonlyArray<{ pattern: RegExp; message: string }> = [
122
- {
123
- pattern: /^Missing mandatory fields:\s*(.+)$/,
124
- message: FIELD_ERROR_KEYS.required,
125
- },
126
- {
127
- pattern: /^Invalid field type\(s\):\s*(.+)$/,
128
- message: FIELD_ERROR_KEYS.invalid,
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 "Missing mandatory fields: a, b" / "Invalid field
341
- * type(s): a" and a `field: message` text. A network failure, a 403, a 429
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), with a
253
- * group separator wide enough to see. `options.useGrouping` still decides.
254
- * Throws as `Intl.NumberFormat` does on options it refuses.
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
- ): ReadableNumberFormat {
260
- const format = new Intl.NumberFormat(locale, {
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
- // Whether `item` should be highlighted for the current route. Prefix matching is
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 = item.children?.length
134
- ? normalizeRoutePath(currentRoute.path) === normalizeRoutePath(target.path)
135
- : matchesRoutePrefix(currentRoute.path, target.path);
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
- // Only the boolean `true` is set on plain entries: Nuxt UI forwards an explicit
142
- // `active` to the underlying link, and a `false` there would suppress the
143
- // built-in route match. Entries carrying a query always get an explicit value,
144
- // because that built-in match is a URL prefix test that would light up
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
- matchedFullId: string | null,
149
- currentRoute: MenuRouteState,
129
+ isActive: boolean,
150
130
  ): Partial<Pick<NavigationMenuItem, "active">> => {
151
- if (isMenuItemActive(item, matchedFullId, currentRoute)) {
131
+ if (isActive) {
152
132
  return { active: true };
153
133
  }
154
- return hasQuery(resolveItemTarget(item.to)) ? { active: false } : {};
134
+ return hasQuery(resolveItemTarget(item.to)) || item.children?.length
135
+ ? { active: false }
136
+ : {};
155
137
  };
156
138
 
157
- export const addActiveStateToMenuItem = (
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
- ): NavigationMenuItem => ({
162
- ...item,
163
- ...resolveActiveState(item, matchedFullId, currentRoute),
164
- children: item.children?.map((child) =>
165
- addActiveStateToMenuItem(child, matchedFullId, currentRoute),
166
- ),
167
- });
168
-
169
- export const addActiveStateToMenuItems = (
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 && shouldExpandMenuItem(item, matchedFullId)) {
188
- ids.push(item.id as string);
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
- export const getExpandedItemIds = (
198
- items: NavigationMenuItem[][],
199
- matchedFullId: string | null,
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.",
@@ -67,8 +67,6 @@ export interface DmsFooterConfig {
67
67
  * the copyright.
68
68
  */
69
69
  copyright?: string | false;
70
- /** The GitHub "Star" button of the DMS repository. */
71
- githubStar?: boolean;
72
70
  /** The language picker. */
73
71
  localeSwitcher?: boolean;
74
72
  }
@@ -215,7 +215,6 @@ export default {
215
215
  defaults: { logo: dmsLogo(), colors: dmsColors() },
216
216
  footer: {
217
217
  copyright: "Antelope",
218
- githubStar: true,
219
218
  localeSwitcher: true,
220
219
  },
221
220
  },
@@ -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;
@@ -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 resolved the buttons' availability for, set
19
- * when one declares an `availability` (`headerActionsQuery`).
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
- const openItems = addDefaultOpenToMenuItems(
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
- const openItems = addDefaultOpenToMenuItems(
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 = getExpandedItemIds(menuItems, currentFullId.value);
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);