@antelopejs/dms 0.12.0 → 0.13.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 (109) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +1 -1
  3. package/data/dbip-country-lite.mmdb +0 -0
  4. package/data/module-store/catalog.json +0 -6
  5. package/dist/config/internal/state.d.ts +5 -0
  6. package/dist/config/internal/state.js.map +1 -1
  7. package/dist/frontend-module.js +1 -0
  8. package/dist/frontend-module.js.map +1 -1
  9. package/dist/implementations/dms/page/quick-action-access.js +4 -1
  10. package/dist/implementations/dms/page/quick-action-access.js.map +1 -1
  11. package/dist/index.js +13 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/store/operations.js +3 -1
  14. package/dist/store/operations.js.map +1 -1
  15. package/dist/store/runtime.d.ts +1 -0
  16. package/dist/store/runtime.js +9 -0
  17. package/dist/store/runtime.js.map +1 -1
  18. package/dist/utils/ensure-default-tenant.js +7 -2
  19. package/dist/utils/ensure-default-tenant.js.map +1 -1
  20. package/frontend-vue/dms.frontend.ts +2 -0
  21. package/frontend-vue/layers/dms-auth/app/build/components/AuthFormAlert.vue +1 -1
  22. package/frontend-vue/layers/dms-auth/app/build/composables/useAuthFormError.ts +11 -11
  23. package/frontend-vue/layers/dms-auth/app/composables/useOAuthErrorToast.ts +3 -8
  24. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/accounts.vue +3 -2
  25. package/frontend-vue/layers/dms-auth/app/custom-pages/auth/login.vue +3 -1
  26. package/frontend-vue/layers/dms-auth/server/oauth.ts +5 -3
  27. package/frontend-vue/layers/dms-core/app/build/composables/api-error.ts +145 -0
  28. package/frontend-vue/layers/dms-core/app/build/composables/auth/usePermissionPreview.ts +5 -1
  29. package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +2 -2
  30. package/frontend-vue/layers/dms-core/app/composables/page/useSiteLayout.ts +6 -5
  31. package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -10
  32. package/frontend-vue/layers/dms-core/app/composables/useApiErrorPresenter.ts +159 -0
  33. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +14 -26
  34. package/frontend-vue/layers/dms-core/app/utils/composedText.ts +11 -4
  35. package/frontend-vue/layers/dms-core/app/utils/row-action-rule-evaluator.ts +8 -36
  36. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +1 -0
  37. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +1 -0
  38. package/frontend-vue/layers/dms-core/shared/types/runtime-config.d.ts +6 -0
  39. package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidePanel.vue +3 -2
  40. package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +14 -0
  41. package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +3 -1
  42. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +3 -3
  43. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +6 -6
  44. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityEmail.vue +7 -9
  45. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityPassword.vue +4 -4
  46. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +6 -7
  47. package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +2 -1
  48. package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +14 -34
  49. package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +3 -11
  50. package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +1 -1
  51. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +9 -0
  52. package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +13 -28
  53. package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +2 -2
  54. package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +5 -2
  55. package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +4 -4
  56. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +2 -4
  57. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +2 -4
  58. package/frontend-vue/layers/dms-layout/server/render-token.ts +5 -3
  59. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +2 -1
  60. package/frontend-vue/layers/dms-ui/app/build/composables/actions/actionTargetRequests.ts +6 -10
  61. package/frontend-vue/layers/dms-ui/app/build/composables/chart/groupPoints.ts +51 -0
  62. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +10 -139
  63. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +2 -8
  64. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/defaultPrimitiveTypes.ts +6 -8
  65. package/frontend-vue/layers/dms-ui/app/build/composables/form/formState.ts +2 -12
  66. package/frontend-vue/layers/dms-ui/app/build/composables/page/blockAnchors.ts +2 -3
  67. package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableConfigClipboard.ts +7 -10
  68. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionBulkRequests.ts +11 -10
  69. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionValues.ts +1 -4
  70. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableViewGrouping.ts +9 -9
  71. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useExpandedRowDetails.ts +4 -1
  72. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRecordLink.ts +8 -3
  73. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +53 -5
  74. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +7 -16
  75. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +3 -1
  76. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +9 -5
  77. package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +4 -3
  78. package/frontend-vue/layers/dms-ui/app/components/block-preview/BlockPreview.vue +6 -1
  79. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +3 -1
  80. package/frontend-vue/layers/dms-ui/app/components/chart/Chart.vue +4 -1
  81. package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +3 -2
  82. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +24 -16
  83. package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -6
  84. package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +2 -4
  85. package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +6 -2
  86. package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +5 -1
  87. package/frontend-vue/layers/dms-ui/app/components/form/components/ImagePresentation.vue +2 -2
  88. package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -6
  89. package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +2 -6
  90. package/frontend-vue/layers/dms-ui/app/components/form/components/imageItems.ts +1 -0
  91. package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +2 -1
  92. package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +13 -9
  93. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +11 -1
  94. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +6 -6
  95. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +14 -21
  96. package/frontend-vue/layers/dms-ui/app/composables/global/keyboardPlatform.ts +36 -9
  97. package/frontend-vue/layers/dms-ui/app/composables/useExportJob.ts +13 -59
  98. package/frontend-vue/layers/dms-ui/app/plugins/kbd-platform.ts +13 -0
  99. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
  100. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
  101. package/frontend-vue/package.json +3 -5
  102. package/frontend-vue/scripts/check-api-error-presentation.mjs +242 -0
  103. package/frontend-vue/scripts/check-invariants.mjs +23 -0
  104. package/frontend-vue/scripts/codemods/api-error-presenter.mjs +196 -0
  105. package/frontend-vue/scripts/test-api-error-presentation.mjs +93 -0
  106. package/package.json +23 -7
  107. package/skills/dms-module/REFERENCE.md +3 -1
  108. package/skills/dms-module/SKILL.md +7 -2
  109. package/skills/dms-project/SKILL.md +9 -3
@@ -1,6 +1,6 @@
1
1
  import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
2
2
  import type { TableViewColumn } from "../../../../composables/table-view/types/column";
3
- import { resolveActionError } from "../../confirm/actionError";
3
+ import { describeFieldErrors } from "#dms-core/app/composables/useFieldErrors";
4
4
 
5
5
  /**
6
6
  * Why a table view's write failed (a card moved on the board, a row moved
@@ -8,14 +8,18 @@ import { resolveActionError } from "../../confirm/actionError";
8
8
  * data API refused named by its column's label ("Loss reason is
9
9
  * required."). Never the raw ofetch error line.
10
10
  */
11
- export function describeTableWriteError(
12
- error: Parameters<typeof resolveActionError>[0],
11
+ export function tableWriteErrorOptions(
12
+ error: unknown,
13
13
  translate: I18nTranslate,
14
14
  columns: ReadonlyArray<Pick<TableViewColumn, "accessorKey" | "header">>,
15
15
  processI18n: (text: string) => string,
16
- ): string {
17
- return resolveActionError(error, translate, (field) => {
16
+ fallbackKey = "dms.action_error.generic",
17
+ ): ResolveApiErrorOptions {
18
+ const summary = describeFieldErrors(error, translate, (field) => {
18
19
  const column = columns.find((entry) => entry.accessorKey === field);
19
20
  return column ? processI18n(column.header) : undefined;
20
21
  });
22
+ return summary
23
+ ? { fallbackKey: "dms.action_error.fields", params: { summary } }
24
+ : { fallbackKey };
21
25
  }
@@ -5,7 +5,7 @@ import {
5
5
  import { HTTP_NOT_FOUND } from "../../../../dms-core/app/utils/http-status";
6
6
  import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
7
7
  import type { BlockAction } from "../components/blocks/BlockActions.vue";
8
- import { userFacingText } from "../composables/confirm/actionError";
8
+ import type { ApiErrorPresenter } from "#dms-core/app/composables/useApiErrorPresenter";
9
9
  import type { Tone } from "#dms-core/app/types/tone";
10
10
 
11
11
  /** Translation key of the not-found title, shared by blocks and guards. */
@@ -76,11 +76,12 @@ export function readFetchNotFound(
76
76
  export function notFoundTexts(
77
77
  notFound: FetchNotFound | null | undefined,
78
78
  translate: I18nTranslate,
79
+ resolveError: ApiErrorPresenter["resolveError"],
79
80
  ) {
80
81
  return {
81
82
  title: translate(NOT_FOUND_TITLE_KEY, {}),
82
83
  description:
83
- userFacingText(notFound?.message, translate) ??
84
- translate(NOT_FOUND_HINT_KEY, {}),
84
+ resolveError(notFound?.message, { fallbackKey: NOT_FOUND_HINT_KEY })
85
+ ?.message ?? translate(NOT_FOUND_HINT_KEY, {}),
85
86
  };
86
87
  }
@@ -14,6 +14,8 @@ import {
14
14
  } from "../../build/utils/blockPreview";
15
15
  import { childComponentId } from "../../utils/componentId";
16
16
 
17
+ const { resolveError } = useApiErrorPresenter();
18
+
17
19
  /**
18
20
  * Draws a tree of registered DMS blocks exactly as a page would.
19
21
  *
@@ -129,7 +131,10 @@ onMounted(async () => {
129
131
  fetchedBlockTypes.value =
130
132
  await $authFetch<PreviewBlockTypes>(BLOCK_TYPES_ENDPOINT);
131
133
  } catch (error: unknown) {
132
- embed?.reportError(error instanceof Error ? error.message : String(error));
134
+ const notice = resolveError(error, {
135
+ fallbackKey: "dms.blocks.load_error_hint",
136
+ });
137
+ if (notice) embed?.reportError(notice.message);
133
138
  }
134
139
  });
135
140
  </script>
@@ -92,12 +92,14 @@ const { data } = useChartFetch<BannerContent | null>({
92
92
  watchSource: () => JSON.stringify(watchState.value),
93
93
  });
94
94
 
95
+ const { presentError } = useApiErrorPresenter();
95
96
  const { handleCustomButton } = useActionTargets({
96
97
  api: useAuthFetch().$authFetch,
97
98
  componentId: props.componentId ?? FALLBACK_COMPONENT_ID,
98
99
  pageId: props.pageId ?? FALLBACK_PAGE_ID,
99
100
  refreshCallback: refreshPageBlocks,
100
- handleApiError: (error, title) => useApiError(error, { title }),
101
+ handleApiError: (error, title) =>
102
+ presentError(error, { fallbackKey: title ?? "error.500.description" }),
101
103
  });
102
104
 
103
105
  const hasFetchedContent = computed(
@@ -43,6 +43,7 @@ import {
43
43
  NESTED_CHART_INJECT_KEY,
44
44
  } from "../../composables/chart/types";
45
45
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
46
+ import { responseSeries } from "../../build/composables/chart/groupPoints";
46
47
 
47
48
  interface Props extends DefaultComponentProps {
48
49
  type: ChartType;
@@ -164,7 +165,9 @@ const seriesFromCtx = computed<ChartSeries[]>(() => {
164
165
 
165
166
  const fetchSeries = computed<ChartSeries[]>(() => {
166
167
  if (!data.value) return [];
167
- return localized(data.value.series ?? []);
168
+ return localized(
169
+ responseSeries(data.value.series ?? [], props.title ?? "", props.xaxisType),
170
+ );
168
171
  });
169
172
 
170
173
  const finalSeries = computed<ChartSeries[]>(() => {
@@ -21,6 +21,7 @@ import {
21
21
  type ValuePrecision,
22
22
  } from "../../composables/chart/types";
23
23
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
24
+ import { drawableSeries } from "../../build/composables/chart/groupPoints";
24
25
 
25
26
  interface Props extends DefaultComponentProps {
26
27
  title: string;
@@ -93,13 +94,13 @@ function localized(entry: ChartSeries): ChartSeries {
93
94
  }
94
95
 
95
96
  const cardSeries = computed<ChartSeries[]>(() =>
96
- (data.value?.series ?? []).map(localized),
97
+ drawableSeries(data.value?.series ?? []).map(localized),
97
98
  );
98
99
 
99
100
  const cardComparisonSeries = computed<ChartSeries[]>(() =>
100
101
  comparison.isOff.value
101
102
  ? []
102
- : (data.value?.comparisonSeries ?? []).map((entry) => ({
103
+ : drawableSeries(data.value?.comparisonSeries ?? []).map((entry) => ({
103
104
  ...localized(entry),
104
105
  color: entry.color ?? comparisonColor.value,
105
106
  })),
@@ -23,7 +23,6 @@ import {
23
23
  ConfirmTextError,
24
24
  type ConfirmValues,
25
25
  } from "../../composables/confirm/types";
26
- import { resolveActionError } from "../../build/composables/confirm/actionError";
27
26
  import { isConfirmTextMatch } from "../../build/composables/confirm/confirmTextMatch";
28
27
  import type { ConfirmDialogField } from "#dms-core/app/types/confirm-dialog";
29
28
  import { resolveFieldErrors } from "#dms-core/app/composables/useFieldErrors";
@@ -113,6 +112,7 @@ const props = withDefaults(defineProps<ConfirmModalProps>(), {
113
112
  const emit = defineEmits<ConfirmModalEmits>();
114
113
  const slots = defineSlots<ConfirmModalSlots>();
115
114
  const { t } = useI18n();
115
+ const { resolveError } = useApiErrorPresenter();
116
116
 
117
117
  const BodyRender: FunctionalComponent = () => props.body?.();
118
118
 
@@ -123,8 +123,6 @@ const DEFAULT_ICONS: Partial<Record<ConfirmColor, string>> = {
123
123
  };
124
124
  const FALLBACK_ICON = "i-ph-question";
125
125
 
126
- const { processApiMessage } = useTranslation();
127
-
128
126
  const values = ref<ConfirmValues>(
129
127
  Object.fromEntries(
130
128
  props.fields.map((field) => [field.id, field.defaultValue]),
@@ -169,7 +167,7 @@ const typedText = ref("");
169
167
  const isPending = ref(false);
170
168
  const isSecondaryPending = ref(false);
171
169
 
172
- const notice = ref<ConfirmNotice & { tone: "error" | "warning" }>();
170
+ const notice = ref<ConfirmNotice & { tone: ApiErrorSeverity }>();
173
171
 
174
172
  const isSettled = ref(false);
175
173
 
@@ -245,13 +243,12 @@ const isPartialOutcome = (
245
243
  ): value is ConfirmPartialOutcome =>
246
244
  isObject(value) && value !== null && "partial" in value;
247
245
 
248
- function errorNotice(
249
- error: Parameters<typeof resolveActionError>[0],
250
- ): ConfirmNotice {
251
- if (error instanceof ConfirmActionError) {
252
- return { title: error.message, description: error.description };
253
- }
254
- return { title: resolveActionError(error, t) };
246
+ function resolveConfirmationFailure(error: unknown): ApiErrorNotice | null {
247
+ const action = error instanceof ConfirmActionError ? error : undefined;
248
+ return resolveError(error, {
249
+ fallbackKey: action?.message ?? "dms.action_error.generic",
250
+ params: action?.params,
251
+ });
255
252
  }
256
253
 
257
254
  /**
@@ -267,9 +264,12 @@ function showFieldErrors(
267
264
  });
268
265
  if (resolved.fields.length === 0) return false;
269
266
  fieldErrors.value = Object.fromEntries(
270
- resolved.fields.map(({ field, message }) => [
267
+ resolved.fields.map(({ field, message, params, fallbackKey }) => [
271
268
  field,
272
- processApiMessage(message),
269
+ resolveError(message, {
270
+ fallbackKey: fallbackKey ?? "dms.field_errors.invalid",
271
+ params,
272
+ })?.message,
273
273
  ]),
274
274
  );
275
275
  return !resolved.hasUnmatched;
@@ -293,9 +293,14 @@ async function runConfirm(
293
293
  }
294
294
  } catch (error) {
295
295
  if (error instanceof ConfirmTextError && props.confirmText) {
296
- typedError.value = error.message;
296
+ typedError.value = resolveError(error, {
297
+ fallbackKey: "dms.field_errors.invalid",
298
+ })?.message;
297
299
  } else if (!showFieldErrors(error)) {
298
- notice.value = { ...errorNotice(error), tone: "error" };
300
+ const resolved = resolveConfirmationFailure(error);
301
+ notice.value = resolved
302
+ ? { title: resolved.message, tone: resolved.severity }
303
+ : undefined;
299
304
  }
300
305
  } finally {
301
306
  isPending.value = false;
@@ -341,7 +346,10 @@ async function handleSecondary() {
341
346
  try {
342
347
  await action.run();
343
348
  } catch (error) {
344
- notice.value = { ...errorNotice(error), tone: "error" };
349
+ const resolved = resolveConfirmationFailure(error);
350
+ notice.value = resolved
351
+ ? { title: resolved.message, tone: resolved.severity }
352
+ : undefined;
345
353
  } finally {
346
354
  isSecondaryPending.value = false;
347
355
  }
@@ -60,7 +60,7 @@ const invalid = computed(() => fieldColor.value === "error");
60
60
 
61
61
  const instanceId = useId();
62
62
  const { $authFetch } = useAuthFetch();
63
- const toast = useToast();
63
+ const { presentError } = useApiErrorPresenter();
64
64
  const { t } = useI18n();
65
65
  const { processI18n } = useTranslation();
66
66
 
@@ -77,11 +77,7 @@ const { data, status, execute } = await useDmsAsyncData(
77
77
  return $authFetch<CascaderResponse>(url, {
78
78
  params: { limit: CASCADER_FETCH_LIMIT },
79
79
  onRequestError: ({ error }: { error: Error }) => {
80
- toast.add({
81
- color: Color.error,
82
- title: t("dms.form.error_title"),
83
- description: error.message,
84
- });
80
+ presentError(error, { fallbackKey: "dms.form.error_title" });
85
81
  },
86
82
  });
87
83
  },
@@ -164,8 +164,7 @@ function trackRangePick(value: Parameters<typeof forwardEmit>[1]): void {
164
164
  report(undefined);
165
165
  return;
166
166
  }
167
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- The forwarded event has no validated range owner; keep optional property reads on arbitrary non-null values rather than adding validation.
168
- const ends = value as Partial<StrictDateRange>;
167
+ const ends: { start?: unknown; end?: unknown } = value;
169
168
  if (!ends.start) return;
170
169
  isHalfPicked = !ends.end;
171
170
  if (!isHalfPicked) report(undefined);
@@ -180,8 +179,7 @@ const isPickerOpen = ref(false);
180
179
  function isPickComplete(value: Parameters<typeof forwardEmit>[1]): boolean {
181
180
  if (!value || props.multiple) return false;
182
181
  if (!props.range) return true;
183
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- The forwarded event has no validated range owner; keep its existing optional-property truthiness checks on arbitrary values.
184
- const ends = value as Partial<StrictDateRange>;
182
+ const ends: { start?: unknown; end?: unknown } = value;
185
183
  return !!ends.start && !!ends.end;
186
184
  }
187
185
 
@@ -41,6 +41,7 @@ const emit = defineEmits<{
41
41
 
42
42
  const { $authFetch } = useAuthFetch();
43
43
  const { t } = useI18n();
44
+ const { presentError, resolveError } = useApiErrorPresenter();
44
45
 
45
46
  const { emitFormChange, color, highlight, ariaAttrs } = useFormField();
46
47
 
@@ -156,11 +157,14 @@ const processFile = async (file: File) => {
156
157
  }
157
158
 
158
159
  emitValue();
159
- } catch (_error) {
160
+ } catch (error) {
160
161
  const uploadingFile = uploadingFiles.value.find((u) => u.id === uploadId);
161
162
  if (uploadingFile) {
162
- uploadingFile.error = "Upload failed";
163
+ uploadingFile.error = resolveError(error, {
164
+ fallbackKey: "dms.form.image.upload_failed",
165
+ })?.message;
163
166
  }
167
+ presentError(error, { fallbackKey: "dms.form.image.upload_failed" });
164
168
  } finally {
165
169
  setFieldLoading?.(uploadId, false);
166
170
  }
@@ -39,6 +39,7 @@ interface ImageProps {
39
39
  }
40
40
 
41
41
  const props = defineProps<ImageProps>();
42
+ const { resolveError } = useApiErrorPresenter();
42
43
  const emit = defineEmits<{
43
44
  "update:modelValue": [ImageItemValue | ImageItemValue[] | null];
44
45
  }>();
@@ -206,8 +207,11 @@ const runUpload = async (item: GalleryItem) => {
206
207
  item.progress = 100;
207
208
  ensurePrincipal();
208
209
  emitValue();
209
- } catch (_error) {
210
+ } catch (error) {
210
211
  item.status = "error";
212
+ item.errorMessage = resolveError(error, {
213
+ fallbackKey: "dms.form.image.upload_failed",
214
+ })?.message;
211
215
  } finally {
212
216
  setFieldLoading?.(item.id, false);
213
217
  }
@@ -168,7 +168,7 @@ const {
168
168
  <span
169
169
  class="bg-error rounded-md px-2 py-0.5 text-[11px] font-semibold text-white"
170
170
  >
171
- {{ t(failureLabelKey(item)!) }}
171
+ {{ item.errorMessage ?? t(failureLabelKey(item)!) }}
172
172
  </span>
173
173
  <UButton
174
174
  v-if="item.file"
@@ -311,7 +311,7 @@ const {
311
311
  <span
312
312
  class="bg-error rounded-md px-2 py-0.5 text-[11px] font-semibold text-white"
313
313
  >
314
- {{ t(failureLabelKey(singleItem)!) }}
314
+ {{ singleItem.errorMessage ?? t(failureLabelKey(singleItem)!) }}
315
315
  </span>
316
316
  <UButton
317
317
  v-if="singleItem.file"
@@ -223,7 +223,7 @@ const mapCountryData = (item: Record<string, string>) => {
223
223
  };
224
224
 
225
225
  const { $authFetch } = useAuthFetch();
226
- const toast = useToast();
226
+ const { presentError } = useApiErrorPresenter();
227
227
 
228
228
  /** The remote country list, already shaped for `USelectMenu`. */
229
229
  interface CountryOptions {
@@ -239,11 +239,7 @@ async function fetchCountryOptions(): Promise<CountryOptions> {
239
239
  q: countrySearchTermDebounced.value,
240
240
  },
241
241
  onRequestError: ({ error }: { error: Error }) => {
242
- toast.add({
243
- color: Color.error,
244
- title: t("dms.form.error_title"),
245
- description: error.message,
246
- });
242
+ presentError(error, { fallbackKey: "dms.form.error_title" });
247
243
  },
248
244
  });
249
245
 
@@ -130,7 +130,7 @@ const loadedFilterSource = filterSourceValue.value;
130
130
 
131
131
  const { $authFetch } = useAuthFetch();
132
132
  const { readRelationRows } = useRelationRows();
133
- const toast = useToast();
133
+ const { presentError } = useApiErrorPresenter();
134
134
  const { t } = useI18n();
135
135
  const { processI18n } = useTranslation();
136
136
  const { hasPermission, loadPermissions } = usePermissions();
@@ -229,11 +229,7 @@ async function fetchRelationItems(): Promise<RelationItems> {
229
229
  ...filterParams.value,
230
230
  },
231
231
  onRequestError: ({ error }: { error: Error }) => {
232
- toast.add({
233
- color: Color.error,
234
- title: t("dms.form.error_title"),
235
- description: error.message,
236
- });
232
+ presentError(error, { fallbackKey: "dms.form.error_title" });
237
233
  },
238
234
  });
239
235
 
@@ -16,6 +16,7 @@ export interface GalleryItem {
16
16
  principal: boolean;
17
17
  status: ItemStatus;
18
18
  progress: number;
19
+ errorMessage?: string;
19
20
  }
20
21
 
21
22
  const FAILURE_LABEL_KEYS: Partial<Record<ItemStatus, string>> = {
@@ -32,6 +32,7 @@ const props = withDefaults(defineProps<GuardProps>(), {
32
32
  });
33
33
 
34
34
  const { t } = useI18n();
35
+ const { resolveError } = useApiErrorPresenter();
35
36
 
36
37
  const state = useGuard(
37
38
  () => props,
@@ -44,7 +45,7 @@ const DEFAULT_FALLBACKS: Record<
44
45
  > = {
45
46
  empty: () => ({ title: t("dms.blocks.empty") }),
46
47
  notFound: ({ notFound }) => ({
47
- ...notFoundTexts(notFound, t),
48
+ ...notFoundTexts(notFound, t, resolveError),
48
49
  variant: "no-result",
49
50
  }),
50
51
  };
@@ -23,7 +23,7 @@ import {
23
23
  import { readSubline } from "../../build/composables/data-types/cellHelpers";
24
24
  import { useComposedText } from "#dms-core/app/composables/translation/useComposedText";
25
25
  import { useCardFields } from "../../build/composables/table-view/useCardFields";
26
- import { describeTableWriteError } from "../../build/composables/table-view/utils/writeError";
26
+ import { tableWriteErrorOptions } from "../../build/composables/table-view/utils/writeError";
27
27
 
28
28
  const DEFAULT_COLUMN_PAGE_SIZE = 10;
29
29
  const DEFAULT_COLUMN_MAX_HEIGHT = "60vh";
@@ -89,6 +89,7 @@ const { getDataType } = useDataTypes();
89
89
  const { t } = useI18n();
90
90
  const { processText } = useComposedText();
91
91
  const toast = useToast();
92
+ const { presentError } = useApiErrorPresenter();
92
93
 
93
94
  const rowIdKey = computed(() => props.rowIdKey ?? DEFAULT_ROW_ID_KEY);
94
95
  const pageSize = computed(
@@ -295,14 +296,17 @@ const awaitNewerMoves = async (itemId: string, current: Promise<void>) => {
295
296
  }
296
297
  };
297
298
 
298
- const notifyMoveError = (
299
- error: Parameters<typeof describeTableWriteError>[0],
300
- ) => {
301
- toast.add({
302
- color: Color.error,
303
- title: t("dms.table.kanban.move_error"),
304
- description: describeTableWriteError(error, t, props.columns, processI18n),
305
- });
299
+ const notifyMoveError = (error: unknown) => {
300
+ presentError(
301
+ error,
302
+ tableWriteErrorOptions(
303
+ error,
304
+ t,
305
+ props.columns,
306
+ processI18n,
307
+ "dms.table.kanban.move_error",
308
+ ),
309
+ );
306
310
  };
307
311
 
308
312
  const onColumnChange = async (
@@ -122,8 +122,18 @@ export interface TopListCardResponse {
122
122
  items: TopListItem[];
123
123
  }
124
124
 
125
+ /** One measured group, as a data source of shape `series` answers it. */
126
+ export interface ChartGroupPoint {
127
+ x: string | number;
128
+ y: number | null;
129
+ }
130
+
131
+ /**
132
+ * What a bare chart's data source answers: one point per group, the shape the
133
+ * block registry declares as `series`, or named series.
134
+ */
125
135
  export interface ChartResponse {
126
- series: ChartSeries[];
136
+ series: ChartSeries[] | ChartGroupPoint[];
127
137
  }
128
138
 
129
139
  export type ValueFormat = "number" | "currency" | "percent" | "compact";
@@ -115,15 +115,15 @@ export interface ConfirmPartialOutcome {
115
115
  }
116
116
 
117
117
  /**
118
- * Thrown by `onConfirm` with a message already worded for users (a title and
119
- * its reason): the modal shows both in its error alert, as they are.
118
+ * Thrown by `onConfirm` with a fallback translation key and optional params.
119
+ * A translated server reason retained in `cause` takes precedence.
120
120
  */
121
121
  export class ConfirmActionError extends Error {
122
- readonly description?: string;
122
+ readonly params?: ApiErrorParams;
123
123
 
124
- constructor(notice: ConfirmNotice) {
125
- super(notice.title);
126
- this.description = notice.description;
124
+ constructor(options: ResolveApiErrorOptions, cause?: ErrorOptions) {
125
+ super(options.fallbackKey, cause);
126
+ this.params = options.params;
127
127
  }
128
128
  }
129
129
 
@@ -9,7 +9,6 @@ import {
9
9
  type ReplaceUrlVariablesContext,
10
10
  resolveLinkVariables,
11
11
  } from "../../build/utils/urlVariables";
12
- import { apiErrorText } from "#dms-core/app/composables/useFieldErrors";
13
12
  import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
14
13
  import { isNumber } from "#dms-core/app/utils/type-check";
15
14
  import { isPlainObject } from "../../build/composables/unsaved-changes/formValue";
@@ -93,6 +92,7 @@ function createFormNotices(
93
92
  context: ReturnType<typeof createFormFields>,
94
93
  options: UseFormOptions,
95
94
  ) {
95
+ const { presentError, resolveError } = useApiErrorPresenter();
96
96
  const {
97
97
  props,
98
98
  toast,
@@ -143,13 +143,6 @@ function createFormNotices(
143
143
  });
144
144
  };
145
145
 
146
- const resolveSubmitErrorDescription = (error: EventError) => {
147
- if (props.errorMessage) return processI18n(props.errorMessage);
148
- const text = apiErrorText(error);
149
- if (text) return processApiMessage(text);
150
- return processI18n("$dms.form.error_unknown");
151
- };
152
-
153
146
  /** Puts a refused submit's field errors under its fields, if any. */
154
147
  const showServerFieldErrors = (
155
148
  error: Parameters<typeof resolveFormFieldErrors>[0],
@@ -165,10 +158,16 @@ function createFormNotices(
165
158
  name: entry.field,
166
159
  message: entry.values?.length
167
160
  ? t("dms.field_errors.with_values", {
168
- message: processApiMessage(entry.message, entry.params),
161
+ message: resolveError(entry.message, {
162
+ fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
163
+ params: entry.params,
164
+ })?.message,
169
165
  values: entry.values.join(", "),
170
166
  })
171
- : processApiMessage(entry.message, entry.params),
167
+ : (resolveError(entry.message, {
168
+ fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
169
+ params: entry.params,
170
+ })?.message ?? ""),
172
171
  values: entry.values,
173
172
  path: entry.path,
174
173
  })),
@@ -176,16 +175,15 @@ function createFormNotices(
176
175
  };
177
176
 
178
177
  const showSubmitErrorToast = (error: EventError) => {
179
- toast.add({
180
- title: processI18n("$dms.form.error_title"),
181
- description: resolveSubmitErrorDescription(error),
182
- color: Color.error,
178
+ presentError(error, {
179
+ fallbackKey: props.errorMessage || "dms.form.error_unknown",
183
180
  });
184
181
  };
185
182
 
186
183
  return {
187
184
  ...context,
188
185
  options,
186
+ presentError,
189
187
  showSubmitSuccessToast,
190
188
  showSubmitNotice,
191
189
  showServerFieldErrors,
@@ -278,8 +276,7 @@ function createSavedForm(context: ReturnType<typeof createFormNotices>) {
278
276
  function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
279
277
  const {
280
278
  props,
281
- toast,
282
- processI18n,
279
+ presentError,
283
280
  buildUrlContext,
284
281
  showSubmitSuccessToast,
285
282
  showSubmitNotice,
@@ -307,11 +304,7 @@ function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
307
304
  };
308
305
 
309
306
  const showUnresolvedTargetToast = () => {
310
- toast.add({
311
- title: processI18n("$dms.form.error_title"),
312
- description: processI18n("$dms.form.error_no_target"),
313
- color: Color.error,
314
- });
307
+ presentError(null, { fallbackKey: "dms.form.error_no_target" });
315
308
  };
316
309
 
317
310
  /** Sends through submit events, marking accepted data clean before success. */