@antelopejs/dms 0.4.4 → 0.4.6

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 (205) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/config.d.ts +1 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/hooks/invite-extensions.js +2 -2
  5. package/dist/hooks/invite-extensions.js.map +1 -1
  6. package/dist/implementations/dms/layout-banners.d.ts +18 -0
  7. package/dist/implementations/dms/layout-banners.js +60 -0
  8. package/dist/implementations/dms/layout-banners.js.map +1 -0
  9. package/dist/implementations/dms/page.d.ts +3 -0
  10. package/dist/implementations/dms/page.js +90 -28
  11. package/dist/implementations/dms/page.js.map +1 -1
  12. package/dist/implementations/dms/quick-actions.d.ts +5 -0
  13. package/dist/implementations/dms/quick-actions.js.map +1 -1
  14. package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
  15. package/dist/implementations/dms/resolver-timeout.js +13 -0
  16. package/dist/implementations/dms/resolver-timeout.js.map +1 -0
  17. package/dist/implementations/dms/saas-mode.d.ts +8 -0
  18. package/dist/implementations/dms/saas-mode.js +20 -0
  19. package/dist/implementations/dms/saas-mode.js.map +1 -0
  20. package/dist/implementations/dms/stale-session.d.ts +2 -0
  21. package/dist/implementations/dms/stale-session.js +16 -0
  22. package/dist/implementations/dms/stale-session.js.map +1 -0
  23. package/dist/implementations/dms-auth/index.d.ts +3 -1
  24. package/dist/implementations/dms-auth/index.js +38 -8
  25. package/dist/implementations/dms-auth/index.js.map +1 -1
  26. package/dist/implementations/dms-html-render/service-jwt.js +2 -1
  27. package/dist/implementations/dms-html-render/service-jwt.js.map +1 -1
  28. package/dist/index.js +2 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/pages/settings/users/index.d.ts +1 -0
  31. package/dist/pages/settings/users/index.js +1 -0
  32. package/dist/pages/settings/users/index.js.map +1 -1
  33. package/dist/pages/settings/users/invite-extension-routes.d.ts +4 -0
  34. package/dist/pages/settings/users/invite-extension-routes.js +57 -0
  35. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -0
  36. package/dist/pages/settings/users/invites.d.ts +2 -1
  37. package/dist/pages/settings/users/invites.js +17 -5
  38. package/dist/pages/settings/users/invites.js.map +1 -1
  39. package/dist/pages/settings/users/members.d.ts +4 -1
  40. package/dist/pages/settings/users/members.js +10 -6
  41. package/dist/pages/settings/users/members.js.map +1 -1
  42. package/dist/pages/settings/users/notifications.js +1 -1
  43. package/dist/pages/settings/users/notifications.js.map +1 -1
  44. package/dist/pages/settings/users/quick-actions.d.ts +1 -0
  45. package/dist/pages/settings/users/quick-actions.js +22 -0
  46. package/dist/pages/settings/users/quick-actions.js.map +1 -0
  47. package/dist/routes/auth/client-ip.d.ts +7 -0
  48. package/dist/routes/auth/client-ip.js +42 -0
  49. package/dist/routes/auth/client-ip.js.map +1 -0
  50. package/dist/routes/auth/index.d.ts +1 -0
  51. package/dist/routes/auth/index.js +1 -0
  52. package/dist/routes/auth/index.js.map +1 -1
  53. package/dist/routes/auth/signup.d.ts +2 -0
  54. package/dist/routes/auth/signup.js +7 -2
  55. package/dist/routes/auth/signup.js.map +1 -1
  56. package/dist/routes/auth.d.ts +3 -0
  57. package/dist/routes/auth.js +15 -4
  58. package/dist/routes/auth.js.map +1 -1
  59. package/dist/test/integration/auth/client-ip.test.d.ts +1 -0
  60. package/dist/test/integration/auth/client-ip.test.js +14 -0
  61. package/dist/test/integration/auth/client-ip.test.js.map +1 -0
  62. package/dist/test/integration/invite-extension-edit.test.d.ts +1 -0
  63. package/dist/test/integration/invite-extension-edit.test.js +122 -0
  64. package/dist/test/integration/invite-extension-edit.test.js.map +1 -0
  65. package/dist/test/integration/page-session.test.d.ts +1 -0
  66. package/dist/test/integration/page-session.test.js +52 -0
  67. package/dist/test/integration/page-session.test.js.map +1 -0
  68. package/dist/test/integration/saas-mode-ownership.test.d.ts +1 -0
  69. package/dist/test/integration/saas-mode-ownership.test.js +81 -0
  70. package/dist/test/integration/saas-mode-ownership.test.js.map +1 -0
  71. package/dist/test/integration/tenant-lifecycle.test.js +38 -0
  72. package/dist/test/integration/tenant-lifecycle.test.js.map +1 -1
  73. package/dist/test/unit/client-ip.test.d.ts +1 -0
  74. package/dist/test/unit/client-ip.test.js +77 -0
  75. package/dist/test/unit/client-ip.test.js.map +1 -0
  76. package/dist/test/unit/implementations/dms/layout-banners.test.d.ts +1 -0
  77. package/dist/test/unit/implementations/dms/layout-banners.test.js +217 -0
  78. package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -0
  79. package/dist/test/unit/implementations/dms/quick-actions-button.test.d.ts +1 -0
  80. package/dist/test/unit/implementations/dms/quick-actions-button.test.js +220 -0
  81. package/dist/test/unit/implementations/dms/quick-actions-button.test.js.map +1 -0
  82. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +1 -1
  83. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  84. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.d.ts +1 -0
  85. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js +37 -0
  86. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js.map +1 -0
  87. package/dist/test/unit/interfaces/dms/invite-extension.test.js +138 -12
  88. package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
  89. package/dist/test/unit/interfaces/dms/saas-mode.test.d.ts +1 -0
  90. package/dist/test/unit/interfaces/dms/saas-mode.test.js +42 -0
  91. package/dist/test/unit/interfaces/dms/saas-mode.test.js.map +1 -0
  92. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.d.ts +1 -0
  93. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js +53 -0
  94. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js.map +1 -0
  95. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.d.ts +1 -0
  96. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js +80 -0
  97. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js.map +1 -0
  98. package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.d.ts +1 -0
  99. package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js +96 -0
  100. package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js.map +1 -0
  101. package/dist/test/unit/pages/settings/invite-member-quick-action.test.d.ts +1 -0
  102. package/dist/test/unit/pages/settings/invite-member-quick-action.test.js +104 -0
  103. package/dist/test/unit/pages/settings/invite-member-quick-action.test.js.map +1 -0
  104. package/dist/test/unit/pages/settings/notifications-access-gate.test.d.ts +1 -0
  105. package/dist/test/unit/pages/settings/notifications-access-gate.test.js +78 -0
  106. package/dist/test/unit/pages/settings/notifications-access-gate.test.js.map +1 -0
  107. package/dist/test/unit/signup-account-guard.test.js +20 -2
  108. package/dist/test/unit/signup-account-guard.test.js.map +1 -1
  109. package/dist/test/unit/utils/admin-invite-email.test.d.ts +1 -0
  110. package/dist/test/unit/utils/admin-invite-email.test.js +69 -0
  111. package/dist/test/unit/utils/admin-invite-email.test.js.map +1 -0
  112. package/dist/utils/admin-invite-email.d.ts +14 -0
  113. package/dist/utils/admin-invite-email.js +51 -0
  114. package/dist/utils/admin-invite-email.js.map +1 -0
  115. package/dist/utils/index.d.ts +0 -1
  116. package/dist/utils/index.js +0 -1
  117. package/dist/utils/index.js.map +1 -1
  118. package/dist/validation/auth.schema.d.ts +3 -3
  119. package/dist/validation/auth.schema.js +1 -1
  120. package/dist/validation/auth.schema.js.map +1 -1
  121. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +37 -8
  122. package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -2
  123. package/frontend-vue/layers/dms-auth/i18n/locales/auth-en-GB.json +3 -1
  124. package/frontend-vue/layers/dms-auth/i18n/locales/auth-fr-FR.json +3 -1
  125. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +27 -2
  126. package/frontend-vue/layers/dms-core/app/utils/menu.ts +26 -10
  127. package/frontend-vue/layers/dms-core/app/utils/routePath.ts +23 -0
  128. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +3 -0
  129. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +3 -0
  130. package/frontend-vue/layers/dms-core/shared/types/runtime-config.d.ts +4 -1
  131. package/frontend-vue/layers/dms-layout/app/app.config.ts +9 -1
  132. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +50 -0
  133. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +16 -13
  134. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +2 -2
  135. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +11 -5
  136. package/frontend-vue/layers/dms-layout/app/build/components/notification/widgetRequest.ts +20 -0
  137. package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -5
  138. package/frontend-vue/layers/dms-layout/app/components/profile/ProfileLanguage.vue +3 -23
  139. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +59 -0
  140. package/frontend-vue/layers/dms-layout/app/composables/page/useQuickActions.ts +15 -2
  141. package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +33 -0
  142. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -0
  143. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +54 -9
  144. package/frontend-vue/layers/dms-layout/app/error.vue +17 -2
  145. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +2 -1
  146. package/frontend-vue/layers/dms-layout/app/types/page.ts +22 -0
  147. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +48 -0
  148. package/frontend-vue/layers/dms-layout/app/utils/favorites.ts +94 -0
  149. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +21 -2
  150. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +21 -2
  151. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/index.ts +1 -1
  152. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/{useOnboardingMiddleware.ts → useOnboarding.ts} +0 -12
  153. package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +4 -1
  154. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +21 -4
  155. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +13 -3
  156. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +13 -3
  157. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +31 -3
  158. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationBadges.ts +36 -0
  159. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +2 -7
  160. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/columnVisibility.ts +13 -0
  161. package/frontend-vue/layers/dms-ui/app/build/types/tree.ts +5 -10
  162. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  163. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +1 -1
  164. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +1 -1
  165. package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +2 -2
  166. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +6 -16
  167. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +2 -2
  168. package/frontend-vue/layers/dms-ui/app/components/chart/internal/TrendBadge.vue +1 -1
  169. package/frontend-vue/layers/dms-ui/app/components/copy/CopyButton.vue +1 -1
  170. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +3 -3
  171. package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +5 -1
  172. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +1 -1
  173. package/frontend-vue/layers/dms-ui/app/components/status/StatusSummary.vue +1 -1
  174. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +33 -10
  175. package/frontend-vue/layers/dms-ui/app/composables/chart/donutRecords.ts +26 -0
  176. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +4 -2
  177. package/frontend-vue/layers/dms-ui/app/composables/tab/useTab.ts +3 -3
  178. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/column.ts +1 -1
  179. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +2 -0
  180. package/frontend-vue/layers/dms-ui/app/types/quick-actions.ts +38 -3
  181. package/frontend-vue/tests/admin-invite-email.test.ts +86 -0
  182. package/frontend-vue/tests/api-error-title.test.ts +24 -0
  183. package/frontend-vue/tests/auth-middleware-error.test.ts +58 -0
  184. package/frontend-vue/tests/breadcrumb-target.test.ts +50 -0
  185. package/frontend-vue/tests/button-disabled-theme.test.ts +21 -0
  186. package/frontend-vue/tests/chart-nested-donut.test.ts +97 -0
  187. package/frontend-vue/tests/error-page-message.test.ts +83 -0
  188. package/frontend-vue/tests/favorite-page.test.ts +104 -0
  189. package/frontend-vue/tests/icon-aria-hidden.test.ts +29 -0
  190. package/frontend-vue/tests/layout-banners.test.ts +224 -0
  191. package/frontend-vue/tests/menu-navigation.test.ts +39 -10
  192. package/frontend-vue/tests/notification-popover-errors.test.ts +147 -0
  193. package/frontend-vue/tests/onboarding-middleware.test.ts +103 -0
  194. package/frontend-vue/tests/password-strength-meter.test.ts +33 -0
  195. package/frontend-vue/tests/quick-action-intent.test.ts +74 -0
  196. package/frontend-vue/tests/quick-actions.test.ts +23 -0
  197. package/frontend-vue/tests/relation-badges.test.ts +32 -0
  198. package/frontend-vue/tests/signup-invitation-link.test.ts +126 -0
  199. package/frontend-vue/tests/skeleton-paragraph.test.ts +30 -0
  200. package/frontend-vue/tests/tab-without-component-id.test.ts +51 -0
  201. package/frontend-vue/tests/table-column-visibility.test.ts +20 -0
  202. package/frontend-vue/tests/user-language.test.ts +58 -0
  203. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  204. package/package.json +2 -2
  205. package/skills/dms/REFERENCE.md +1 -1
@@ -40,7 +40,9 @@
40
40
  "soon": "Bientôt disponible"
41
41
  },
42
42
  "quickActions": {
43
- "title": "Actions rapides"
43
+ "title": "Actions rapides",
44
+ "category_members": "Membres",
45
+ "invite_member": "Inviter un membre"
44
46
  },
45
47
  "commandPalette": {
46
48
  "button": "Rechercher…",
@@ -222,7 +224,8 @@
222
224
  "cancel_confirm_description": "Êtes-vous sûr de vouloir annuler cette invitation ? Le lien d'invitation ne fonctionnera plus."
223
225
  },
224
226
  "error": {
225
- "not_found": "Invitation introuvable"
227
+ "not_found": "Invitation introuvable",
228
+ "resolved": "Cette invitation a déjà été acceptée ou annulée"
226
229
  }
227
230
  },
228
231
  "roles": {
@@ -273,5 +276,21 @@
273
276
  "intro": "Parcourez les modules disponibles sur cette plateforme.",
274
277
  "empty": "Aucun module ne vous est accessible pour le moment.",
275
278
  "open": "Ouvrir le module"
279
+ },
280
+ "emails": {
281
+ "admin_invite": {
282
+ "title": "Vous êtes invité !",
283
+ "title_workspace": "Vous êtes invité à rejoindre {workspace}",
284
+ "greeting": "Bonjour,",
285
+ "invitation": "Vous avez été invité à rejoindre notre plateforme.",
286
+ "invitation_to_target": "Vous avez été invité à rejoindre {target}.",
287
+ "invitation_from_inviter": "{inviter} vous invite à rejoindre {target}.",
288
+ "workspace_on_platform": "{workspace} sur {platform}",
289
+ "call_to_action": "Cliquez sur le bouton ci-dessous pour créer votre compte :",
290
+ "button": "Créer mon compte",
291
+ "expires_label": "Cette invitation expire dans",
292
+ "expires_in": "{days} jours",
293
+ "footer": "Si vous n'attendiez pas cette invitation, vous pouvez ignorer cet e-mail."
294
+ }
276
295
  }
277
296
  }
@@ -1 +1 @@
1
- export * from "./useOnboardingMiddleware";
1
+ export * from "./useOnboarding";
@@ -37,15 +37,3 @@ export const setOnboardingComplete = () => {
37
37
  );
38
38
  cache.value = { hasOnboarded: true };
39
39
  };
40
-
41
- export const useOnboardingMiddleware = async () => {
42
- const { hasOnboarded } = await useOnboarding();
43
-
44
- if (!hasOnboarded) return;
45
-
46
- throw createError({
47
- statusCode: HTTP_PRECONDITION_FAILED,
48
- statusMessage: "Invalid request",
49
- message: "Onboarding already completed",
50
- });
51
- };
@@ -1,7 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import Register from "../components/onboarding/steps/register.vue";
3
3
 
4
- await useOnboardingMiddleware();
4
+ // A custom page runs the global middleware only once it declares its meta:
5
+ // that is what lets the onboarding middleware send a visitor away from a
6
+ // wizard that is already finished, on a direct load as on a navigation.
7
+ defineDmsPageMeta({});
5
8
  </script>
6
9
 
7
10
  <template>
@@ -1,6 +1,16 @@
1
+ const ONBOARDING_PATH = "/onboarding";
2
+ const LOGIN_PATH = "/auth";
3
+
4
+ // Where a visit to the finished wizard lands: the app for a signed-in user, the
5
+ // login page for anyone else rather than a homepage whose 401 would bounce
6
+ // them there anyway.
7
+ function completedOnboardingDestination(): string {
8
+ const { loggedIn } = useUserSession();
9
+ return loggedIn.value ? useHomepage() : LOGIN_PATH;
10
+ }
11
+
1
12
  export default defineDmsMiddleware(async (to) => {
2
- // The onboarding wizard itself must always be reachable so it can render.
3
- if (to.path.startsWith("/onboarding")) return;
13
+ const isOnboardingRoute = to.path.startsWith(ONBOARDING_PATH);
4
14
 
5
15
  // While the instance has no admin yet, funnel every route to the onboarding
6
16
  // wizard. Running as a global middleware (instead of a plugin) lets this take
@@ -11,11 +21,18 @@ export default defineDmsMiddleware(async (to) => {
11
21
  try {
12
22
  info = await useOnboarding();
13
23
  } catch {
14
- // If the onboarding status can't be resolved, don't block navigation.
24
+ // If the onboarding status can't be resolved, don't block navigation: the
25
+ // wizard stays reachable, and it is its own register call that refuses a
26
+ // second admin.
15
27
  return;
16
28
  }
17
29
 
30
+ if (isOnboardingRoute) {
31
+ if (!info.hasOnboarded) return;
32
+ return navigateDms(completedOnboardingDestination(), { replace: true });
33
+ }
34
+
18
35
  if (info.hasOnboarded) return;
19
36
 
20
- return navigateDms("/onboarding");
37
+ return navigateDms(ONBOARDING_PATH);
21
38
  });
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from "vue";
3
+ import { DialogDescription, DialogTitle, VisuallyHidden } from "reka-ui";
3
4
 
4
5
  type DrawerDirection = "top" | "bottom" | "left" | "right";
5
6
 
@@ -82,15 +83,24 @@ function onContentResolve() {
82
83
  <!-- Same container as the body so the title aligns with the form -->
83
84
  <UContainer class="flex w-full flex-1 items-center justify-between gap-4">
84
85
  <div v-if="headerComponent" class="flex-1">
86
+ <VisuallyHidden>
87
+ <DialogTitle>{{ title }}</DialogTitle>
88
+ <DialogDescription>{{ description }}</DialogDescription>
89
+ </VisuallyHidden>
85
90
  <Suspense>
86
91
  <component :is="headerComponent" v-bind="headerComponentOptions" />
87
92
  </Suspense>
88
93
  </div>
89
94
  <div v-else class="flex-1">
90
- <h2 class="text-highlighted font-semibold">{{ title }}</h2>
91
- <p v-if="description" class="text-muted text-sm">
95
+ <DialogTitle as="h2" class="text-highlighted font-semibold">
96
+ {{ title }}
97
+ </DialogTitle>
98
+ <DialogDescription v-if="description" class="text-muted text-sm">
92
99
  {{ description }}
93
- </p>
100
+ </DialogDescription>
101
+ <VisuallyHidden v-else>
102
+ <DialogDescription />
103
+ </VisuallyHidden>
94
104
  </div>
95
105
  <UButton
96
106
  icon="i-lucide-x"
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from "vue";
3
+ import { DialogDescription, DialogTitle, VisuallyHidden } from "reka-ui";
3
4
  import type { ModalSize } from "../../../../types/modal";
4
5
 
5
6
  interface DynamicModalProps {
@@ -98,15 +99,24 @@ function onContentResolve() {
98
99
  <template #header>
99
100
  <div class="flex w-full items-center justify-between gap-4">
100
101
  <div v-if="headerComponent" class="flex-1">
102
+ <VisuallyHidden>
103
+ <DialogTitle>{{ title }}</DialogTitle>
104
+ <DialogDescription>{{ description }}</DialogDescription>
105
+ </VisuallyHidden>
101
106
  <Suspense>
102
107
  <component :is="headerComponent" v-bind="headerComponentOptions" />
103
108
  </Suspense>
104
109
  </div>
105
110
  <div v-else class="flex-1">
106
- <h2 class="text-highlighted font-semibold">{{ title }}</h2>
107
- <p v-if="description" class="text-muted text-sm">
111
+ <DialogTitle as="h2" class="text-highlighted font-semibold">
112
+ {{ title }}
113
+ </DialogTitle>
114
+ <DialogDescription v-if="description" class="text-muted text-sm">
108
115
  {{ description }}
109
- </p>
116
+ </DialogDescription>
117
+ <VisuallyHidden v-else>
118
+ <DialogDescription />
119
+ </VisuallyHidden>
110
120
  </div>
111
121
  <UButton
112
122
  icon="i-lucide-x"
@@ -1,8 +1,10 @@
1
1
  import { defineAsyncComponent, type VNode } from "vue";
2
2
  import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
3
3
  import UAvatar from "@nuxt/ui/components/Avatar.vue";
4
+ import UBadge from "@nuxt/ui/components/Badge.vue";
4
5
  import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
5
6
  import ULink from "@nuxt/ui/components/Link.vue";
7
+ import { buildRelationBadges } from "./relationBadges";
6
8
 
7
9
  const FilePreview = defineAsyncComponent(
8
10
  () => import("../../../components/table-view/FilePreview.vue"),
@@ -37,6 +39,9 @@ const LINK_CLASS =
37
39
  const LINK_ICON_CLASS = "size-3 text-primary/70 flex-shrink-0";
38
40
  const LINK_ICON_NAME = "i-ph-arrow-up-right-light";
39
41
  const DEFAULT_RELATION_VALUE_KEY = "_id";
42
+ const DEFAULT_RELATION_LABEL_KEY = "name";
43
+ const RELATION_BADGES_CLASS = "inline-flex flex-wrap items-center gap-1";
44
+ const RELATION_BADGE_CLASS = "max-w-40";
40
45
  const PERMISSION_SEPARATOR = ".";
41
46
 
42
47
  interface LinkRendererOptions {
@@ -263,18 +268,41 @@ function countLeafPermissions(value: string[]): number {
263
268
  return value.filter((id) => !parentIds.has(id)).length;
264
269
  }
265
270
 
271
+ function renderRelationBadge(label: string, title?: string) {
272
+ return h(UBadge, {
273
+ label,
274
+ title: title ?? label,
275
+ color: "neutral",
276
+ variant: "subtle",
277
+ size: Size.small,
278
+ class: RELATION_BADGE_CLASS,
279
+ });
280
+ }
281
+
282
+ function renderRelationBadges(values: unknown[], labelKey: string) {
283
+ const { processI18n } = useTranslation();
284
+ const { visible, hidden } = buildRelationBadges(values, labelKey);
285
+ const badges = visible.map((label) =>
286
+ renderRelationBadge(processI18n(label)),
287
+ );
288
+ if (hidden.length > 0) {
289
+ const hiddenLabels = hidden.map((label) => processI18n(label)).join(", ");
290
+ badges.push(renderRelationBadge(`+${hidden.length}`, hiddenLabels));
291
+ }
292
+ return h("span", { class: RELATION_BADGES_CLASS }, badges);
293
+ }
294
+
266
295
  function renderRelation(value: unknown, options: unknown) {
267
- const { t } = useI18n();
268
296
  const opts = options as RelationOptions | undefined;
297
+ const labelKey = opts?.keyMapping?.label || DEFAULT_RELATION_LABEL_KEY;
269
298
 
270
299
  if (opts?.multiple && Array.isArray(value)) {
271
- return t("dms.form.relation.selected_count", { count: value.length });
300
+ return renderRelationBadges(value, labelKey);
272
301
  }
273
302
 
274
303
  if (!value || !isObject(value)) return value;
275
304
 
276
305
  const valueObject = value as Record<string, string>;
277
- const labelKey = opts?.keyMapping?.label || "name";
278
306
  const avatarKey = opts?.keyMapping?.avatar;
279
307
  const label = valueObject[labelKey] || String(value);
280
308
 
@@ -0,0 +1,36 @@
1
+ /** How many labels a multi-value relation cell shows before collapsing. */
2
+ export const MAX_RELATION_BADGES = 3;
3
+
4
+ /** The labels a multi-value cell shows, and how many it folds into `+N`. */
5
+ export interface RelationBadges {
6
+ visible: string[];
7
+ hidden: string[];
8
+ }
9
+
10
+ function readRelationLabel(item: unknown, labelKey: string): string {
11
+ if (typeof item === "object" && item !== null) {
12
+ const label = (item as Record<string, unknown>)[labelKey];
13
+ return label === undefined || label === null ? "" : String(label);
14
+ }
15
+ return item === undefined || item === null ? "" : String(item);
16
+ }
17
+
18
+ /**
19
+ * Split a multi-value relation into the labels rendered as badges and the ones
20
+ * folded into a trailing `+N`. List rows carry the related rows resolved by the
21
+ * relation join, so each item is read through the `label` key mapping; a raw id
22
+ * (join skipped) is shown as is rather than dropped.
23
+ */
24
+ export function buildRelationBadges(
25
+ values: unknown[],
26
+ labelKey: string,
27
+ maxVisible = MAX_RELATION_BADGES,
28
+ ): RelationBadges {
29
+ const labels = values
30
+ .map((item) => readRelationLabel(item, labelKey))
31
+ .filter((label) => label !== "");
32
+ return {
33
+ visible: labels.slice(0, maxVisible),
34
+ hidden: labels.slice(maxVisible),
35
+ };
36
+ }
@@ -8,6 +8,7 @@ import {
8
8
  KANBAN_DISPLAY_ID,
9
9
  } from "../../../composables/table-view/types";
10
10
  import type { FormContainerType } from "./types";
11
+ import { buildInitialColumnVisibility } from "./utils/columnVisibility";
11
12
 
12
13
  const BUILTIN_DISPLAYS: Record<
13
14
  string,
@@ -110,13 +111,7 @@ export const useTableViewConfig = <T extends Data>(
110
111
 
111
112
  const defaultDisplay = config.defaultDisplay ?? TABLE_DISPLAY_ID;
112
113
 
113
- const initialVisibility = config.columns?.reduce(
114
- (acc, column) => {
115
- acc[column.id] = column.visible ?? true;
116
- return acc;
117
- },
118
- {} as Record<string, boolean>,
119
- );
114
+ const initialVisibility = buildInitialColumnVisibility(config.columns ?? []);
120
115
 
121
116
  const tableProps = computed<Partial<TableProps<T>>>(() => ({
122
117
  caption: processI18n(config.caption ?? ""),
@@ -0,0 +1,13 @@
1
+ import type { VisibilityState } from "@tanstack/vue-table";
2
+ import type { TableViewColumn } from "../../../../composables/table-view/types/column";
3
+
4
+ /**
5
+ * The visibility a table opens with before the user picks columns: a column
6
+ * declared `isVisible: false` starts hidden but stays in the column picker.
7
+ */
8
+ export const buildInitialColumnVisibility = (
9
+ columns: Pick<TableViewColumn, "id" | "isVisible">[],
10
+ ): VisibilityState =>
11
+ Object.fromEntries(
12
+ columns.map((column) => [column.id, column.isVisible ?? true]),
13
+ );
@@ -1,4 +1,5 @@
1
1
  import type { LinkProps, NavigationMenuItem } from "@nuxt/ui";
2
+ import { buildLinkWithQuery } from "#dms-core/app/utils/routePath";
2
3
 
3
4
  export interface TreeItem extends LinkProps {
4
5
  id: string;
@@ -29,18 +30,13 @@ function isExcludedFromRoot(item: SiteLayoutTree): boolean {
29
30
 
30
31
  // A registered entry links to its own slug, narrowed by the query parameters it
31
32
  // declares so a single page (with `validation.requiredQueryParams`) can back
32
- // several entries. `exactQuery: "partial"` keeps sibling entries of that page
33
- // from all highlighting at once while tolerating unrelated query parameters.
34
- export function buildMenuItemTarget(
35
- item: SiteLayoutTree,
36
- ): MenuItemTarget | string | undefined {
33
+ // several entries. The sidebar tells those sibling entries apart itself (see
34
+ // `isMenuItemActive`), so the link stays a plain string.
35
+ export function buildMenuItemTarget(item: SiteLayoutTree): string | undefined {
37
36
  if (!item.layoutUrl) {
38
37
  return undefined;
39
38
  }
40
- if (!item.query || Object.keys(item.query).length === 0) {
41
- return item.fullSlug;
42
- }
43
- return { path: item.fullSlug, query: item.query };
39
+ return buildLinkWithQuery(item.fullSlug, item.query);
44
40
  }
45
41
 
46
42
  function buildNavigationItem(
@@ -54,7 +50,6 @@ function buildNavigationItem(
54
50
  label: item.displayName,
55
51
  icon: item.icon,
56
52
  to,
57
- ...(typeof to === "object" ? { exactQuery: "partial" } : {}),
58
53
  variant: item.variant,
59
54
  status: item.status,
60
55
  children: children.length > 0 ? children : undefined,
@@ -6,7 +6,7 @@
6
6
  <UProgress
7
7
  :color="props.color"
8
8
  :model-value="props.score"
9
- :max="4"
9
+ :max="props.strength.length"
10
10
  size="sm"
11
11
  />
12
12
  </div>
@@ -35,7 +35,7 @@ const bubbleClass = computed(() => SOFT_TINT[props.iconColor]);
35
35
  :class="bubbleClass"
36
36
  class="grid size-[26px] shrink-0 place-items-center rounded-full"
37
37
  >
38
- <UIcon :name="icon" class="size-3.5" aria-hidden="true" />
38
+ <UIcon :name="icon" class="size-3.5" :aria-hidden="true" />
39
39
  </div>
40
40
 
41
41
  <div class="min-w-0 flex-1">
@@ -75,7 +75,7 @@ const iconBoxClass = computed(
75
75
  >
76
76
  <template #leading>
77
77
  <div :class="iconBoxClass">
78
- <UIcon :name="icon" class="size-[21px]" aria-hidden="true" />
78
+ <UIcon :name="icon" class="size-[21px]" :aria-hidden="true" />
79
79
  </div>
80
80
  </template>
81
81
 
@@ -24,7 +24,7 @@ defineProps<Props>();
24
24
  <div
25
25
  class="bg-primary/10 ring-primary/20 group-hover:bg-primary/15 flex size-10 shrink-0 items-center justify-center rounded-lg ring transition-colors"
26
26
  >
27
- <UIcon :name="icon" class="text-primary size-5" aria-hidden="true" />
27
+ <UIcon :name="icon" class="text-primary size-5" :aria-hidden="true" />
28
28
  </div>
29
29
 
30
30
  <h3 class="text-highlighted min-w-0 truncate text-base font-semibold">
@@ -34,7 +34,7 @@ defineProps<Props>();
34
34
  <UIcon
35
35
  name="i-ph-caret-right"
36
36
  class="text-dimmed ml-auto size-4 shrink-0 opacity-0 transition-all group-hover:translate-x-0.5 group-hover:opacity-100"
37
- aria-hidden="true"
37
+ :aria-hidden="true"
38
38
  />
39
39
  </div>
40
40
 
@@ -5,6 +5,7 @@ import type { MixedSeriesDef } from "../../composables/chart/useApexChart.types"
5
5
  import { useChartFetch } from "../../composables/chart/useChartFetch";
6
6
  import { useThemeRevision } from "../../composables/chart/useThemeRevision";
7
7
  import { formatValue } from "../../composables/chart/formatValue";
8
+ import { toDonutRecords } from "../../composables/chart/donutRecords";
8
9
  import {
9
10
  CHART_EVENT_NAMES,
10
11
  isSegmentChart,
@@ -144,24 +145,13 @@ const finalSeries = computed<ChartSeries[]>(() => {
144
145
  return staticSeries.value;
145
146
  });
146
147
 
148
+ // A donut plots the first series only; a nested one plots its card's, like
149
+ // every other nested chart type.
147
150
  const finalDonutData = computed<DonutRecord[]>(() => {
148
151
  if (!isCircular.value) return [];
149
- if (data.value && Array.isArray(data.value.series)) {
150
- const first = data.value.series[0];
151
- if (first && Array.isArray(first.data)) {
152
- return first.data.map((entry) => {
153
- if (typeof entry === "number") {
154
- return { label: String(entry), value: entry };
155
- }
156
- const point = entry as { label?: string; x?: unknown; y?: unknown };
157
- return {
158
- label: point.label ?? String(point.x ?? ""),
159
- value: Number(point.y ?? 0),
160
- };
161
- });
162
- }
163
- }
164
- return staticDonut.value;
152
+ const source = nestedContext ? nestedContext.cardSeries : fetchSeries.value;
153
+ const records = toDonutRecords(source[0]);
154
+ return records.length > 0 ? records : staticDonut.value;
165
155
  });
166
156
 
167
157
  function buildCircularClickPayload(opts: {
@@ -155,10 +155,10 @@ const primaryDotColor = computed(() => resolveChartColor("primary"));
155
155
  <div class="mb-4 flex flex-wrap items-start justify-between gap-3">
156
156
  <div class="min-w-0">
157
157
  <p class="text-muted text-sm">{{ processI18n(title) }}</p>
158
- <p class="text-2xl font-semibold tracking-tight tabular-nums">
158
+ <div class="text-2xl font-semibold tracking-tight tabular-nums">
159
159
  <USkeleton v-if="isLoading && data === null" class="h-8 w-32" />
160
160
  <span v-else>{{ formatted }}</span>
161
- </p>
161
+ </div>
162
162
  <div
163
163
  v-if="
164
164
  (showDelta && delta !== null) || previousFormatted || description
@@ -51,7 +51,7 @@ const display = computed(() => {
51
51
  class="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap tabular-nums transition-colors"
52
52
  :class="colorClass"
53
53
  >
54
- <UIcon :name="icon" class="size-3" aria-hidden="true" />
54
+ <UIcon :name="icon" class="size-3" :aria-hidden="true" />
55
55
  {{ display }}
56
56
  </span>
57
57
  </template>
@@ -32,7 +32,7 @@ const label = computed(() =>
32
32
  <UIcon
33
33
  :name="copied ? 'i-lucide-check' : 'i-lucide-copy'"
34
34
  class="size-4"
35
- aria-hidden="true"
35
+ :aria-hidden="true"
36
36
  />
37
37
  </button>
38
38
  </template>
@@ -118,7 +118,7 @@ const iconWrapClass = computed(() =>
118
118
  <div class="flex items-start justify-between gap-3">
119
119
  <div class="min-w-0 flex-1">
120
120
  <p :class="labelClass">{{ processI18n(title) }}</p>
121
- <p class="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
121
+ <div class="mt-1 text-2xl font-semibold tracking-tight tabular-nums">
122
122
  <USkeleton v-if="isLoading && data === null" class="h-8 w-24" />
123
123
  <span v-else class="inline-flex items-baseline gap-x-1">
124
124
  <span
@@ -135,7 +135,7 @@ const iconWrapClass = computed(() =>
135
135
  {{ formattedParts.unit }}
136
136
  </span>
137
137
  </span>
138
- </p>
138
+ </div>
139
139
  <div
140
140
  v-if="showTrend || compareLabel || description"
141
141
  class="mt-2 flex min-w-0 items-center gap-x-2"
@@ -153,7 +153,7 @@ const iconWrapClass = computed(() =>
153
153
  <UIcon
154
154
  :name="resolvedIcon"
155
155
  :class="isStat ? 'size-4' : 'size-5'"
156
- aria-hidden="true"
156
+ :aria-hidden="true"
157
157
  />
158
158
  </div>
159
159
  </div>
@@ -49,7 +49,11 @@ defineSlots<{
49
49
  class="grid size-[26px] shrink-0 place-items-center"
50
50
  :class="selected === item.value ? 'text-primary' : 'text-toned'"
51
51
  >
52
- <UIcon :name="item.icon" class="size-[18px]" aria-hidden="true" />
52
+ <UIcon
53
+ :name="item.icon"
54
+ class="size-[18px]"
55
+ :aria-hidden="true"
56
+ />
53
57
  </span>
54
58
  <span class="min-w-0">
55
59
  <span class="text-highlighted block text-sm font-semibold">
@@ -98,7 +98,7 @@ function onKeydown(event: KeyboardEvent, index: number) {
98
98
  v-if="item.icon"
99
99
  :name="item.icon"
100
100
  class="size-3.5"
101
- aria-hidden="true"
101
+ :aria-hidden="true"
102
102
  />
103
103
  {{ item.label }}
104
104
  </button>
@@ -68,7 +68,7 @@ const resolvedIcon = computed(() => props.icon || DEFAULT_ICON[props.status]);
68
68
  :class="RING[status]"
69
69
  class="grid size-16 shrink-0 place-items-center rounded-full border-2"
70
70
  >
71
- <UIcon :name="resolvedIcon" class="size-[30px]" aria-hidden="true" />
71
+ <UIcon :name="resolvedIcon" class="size-[30px]" :aria-hidden="true" />
72
72
  </div>
73
73
  <div>
74
74
  <p
@@ -9,9 +9,11 @@ import type {
9
9
  } from "@tanstack/vue-table";
10
10
  import { refDebounced, watchDebounced } from "@vueuse/core";
11
11
  import {
12
- QUICK_ACTION_ADD,
12
+ QUICK_ACTION_BUTTON_KEY,
13
13
  QUICK_ACTION_COMPONENT_KEY,
14
14
  QUICK_ACTION_QUERY_KEY,
15
+ type QuickActionIntent,
16
+ readQuickActionIntent,
15
17
  } from "../../types/quick-actions";
16
18
  import type {
17
19
  KanbanConfig,
@@ -957,24 +959,45 @@ async function clearQuickActionQuery() {
957
959
  const {
958
960
  [QUICK_ACTION_QUERY_KEY]: _discarded,
959
961
  [QUICK_ACTION_COMPONENT_KEY]: _discardedComponent,
962
+ [QUICK_ACTION_BUTTON_KEY]: _discardedButton,
960
963
  ...rest
961
964
  } = route.query;
962
965
  await router.replace({ query: rest });
963
966
  }
964
967
 
965
- // A page may mount several table views, so the quick action always names the
966
- // one it means — resolved server-side — and only that one answers.
967
- function isQuickActionTarget(): boolean {
968
- return route.query[QUICK_ACTION_COMPONENT_KEY] === componentId;
968
+ // The button is looked up among the ones the server kept for this caller: a
969
+ // button it stripped cannot be pressed through the URL either.
970
+ function pressCustomButton(id: string) {
971
+ const button = customButtons?.find((candidate) => candidate.id === id);
972
+ if (button) handleCustomButton(button);
969
973
  }
970
974
 
975
+ const quickActionHandlers: {
976
+ [K in QuickActionIntent["kind"]]: (
977
+ intent: Extract<QuickActionIntent, { kind: K }>,
978
+ ) => void;
979
+ } = {
980
+ add: () => handleRowAdd(),
981
+ button: (intent) => pressCustomButton(intent.button),
982
+ };
983
+
984
+ // Watched key by key rather than through the parsed intent: a fresh intent
985
+ // object on every unrelated query change would press the button twice.
971
986
  watch(
972
- () => route.query[QUICK_ACTION_QUERY_KEY],
973
- async (value) => {
974
- if (value !== QUICK_ACTION_ADD) return;
975
- if (!isQuickActionTarget()) return;
987
+ [
988
+ QUICK_ACTION_QUERY_KEY,
989
+ QUICK_ACTION_COMPONENT_KEY,
990
+ QUICK_ACTION_BUTTON_KEY,
991
+ ].map((key) => () => route.query[key]),
992
+ async () => {
993
+ const intent = readQuickActionIntent(route.query, componentId);
994
+ if (!intent) return;
976
995
  await clearQuickActionQuery();
977
- handleRowAdd();
996
+ // Keyed by the intent's own discriminant, so the handler always matches.
997
+ const handler = quickActionHandlers[intent.kind] as (
998
+ value: QuickActionIntent,
999
+ ) => void;
1000
+ handler(intent);
978
1001
  },
979
1002
  { immediate: true },
980
1003
  );
@@ -0,0 +1,26 @@
1
+ import type { ChartSeries, DonutRecord } from "./types";
2
+
3
+ type ChartSeriesEntry = ChartSeries["data"][number];
4
+
5
+ interface LabelledPoint {
6
+ label?: string;
7
+ x?: unknown;
8
+ y?: unknown;
9
+ }
10
+
11
+ function toDonutRecord(entry: ChartSeriesEntry): DonutRecord {
12
+ if (typeof entry === "number") {
13
+ return { label: String(entry), value: entry };
14
+ }
15
+ const point = entry as LabelledPoint;
16
+ return {
17
+ label: point.label ?? String(point.x ?? ""),
18
+ value: Number(point.y ?? 0),
19
+ };
20
+ }
21
+
22
+ /** Turns one chart series into the slices of a circular chart. */
23
+ export function toDonutRecords(series: ChartSeries | undefined): DonutRecord[] {
24
+ if (!series || !Array.isArray(series.data)) return [];
25
+ return series.data.map(toDonutRecord);
26
+ }