@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
@@ -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
@@ -7,11 +7,13 @@ import type {
7
7
  Table,
8
8
  VisibilityState,
9
9
  } from "@tanstack/vue-table";
10
- import { refDebounced, watchDebounced } from "@vueuse/core";
10
+ import { refDebounced, useActiveElement, 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
  );
@@ -987,6 +1010,7 @@ defineShortcuts(
987
1010
  globalFilter,
988
1011
  rowSelect,
989
1012
  data: shownData,
1013
+ activeElement: useActiveElement(),
990
1014
  }),
991
1015
  );
992
1016
 
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { useActiveElement } from "@vueuse/core";
2
3
  import type { TreeNode, TreeProps } from "../../composables/tree/types";
3
4
  import {
4
5
  TreeSelectionBehavior,
@@ -120,15 +121,15 @@ function navigateTree(direction: TreeNavigationDirection) {
120
121
  navigationHandlers[direction]();
121
122
  }
122
123
 
123
- const shortcutHandlers: Record<string, () => void> = (
124
- buildTreeShortcuts as (...args: unknown[]) => Record<string, () => void>
125
- )({
126
- navigateTree,
127
- props,
128
- items,
129
- selected,
130
- });
131
- defineShortcuts(shortcutHandlers as Record<string, (() => void) | undefined>);
124
+ defineShortcuts(
125
+ buildTreeShortcuts({
126
+ navigateTree,
127
+ props,
128
+ items,
129
+ selected,
130
+ activeElement: useActiveElement(),
131
+ }),
132
+ );
132
133
  </script>
133
134
 
134
135
  <template>
@@ -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
+ }
@@ -1,8 +1,10 @@
1
1
  import type { TabItem } from "./item";
2
2
 
3
+ // A tab set is also used directly in hand-written pages, outside any
4
+ // registered component: the ids exist only for server-driven ones.
3
5
  interface TabComponentProps {
4
- componentId: string;
5
- pageId: string;
6
+ componentId?: string;
7
+ pageId?: string;
6
8
  routeParams?: Record<string, string>;
7
9
  watchActions?: WatchAction[];
8
10
  childCount?: number;
@@ -80,14 +80,14 @@ export function useTab(
80
80
  };
81
81
 
82
82
  watch(activeTab, async (newTab, oldTab) => {
83
- if (newTab !== oldTab) {
83
+ if (newTab === oldTab) return;
84
+ if (props.componentId) {
84
85
  sendComponentEvent(TabEvents.TAB_CHANGE, props.componentId, {
85
86
  previousTab: Number.parseInt(oldTab, 10),
86
87
  currentTab: Number.parseInt(newTab, 10),
87
88
  });
88
-
89
- await updateUrl(newTab);
90
89
  }
90
+ await updateUrl(newTab);
91
91
  });
92
92
 
93
93
  watch(
@@ -1,4 +1,4 @@
1
- import type { ShallowRef } from "vue";
1
+ import { computed, type ComputedRef, type ShallowRef } from "vue";
2
2
  import type { RowSelectionState } from "@tanstack/vue-table";
3
3
  import type {
4
4
  TableRowActionOptions,
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  TABLE_VIEW_ESCAPE_METADATA,
17
17
  createTableViewEscapeShortcut,
18
+ isTableViewEscapeActive,
18
19
  } from "./tableViewEscape";
19
20
  import {
20
21
  TABLE_VIEW_SHIFT_A_METADATA,
@@ -42,8 +43,11 @@ interface BuildTableViewShortcutsParams<T extends Data> {
42
43
  globalFilter: ShallowRef<string | undefined>;
43
44
  rowSelect: Ref<RowSelectionState>;
44
45
  data: Ref<{ results: T[] } | null | undefined>;
46
+ activeElement: Ref<Element | null | undefined>;
45
47
  }
46
48
 
49
+ type TableViewShortcuts = Record<string, (() => void) | undefined>;
50
+
47
51
  export function buildTableViewShortcuts<T extends Data>({
48
52
  refresh,
49
53
  tableProps,
@@ -51,11 +55,20 @@ export function buildTableViewShortcuts<T extends Data>({
51
55
  globalFilter,
52
56
  rowSelect,
53
57
  data,
54
- }: BuildTableViewShortcutsParams<T>): Record<string, () => void> {
55
- return {
58
+ activeElement,
59
+ }: BuildTableViewShortcutsParams<T>): ComputedRef<TableViewShortcuts> {
60
+ const shortcuts = {
56
61
  meta_r: createTableViewMetaRShortcut(refresh),
57
62
  shift_n: createTableViewShiftNShortcut(tableProps, newRow),
58
- escape: createTableViewEscapeShortcut(globalFilter, rowSelect),
59
63
  shift_a: createTableViewShiftAShortcut(tableProps, data, rowSelect),
60
64
  };
65
+ const escape = createTableViewEscapeShortcut(globalFilter, rowSelect);
66
+ // An unregistered key is left untouched by `defineShortcuts`, so Escape
67
+ // reaches overlays whenever the table has nothing to clear.
68
+ return computed(() => ({
69
+ ...shortcuts,
70
+ escape: isTableViewEscapeActive(globalFilter, rowSelect, activeElement)
71
+ ? escape
72
+ : undefined,
73
+ }));
61
74
  }
@@ -1,12 +1,34 @@
1
1
  import type { ShallowRef } from "vue";
2
2
  import type { RowSelectionState } from "@tanstack/vue-table";
3
+ import { isFocusInsideOverlay } from "../../../utils/overlayFocus";
3
4
 
4
5
  export const TABLE_VIEW_ESCAPE_METADATA: ShortcutMetadata = {
5
6
  key: ["$keyboard.escape"],
6
7
  descriptionKey: "$dms.shortcuts.tableview.escape.description",
7
8
  component: "$dms.components.tableview",
9
+ condition: {
10
+ descriptionKey: "$dms.shortcuts.tableview.escape.condition",
11
+ },
8
12
  };
9
13
 
14
+ /**
15
+ * Nuxt UI's `defineShortcuts` calls `preventDefault()` on every key it
16
+ * handles, and reka-ui's DismissableLayer ignores a prevented Escape. The
17
+ * shortcut is therefore only registered when there is a search or a
18
+ * selection to clear, and never while focus sits inside an overlay, so the
19
+ * overlay above the table keeps closing on Escape.
20
+ */
21
+ export function isTableViewEscapeActive(
22
+ globalFilter: ShallowRef<string | undefined>,
23
+ rowSelect: Ref<RowSelectionState>,
24
+ activeElement: Ref<Element | null | undefined>,
25
+ ): boolean {
26
+ if (isFocusInsideOverlay(activeElement.value)) return false;
27
+ return (
28
+ globalFilter.value !== undefined || Object.keys(rowSelect.value).length > 0
29
+ );
30
+ }
31
+
10
32
  export function createTableViewEscapeShortcut(
11
33
  globalFilter: ShallowRef<string | undefined>,
12
34
  rowSelect: Ref<RowSelectionState>,
@@ -19,7 +19,7 @@ export interface ReadonlyBehavior {
19
19
  export interface TableViewColumn {
20
20
  id: string;
21
21
  header: string;
22
- visible?: boolean;
22
+ isVisible?: boolean;
23
23
  order?: number;
24
24
  accessorKey: string;
25
25
  listable: boolean;
@@ -3,6 +3,8 @@ import type { ActionTarget } from "./action-target";
3
3
  export type CustomButtonVariant = "solid" | "outline" | "ghost" | "link";
4
4
 
5
5
  export interface CustomButton {
6
+ /** Set when a quick action can press the button. */
7
+ id?: string;
6
8
  label: string;
7
9
  icon?: string;
8
10
  variant?: CustomButtonVariant;
@@ -1,3 +1,4 @@
1
+ import { computed, type ComputedRef } from "vue";
1
2
  import {
2
3
  TREE_ARROW_DOWN_METADATA,
3
4
  createTreeArrowDownShortcut,
@@ -11,7 +12,11 @@ import { TREE_SPACE_METADATA, createTreeSpaceShortcut } from "./treeSpace";
11
12
  import { TREE_HOME_METADATA, createTreeHomeShortcut } from "./treeHome";
12
13
  import { TREE_END_METADATA, createTreeEndShortcut } from "./treeEnd";
13
14
  import { TREE_SHIFT_A_METADATA, createTreeShiftAShortcut } from "./treeShiftA";
14
- import { TREE_ESCAPE_METADATA, createTreeEscapeShortcut } from "./treeEscape";
15
+ import {
16
+ TREE_ESCAPE_METADATA,
17
+ createTreeEscapeShortcut,
18
+ isTreeEscapeActive,
19
+ } from "./treeEscape";
15
20
 
16
21
  export const TREE_SHORTCUTS_METADATA = [
17
22
  TREE_ARROW_DOWN_METADATA,
@@ -29,15 +34,19 @@ interface BuildTreeShortcutsParams {
29
34
  props: TreeProps;
30
35
  items: ComputedRef<TreeNode[]>;
31
36
  selected: Ref<TreeNode | TreeNode[] | undefined>;
37
+ activeElement: Ref<Element | null | undefined>;
32
38
  }
33
39
 
40
+ type TreeShortcuts = Record<string, (() => void) | undefined>;
41
+
34
42
  export function buildTreeShortcuts({
35
43
  navigateTree,
36
44
  props,
37
45
  items,
38
46
  selected,
39
- }: BuildTreeShortcutsParams): Record<string, () => void> {
40
- return {
47
+ activeElement,
48
+ }: BuildTreeShortcutsParams): ComputedRef<TreeShortcuts> {
49
+ const shortcuts = {
41
50
  arrowdown: createTreeArrowDownShortcut(navigateTree),
42
51
  arrowup: createTreeArrowUpShortcut(navigateTree),
43
52
  enter: createTreeEnterShortcut(navigateTree),
@@ -45,6 +54,12 @@ export function buildTreeShortcuts({
45
54
  home: createTreeHomeShortcut(navigateTree),
46
55
  end: createTreeEndShortcut(navigateTree),
47
56
  shift_a: createTreeShiftAShortcut(props, items, selected),
48
- escape: createTreeEscapeShortcut(props, selected),
49
57
  };
58
+ const escape = createTreeEscapeShortcut(props, selected);
59
+ // An unregistered key is left untouched by `defineShortcuts`, so Escape
60
+ // reaches overlays whenever the tree has nothing to clear.
61
+ return computed(() => ({
62
+ ...shortcuts,
63
+ escape: isTreeEscapeActive(selected, activeElement) ? escape : undefined,
64
+ }));
50
65
  }
@@ -1,9 +1,30 @@
1
+ import { isFocusInsideOverlay } from "../../../utils/overlayFocus";
2
+
1
3
  export const TREE_ESCAPE_METADATA: ShortcutMetadata = {
2
4
  key: ["$keyboard.escape"],
3
5
  descriptionKey: "$dms.shortcuts.tree.escape.description",
4
6
  component: "$dms.components.tree",
7
+ condition: {
8
+ descriptionKey: "$dms.shortcuts.tree.escape.condition",
9
+ },
5
10
  };
6
11
 
12
+ /**
13
+ * Nuxt UI's `defineShortcuts` calls `preventDefault()` on every key it
14
+ * handles, and reka-ui's DismissableLayer ignores a prevented Escape. The
15
+ * shortcut is therefore only registered when there is a selection to clear,
16
+ * and never while focus sits inside an overlay, so the overlay above the tree
17
+ * keeps closing on Escape.
18
+ */
19
+ export function isTreeEscapeActive(
20
+ selected: Ref<TreeNode | TreeNode[] | undefined>,
21
+ activeElement: Ref<Element | null | undefined>,
22
+ ): boolean {
23
+ if (isFocusInsideOverlay(activeElement.value)) return false;
24
+ const value = selected.value;
25
+ return Array.isArray(value) ? value.length > 0 : value != null;
26
+ }
27
+
7
28
  export function createTreeEscapeShortcut(
8
29
  props: TreeProps,
9
30
  selected: Ref<TreeNode | TreeNode[] | undefined>,
@@ -1,7 +1,7 @@
1
1
  /**
2
- * How an `openForm` quick action reaches its table view: it routes to the
3
- * action's page with these query parameters, and the table view opens its
4
- * creation form when it mounts. Carried by the URL rather than a window event
2
+ * How an `openForm` or `button` quick action reaches its table view: it routes
3
+ * to the action's page with these query parameters, and the table view opens
4
+ * its creation form, or presses the named button, when it mounts. Carried by the URL rather than a window event
5
5
  * so the action works from any page — the listener would otherwise have to
6
6
  * exist before the navigation that creates it.
7
7
  */
@@ -9,3 +9,38 @@ export const QUICK_ACTION_QUERY_KEY = "quickAction";
9
9
  export const QUICK_ACTION_ADD = "add";
10
10
  /** Which table view answers, when the page mounts more than one. */
11
11
  export const QUICK_ACTION_COMPONENT_KEY = "quickActionComponent";
12
+ export const QUICK_ACTION_BUTTON = "button";
13
+ /** Which of the table view's custom buttons a `button` quick action presses. */
14
+ export const QUICK_ACTION_BUTTON_KEY = "quickActionButton";
15
+
16
+ export type QuickActionIntent =
17
+ | { kind: "add" }
18
+ | { kind: "button"; button: string };
19
+
20
+ type QuickActionQuery = Record<string, unknown>;
21
+
22
+ const intentReaders: Record<
23
+ string,
24
+ (query: QuickActionQuery) => QuickActionIntent | undefined
25
+ > = {
26
+ [QUICK_ACTION_ADD]: () => ({ kind: "add" }),
27
+ [QUICK_ACTION_BUTTON]: (query) => {
28
+ const button = query[QUICK_ACTION_BUTTON_KEY];
29
+ return typeof button === "string" ? { kind: "button", button } : undefined;
30
+ },
31
+ };
32
+
33
+ /**
34
+ * What the route asks the table view `componentId` to run. A page may mount
35
+ * several table views, so the quick action always names the one it means —
36
+ * resolved server-side — and only that one answers.
37
+ */
38
+ export function readQuickActionIntent(
39
+ query: QuickActionQuery,
40
+ componentId: string | undefined,
41
+ ): QuickActionIntent | undefined {
42
+ if (query[QUICK_ACTION_COMPONENT_KEY] !== componentId) return undefined;
43
+ const action = query[QUICK_ACTION_QUERY_KEY];
44
+ if (typeof action !== "string") return undefined;
45
+ return intentReaders[action]?.(query);
46
+ }
@@ -0,0 +1,16 @@
1
+ // Overlays reka-ui dismisses on Escape (modals, slideovers, popovers, menus,
2
+ // selects). They move focus inside themselves while open.
3
+ const OVERLAY_SELECTOR =
4
+ '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]';
5
+
6
+ /**
7
+ * Nuxt UI's `defineShortcuts` calls `preventDefault()` on every key it
8
+ * handles, and reka-ui's DismissableLayer ignores a prevented Escape. A
9
+ * component-level Escape shortcut must therefore stay unregistered while
10
+ * focus sits inside an overlay, so the overlay keeps closing on Escape.
11
+ */
12
+ export function isFocusInsideOverlay(
13
+ activeElement: Element | null | undefined,
14
+ ): boolean {
15
+ return Boolean(activeElement?.closest(OVERLAY_SELECTOR));
16
+ }
@@ -292,7 +292,8 @@
292
292
  "description": "Refresh table data"
293
293
  },
294
294
  "escape": {
295
- "description": "Close search or deselect rows"
295
+ "description": "Close search or deselect rows",
296
+ "condition": "Only when a search or a row selection is active and no dialog is open"
296
297
  },
297
298
  "shift_n": {
298
299
  "description": "Create new row",
@@ -330,7 +331,8 @@
330
331
  "description": "Go to last node"
331
332
  },
332
333
  "escape": {
333
- "description": "Deselect all nodes"
334
+ "description": "Deselect all nodes",
335
+ "condition": "Only when nodes are selected and no dialog is open"
334
336
  },
335
337
  "shift_a": {
336
338
  "description": "Select all nodes recursively",
@@ -293,7 +293,8 @@
293
293
  "description": "Actualiser les données du tableau"
294
294
  },
295
295
  "escape": {
296
- "description": "Fermer la recherche ou désélectionner les lignes"
296
+ "description": "Fermer la recherche ou désélectionner les lignes",
297
+ "condition": "Uniquement quand une recherche ou une sélection est active et qu'aucune fenêtre n'est ouverte"
297
298
  },
298
299
  "shift_n": {
299
300
  "description": "Créer une nouvelle ligne",
@@ -331,7 +332,8 @@
331
332
  "description": "Aller au dernier nœud"
332
333
  },
333
334
  "escape": {
334
- "description": "Désélectionner tous les nœuds"
335
+ "description": "Désélectionner tous les nœuds",
336
+ "condition": "Uniquement quand des nœuds sont sélectionnés et qu'aucune fenêtre n'est ouverte"
335
337
  },
336
338
  "shift_a": {
337
339
  "description": "Sélectionner tous les nœuds récursivement",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@antelopejs/core": ">=1.9.1 <2",
62
- "@antelopejs/dms-frontend": ">=0.2.8 <1.0.0",
62
+ "@antelopejs/dms-frontend": ">=0.3.2 <1.0.0",
63
63
  "@eslint/js": "^8.57.0",
64
64
  "@inertiajs/vue3": "^3.7.0",
65
65
  "@nuxt/ui": "4.10.0",
@@ -124,8 +124,8 @@ importers:
124
124
  specifier: '>=1.9.1 <2'
125
125
  version: 1.9.1(@types/node@24.8.1)
126
126
  '@antelopejs/dms-frontend':
127
- specifier: '>=0.2.8 <1.0.0'
128
- version: 0.2.8(@antelopejs/core@1.9.1(@types/node@24.8.1))
127
+ specifier: '>=0.3.2 <1.0.0'
128
+ version: 0.3.2(@antelopejs/core@1.9.1(@types/node@24.8.1))
129
129
  '@eslint/js':
130
130
  specifier: ^8.57.0
131
131
  version: 8.57.0
@@ -200,8 +200,8 @@ packages:
200
200
  resolution: {integrity: sha512-MPLJak0rPluV4lgsdsO5ba03sgTxIGSncajBcoaoppuppQmksdpaemojIHiVLJ4LiG0iiH21Roaed8XXy1XkYQ==}
201
201
  hasBin: true
202
202
 
203
- '@antelopejs/dms-frontend@0.2.8':
204
- resolution: {integrity: sha512-7qrlf2M3J26DZxGZVvM/3qjPDeDfoRMrE7HEPFxWav8Pe1QFct7LtgY7NmKup9XdrRyUw4nqckxvW8UZDSjpeg==}
203
+ '@antelopejs/dms-frontend@0.3.2':
204
+ resolution: {integrity: sha512-BNqLqW3UbfeOMnd4RkEynr/1mMWFhwB8dt1JbggaGte8/KrJQ3MQ5TOovnACMmrqAD7FxsMLBsNqjzGjX4O4ZA==}
205
205
  engines: {node: '>=20.12.0'}
206
206
  hasBin: true
207
207
  peerDependencies:
@@ -4331,7 +4331,7 @@ snapshots:
4331
4331
  transitivePeerDependencies:
4332
4332
  - '@types/node'
4333
4333
 
4334
- '@antelopejs/dms-frontend@0.2.8(@antelopejs/core@1.9.1(@types/node@24.8.1))':
4334
+ '@antelopejs/dms-frontend@0.3.2(@antelopejs/core@1.9.1(@types/node@24.8.1))':
4335
4335
  dependencies:
4336
4336
  boxen: 5.1.2
4337
4337
  chalk: 4.1.2