@antelopejs/dms 0.7.21 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/implementations/dms/resolver-timeout.d.ts +1 -0
  3. package/dist/implementations/dms/resolver-timeout.js +13 -3
  4. package/dist/implementations/dms/resolver-timeout.js.map +1 -1
  5. package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
  6. package/dist/implementations/dms-auth/account-emails.js +62 -0
  7. package/dist/implementations/dms-auth/account-emails.js.map +1 -0
  8. package/dist/implementations/dms-auth/index.d.ts +1 -3
  9. package/dist/implementations/dms-auth/index.js +15 -61
  10. package/dist/implementations/dms-auth/index.js.map +1 -1
  11. package/dist/pages/settings/users/email-change.js +3 -4
  12. package/dist/pages/settings/users/email-change.js.map +1 -1
  13. package/dist/pages/settings/users/invites.js +1 -1
  14. package/dist/pages/settings/users/invites.js.map +1 -1
  15. package/dist/pages/settings/users/members.js +1 -1
  16. package/dist/pages/settings/users/members.js.map +1 -1
  17. package/dist/pages/settings/users/notifications.js +1 -0
  18. package/dist/pages/settings/users/notifications.js.map +1 -1
  19. package/dist/test/antelope.test.js +2 -1
  20. package/dist/test/antelope.test.js.map +1 -1
  21. package/dist/test/helpers/db.js +19 -11
  22. package/dist/test/helpers/db.js.map +1 -1
  23. package/dist/test/integration/auth/two-factor-codes.test.js +4 -2
  24. package/dist/test/integration/auth/two-factor-codes.test.js.map +1 -1
  25. package/dist/test/unit/implementations/dms/dynamic-menu-hardening.test.js +8 -5
  26. package/dist/test/unit/implementations/dms/dynamic-menu-hardening.test.js.map +1 -1
  27. package/dist/test/unit/implementations/dms/layout-banners.test.js +20 -7
  28. package/dist/test/unit/implementations/dms/layout-banners.test.js.map +1 -1
  29. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
  30. package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
  31. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
  32. package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
  33. package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
  34. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  35. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
  36. package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
  37. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
  38. package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
  39. package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
  40. package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
  41. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
  42. package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
  43. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
  44. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  45. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +14 -11
  46. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  47. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
  48. package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
  49. package/dist/test/unit/utils/email-subjects.test.js +27 -0
  50. package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
  51. package/dist/utils/admin-invite-email.js +2 -4
  52. package/dist/utils/admin-invite-email.js.map +1 -1
  53. package/dist/utils/email-change-mail.d.ts +1 -1
  54. package/dist/utils/email-change-mail.js +11 -6
  55. package/dist/utils/email-change-mail.js.map +1 -1
  56. package/dist/utils/email-subjects.d.ts +3 -0
  57. package/dist/utils/email-subjects.js +32 -0
  58. package/dist/utils/email-subjects.js.map +1 -0
  59. package/dist/utils/export-jobs.js +15 -10
  60. package/dist/utils/export-jobs.js.map +1 -1
  61. package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
  62. package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
  63. package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
  64. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
  65. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
  66. package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
  67. package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
  68. package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
  69. package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
  70. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
  71. package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
  72. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
  73. package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
  74. package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
  75. package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
  76. package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
  77. package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
  78. package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
  79. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
  80. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
  81. package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
  82. package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
  83. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +27 -36
  84. package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
  85. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
  86. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
  87. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
  88. package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
  89. package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
  90. package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
  91. package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
  92. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
  93. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
  94. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -12
  95. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
  96. package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
  97. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
  98. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
  99. package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
  100. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
  101. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
  102. package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
  103. package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
  104. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
  105. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
  106. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
  107. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
  108. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
  109. package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
  110. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
  111. package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +14 -6
  112. package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +52 -3
  113. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
  114. package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
  115. package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
  116. package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
  117. package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
  118. package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
  119. package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
  120. package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
  121. package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
  122. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
  123. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
  124. package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
  125. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +8 -6
  126. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
  127. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -5
  128. package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
  129. package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
  130. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
  131. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
  132. package/frontend-vue/package.json +1 -1
  133. package/frontend-vue/pnpm-lock.yaml +5 -5
  134. package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
  135. package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
  136. package/frontend-vue/tests/builtin-emails.test.ts +141 -0
  137. package/frontend-vue/tests/cascader.test.ts +27 -0
  138. package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
  139. package/frontend-vue/tests/chart-colors.test.ts +8 -0
  140. package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
  141. package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
  142. package/frontend-vue/tests/check-list-marker.test.ts +58 -0
  143. package/frontend-vue/tests/confirm-from.test.ts +1 -1
  144. package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
  145. package/frontend-vue/tests/context-links.test.ts +2 -2
  146. package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
  147. package/frontend-vue/tests/download-action-target.test.ts +2 -2
  148. package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
  149. package/frontend-vue/tests/footer-config.test.ts +135 -0
  150. package/frontend-vue/tests/form-actions.test.ts +7 -0
  151. package/frontend-vue/tests/form-enclosing-form.test.ts +0 -21
  152. package/frontend-vue/tests/form-entry-order.test.ts +45 -0
  153. package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
  154. package/frontend-vue/tests/form-load-error.test.ts +38 -23
  155. package/frontend-vue/tests/grid-responsive-columns.test.ts +39 -31
  156. package/frontend-vue/tests/header-actions.test.ts +55 -9
  157. package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
  158. package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
  159. package/frontend-vue/tests/nuxt-ui-aria-labels.test.ts +1 -28
  160. package/frontend-vue/tests/period-live-pause.test.ts +0 -13
  161. package/frontend-vue/tests/period-max-days.test.ts +0 -14
  162. package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
  163. package/frontend-vue/tests/row-side-panel.test.ts +0 -12
  164. package/frontend-vue/tests/save-bar.test.ts +15 -3
  165. package/frontend-vue/tests/save-status-error.test.ts +2 -2
  166. package/frontend-vue/tests/table-cell-wrap.test.ts +0 -12
  167. package/frontend-vue/tests/table-density-menu.test.ts +0 -21
  168. package/frontend-vue/tests/table-live-hold.test.ts +0 -44
  169. package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
  170. package/frontend-vue/tests/table-row-hover.test.ts +2 -12
  171. package/frontend-vue/tests/table-scroll-area.test.ts +0 -8
  172. package/frontend-vue/tests/table-view-card-props.test.ts +0 -17
  173. package/frontend-vue/tests/table-view-cards-reorder.test.ts +0 -16
  174. package/frontend-vue/tests/table-view-grouped.test.ts +0 -55
  175. package/frontend-vue/tests/table-view-pinned-rows.test.ts +0 -15
  176. package/frontend-vue/tests/table-view-record-link.test.ts +0 -17
  177. package/frontend-vue/tests/table-view-refresh.test.ts +0 -17
  178. package/frontend-vue/tests/table-view-sources.test.ts +1 -1
  179. package/frontend-vue/tests/table-view-url-tab.test.ts +0 -14
  180. package/frontend-vue/tests/template-tokens.test.ts +235 -0
  181. package/frontend-vue/tests/tone.test.ts +0 -19
  182. package/frontend-vue/tests/url-interpolation.test.ts +6 -6
  183. package/package.json +6 -6
  184. package/skills/dms/SKILL.md +2 -1
  185. package/skills/dms-pages/REFERENCE.md +2 -2
  186. package/skills/dms-project/SKILL.md +0 -1
  187. package/dist/test/integration/auth/login.test.js +0 -57
  188. package/dist/test/integration/auth/login.test.js.map +0 -1
  189. package/dist/test/integration/auth/refresh.test.js +0 -59
  190. package/dist/test/integration/auth/refresh.test.js.map +0 -1
  191. package/dist/test/integration/auth/sessions.test.js +0 -58
  192. package/dist/test/integration/auth/sessions.test.js.map +0 -1
  193. package/dist/test/integration/auth/signup.test.js +0 -99
  194. package/dist/test/integration/auth/signup.test.js.map +0 -1
  195. package/dist/test/unit/skills/dms-dev-script.test.d.ts +0 -1
  196. package/dist/test/unit/skills/dms-dev-script.test.js +0 -137
  197. package/dist/test/unit/skills/dms-dev-script.test.js.map +0 -1
  198. package/frontend-vue/tests/form-reload-on-reopen.test.ts +0 -20
  199. package/frontend-vue/tests/icon-aria-hidden.test.ts +0 -29
  200. package/frontend-vue/tests/skeleton-paragraph.test.ts +0 -30
  201. package/frontend-vue/tests/tab-slot-spacing.test.ts +0 -29
  202. package/frontend-vue/tests/table-empty-body-viewport.test.ts +0 -25
  203. package/frontend-vue/tests/table-header-placement.test.ts +0 -21
  204. package/frontend-vue/tests/vue-inventory.test.ts +0 -107
  205. /package/dist/test/{integration/auth/login.test.d.ts → unit/implementations/dms-html-render/built-in-emails.test.d.ts} +0 -0
  206. /package/dist/test/{integration/auth/sessions.test.d.ts → unit/interfaces/dms/hook-handler-results.test.d.ts} +0 -0
  207. /package/dist/test/{integration/auth/refresh.test.d.ts → unit/interfaces/dms-base/query-response-delta.test.d.ts} +0 -0
  208. /package/dist/test/{integration/auth/signup.test.d.ts → unit/utils/email-subjects.test.d.ts} +0 -0
@@ -6,10 +6,10 @@
6
6
  * it has somewhere to go back to, then "Unsaved changes" with Discard and
7
7
  * Save.
8
8
  * - `action`: a form that does something each time (send a notification,
9
- * invite someone, run a job). Reset and the form's own submit label once a
10
- * value changed; untouched — pre-filled or not —, no buttons on a page, and
11
- * Cancel beside a held submit in a drawer or a modal. It empties after a
12
- * successful submit.
9
+ * invite someone, run a job). Its own submit label, always ready: what it
10
+ * holds untouched, its defaults included, is a payload too. Beside it,
11
+ * Reset once a value changed, Cancel before that in a drawer or a modal.
12
+ * It empties after a successful submit.
13
13
  */
14
14
  export type FormKind = "record" | "action";
15
15
 
@@ -83,21 +83,25 @@ function recordSaveBar(context: SaveBarContext): SaveBarState {
83
83
  }
84
84
 
85
85
  /**
86
- * An action form's bar: Reset and its submit once a value changed. Before
87
- * that, a form in a drawer or a modal offers Cancel beside its held submit —
88
- * the modal's only way out and the proof it sends something —, a page form
89
- * nothing; a form nothing in which can be changed, its submit alone.
86
+ * An action form's bar: its submit, never held, so a pre-filled form sends as
87
+ * it is. Beside it, Reset once a value changed; before that, Cancel in a
88
+ * drawer or a modal, its only way out. A form nothing in which can be changed
89
+ * has nothing to reset.
90
90
  */
91
91
  function actionSaveBar(context: SaveBarContext): SaveBarState {
92
- const bar = { showsStatus: false, showsSubmit: true, isHidden: false };
93
- if (!context.resettable) return { ...bar, isSubmitHeld: false };
94
- if (context.dirty) {
95
- return { ...bar, secondary: "discard", isSubmitHeld: false };
92
+ const bar: SaveBarState = {
93
+ showsStatus: false,
94
+ showsSubmit: true,
95
+ isSubmitHeld: false,
96
+ isHidden: false,
97
+ };
98
+ if (context.dirty && context.resettable) {
99
+ return { ...bar, secondary: "discard" };
96
100
  }
97
- if (!context.cancellable) {
98
- return { ...HIDDEN_BAR, showsStatus: false };
101
+ if (!context.dirty && context.cancellable) {
102
+ return { ...bar, secondary: "cancel" };
99
103
  }
100
- return { ...bar, secondary: "cancel", isSubmitHeld: true };
104
+ return bar;
101
105
  }
102
106
 
103
107
  const SAVE_BARS: Record<FormKind, (context: SaveBarContext) => SaveBarState> = {
@@ -5,9 +5,33 @@ import { HTTP_FORBIDDEN } from "#dms-core/app/utils/http-status";
5
5
  /**
6
6
  * Why a form could not load the record it edits: `forbidden` when the server
7
7
  * refused the caller the read (a role that may edit rows but not view them),
8
- * `error` for anything else (a network failure, a server error, a row gone).
8
+ * `missing` when its address lacks the record's id (a page reached without
9
+ * its `?id=`), `error` for anything else (a network failure, a server error,
10
+ * a row gone).
9
11
  */
10
- export type FormLoadFailure = "forbidden" | "error";
12
+ export type FormLoadFailure = "forbidden" | "missing" | "error";
13
+
14
+ /** What a form knows of the record it is about, as far as `missing` goes. */
15
+ export interface FormRecordAddress {
16
+ requiresRecord?: boolean;
17
+ fetchUrl?: string;
18
+ /** `fetchUrl` with its route values filled; unset while one is missing. */
19
+ resolvedFetchUrl?: string;
20
+ }
21
+
22
+ /**
23
+ * `missing` for a form about one record whose address names no record yet:
24
+ * its fields would only hold their defaults, which read as a record.
25
+ */
26
+ export function missingRecordFailure(
27
+ address: FormRecordAddress,
28
+ ): FormLoadFailure | null {
29
+ const isMissing =
30
+ !!address.requiresRecord &&
31
+ !!address.fetchUrl &&
32
+ !address.resolvedFetchUrl;
33
+ return isMissing ? "missing" : null;
34
+ }
11
35
 
12
36
  /** The cached load of a form's record (`useDmsAsyncData`), as far as it is read. */
13
37
  export interface FormLoadSource<T> {
@@ -0,0 +1,41 @@
1
+ import {
2
+ namesPageValue,
3
+ type ReplaceUrlVariablesContext,
4
+ resolveLinkVariables,
5
+ resolveUrlVariables,
6
+ } from "../../utils/urlVariables";
7
+
8
+ /**
9
+ * The values of the page being shown that a `{{params.X}}` / `{{query.X}}`
10
+ * token names: the route parameters of the DMS page matching the URL, and its
11
+ * query. Read when called, so it follows the URL as it changes.
12
+ */
13
+ export function currentPageUrlValues(): ReplaceUrlVariablesContext {
14
+ const route = useDmsRoute();
15
+ return {
16
+ routeParams: useSiteLayout().findMatchingRoute(route.path)?.params,
17
+ routeQuery: route.query as Record<string, unknown>,
18
+ };
19
+ }
20
+
21
+ /**
22
+ * A link declared on a block (`to`) with the page's tokens filled, as a page
23
+ * header button's: a query pair the page has no value for is left out. The
24
+ * route is only read for a link holding a token.
25
+ */
26
+ export function pageLink(to: string): string;
27
+ export function pageLink(to: string | undefined): string | undefined;
28
+ export function pageLink(to: string | undefined): string | undefined {
29
+ if (!to || !namesPageValue(to)) return to;
30
+ return resolveLinkVariables(to, currentPageUrlValues());
31
+ }
32
+
33
+ /**
34
+ * A route a field or block requests (`searchUrl`, `rotateUrl`, …) with the
35
+ * page's tokens filled, or `undefined` while one has no value, as a block's
36
+ * `fetchUrl`.
37
+ */
38
+ export function pageRequestUrl(url: string | undefined): string | undefined {
39
+ if (!url || !namesPageValue(url)) return url;
40
+ return resolveUrlVariables(url, currentPageUrlValues());
41
+ }
@@ -1,5 +1,5 @@
1
1
  import type { LocationQueryValue } from "#dms/frontend-module";
2
- import { replaceUrlVariables } from "../../utils/urlVariables";
2
+ import { resolveLinkVariables } from "../../utils/urlVariables";
3
3
 
4
4
  type RouteQuery = Record<string, LocationQueryValue | LocationQueryValue[]>;
5
5
 
@@ -23,7 +23,7 @@ export function resolveFormSuccessTarget(
23
23
  { url, preserveQuery }: FormSuccessParams,
24
24
  route: FormSuccessRoute,
25
25
  ): { path: string; query?: RouteQuery } {
26
- const path = replaceUrlVariables(url, {
26
+ const path = resolveLinkVariables(url, {
27
27
  routeParams: route.params,
28
28
  routeQuery: route.query,
29
29
  });
@@ -1,5 +1,6 @@
1
1
  import { get } from "@nuxt/ui/runtime/utils/index.js";
2
2
  import { computed } from "vue";
3
+ import { interpolateUrl } from "#dms-core/app/utils/url-interpolation";
3
4
  import type {
4
5
  TableViewColumn,
5
6
  TableViewListResponse,
@@ -54,7 +55,6 @@ const DESCENDING = "desc";
54
55
  */
55
56
  export const LIVE_HELD_AT_QUERY_KEY = "liveHeldAt";
56
57
  const NO_ROUTE_VALUES: ReplaceUrlVariablesContext = { routeQuery: {} };
57
- const ROW_ID_TOKEN = "{id}";
58
58
 
59
59
  type SourceCapabilities = TableViewSourceConfig["capabilities"];
60
60
 
@@ -251,10 +251,8 @@ export function useTableRows<T extends Record<string, unknown>>(
251
251
  id: string,
252
252
  ): Promise<T | undefined> => {
253
253
  const url = resolveUrlVariables(
254
- sourceConfig.fetchRowUrl?.replaceAll(
255
- ROW_ID_TOKEN,
256
- encodeURIComponent(id),
257
- ),
254
+ sourceConfig.fetchRowUrl &&
255
+ interpolateUrl(sourceConfig.fetchRowUrl, { id }),
258
256
  options.route?.() ?? NO_ROUTE_VALUES,
259
257
  );
260
258
  return url ? api<T>(url) : Promise.resolve(undefined);
@@ -102,20 +102,15 @@ export const useTableViewConfig = <T extends Data>(
102
102
  .map((col) => ({ ...col, header: processI18n(col.header) })),
103
103
  );
104
104
 
105
- // Config-derived (SSR-safe; the registry is client-only): the implicit `table`
106
- // display (unless `tableDisplay` is false) plus explicit `displays`. Built-in defaults
105
+ // Config-derived (SSR-safe; the registry is client-only): the listed
106
+ // `displays`, else the built-in `table` alone. Built-in defaults
107
107
  // (selfManagedData/capabilities) are merged per id.
108
- const resolvedDisplays: TableViewDisplayConfig[] = (() => {
109
- const list: TableViewDisplayConfig[] =
110
- config.tableDisplay === false ? [] : [{ id: TABLE_DISPLAY_ID }];
111
- for (const display of config.displays ?? []) {
112
- if (display.id === TABLE_DISPLAY_ID) continue;
113
- list.push(display);
114
- }
115
- return list.map(withBuiltinDefaults);
116
- })();
108
+ const resolvedDisplays: TableViewDisplayConfig[] = (
109
+ config.displays ?? [{ id: TABLE_DISPLAY_ID }]
110
+ ).map(withBuiltinDefaults);
117
111
 
118
- const defaultDisplay = config.defaultDisplay ?? TABLE_DISPLAY_ID;
112
+ const defaultDisplay =
113
+ config.defaultDisplay ?? resolvedDisplays[0]?.id ?? TABLE_DISPLAY_ID;
119
114
 
120
115
  const initialVisibility = buildInitialColumnVisibility(config.columns ?? []);
121
116
 
@@ -1,3 +1,4 @@
1
+ import { namesRowField } from "#dms-core/app/utils/url-interpolation";
1
2
  import {
2
3
  fillFormPageUrl,
3
4
  type FormContainer,
@@ -91,7 +92,6 @@ interface BulkActionConfig {
91
92
  /** Runs a confirmed action from inside its dialog (see `onConfirm`). */
92
93
  type ConfirmedRun = NonNullable<ConfirmOptions["onConfirm"]>;
93
94
 
94
- const ROW_URL_PLACEHOLDER = /\{[^}]+\}/;
95
95
  /** Options of a delete: from the archive, it is a permanent one. */
96
96
  interface DeleteRowsOptions {
97
97
  permanently?: boolean;
@@ -634,7 +634,7 @@ export const useTableRowActions = <T extends Data>(
634
634
  * reaches: a `from` dialog is worded for one row, so several rows keep the
635
635
  * generic confirmation. Undefined when none applies.
636
636
  */
637
- // A `from` URL naming a row (`{id}`, `{_id}`…) is worded for that row
637
+ // A `from` URL naming a row (`{{row.id}}`, `{{row._id}}`…) is worded for that row
638
638
  // alone; one naming none receives the rows as `?ids=`, one or several, like
639
639
  // the `from` of a bulk custom action.
640
640
  const declaredBulkConfirm = (
@@ -643,7 +643,7 @@ export const useTableRowActions = <T extends Data>(
643
643
  ): ActionConfirm | undefined => {
644
644
  const declared = normalizeActionConfig(actionConfig).confirm;
645
645
  if (!declared || !isConfirmFrom(declared)) return declared;
646
- if (ROW_URL_PLACEHOLDER.test(declared.from)) {
646
+ if (namesRowField(declared.from)) {
647
647
  return ids.length === 1 ? declared : undefined;
648
648
  }
649
649
  return {
@@ -3,6 +3,8 @@
3
3
  // bare `<a href="#…">` (an Inertia visit would reload the page to scroll), an
4
4
  // absolute URL opens in a new tab, and everything else is a DMS route.
5
5
 
6
+ import { pageLink } from "../composables/page/pageUrlValues";
7
+
6
8
  export type LinkKind = "internal" | "anchor" | "external";
7
9
 
8
10
  const EXTERNAL_LINK_PATTERN = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i;
@@ -20,12 +22,14 @@ export const EXTERNAL_LINK_ATTRIBUTES = {
20
22
  } as const;
21
23
 
22
24
  /**
23
- * Props for a `UButton` (or `ULink`) following `to`: an external target opens
24
- * in a new tab, the others navigate in place.
25
+ * Props for a `UButton` (or `ULink`) following `to`, its page tokens
26
+ * (`{{params.X}}`, `{{query.X}}`) filled: an external target opens in a new
27
+ * tab, the others navigate in place.
25
28
  */
26
29
  export function buttonLinkProps(
27
- to: string | undefined,
30
+ declared: string | undefined,
28
31
  ): Record<string, string> {
32
+ const to = pageLink(declared);
29
33
  if (!to) return {};
30
34
  return linkKind(to) === "external"
31
35
  ? { to, ...EXTERNAL_LINK_ATTRIBUTES }
@@ -1,11 +1,24 @@
1
+ import { get } from "@nuxt/ui/runtime/utils/index.js";
2
+
3
+ // Every token is `{{<scope>.<key>}}`, spaces inside the braces allowed, as the
4
+ // server's filter tokens (`{{ user.id }}`) are.
5
+ //
1
6
  // `{{params.id}}` takes the bare name, which on a route repeating a placeholder
2
7
  // is its last occurrence — the row id of a form page. The `:<n>` suffix reaches
3
8
  // a specific occurrence, `{{params.id:1}}` being the id of the page carrying the
4
9
  // table view (see extractRouteParams).
5
- const PARAM_TOKEN = /\{\{params\.(\w+(?::\d+)?)\}\}/g;
10
+ const PARAM_KEY = String.raw`params\.(\w+(?::\d+)?)`;
6
11
  // A query key may carry dots and dashes: a period selector writes `<id>.period`.
7
- const QUERY_TOKEN = /\{\{query\.([\w.-]+)\}\}/g;
8
- const RESPONSE_TOKEN = /\{\{response\.(\w+)\}\}/g;
12
+ const QUERY_KEY = String.raw`query\.([\w.-]+)`;
13
+ // A response key is a path into the answered body: `{{response.data.id}}`.
14
+ const RESPONSE_KEY = String.raw`response\.([\w.-]+)`;
15
+
16
+ const tokenPattern = (key: string, flags = "g"): RegExp =>
17
+ new RegExp(String.raw`\{\{\s*${key}\s*\}\}`, flags);
18
+
19
+ const PARAM_TOKEN = tokenPattern(PARAM_KEY);
20
+ const QUERY_TOKEN = tokenPattern(QUERY_KEY);
21
+ const RESPONSE_TOKEN = tokenPattern(RESPONSE_KEY);
9
22
  const TOKEN_OPENING = "{{";
10
23
 
11
24
  export interface ReplaceUrlVariablesContext {
@@ -63,8 +76,8 @@ function fillResponseTokens(
63
76
  response: Record<string, unknown> | undefined,
64
77
  ): string {
65
78
  if (!response) return text;
66
- return text.replace(RESPONSE_TOKEN, (match, key) => {
67
- const value = response[key];
79
+ return text.replace(RESPONSE_TOKEN, (match, key: string) => {
80
+ const value: unknown = get(response, key);
68
81
  return typeof value === "string" || typeof value === "number"
69
82
  ? String(value)
70
83
  : match;
@@ -122,10 +135,20 @@ export function resolveTextVariables(
122
135
 
123
136
  // A route token left without a value: `{{params.id}}`, `{{query.workspace}}`,
124
137
  // `{{query.sales.period}}` (a query key may carry dots and dashes, as above).
125
- const UNRESOLVED_ROUTE_TOKEN =
126
- /\{\{(?:params\.\w+(?::\d+)?|query\.[\w.-]+)\}\}/g;
127
- const UNRESOLVED_ROUTE_VALUE =
128
- /^\{\{(?:params\.\w+(?::\d+)?|query\.[\w.-]+)\}\}$/;
138
+ const ROUTE_KEY = `(?:${PARAM_KEY}|${QUERY_KEY})`;
139
+ const UNRESOLVED_ROUTE_TOKEN = tokenPattern(ROUTE_KEY);
140
+ const UNRESOLVED_ROUTE_VALUE = new RegExp(
141
+ `^${tokenPattern(ROUTE_KEY, "").source}$`,
142
+ );
143
+
144
+ /**
145
+ * Whether a text names a value of the page (`{{params.X}}`, `{{query.X}}`):
146
+ * the route is only read for one that does.
147
+ */
148
+ export function namesPageValue(text: string): boolean {
149
+ return new RegExp(UNRESOLVED_ROUTE_TOKEN.source).test(text);
150
+ }
151
+
129
152
  const QUERY_SEPARATOR = "?";
130
153
  const FRAGMENT_SEPARATOR = "#";
131
154
  const PAIR_SEPARATOR = "&";
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { pageLink } from "../../build/composables/page/pageUrlValues";
2
3
  import { computed, ref, resolveComponent } from "vue";
3
4
  import DmsActivityItem from "../activity/ActivityItem.vue";
4
5
  import DmsBlockActions, {
@@ -236,6 +237,11 @@ const scrollArea = computed(() =>
236
237
  const dayHeadClass = computed(
237
238
  () => props.fillHeight && "sticky top-0 z-[1] bg-(--dms-surface-card)",
238
239
  );
240
+ // …and the "Show N more" button at its foot, where entries taller than the
241
+ // row would otherwise push it out of sight.
242
+ const foldClass = computed(
243
+ () => props.fillHeight && "sticky bottom-0 z-[1] bg-(--dms-surface-card)",
244
+ );
239
245
 
240
246
  const Wrapper = props.card ? resolveComponent("DmsCard") : "div";
241
247
  const wrapperProps = computed(() =>
@@ -365,13 +371,14 @@ const wrapperProps = computed(() =>
365
371
  :unread="item.unread"
366
372
  :mono="props.mono"
367
373
  :wrap="props.wrap"
368
- :to="item.to"
374
+ :to="pageLink(item.to)"
369
375
  :interactive="!!item.to"
370
376
  />
371
377
  </section>
372
378
  <div
373
379
  v-if="foldedCount > 0"
374
380
  class="dms-activity-feed__fold border-t border-(--ui-border-muted) px-[18px] pt-2 pb-1"
381
+ :class="foldClass"
375
382
  >
376
383
  <UButton
377
384
  :label="foldLabel"
@@ -1,19 +1,22 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from "vue";
3
3
  import DmsIconWell, { type IconWellSize } from "../icon-well/IconWell.vue";
4
- import type { SemanticColor } from "../../utils/semanticTint";
4
+ import {
5
+ BANNER_TONE_ICONS,
6
+ type BannerTone,
7
+ } from "../../utils/bannerToneIcons";
5
8
 
6
9
  // DMS-styled notice banner (design .banner): a left-to-right tinted wash over
7
10
  // the card surface with a boxed leading icon, used for module beta / info
8
11
  // notices. Wraps UAlert so we keep its a11y + slots while pinning the exact
9
12
  // reference look. The `cli` slot hosts the optional inset command box
10
13
  // (compose with DmsCopyButton), `actions` the right-aligned buttons.
11
- type BannerTone = Exclude<SemanticColor, "neutral">;
12
14
  type BannerSize = "sm" | "md";
13
15
 
14
16
  interface BannerProps {
15
17
  title?: string;
16
18
  description?: string;
19
+ /** Defaults to the tone's icon. */
17
20
  icon?: string;
18
21
  /** Colour of the wash and the icon well. */
19
22
  tone?: BannerTone;
@@ -39,7 +42,7 @@ interface BannerSlots {
39
42
  const props = withDefaults(defineProps<BannerProps>(), {
40
43
  title: undefined,
41
44
  description: undefined,
42
- icon: "i-ph-warning",
45
+ icon: undefined,
43
46
  tone: "warning",
44
47
  size: "md",
45
48
  dismissible: false,
@@ -99,6 +102,8 @@ const alertUi = computed(() => ({
99
102
  actions: `items-center gap-2 mt-0 ${slots.actions ? "max-sm:basis-full max-sm:flex-wrap" : ""}`,
100
103
  }));
101
104
 
105
+ const leadingIcon = computed(() => props.icon ?? BANNER_TONE_ICONS[props.tone]);
106
+
102
107
  function onOpenChange(open: boolean) {
103
108
  if (!open) emit("dismiss");
104
109
  }
@@ -116,7 +121,7 @@ function onOpenChange(open: boolean) {
116
121
  @update:open="onOpenChange"
117
122
  >
118
123
  <template #leading>
119
- <DmsIconWell :icon="icon" :tone="tone" :size="SIZE_WELL[size]" />
124
+ <DmsIconWell :icon="leadingIcon" :tone="tone" :size="SIZE_WELL[size]" />
120
125
  </template>
121
126
 
122
127
  <template v-if="slots.description || slots.cli" #description>
@@ -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
  },