@antelopejs/dms 0.4.5 → 0.4.7

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 (178) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/hooks/invite-extensions.js +2 -2
  3. package/dist/hooks/invite-extensions.js.map +1 -1
  4. package/dist/implementations/dms/layout-banners.d.ts +18 -0
  5. package/dist/implementations/dms/layout-banners.js +60 -0
  6. package/dist/implementations/dms/layout-banners.js.map +1 -0
  7. package/dist/implementations/dms/page.d.ts +2 -0
  8. package/dist/implementations/dms/page.js +82 -27
  9. package/dist/implementations/dms/page.js.map +1 -1
  10. package/dist/implementations/dms/quick-actions.d.ts +5 -0
  11. package/dist/implementations/dms/quick-actions.js.map +1 -1
  12. package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
  13. package/dist/implementations/dms/resolver-timeout.js +13 -0
  14. package/dist/implementations/dms/resolver-timeout.js.map +1 -0
  15. package/dist/implementations/dms/saas-mode.d.ts +8 -0
  16. package/dist/implementations/dms/saas-mode.js +20 -0
  17. package/dist/implementations/dms/saas-mode.js.map +1 -0
  18. package/dist/implementations/dms-auth/index.d.ts +2 -1
  19. package/dist/implementations/dms-auth/index.js +19 -8
  20. package/dist/implementations/dms-auth/index.js.map +1 -1
  21. package/dist/implementations/dms-html-render/service-jwt.js +2 -1
  22. package/dist/implementations/dms-html-render/service-jwt.js.map +1 -1
  23. package/dist/index.js +2 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/pages/settings/users/index.d.ts +1 -0
  26. package/dist/pages/settings/users/index.js +1 -0
  27. package/dist/pages/settings/users/index.js.map +1 -1
  28. package/dist/pages/settings/users/invite-extension-routes.d.ts +4 -0
  29. package/dist/pages/settings/users/invite-extension-routes.js +57 -0
  30. package/dist/pages/settings/users/invite-extension-routes.js.map +1 -0
  31. package/dist/pages/settings/users/invites.d.ts +2 -1
  32. package/dist/pages/settings/users/invites.js +17 -5
  33. package/dist/pages/settings/users/invites.js.map +1 -1
  34. package/dist/pages/settings/users/members.d.ts +4 -1
  35. package/dist/pages/settings/users/members.js +10 -6
  36. package/dist/pages/settings/users/members.js.map +1 -1
  37. package/dist/pages/settings/users/notifications.js +1 -1
  38. package/dist/pages/settings/users/notifications.js.map +1 -1
  39. package/dist/pages/settings/users/quick-actions.d.ts +1 -0
  40. package/dist/pages/settings/users/quick-actions.js +22 -0
  41. package/dist/pages/settings/users/quick-actions.js.map +1 -0
  42. package/dist/routes/auth/signup.d.ts +2 -0
  43. package/dist/routes/auth/signup.js +7 -2
  44. package/dist/routes/auth/signup.js.map +1 -1
  45. package/dist/test/integration/invite-extension-edit.test.d.ts +1 -0
  46. package/dist/test/integration/invite-extension-edit.test.js +122 -0
  47. package/dist/test/integration/invite-extension-edit.test.js.map +1 -0
  48. package/dist/test/integration/saas-mode-ownership.test.d.ts +1 -0
  49. package/dist/test/integration/saas-mode-ownership.test.js +81 -0
  50. package/dist/test/integration/saas-mode-ownership.test.js.map +1 -0
  51. package/dist/test/integration/tenant-lifecycle.test.js +38 -0
  52. package/dist/test/integration/tenant-lifecycle.test.js.map +1 -1
  53. package/dist/test/unit/implementations/dms/layout-banners.test.d.ts +1 -0
  54. package/dist/test/unit/implementations/dms/layout-banners.test.js +217 -0
  55. package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -0
  56. package/dist/test/unit/implementations/dms/quick-actions-button.test.d.ts +1 -0
  57. package/dist/test/unit/implementations/dms/quick-actions-button.test.js +220 -0
  58. package/dist/test/unit/implementations/dms/quick-actions-button.test.js.map +1 -0
  59. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js +1 -1
  60. package/dist/test/unit/implementations/dms-html-render/generate-html.test.js.map +1 -1
  61. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.d.ts +1 -0
  62. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js +37 -0
  63. package/dist/test/unit/implementations/dms-html-render/service-jwt.test.js.map +1 -0
  64. package/dist/test/unit/interfaces/dms/invite-extension.test.js +138 -12
  65. package/dist/test/unit/interfaces/dms/invite-extension.test.js.map +1 -1
  66. package/dist/test/unit/interfaces/dms/password-policy.test.d.ts +1 -0
  67. package/dist/test/unit/interfaces/dms/password-policy.test.js +43 -0
  68. package/dist/test/unit/interfaces/dms/password-policy.test.js.map +1 -0
  69. package/dist/test/unit/interfaces/dms/saas-mode.test.d.ts +1 -0
  70. package/dist/test/unit/interfaces/dms/saas-mode.test.js +42 -0
  71. package/dist/test/unit/interfaces/dms/saas-mode.test.js.map +1 -0
  72. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.d.ts +1 -0
  73. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js +53 -0
  74. package/dist/test/unit/interfaces/dms/tenant-invite-email.test.js.map +1 -0
  75. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.d.ts +1 -0
  76. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js +80 -0
  77. package/dist/test/unit/interfaces/dms-base/table-view-form-slots.test.js.map +1 -0
  78. package/dist/test/unit/pages/settings/invite-member-quick-action.test.d.ts +1 -0
  79. package/dist/test/unit/pages/settings/invite-member-quick-action.test.js +104 -0
  80. package/dist/test/unit/pages/settings/invite-member-quick-action.test.js.map +1 -0
  81. package/dist/test/unit/pages/settings/notifications-access-gate.test.d.ts +1 -0
  82. package/dist/test/unit/pages/settings/notifications-access-gate.test.js +78 -0
  83. package/dist/test/unit/pages/settings/notifications-access-gate.test.js.map +1 -0
  84. package/dist/test/unit/signup-account-guard.test.js +20 -2
  85. package/dist/test/unit/signup-account-guard.test.js.map +1 -1
  86. package/dist/test/unit/utils/admin-invite-email.test.d.ts +1 -0
  87. package/dist/test/unit/utils/admin-invite-email.test.js +69 -0
  88. package/dist/test/unit/utils/admin-invite-email.test.js.map +1 -0
  89. package/dist/utils/admin-invite-email.d.ts +14 -0
  90. package/dist/utils/admin-invite-email.js +51 -0
  91. package/dist/utils/admin-invite-email.js.map +1 -0
  92. package/dist/utils/index.d.ts +0 -1
  93. package/dist/utils/index.js +0 -1
  94. package/dist/utils/index.js.map +1 -1
  95. package/dist/validation/auth.schema.d.ts +3 -3
  96. package/dist/validation/auth.schema.js +1 -1
  97. package/dist/validation/auth.schema.js.map +1 -1
  98. package/dist/validation/password.d.ts +0 -1
  99. package/dist/validation/password.js +6 -3
  100. package/dist/validation/password.js.map +1 -1
  101. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/signup.vue +37 -8
  102. package/frontend-vue/layers/dms-auth/i18n/locales/auth-en-GB.json +3 -1
  103. package/frontend-vue/layers/dms-auth/i18n/locales/auth-fr-FR.json +3 -1
  104. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +4 -0
  105. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +4 -0
  106. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardBanners.vue +50 -0
  107. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +16 -13
  108. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidebar.vue +2 -2
  109. package/frontend-vue/layers/dms-layout/app/build/components/notification/NotificationPopover.vue +11 -5
  110. package/frontend-vue/layers/dms-layout/app/build/components/notification/widgetRequest.ts +20 -0
  111. package/frontend-vue/layers/dms-layout/app/composables/layout/useLayoutBanners.ts +59 -0
  112. package/frontend-vue/layers/dms-layout/app/composables/page/useQuickActions.ts +15 -2
  113. package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -0
  114. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +54 -9
  115. package/frontend-vue/layers/dms-layout/app/error.vue +17 -2
  116. package/frontend-vue/layers/dms-layout/app/types/page.ts +22 -0
  117. package/frontend-vue/layers/dms-layout/app/utils/breadcrumb.ts +48 -0
  118. package/frontend-vue/layers/dms-layout/app/utils/favorites.ts +94 -0
  119. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +21 -2
  120. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +21 -2
  121. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/index.ts +1 -1
  122. package/frontend-vue/layers/dms-onboarding/app/composables/onboarding/{useOnboardingMiddleware.ts → useOnboarding.ts} +0 -12
  123. package/frontend-vue/layers/dms-onboarding/app/custom-pages/onboarding.vue +4 -1
  124. package/frontend-vue/layers/dms-onboarding/app/middleware/onboarding.global.ts +21 -4
  125. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +13 -3
  126. package/frontend-vue/layers/dms-ui/app/build/components/containers/modal/DynamicModal.vue +13 -3
  127. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +2 -7
  128. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/columnVisibility.ts +13 -0
  129. package/frontend-vue/layers/dms-ui/app/components/PasswordStrength.vue +1 -1
  130. package/frontend-vue/layers/dms-ui/app/components/activity/ActivityItem.vue +1 -1
  131. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +1 -1
  132. package/frontend-vue/layers/dms-ui/app/components/card/NavCard.vue +2 -2
  133. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +6 -16
  134. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +2 -2
  135. package/frontend-vue/layers/dms-ui/app/components/chart/internal/TrendBadge.vue +1 -1
  136. package/frontend-vue/layers/dms-ui/app/components/copy/CopyButton.vue +1 -1
  137. package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +3 -3
  138. package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +5 -1
  139. package/frontend-vue/layers/dms-ui/app/components/segmented/Segmented.vue +1 -1
  140. package/frontend-vue/layers/dms-ui/app/components/status/StatusSummary.vue +1 -1
  141. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +35 -11
  142. package/frontend-vue/layers/dms-ui/app/components/tree/Tree.vue +10 -9
  143. package/frontend-vue/layers/dms-ui/app/composables/chart/donutRecords.ts +26 -0
  144. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +4 -2
  145. package/frontend-vue/layers/dms-ui/app/composables/tab/useTab.ts +3 -3
  146. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/index.ts +17 -4
  147. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewEscape.ts +22 -0
  148. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/column.ts +1 -1
  149. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/custom-button.ts +2 -0
  150. package/frontend-vue/layers/dms-ui/app/composables/tree/shortcuts/index.ts +19 -4
  151. package/frontend-vue/layers/dms-ui/app/composables/tree/shortcuts/treeEscape.ts +21 -0
  152. package/frontend-vue/layers/dms-ui/app/types/quick-actions.ts +38 -3
  153. package/frontend-vue/layers/dms-ui/app/utils/overlayFocus.ts +16 -0
  154. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -2
  155. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -2
  156. package/frontend-vue/package.json +1 -1
  157. package/frontend-vue/pnpm-lock.yaml +5 -5
  158. package/frontend-vue/tests/admin-invite-email.test.ts +86 -0
  159. package/frontend-vue/tests/breadcrumb-target.test.ts +50 -0
  160. package/frontend-vue/tests/chart-nested-donut.test.ts +97 -0
  161. package/frontend-vue/tests/error-page-message.test.ts +83 -0
  162. package/frontend-vue/tests/favorite-page.test.ts +104 -0
  163. package/frontend-vue/tests/icon-aria-hidden.test.ts +29 -0
  164. package/frontend-vue/tests/layout-banners.test.ts +224 -0
  165. package/frontend-vue/tests/notification-popover-errors.test.ts +147 -0
  166. package/frontend-vue/tests/onboarding-middleware.test.ts +103 -0
  167. package/frontend-vue/tests/password-strength-meter.test.ts +33 -0
  168. package/frontend-vue/tests/quick-action-intent.test.ts +74 -0
  169. package/frontend-vue/tests/quick-actions.test.ts +23 -0
  170. package/frontend-vue/tests/signup-invitation-link.test.ts +126 -0
  171. package/frontend-vue/tests/skeleton-paragraph.test.ts +30 -0
  172. package/frontend-vue/tests/tab-without-component-id.test.ts +51 -0
  173. package/frontend-vue/tests/table-column-visibility.test.ts +20 -0
  174. package/frontend-vue/tests/table-view-escape-shortcut.test.ts +141 -0
  175. package/frontend-vue/tests/tree-escape-shortcut.test.ts +138 -0
  176. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  177. package/package.json +2 -2
  178. package/skills/dms/REFERENCE.md +1 -1
@@ -1,31 +1,76 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { useI18n } from "vue-i18n";
2
4
  import EmailLayout from "./components/EmailLayout.vue";
3
5
  import EmailButton from "./components/EmailButton.vue";
4
6
  import { emailStyles } from "./components/index";
5
7
 
6
- defineProps<{
8
+ const props = defineProps<{
7
9
  email: string;
8
10
  signupLink: string;
9
- expiresIn: string;
11
+ expiresInDays: number;
12
+ workspaceName?: string;
13
+ inviterName?: string;
14
+ platformName?: string;
10
15
  }>();
16
+
17
+ const { t, locale } = useI18n();
18
+
19
+ const joinTarget = computed(() => {
20
+ const { workspaceName, platformName } = props;
21
+ if (workspaceName && platformName) {
22
+ return t("emails.admin_invite.workspace_on_platform", {
23
+ workspace: workspaceName,
24
+ platform: platformName,
25
+ });
26
+ }
27
+ return workspaceName || platformName;
28
+ });
29
+
30
+ const title = computed(() =>
31
+ props.workspaceName
32
+ ? t("emails.admin_invite.title_workspace", {
33
+ workspace: props.workspaceName,
34
+ })
35
+ : t("emails.admin_invite.title"),
36
+ );
37
+
38
+ const invitation = computed(() => {
39
+ if (!joinTarget.value) {
40
+ return t("emails.admin_invite.invitation");
41
+ }
42
+ return props.inviterName
43
+ ? t("emails.admin_invite.invitation_from_inviter", {
44
+ inviter: props.inviterName,
45
+ target: joinTarget.value,
46
+ })
47
+ : t("emails.admin_invite.invitation_to_target", {
48
+ target: joinTarget.value,
49
+ });
50
+ });
11
51
  </script>
12
52
 
13
53
  <template>
14
- <EmailLayout title="You're Invited!">
15
- <EText :style="emailStyles.text">Hello,</EText>
54
+ <EmailLayout :title="title" :lang="locale">
55
+ <EText :style="emailStyles.text">
56
+ {{ t("emails.admin_invite.greeting") }}
57
+ </EText>
16
58
 
17
59
  <EText :style="emailStyles.text">
18
- You've been invited to join our platform. Click the button below to create
19
- your account:
60
+ {{ invitation }} {{ t("emails.admin_invite.call_to_action") }}
20
61
  </EText>
21
62
 
22
- <EmailButton :href="signupLink" :expires-in="expiresIn">
23
- Create Account
63
+ <EmailButton
64
+ :href="signupLink"
65
+ :expires-in="t('emails.admin_invite.expires_in', { days: expiresInDays })"
66
+ :expires-label="t('emails.admin_invite.expires_label')"
67
+ >
68
+ {{ t("emails.admin_invite.button") }}
24
69
  </EmailButton>
25
70
 
26
71
  <template #footer>
27
72
  <EText :style="emailStyles.footer">
28
- If you didn't expect this invitation, you can safely ignore this email.
73
+ {{ t("emails.admin_invite.footer") }}
29
74
  </EText>
30
75
  </template>
31
76
  </EmailLayout>
@@ -34,6 +34,13 @@ const DEFAULT_ERROR_CONFIG = {
34
34
 
35
35
  const DEFAULT_STATUS_CODE = 500;
36
36
 
37
+ // Their cards already say what happened; the message behind them is the
38
+ // technical cause ("DMS backend request failed"), which the server has logged.
39
+ const STATUSES_WITHOUT_ERROR_MESSAGE = new Set([
40
+ HTTP_NOT_FOUND,
41
+ HTTP_FORBIDDEN,
42
+ ]);
43
+
37
44
  const props = defineProps<{
38
45
  error: DmsErrorData;
39
46
  }>();
@@ -45,6 +52,14 @@ const { loggedIn, reconcileSession, redirectToAuth } = useSessionRecovery();
45
52
 
46
53
  const isRecovering = ref(false);
47
54
 
55
+ const visibleErrorMessage = computed(() =>
56
+ STATUSES_WITHOUT_ERROR_MESSAGE.has(
57
+ props.error.statusCode || DEFAULT_STATUS_CODE,
58
+ )
59
+ ? undefined
60
+ : props.error.message,
61
+ );
62
+
48
63
  const errorConfig = computed(() => {
49
64
  const statusCode = props.error.statusCode || DEFAULT_STATUS_CODE;
50
65
  const config = ERROR_CONFIGS[statusCode] || DEFAULT_ERROR_CONFIG;
@@ -121,10 +136,10 @@ onMounted(() => {
121
136
  </p>
122
137
 
123
138
  <p
124
- v-if="!isRecovering && error.message"
139
+ v-if="!isRecovering && visibleErrorMessage"
125
140
  class="text-dimmed mb-6 text-sm"
126
141
  >
127
- {{ error.message }}
142
+ {{ visibleErrorMessage }}
128
143
  </p>
129
144
 
130
145
  <div v-if="!isRecovering" class="flex gap-3">
@@ -65,9 +65,30 @@ export type SiteLayoutTree = Omit<PageInfo, "layoutUrl"> & {
65
65
  layoutUrl?: string;
66
66
  hasAccess?: boolean;
67
67
  };
68
+ // Mirror of LayoutBannerVariant from @antelopejs/interface-dms/layout-banners.
69
+ export type LayoutBannerVariant = "info" | "warning" | "error";
70
+
71
+ /**
72
+ * Mirror of the backend's serialized layout banner. The payload only carries
73
+ * the banners whose visibility resolver passed for this request, so there is
74
+ * nothing left to decide here but the user's own dismissals.
75
+ */
76
+ export interface LayoutBanner {
77
+ key: string;
78
+ variant: LayoutBannerVariant;
79
+ order: number;
80
+ dismissible: boolean;
81
+ icon?: string;
82
+ text?: string;
83
+ component?: string;
84
+ props?: Record<string, unknown>;
85
+ }
86
+
68
87
  export type SiteLayout = {
69
88
  pages: Record<string, PageInfo>;
70
89
  categories: Record<string, CategoryInfo>;
90
+ /** Absent when the backend predates layout banners. */
91
+ banners?: LayoutBanner[];
71
92
  };
72
93
 
73
94
  export interface QuickActionCategoryInfo {
@@ -80,6 +101,7 @@ export interface QuickActionCategoryInfo {
80
101
  export type QuickActionTarget =
81
102
  | { type: "navigate"; to: string; query?: Record<string, string> }
82
103
  | { type: "openForm"; to: string; component: string }
104
+ | { type: "button"; to: string; component: string; button: string }
83
105
  | { type: "event"; name: string; payload?: unknown };
84
106
 
85
107
  /**
@@ -0,0 +1,48 @@
1
+ import type { PageValidation } from "../types/page";
2
+
3
+ export interface BreadcrumbTargetSource {
4
+ layoutUrl?: string;
5
+ validation?: PageValidation;
6
+ }
7
+
8
+ /**
9
+ * The URL that opens the page at `path`. A page that declares
10
+ * `validation.requiredQueryParams` 404s without them, so the URL carries the
11
+ * values `currentQuery` holds for them — and only those — and there is no URL
12
+ * when any is missing, empty or repeated.
13
+ */
14
+ export function buildPageTarget(
15
+ path: string,
16
+ validation: PageValidation | undefined,
17
+ currentQuery: Record<string, unknown>,
18
+ ): string | undefined {
19
+ const requiredParams = validation?.requiredQueryParams ?? [];
20
+ const carriedQuery = new URLSearchParams();
21
+
22
+ for (const param of requiredParams) {
23
+ const value = currentQuery[param];
24
+ if (typeof value !== "string" || value === "") {
25
+ return undefined;
26
+ }
27
+ carriedQuery.set(param, value);
28
+ }
29
+
30
+ const search = carriedQuery.toString();
31
+ return search ? `${path}?${search}` : path;
32
+ }
33
+
34
+ /**
35
+ * The link of a breadcrumb crumb: the page's {@link buildPageTarget}.
36
+ * Categories without a page behind them get no link.
37
+ */
38
+ export function buildBreadcrumbTarget(
39
+ path: string,
40
+ metadata: BreadcrumbTargetSource,
41
+ currentQuery: Record<string, unknown>,
42
+ ): string | undefined {
43
+ if (!metadata.layoutUrl) {
44
+ return undefined;
45
+ }
46
+
47
+ return buildPageTarget(path, metadata.validation, currentQuery);
48
+ }
@@ -0,0 +1,94 @@
1
+ import { getQuery, parseURL } from "ufo";
2
+ import type { FavoritePage } from "../composables/favorites/useFavoritePages";
3
+ import type { PageValidation } from "../types/page";
4
+ import { buildPageTarget } from "./breadcrumb";
5
+
6
+ const FAVORITE_TITLE_SEPARATOR = " · ";
7
+
8
+ export interface FavoritePageMetadata {
9
+ id?: string;
10
+ displayName?: string;
11
+ icon?: string;
12
+ validation?: PageValidation;
13
+ }
14
+
15
+ export interface FavoritePageSource {
16
+ path: string;
17
+ metadata: FavoritePageMetadata;
18
+ query: Record<string, unknown>;
19
+ translate: (text: string) => string;
20
+ }
21
+
22
+ export type FavoriteRouteResolver = (
23
+ path: string,
24
+ ) => { metadata: FavoritePageMetadata } | null;
25
+
26
+ function requiredQueryValues(
27
+ validation: PageValidation | undefined,
28
+ query: Record<string, unknown>,
29
+ ): string[] {
30
+ return (validation?.requiredQueryParams ?? []).map((param) =>
31
+ String(query[param]),
32
+ );
33
+ }
34
+
35
+ // A page narrowed by its query shows one entity out of many: its generic
36
+ // display name would label every favorite of it the same way.
37
+ function entityTitle(pageTitle: string, queryValues: string[]): string {
38
+ return [pageTitle, ...queryValues].join(FAVORITE_TITLE_SEPARATOR);
39
+ }
40
+
41
+ /**
42
+ * The favorite that reopens the page at `path` as it is showing now. A page
43
+ * requiring query parameters keeps them in its path and gets a title telling
44
+ * its entities apart; there is no favorite when a required value is missing.
45
+ */
46
+ export function buildFavoritePage(
47
+ source: FavoritePageSource,
48
+ ): FavoritePage | null {
49
+ const { path, metadata, query } = source;
50
+ const target = buildPageTarget(path, metadata.validation, query);
51
+ if (!target) {
52
+ return null;
53
+ }
54
+
55
+ const displayName = metadata.displayName || path;
56
+ const queryValues = requiredQueryValues(metadata.validation, query);
57
+ if (queryValues.length === 0) {
58
+ return {
59
+ id: metadata.id || path,
60
+ path,
61
+ title: displayName,
62
+ icon: metadata.icon,
63
+ };
64
+ }
65
+
66
+ return {
67
+ id: target,
68
+ path: target,
69
+ title: entityTitle(source.translate(displayName), queryValues),
70
+ icon: metadata.icon,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Whether a stored favorite still opens a page: its path matches one, and its
76
+ * query holds every value that page requires.
77
+ */
78
+ export function isFavoritePathValid(
79
+ favoritePath: string,
80
+ resolveRoute: FavoriteRouteResolver,
81
+ ): boolean {
82
+ const { pathname } = parseURL(favoritePath);
83
+ const route = resolveRoute(pathname);
84
+ if (!route) {
85
+ return false;
86
+ }
87
+ return (
88
+ buildPageTarget(
89
+ pathname,
90
+ route.metadata.validation,
91
+ getQuery(favoritePath),
92
+ ) !== undefined
93
+ );
94
+ }
@@ -40,7 +40,9 @@
40
40
  "soon": "Coming soon"
41
41
  },
42
42
  "quickActions": {
43
- "title": "Quick actions"
43
+ "title": "Quick actions",
44
+ "category_members": "Members",
45
+ "invite_member": "Invite a member"
44
46
  },
45
47
  "commandPalette": {
46
48
  "button": "Search…",
@@ -218,7 +220,8 @@
218
220
  "cancel_confirm_description": "Are you sure you want to cancel this invitation? The invite link will no longer work."
219
221
  },
220
222
  "error": {
221
- "not_found": "Invitation not found"
223
+ "not_found": "Invitation not found",
224
+ "resolved": "This invitation has already been accepted or cancelled"
222
225
  }
223
226
  },
224
227
  "roles": {
@@ -269,5 +272,21 @@
269
272
  "intro": "Browse the modules available on this platform.",
270
273
  "empty": "No modules are accessible to you yet.",
271
274
  "open": "Open module"
275
+ },
276
+ "emails": {
277
+ "admin_invite": {
278
+ "title": "You're Invited!",
279
+ "title_workspace": "You're invited to {workspace}",
280
+ "greeting": "Hello,",
281
+ "invitation": "You've been invited to join our platform.",
282
+ "invitation_to_target": "You've been invited to join {target}.",
283
+ "invitation_from_inviter": "{inviter} invited you to join {target}.",
284
+ "workspace_on_platform": "{workspace} on {platform}",
285
+ "call_to_action": "Click the button below to create your account:",
286
+ "button": "Create Account",
287
+ "expires_label": "This invitation expires in",
288
+ "expires_in": "{days} days",
289
+ "footer": "If you didn't expect this invitation, you can safely ignore this email."
290
+ }
272
291
  }
273
292
  }
@@ -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"
@@ -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
+ );
@@ -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: {