@antelopejs/dms 0.7.21 → 0.8.0

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 (157) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
  3. package/dist/implementations/dms-auth/account-emails.js +62 -0
  4. package/dist/implementations/dms-auth/account-emails.js.map +1 -0
  5. package/dist/implementations/dms-auth/index.d.ts +1 -3
  6. package/dist/implementations/dms-auth/index.js +15 -61
  7. package/dist/implementations/dms-auth/index.js.map +1 -1
  8. package/dist/pages/settings/users/email-change.js +3 -4
  9. package/dist/pages/settings/users/email-change.js.map +1 -1
  10. package/dist/pages/settings/users/invites.js +1 -1
  11. package/dist/pages/settings/users/invites.js.map +1 -1
  12. package/dist/pages/settings/users/members.js +1 -1
  13. package/dist/pages/settings/users/members.js.map +1 -1
  14. package/dist/pages/settings/users/notifications.js +1 -0
  15. package/dist/pages/settings/users/notifications.js.map +1 -1
  16. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
  17. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
  18. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
  19. package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
  20. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
  21. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
  22. package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
  23. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  24. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
  25. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
  26. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
  27. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
  28. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
  29. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
  30. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  31. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
  32. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
  33. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
  34. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
  36. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
  38. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
  39. package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
  40. package/dist/test/unit/utils/email-subjects.test.js +27 -0
  41. package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
  42. package/dist/utils/admin-invite-email.js +2 -4
  43. package/dist/utils/admin-invite-email.js.map +1 -1
  44. package/dist/utils/email-change-mail.d.ts +1 -1
  45. package/dist/utils/email-change-mail.js +11 -6
  46. package/dist/utils/email-change-mail.js.map +1 -1
  47. package/dist/utils/email-subjects.d.ts +3 -0
  48. package/dist/utils/email-subjects.js +32 -0
  49. package/dist/utils/email-subjects.js.map +1 -0
  50. package/dist/utils/export-jobs.js +15 -10
  51. package/dist/utils/export-jobs.js.map +1 -1
  52. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
  53. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
  54. package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
  55. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
  56. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
  57. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
  58. package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
  59. package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
  60. package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
  61. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
  62. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
  63. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
  64. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
  65. package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
  66. package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
  67. package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
  68. package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
  69. package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
  70. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
  71. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
  72. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
  73. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
  74. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
  75. package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
  76. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
  77. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
  78. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
  79. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
  80. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
  81. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
  82. package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
  83. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
  84. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
  85. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
  86. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
  87. package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
  88. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
  89. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
  90. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
  91. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
  92. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
  93. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
  94. package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
  95. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
  96. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
  97. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
  98. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
  99. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
  100. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
  101. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
  102. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
  103. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
  104. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
  105. package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
  106. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
  107. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
  108. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
  109. package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
  110. package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
  111. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
  112. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
  113. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
  114. package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
  115. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
  116. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
  117. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
  118. package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
  119. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
  120. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
  121. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
  122. package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
  123. package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
  124. package/frontend-vue/tests/builtin-emails.test.ts +141 -0
  125. package/frontend-vue/tests/cascader.test.ts +27 -0
  126. package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
  127. package/frontend-vue/tests/chart-colors.test.ts +8 -0
  128. package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
  129. package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
  130. package/frontend-vue/tests/check-list-marker.test.ts +58 -0
  131. package/frontend-vue/tests/confirm-from.test.ts +1 -1
  132. package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
  133. package/frontend-vue/tests/context-links.test.ts +2 -2
  134. package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
  135. package/frontend-vue/tests/download-action-target.test.ts +2 -2
  136. package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
  137. package/frontend-vue/tests/footer-config.test.ts +135 -0
  138. package/frontend-vue/tests/form-actions.test.ts +7 -0
  139. package/frontend-vue/tests/form-entry-order.test.ts +45 -0
  140. package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
  141. package/frontend-vue/tests/form-load-error.test.ts +38 -1
  142. package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
  143. package/frontend-vue/tests/header-actions.test.ts +55 -9
  144. package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
  145. package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
  146. package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
  147. package/frontend-vue/tests/save-bar.test.ts +15 -3
  148. package/frontend-vue/tests/save-status-error.test.ts +2 -2
  149. package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
  150. package/frontend-vue/tests/table-row-hover.test.ts +2 -2
  151. package/frontend-vue/tests/table-view-sources.test.ts +1 -1
  152. package/frontend-vue/tests/template-tokens.test.ts +235 -0
  153. package/frontend-vue/tests/url-interpolation.test.ts +6 -6
  154. package/package.json +2 -2
  155. package/skills/dms/SKILL.md +2 -1
  156. package/skills/dms-pages/REFERENCE.md +2 -2
  157. package/skills/dms-project/SKILL.md +0 -1
@@ -8,14 +8,16 @@ import DmsBlockActions, {
8
8
  import { useActionTargets } from "../../build/composables/actions/useActionTargets";
9
9
  import { useChartFetch } from "../../composables/chart/useChartFetch";
10
10
  import { refreshPageBlocks } from "../../utils/blockRefresh";
11
+ import {
12
+ BANNER_TONE_ICONS,
13
+ type BannerTone,
14
+ } from "../../utils/bannerToneIcons";
11
15
  import type { CustomButton } from "../../composables/table-view/types/custom-button";
12
16
  import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
13
17
  import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
14
18
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
15
19
  import type { BlockText } from "../../../../dms-core/app/types/composed-text";
16
20
 
17
- type BannerTone = "info" | "success" | "warning" | "error" | "primary";
18
-
19
21
  /** What a banner shows (interface-dms `BannerContent`). */
20
22
  interface BannerContent {
21
23
  /** A string (`$` for an i18n key) or a composed text. */
@@ -83,14 +85,6 @@ const DISMISSED_MAX_KEYS = 40;
83
85
  const FALLBACK_COMPONENT_ID = "banner";
84
86
  const FALLBACK_PAGE_ID = "page";
85
87
 
86
- const TONE_ICONS: Record<BannerTone, string> = {
87
- info: "i-ph-info",
88
- success: "i-ph-check-circle",
89
- warning: "i-ph-warning",
90
- error: "i-ph-warning-circle",
91
- primary: "i-ph-sparkle",
92
- };
93
-
94
88
  const { state: watchState } = useWatch(
95
89
  props.watchActions || [],
96
90
  props.componentId,
@@ -183,7 +177,7 @@ function press(action: BlockButtonAction): void {
183
177
  :description="
184
178
  banner.description ? processText(banner.description) : undefined
185
179
  "
186
- :icon="banner.icon ?? TONE_ICONS[banner.tone]"
180
+ :icon="banner.icon ?? BANNER_TONE_ICONS[banner.tone]"
187
181
  :tone="banner.tone"
188
182
  :size="banner.size"
189
183
  :dismissible="banner.dismissible"
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import { gridColumnsTemplate } from "../grid/columns";
3
4
  import {
4
5
  useCategoryNavCards,
@@ -217,7 +218,7 @@ const { t } = useI18n();
217
218
  >
218
219
  <DmsNavCard
219
220
  class="h-full"
220
- :to="card.to"
221
+ :to="pageLink(card.to)"
221
222
  :icon="card.icon"
222
223
  :icon-tone="card.iconTone"
223
224
  :title="card.title"
@@ -24,7 +24,14 @@ interface Emits {
24
24
  (event: "drawn"): void;
25
25
  }
26
26
 
27
+ /** The ApexCharts instance the wrapper exposes as `chart`. */
28
+ interface ApexCore {
29
+ /** Apex's own (debounced) window resize handler: redraws at the container width. */
30
+ windowResizeHandler: () => void;
31
+ }
32
+
27
33
  interface ApexChartInstance {
34
+ chart: ApexCore | null;
28
35
  updateOptions: (
29
36
  options: Record<string, unknown>,
30
37
  redrawPaths?: boolean,
@@ -39,6 +46,7 @@ const emit = defineEmits<Emits>();
39
46
  const REDRAW_PATHS = false;
40
47
  const ANIMATE = true;
41
48
  const UPDATE_SYNCED_CHARTS = false;
49
+ const WIDTH_TOLERANCE_PX = 1;
42
50
 
43
51
  const { locale } = useI18n();
44
52
 
@@ -57,6 +65,7 @@ onMounted(() => {
57
65
  void ensureApexLocale(locale.value).catch(() => undefined);
58
66
  });
59
67
 
68
+ const hostRef = ref<HTMLElement | null>(null);
60
69
  const chartRef = ref<ApexChartInstance | null>(null);
61
70
 
62
71
  /**
@@ -123,10 +132,31 @@ watch(
123
132
  function onDrawn() {
124
133
  emit("drawn");
125
134
  }
135
+
136
+ function isDrawnAtStaleWidth(host: HTMLElement): boolean {
137
+ const svg = host.querySelector("svg.apexcharts-svg");
138
+ if (!svg) return false;
139
+ const drawnWidth = svg.getBoundingClientRect().width;
140
+ const hostWidth = host.getBoundingClientRect().width;
141
+ return Math.abs(drawnWidth - hostWidth) > WIDTH_TOLERANCE_PX;
142
+ }
143
+
144
+ /**
145
+ * Apex drops its container's resizes while an animation runs, so a sidebar
146
+ * collapsed or expanded during one leaves the chart at its old width, past
147
+ * its card when the container shrank, until the next window resize. Once the
148
+ * animation is over, a chart drawn at another width than its host redraws.
149
+ */
150
+ function redrawIfResizedWhileAnimating() {
151
+ const host = hostRef.value;
152
+ const chart = chartRef.value?.chart;
153
+ if (!host || !chart || !isDrawnAtStaleWidth(host)) return;
154
+ chart.windowResizeHandler();
155
+ }
126
156
  </script>
127
157
 
128
158
  <template>
129
- <div class="dms-apex">
159
+ <div ref="hostRef" class="dms-apex">
130
160
  <ApexChart
131
161
  ref="chartRef"
132
162
  :type="apexType"
@@ -135,6 +165,7 @@ function onDrawn() {
135
165
  :series="apexSeries"
136
166
  @mounted="onDrawn"
137
167
  @updated="onDrawn"
168
+ @animation-end="redrawIfResizedWhileAnimating"
138
169
  />
139
170
  </div>
140
171
  </template>
@@ -75,7 +75,7 @@ const ICONS: Record<
75
75
  ok: "i-ph-check",
76
76
  warn: "i-ph-warning",
77
77
  error: "i-ph-x",
78
- pending: "i-ph-x",
78
+ pending: "i-ph-circle",
79
79
  info: "i-ph-arrow-right",
80
80
  },
81
81
  };
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import type { FormProps } from "../../composables/form/types/props";
3
4
  import type {
4
5
  FormData,
@@ -64,6 +65,7 @@ import DmsFormErrorSummary from "../../build/components/form/FormErrorSummary.vu
64
65
  import DmsFormLoadError from "../../build/components/form/FormLoadError.vue";
65
66
  import {
66
67
  type FormRecordLoad,
68
+ missingRecordFailure,
67
69
  trackFormRecordLoad,
68
70
  } from "../../build/composables/form/formRecordLoad";
69
71
  import DmsFormRequiredLegend from "./FormRequiredLegend.vue";
@@ -492,7 +494,7 @@ function cancelForm(): void {
492
494
  window.history.back();
493
495
  return;
494
496
  }
495
- if (props.backTo) void router.push(props.backTo);
497
+ if (props.backTo) void router.push(pageLink(props.backTo));
496
498
  }
497
499
 
498
500
  const validators = new Set<FormValidator>();
@@ -573,7 +575,15 @@ if (props.fetchUrl) {
573
575
  });
574
576
  }
575
577
 
576
- const loadFailure = computed(() => recordLoad?.failure.value ?? null);
578
+ const loadFailure = computed(
579
+ () =>
580
+ recordLoad?.failure.value ??
581
+ missingRecordFailure({
582
+ requiresRecord: props.requiresRecord,
583
+ fetchUrl: props.fetchUrl,
584
+ resolvedFetchUrl: resolvedFetchUrl.value,
585
+ }),
586
+ );
577
587
  const isRetryingLoad = computed(() => recordLoad?.retrying.value ?? false);
578
588
 
579
589
  /** Load the record again after a failure; it is what the form starts from. */
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
2
3
  import {
3
4
  buildCascaderTree,
4
5
  CASCADER_FETCH_LIMIT,
@@ -68,8 +69,10 @@ interface CascaderResponse {
68
69
 
69
70
  const { data, status, execute } = await useDmsAsyncData(
70
71
  `cascader-${instanceId}`,
71
- () =>
72
- $authFetch<CascaderResponse>(props.searchUrl, {
72
+ () => {
73
+ const url = pageRequestUrl(props.searchUrl);
74
+ if (!url) return Promise.resolve({ results: [], total: 0 });
75
+ return $authFetch<CascaderResponse>(url, {
73
76
  params: { limit: CASCADER_FETCH_LIMIT },
74
77
  onRequestError: ({ error }: { error: Error }) => {
75
78
  toast.add({
@@ -78,7 +81,8 @@ const { data, status, execute } = await useDmsAsyncData(
78
81
  description: error.message,
79
82
  });
80
83
  },
81
- }),
84
+ });
85
+ },
82
86
  { lazy: true, immediate: false },
83
87
  );
84
88
 
@@ -349,6 +349,12 @@ const acceptString = computed(() =>
349
349
  props.constraints?.allowedMimetypes?.join(","),
350
350
  );
351
351
 
352
+ const dropPromptKey = computed(() =>
353
+ props.multiple
354
+ ? "dms.form.file.drop_prompt_multiple"
355
+ : "dms.form.file.drop_prompt",
356
+ );
357
+
352
358
  const maxSizeFormatted = computed(() => {
353
359
  if (!props.constraints?.maxSize) return null;
354
360
  return formatFileSize(props.constraints.maxSize);
@@ -373,6 +379,7 @@ onScopeDispose(() => {
373
379
  :disabled="disabled || isUploading"
374
380
  :color="color"
375
381
  :highlight="highlight"
382
+ :label="$t(dropPromptKey)"
376
383
  v-bind="dropZoneAria"
377
384
  >
378
385
  <template v-if="maxSizeFormatted" #description>
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
2
3
  import type { SelectMenuItem } from "@nuxt/ui";
3
4
  import { refDebounced } from "@vueuse/core";
4
5
  import {
@@ -225,7 +226,9 @@ interface CountryOptions {
225
226
  }
226
227
 
227
228
  async function fetchCountryOptions(): Promise<CountryOptions> {
228
- const data = await $authFetch<CountryApiResponse>(props.searchUrl!, {
229
+ const url = pageRequestUrl(props.searchUrl);
230
+ if (!url) return { results: [], total: 0 };
231
+ const data = await $authFetch<CountryApiResponse>(url, {
229
232
  params: {
230
233
  q: countrySearchTermDebounced.value,
231
234
  },
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
2
3
  import { getCurrentInstance, h, render } from "vue";
3
4
  import { useFormField } from "@nuxt/ui/composables/useFormField";
4
5
  import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
@@ -84,10 +85,11 @@ function checkSyntax(text: string): void {
84
85
 
85
86
  async function loadCompletions(): Promise<CodeEditorCompletion[]> {
86
87
  const fixed = props.completions ?? [];
87
- if (!props.completionsUrl) return fixed;
88
+ const url = pageRequestUrl(props.completionsUrl);
89
+ if (!url) return fixed;
88
90
  const response = await $authFetch<
89
91
  CodeEditorCompletion[] | CompletionsResponse
90
- >(props.completionsUrl).catch(() => []);
92
+ >(url).catch(() => []);
91
93
  const fetched = Array.isArray(response) ? response : (response.items ?? []);
92
94
  return [...fixed, ...fetched];
93
95
  }
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
2
3
  import { useConfirm } from "../../../composables/confirm/useConfirm";
3
4
  import DmsCopyButton from "../../copy/CopyButton.vue";
4
5
 
@@ -43,7 +44,7 @@ const masked = computed(() => {
43
44
  });
44
45
 
45
46
  async function rotate(): Promise<void> {
46
- const url = props.rotateUrl;
47
+ const url = pageRequestUrl(props.rotateUrl);
47
48
  if (!url) return;
48
49
  const confirmed = await confirm({
49
50
  title: t("dms.form.secret.rotate_title"),
@@ -95,7 +96,7 @@ async function rotate(): Promise<void> {
95
96
  </UInput>
96
97
  <DmsCopyButton v-if="props.copyable && value" :value="value" />
97
98
  <UButton
98
- v-if="props.rotateUrl"
99
+ v-if="pageRequestUrl(props.rotateUrl)"
99
100
  icon="i-ph-arrows-clockwise"
100
101
  :label="t('dms.form.secret.rotate')"
101
102
  color="neutral"
@@ -3,6 +3,7 @@
3
3
  lang="ts"
4
4
  generic="T extends ArrayOrNested<SelectMenuItem>, M extends boolean = false"
5
5
  >
6
+ import { pageRequestUrl } from "../../../build/composables/page/pageUrlValues";
6
7
  import { useForwardPropsEmits } from "reka-ui";
7
8
  import type {
8
9
  SelectMenuProps,
@@ -193,7 +194,9 @@ interface RelationItems {
193
194
  }
194
195
 
195
196
  async function fetchRelationItems(): Promise<RelationItems> {
196
- const data = await $authFetch<RelationResponse>(props.searchUrl, {
197
+ const url = pageRequestUrl(props.searchUrl);
198
+ if (!url) return { results: [], total: 0 };
199
+ const data = await $authFetch<RelationResponse>(url, {
197
200
  params: {
198
201
  search: searchTermDebounced.value || undefined,
199
202
  },
@@ -5,6 +5,18 @@ const SINGLE_COLUMN = 1;
5
5
  /** Track floor below which a column stops being usable, as a CSS length. */
6
6
  export const GRID_DEFAULT_MIN_COLUMN_WIDTH = "240px";
7
7
 
8
+ const UNITLESS_NUMBER = /^[+-]?(\d+\.?\d*|\.\d+)$/;
9
+
10
+ /**
11
+ * A length usable inside `calc()`. A bare `0` is a valid `gap` but not a
12
+ * length in a calculation, where it would void the whole template and leave
13
+ * the grid on a single column; a unitless number reads as pixels.
14
+ */
15
+ function calcLength(value: string): string {
16
+ const trimmed = value.trim();
17
+ return UNITLESS_NUMBER.test(trimmed) ? `${trimmed}px` : trimmed;
18
+ }
19
+
8
20
  /**
9
21
  * The column track template a `Grid` and every `GridRow` inside it share.
10
22
  *
@@ -26,8 +38,8 @@ export function gridColumnsTemplate(
26
38
  minColumnWidth: string,
27
39
  ): string {
28
40
  const columns = Math.max(maxColumns, SINGLE_COLUMN);
29
- const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${gap}) / ${columns}`;
30
- return `repeat(auto-fill, minmax(min(100%, max(${minColumnWidth}, ${columnShare})), 1fr))`;
41
+ const columnShare = `(100% - ${columns - SINGLE_COLUMN} * ${calcLength(gap)}) / ${columns}`;
42
+ return `repeat(auto-fill, minmax(min(100%, max(${calcLength(minColumnWidth)}, ${columnShare})), 1fr))`;
31
43
  }
32
44
 
33
45
  /** Custom property a `Grid` / `GridRow` hands its cells: the tracks left. */
@@ -43,7 +55,7 @@ function trackBreakpoint(
43
55
  gap: string,
44
56
  minColumnWidth: string,
45
57
  ): string {
46
- return `calc(${columns} * ${minColumnWidth} + ${columns - SINGLE_COLUMN} * ${gap})`;
58
+ return `calc(${columns} * ${calcLength(minColumnWidth)} + ${columns - SINGLE_COLUMN} * ${calcLength(gap)})`;
47
59
  }
48
60
 
49
61
  /** A container name / attribute value built from any id. */
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import { computed } from "vue";
3
4
  import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
4
5
  import DmsStatusPill from "../status-pill/StatusPill.vue";
@@ -216,7 +217,7 @@ function keyOf(item: KeyValueItem, index: number): string {
216
217
  <template v-else>
217
218
  <component
218
219
  :is="item.to ? DmsAutoLink : 'span'"
219
- :to="item.to"
220
+ :to="pageLink(item.to)"
220
221
  class="min-w-0"
221
222
  :class="item.to && item.type !== 'status' && 'hover:underline'"
222
223
  >
@@ -125,6 +125,7 @@ const theme = tv({
125
125
  dot: "size-2 rounded-full bg-(--dms-accent-fill) shadow-[0_0_0_3px_var(--dms-accent-tint)]",
126
126
  body: "min-w-0 flex-1",
127
127
  title: "flex items-center gap-2",
128
+ titleText: "",
128
129
  description: "text-muted mt-0.5 text-[12.5px]",
129
130
  // The "·" closes each entry followed by another (rather than opening it),
130
131
  // so a wrapped line never starts with a separator.
@@ -188,7 +189,11 @@ const theme = tv({
188
189
  // end, so the title and meta wrap there instead.
189
190
  truncate: {
190
191
  true: {
191
- title: "truncate max-sm:whitespace-normal",
192
+ title:
193
+ "truncate flex-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
194
+ // The text box, not the flex title, carries the ellipsis: a flex
195
+ // container only clips its overflowing children.
196
+ titleText: "truncate max-sm:whitespace-normal",
192
197
  meta: "flex-nowrap overflow-hidden whitespace-nowrap max-sm:flex-wrap max-sm:whitespace-normal",
193
198
  detail: "truncate max-sm:whitespace-normal",
194
199
  },
@@ -281,7 +286,9 @@ const unreadText = computed(
281
286
  :class="ui.title()"
282
287
  :title="props.truncate ? props.title : undefined"
283
288
  >
284
- <slot>{{ props.title }}</slot>
289
+ <slot>
290
+ <span :class="ui.titleText()">{{ props.title }}</span>
291
+ </slot>
285
292
  </div>
286
293
  <p v-if="hasDescription" :class="ui.description()">
287
294
  <slot name="description">{{ props.description }}</slot>
@@ -3,10 +3,11 @@ import type { SaveStatusState } from "./SaveStatus.vue";
3
3
 
4
4
  interface InstantSaveBadgeProps {
5
5
  /**
6
- * Save activity of the page, for a subtle "just saved" flash: the icon
7
- * spins while a change is being written and turns into a tick once it is,
8
- * and the pill turns red while a save failed (the control says which).
9
- * The wording never changes. Defaults to `idle` (a lightning bolt).
6
+ * Save activity of the page, for a subtle "just saved" flash: the pill
7
+ * reads "Saving" with a spinning icon while a change is being written,
8
+ * then "Saved instantly" with a tick once it is, and turns red with "Not
9
+ * saved" while a save failed (the control says which). Defaults to `idle`
10
+ * (a lightning bolt and "Saved instantly").
10
11
  */
11
12
  state?: SaveStatusState;
12
13
  }
@@ -23,6 +24,13 @@ const ICONS: Record<SaveStatusState, string> = {
23
24
  error: "i-ph-warning-circle",
24
25
  };
25
26
 
27
+ const LABELS: Record<SaveStatusState, string> = {
28
+ idle: "dms.save_bar.instant",
29
+ saving: "dms.save_bar.saving",
30
+ saved: "dms.save_bar.instant",
31
+ error: "dms.save_bar.not_saved",
32
+ };
33
+
26
34
  const TONE_CLASSES = {
27
35
  ok: "border-success/40 bg-success/10 text-success",
28
36
  error: "border-error/40 bg-error/10 text-error",
@@ -49,6 +57,6 @@ const TONE_CLASSES = {
49
57
  class="size-3.5 shrink-0"
50
58
  :class="{ 'animate-spin': props.state === 'saving' }"
51
59
  />
52
- {{ t("dms.save_bar.instant") }}
60
+ {{ t(LABELS[props.state]) }}
53
61
  </span>
54
62
  </template>
@@ -40,7 +40,7 @@ interface SaveBarProps {
40
40
  kind?: FormKind;
41
41
  /**
42
42
  * Whether anything in the form can be changed: an `action` form nothing in
43
- * which can be changed shows its submit alone, all the time.
43
+ * which can be changed never offers Reset.
44
44
  */
45
45
  resettable?: boolean;
46
46
  }
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import { computed } from "vue";
3
4
  import type { IconWellTone } from "../icon-well/IconWell.vue";
4
5
  import DmsStatCell from "../../build/components/stat/StatCell.vue";
@@ -146,7 +147,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
146
147
  :icon="item.icon"
147
148
  :detail="item.detail"
148
149
  :detail-tone="item.detailTone"
149
- :to="item.to"
150
+ :to="pageLink(item.to)"
150
151
  size="sm"
151
152
  :tone="wellTone(item)"
152
153
  :loading="props.loading"
@@ -175,7 +176,7 @@ function wellTone(item: StatGroupItem): IconWellTone {
175
176
  :icon="item.icon"
176
177
  :detail="item.detail"
177
178
  :detail-tone="item.detailTone"
178
- :to="item.to"
179
+ :to="pageLink(item.to)"
179
180
  :tone="wellTone(item)"
180
181
  :loading="props.loading"
181
182
  stack
@@ -1,20 +1,28 @@
1
1
  <script setup lang="ts">
2
2
  import {
3
- buildCascaderPathLabels,
4
3
  CASCADER_FETCH_LIMIT,
5
4
  CASCADER_PATH_SEPARATOR,
5
+ cascaderValuePath,
6
6
  resolveCascaderKeyMapping,
7
7
  type CascaderKeyMapping,
8
8
  } from "../../utils/cascader";
9
+ import { MAX_RELATION_BADGES } from "../../build/composables/data-types/relationBadges";
9
10
 
10
11
  const DEFAULT_VALUE_KEY = "_id";
11
12
 
12
13
  interface CascaderPathProps {
14
+ /** One picked value, or the list a multiple cascader holds. */
13
15
  value: unknown;
14
16
  searchUrl?: string;
15
17
  keyMapping?: Partial<CascaderKeyMapping>;
16
18
  }
17
19
 
20
+ /** A picked value of a multiple cascader: its name, its path on hover. */
21
+ interface CascaderBadge {
22
+ label: string;
23
+ title: string;
24
+ }
25
+
18
26
  const props = defineProps<CascaderPathProps>();
19
27
 
20
28
  const { $authFetch } = useAuthFetch();
@@ -31,22 +39,6 @@ const keyMapping = computed(() =>
31
39
  }),
32
40
  );
33
41
 
34
- const valueObject = computed(() =>
35
- isObject(props.value) ? (props.value as Record<string, unknown>) : undefined,
36
- );
37
-
38
- const valueId = computed(() => {
39
- const raw = valueObject.value
40
- ? valueObject.value[keyMapping.value.value]
41
- : props.value;
42
- return raw === undefined || raw === null ? undefined : String(raw);
43
- });
44
-
45
- const ownLabel = computed(() => {
46
- const label = valueObject.value?.[keyMapping.value.label];
47
- return label === undefined || label === null ? undefined : String(label);
48
- });
49
-
50
42
  const { data } = await useDmsAsyncData(
51
43
  `cascader-path-${props.searchUrl}`,
52
44
  () =>
@@ -58,21 +50,51 @@ const { data } = await useDmsAsyncData(
58
50
  { lazy: true },
59
51
  );
60
52
 
61
- const pathLabel = computed(() => {
62
- if (!valueId.value) return ownLabel.value ?? "";
63
- const rows = data.value?.results;
64
- if (rows) {
65
- const labels = buildCascaderPathLabels(
66
- rows,
67
- keyMapping.value,
68
- valueId.value,
69
- );
70
- if (labels.length > 0) return labels.join(CASCADER_PATH_SEPARATOR);
71
- }
72
- return ownLabel.value ?? valueId.value;
73
- });
53
+ const pathOf = (item: unknown): string[] =>
54
+ cascaderValuePath(item, keyMapping.value, data.value?.results);
55
+
56
+ const pathLabel = computed(() =>
57
+ pathOf(props.value).join(CASCADER_PATH_SEPARATOR),
58
+ );
59
+
60
+ const badges = computed<CascaderBadge[]>(() =>
61
+ (Array.isArray(props.value) ? props.value : [])
62
+ .map(pathOf)
63
+ .filter((path) => path.length > 0)
64
+ .map((path) => ({
65
+ label: path.at(-1) ?? "",
66
+ title: path.join(CASCADER_PATH_SEPARATOR),
67
+ })),
68
+ );
69
+ const visibleBadges = computed(() =>
70
+ badges.value.slice(0, MAX_RELATION_BADGES),
71
+ );
72
+ const hiddenBadges = computed(() => badges.value.slice(MAX_RELATION_BADGES));
74
73
  </script>
75
74
 
76
75
  <template>
77
- <span class="truncate" :title="pathLabel">{{ pathLabel }}</span>
76
+ <span
77
+ v-if="Array.isArray(props.value)"
78
+ class="inline-flex flex-wrap items-center gap-1"
79
+ >
80
+ <UBadge
81
+ v-for="badge in visibleBadges"
82
+ :key="badge.title"
83
+ :label="badge.label"
84
+ :title="badge.title"
85
+ color="neutral"
86
+ variant="subtle"
87
+ size="sm"
88
+ class="max-w-40"
89
+ />
90
+ <UBadge
91
+ v-if="hiddenBadges.length"
92
+ :label="`+${hiddenBadges.length}`"
93
+ :title="hiddenBadges.map((badge) => badge.title).join(', ')"
94
+ color="neutral"
95
+ variant="subtle"
96
+ size="sm"
97
+ />
98
+ </span>
99
+ <span v-else class="truncate" :title="pathLabel">{{ pathLabel }}</span>
78
100
  </template>
@@ -9,7 +9,14 @@ import {
9
9
 
10
10
  const DEFAULT_BAR_RADIUS = 4;
11
11
  const DEFAULT_BAR_WIDTH_PCT = "60%";
12
- const DEFAULT_DONUT_HOLE = "76%";
12
+ const HUNDRED_PERCENT = 100;
13
+ /** Ring thickness, in percent of the radius, of a donut without `arcWidth`. */
14
+ const DEFAULT_ARC_WIDTH = 24;
15
+ /**
16
+ * The thinnest ring, in percent of the radius, a slice label ("33.0%") fits
17
+ * in: on a thinner one its white text runs past the ring onto the card.
18
+ */
19
+ const MIN_LABELLED_ARC_WIDTH = 35;
13
20
  const DEFAULT_RADIAL_HOLE = "60%";
14
21
  const CENTER_NAME_FONT_SIZE = "10.5px";
15
22
  const CENTER_NAME_FONT_WEIGHT = 600;
@@ -17,7 +24,6 @@ const CENTER_VALUE_FONT_SIZE = "20px";
17
24
  const CENTER_VALUE_FONT_WEIGHT = 650;
18
25
  const HEATMAP_RADIUS = 4;
19
26
  const HEATMAP_DEFAULT_INTENSITY = 0.6;
20
- const HUNDRED_PERCENT = 100;
21
27
 
22
28
  type PlotBuilder = (props: UseApexChartInput) => Record<string, unknown>;
23
29
 
@@ -47,8 +53,13 @@ function buildBarPlotOptions(
47
53
  }
48
54
 
49
55
  function donutSizeFor(arcWidth: number | undefined): string {
50
- if (arcWidth === undefined) return DEFAULT_DONUT_HOLE;
51
- return `${HUNDRED_PERCENT - arcWidth}%`;
56
+ return `${HUNDRED_PERCENT - (arcWidth ?? DEFAULT_ARC_WIDTH)}%`;
57
+ }
58
+
59
+ /** Whether a slice label fits inside the chart's ring; always true off donuts. */
60
+ export function ringFitsSliceLabels(input: UseApexChartInput): boolean {
61
+ if (input.type !== "donut") return true;
62
+ return (input.arcWidth ?? DEFAULT_ARC_WIDTH) >= MIN_LABELLED_ARC_WIDTH;
52
63
  }
53
64
 
54
65
  /** v2 donut centre: mono eyebrow over a 20px value. */