@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
@@ -27,7 +27,7 @@ const props = defineProps<NotificationInboxDisplayProps>();
27
27
  const SKELETON_ROWS = 3;
28
28
 
29
29
  const { t, locale } = useI18n();
30
- const toast = useToast();
30
+ const { presentError } = useApiErrorPresenter();
31
31
  const { confirm } = useConfirm();
32
32
  const catalog = useNotificationCatalog();
33
33
  const {
@@ -70,8 +70,8 @@ const runThenRefresh = async (action: () => ReturnType<typeof markAsRead>) => {
70
70
  await action();
71
71
 
72
72
  await nextTick();
73
- } catch {
74
- toast.add({ title: t("dms.form.error_title"), color: "error" });
73
+ } catch (error) {
74
+ presentError(error, { fallbackKey: "dms.form.error_title" });
75
75
  } finally {
76
76
  ownChanges--;
77
77
  }
@@ -44,6 +44,7 @@ const EMAIL_ANCHOR = "#email";
44
44
 
45
45
  const { t } = useI18n();
46
46
  const toast = useToast();
47
+ const { presentError } = useApiErrorPresenter();
47
48
  const { $authFetch } = useAuthFetch();
48
49
  const { processApiMessage } = useTranslation();
49
50
  const { user, refresh: refreshSession } = useCurrentUser();
@@ -112,8 +113,8 @@ function pickFile(): void {
112
113
  }
113
114
 
114
115
  async function onFileChange(event: Event): Promise<void> {
115
- // SAFETY: The template binds this change listener directly to the file input, which supplies the event target.
116
- const input = event.target as HTMLInputElement;
116
+ const input = event.target;
117
+ if (!(input instanceof HTMLInputElement)) return;
117
118
  const file = input.files?.[0];
118
119
  input.value = "";
119
120
  if (!file) return;
@@ -130,10 +131,9 @@ async function onFileChange(event: Event): Promise<void> {
130
131
  }
131
132
  avatar.value = { key: result.key! };
132
133
  pickedAvatarUrl.value = URL.createObjectURL(file);
133
- } catch {
134
- toast.add({
135
- title: t("page.settings.profile.avatar_error"),
136
- color: "error",
134
+ } catch (error) {
135
+ presentError(error, {
136
+ fallbackKey: "page.settings.profile.avatar_error",
137
137
  });
138
138
  } finally {
139
139
  isUploading.value = false;
@@ -38,6 +38,7 @@ const FIELD_CODES = {
38
38
 
39
39
  const { t } = useI18n();
40
40
  const toast = useToast();
41
+ const { presentError } = useApiErrorPresenter();
41
42
  const { $authFetch } = useAuthFetch();
42
43
  const { refresh: refreshSession } = useCurrentUser();
43
44
  const { overview, refresh } = useSecurityOverview();
@@ -180,9 +181,8 @@ async function submit(): Promise<void> {
180
181
  isCodeOpen.value = true;
181
182
  } catch (error) {
182
183
  if (await flagField(error)) return;
183
- toast.add({
184
- title: errorMessage(error, "page.settings.security.email.error"),
185
- color: "error",
184
+ presentError(error, {
185
+ fallbackKey: "page.settings.security.email.error",
186
186
  });
187
187
  } finally {
188
188
  isSaving.value = false;
@@ -217,9 +217,8 @@ async function confirmCode(code: string): Promise<void> {
217
217
  );
218
218
  return;
219
219
  }
220
- toast.add({
221
- title: errorMessage(error, "page.settings.security.email.error"),
222
- color: "error",
220
+ presentError(error, {
221
+ fallbackKey: "page.settings.security.email.error",
223
222
  });
224
223
  if (errorCode(error) === CODE_BURNT) {
225
224
  isCodeOpen.value = false;
@@ -239,9 +238,8 @@ async function cancelChange(): Promise<void> {
239
238
  });
240
239
  await refresh();
241
240
  } catch (error) {
242
- toast.add({
243
- title: errorMessage(error, "page.settings.security.email.error"),
244
- color: "error",
241
+ presentError(error, {
242
+ fallbackKey: "page.settings.security.email.error",
245
243
  });
246
244
  }
247
245
  }
@@ -36,10 +36,11 @@ const CONFIRM_FIELD_ID = "security-confirm-password";
36
36
 
37
37
  const { t } = useI18n();
38
38
  const toast = useToast();
39
+ const { presentError } = useApiErrorPresenter();
39
40
  const { $authFetch } = useAuthFetch();
40
41
  const { processApiMessage } = useTranslation();
41
42
  const { overview, refresh } = useSecurityOverview();
42
- const { formatDate, daysSince, errorMessage } = useSecurityFormat();
43
+ const { formatDate, daysSince } = useSecurityFormat();
43
44
  const { passwordSchema } = usePasswordStrength(ref(""));
44
45
  const adoptSessionHandoff = useSessionHandoff();
45
46
 
@@ -170,9 +171,8 @@ async function submit(): Promise<void> {
170
171
  document.getElementById(CURRENT_FIELD_ID)?.focus();
171
172
  return;
172
173
  }
173
- toast.add({
174
- title: errorMessage(error, "page.settings.security.password.error"),
175
- color: "error",
174
+ presentError(error, {
175
+ fallbackKey: "page.settings.security.password.error",
176
176
  });
177
177
  } finally {
178
178
  isSaving.value = false;
@@ -50,10 +50,11 @@ const DEVICE_ICONS: Readonly<
50
50
 
51
51
  const { t } = useI18n();
52
52
  const toast = useToast();
53
+ const { presentError } = useApiErrorPresenter();
53
54
  const { $authFetch } = useAuthFetch();
54
55
  const adoptSessionHandoff = useSessionHandoff();
55
56
  const { overview, refresh: refreshOverview } = useSecurityOverview();
56
- const { formatDate, formatRelative, errorMessage } = useSecurityFormat();
57
+ const { formatDate, formatRelative } = useSecurityFormat();
57
58
 
58
59
  const list = useApiData<SessionsRead>(SESSIONS_URL, {
59
60
  transform: (sessions) => ({
@@ -106,9 +107,8 @@ async function revoke(session: SessionInfo): Promise<void> {
106
107
  toast.add({ title: t("page.settings.sessions.revoked"), color: "success" });
107
108
  await refreshOverview();
108
109
  } catch (error) {
109
- toast.add({
110
- title: errorMessage(error, "page.settings.sessions.revoke_error"),
111
- color: "error",
110
+ presentError(error, {
111
+ fallbackKey: "page.settings.sessions.revoke_error",
112
112
  });
113
113
  } finally {
114
114
  revokingId.value = null;
@@ -134,9 +134,8 @@ async function signOutOthers(): Promise<void> {
134
134
  });
135
135
  await Promise.all([fetchSessions(), refreshOverview()]);
136
136
  } catch (error) {
137
- toast.add({
138
- title: errorMessage(error, "page.settings.sessions.revoke_all_error"),
139
- color: "error",
137
+ presentError(error, {
138
+ fallbackKey: "page.settings.sessions.revoke_all_error",
140
139
  });
141
140
  } finally {
142
141
  isSigningOutOthers.value = false;
@@ -65,6 +65,7 @@ const METHODS: MethodConfig[] = [
65
65
 
66
66
  const { t } = useI18n();
67
67
  const toast = useToast();
68
+ const { presentError } = useApiErrorPresenter();
68
69
  const { $authFetch } = useAuthFetch();
69
70
  const { overview, attention, attentionToneOf, refresh } = useSecurityOverview();
70
71
  const { formatDate, errorMessage } = useSecurityFormat();
@@ -148,7 +149,7 @@ function fail(
148
149
  error: Parameters<typeof errorMessage>[0],
149
150
  fallbackKey: string,
150
151
  ): void {
151
- toast.add({ title: errorMessage(error, fallbackKey), color: "error" });
152
+ presentError(error, { fallbackKey });
152
153
  }
153
154
 
154
155
  function showCodes(
@@ -12,7 +12,6 @@ import {
12
12
  } from "#dms-ui/app/composables/confirm/types";
13
13
  import { resolveFieldErrors } from "#dms-core/app/composables/useFieldErrors";
14
14
  import { REQUIRED_MESSAGE } from "#dms-core/app/composables/useFormValidation";
15
- import { useSecurityFormat } from "../security/useSecurityFormat";
16
15
 
17
16
  const PROFILE_ENDPOINT = pageRoutePath("/settings/user/profile");
18
17
  const EXPORT_ENDPOINT = `${PROFILE_ENDPOINT}/account-export`;
@@ -63,24 +62,14 @@ function useAccountDataContext() {
63
62
  const { confirm } = useConfirm();
64
63
  const { user, clear } = useDmsUserSession<User>();
65
64
  const { removeAccount } = useMultiAccount();
66
- const { processI18n, processApiMessage } = useTranslation();
67
- const { errorMessage, errorCode } = useSecurityFormat();
65
+ const { processApiMessage } = useTranslation();
66
+ const { presentError, resolveError } = useApiErrorPresenter();
68
67
  const toast = useToast();
69
68
  const { t } = useI18n();
70
69
 
71
70
  const isExporting = ref(false);
72
71
  const isCheckingDeletion = ref(false);
73
72
 
74
- /** A message key the API answers with (`$…`), else a known error code. */
75
- function describeError(
76
- error: Parameters<typeof errorCode>[0],
77
- fallbackKey: string,
78
- ): string {
79
- const code = errorCode(error);
80
- if (code?.startsWith("$")) return processI18n(code);
81
- return errorMessage(error, fallbackKey);
82
- }
83
-
84
73
  async function exportData(): Promise<void> {
85
74
  isExporting.value = true;
86
75
  try {
@@ -97,11 +86,7 @@ function useAccountDataContext() {
97
86
  title: t(`${I18N}.export_done`),
98
87
  });
99
88
  } catch (error) {
100
- toast.add({
101
- color: "error",
102
- icon: "i-ph-warning-circle",
103
- title: describeError(error, `${I18N}.export_error`),
104
- });
89
+ presentError(error, { fallbackKey: `${I18N}.export_error` });
105
90
  } finally {
106
91
  isExporting.value = false;
107
92
  }
@@ -128,7 +113,8 @@ function useAccountDataContext() {
128
113
  t,
129
114
  isExporting,
130
115
  isCheckingDeletion,
131
- describeError,
116
+ presentError,
117
+ resolveError,
132
118
  exportData,
133
119
  signOutDeleted,
134
120
  };
@@ -260,11 +246,10 @@ export function useAccountData() {
260
246
  const {
261
247
  $authFetch,
262
248
  confirm,
263
- processApiMessage,
264
- toast,
249
+ presentError,
250
+ resolveError,
265
251
  t,
266
252
  isCheckingDeletion,
267
- describeError,
268
253
  signOutDeleted,
269
254
  } = context;
270
255
  const { impactList, passwordField, explainBlockers, passwordCheck } =
@@ -305,19 +290,18 @@ export function useAccountData() {
305
290
  },
306
291
  }).fields;
307
292
  if (fieldError?.field === "confirmation") {
308
- throw new ConfirmTextError(processApiMessage(fieldError.message));
293
+ throw new ConfirmTextError(fieldError.message, { cause: error });
309
294
  }
310
295
  if (fieldError?.field === "password") {
311
- passwordError.value = processApiMessage(fieldError.message);
296
+ passwordError.value = resolveError(fieldError.message, {
297
+ fallbackKey: fieldError.fallbackKey ?? "dms.field_errors.invalid",
298
+ params: fieldError.params,
299
+ })?.message;
312
300
  await nextTick();
313
301
  passwordInput.value?.inputRef?.focus();
314
302
  return false;
315
303
  }
316
- toast.add({
317
- color: "error",
318
- icon: "i-ph-warning-circle",
319
- title: describeError(error, `${I18N}.delete_error`),
320
- });
304
+ presentError(error, { fallbackKey: `${I18N}.delete_error` });
321
305
  return false;
322
306
  }
323
307
  }
@@ -353,11 +337,7 @@ export function useAccountData() {
353
337
  try {
354
338
  impact = await $authFetch<AccountDeletionImpact>(DELETION_ENDPOINT);
355
339
  } catch (error) {
356
- toast.add({
357
- color: "error",
358
- icon: "i-ph-warning-circle",
359
- title: describeError(error, `${I18N}.delete_error`),
360
- });
340
+ presentError(error, { fallbackKey: `${I18N}.delete_error` });
361
341
  return;
362
342
  } finally {
363
343
  isCheckingDeletion.value = false;
@@ -7,8 +7,6 @@ const MS_PER_HOUR = 60 * MS_PER_MINUTE;
7
7
  const MS_PER_DAY = 24 * MS_PER_HOUR;
8
8
  const MS_PER_MONTH = 30 * MS_PER_DAY;
9
9
  const MS_PER_YEAR = 365 * MS_PER_DAY;
10
- const ERROR_CODE_PREFIX = "error.";
11
- const SECURITY_ERRORS_PREFIX = "page.settings.security.errors.";
12
10
 
13
11
  interface RelativeUnit {
14
12
  unit: Intl.RelativeTimeFormatUnit;
@@ -35,7 +33,8 @@ type DateInput = string | Date;
35
33
  * translation of API error codes into sentences.
36
34
  */
37
35
  export function useSecurityFormat() {
38
- const { locale, t, te } = useI18n();
36
+ const { locale } = useI18n();
37
+ const { resolveError } = useApiErrorPresenter();
39
38
 
40
39
  const formatDate = (value: DateInput): string =>
41
40
  regionalDateTimeFormat(locale.value, { dateStyle: "medium" }).format(
@@ -74,14 +73,7 @@ export function useSecurityFormat() {
74
73
  */
75
74
  // oxlint-disable-next-line anti-slop/no-unknown-parameters -- JavaScript throws arbitrary values. FetchError-derived parameters would reject the boundary inputs this formatter already handles.
76
75
  const errorMessage = (error: unknown, fallbackKey: string): string => {
77
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- Unchecked optional property projection preserves primitive and nullish thrown values; this is not a validated FetchError and owner typing cannot guarantee one.
78
- const code = (error as FetchErrorLike | undefined)?.data;
79
- if (!isString(code) || !code.startsWith(ERROR_CODE_PREFIX)) {
80
- return t(fallbackKey);
81
- }
82
- const ownKey = `${SECURITY_ERRORS_PREFIX}${code.slice(ERROR_CODE_PREFIX.length)}`;
83
- if (te(ownKey)) return t(ownKey);
84
- return te(code) ? t(code) : t(fallbackKey);
76
+ return resolveError(error, { fallbackKey })?.message ?? "";
85
77
  };
86
78
 
87
79
  /** @returns The error code (`error.xxx`) the API answered with, if any */
@@ -1,5 +1,5 @@
1
1
  /** The DMS repository on GitHub, as `owner/name`. */
2
2
  const GITHUB_REPOSITORY = "AntelopeJS/dms";
3
3
 
4
- /** Opened by the dashboard's star prompt. */
4
+ /** The DMS repository page on GitHub. */
5
5
  export const GITHUB_REPOSITORY_URL = `https://github.com/${GITHUB_REPOSITORY}`;
@@ -23,6 +23,7 @@ const FOLLOW_INTERVAL_MS = 1000;
23
23
  */
24
24
  export const useModuleStore = () => {
25
25
  const { $authFetch } = useAuthFetch();
26
+ const { resolveError } = useApiErrorPresenter();
26
27
  const isOwner = useIsOwner();
27
28
 
28
29
  const initial = useApiData<StoreListing>(() =>
@@ -42,6 +43,13 @@ export const useModuleStore = () => {
42
43
  ? (initial.error.value ?? null)
43
44
  : followError.value,
44
45
  );
46
+ const loadingNotice = computed(() =>
47
+ loadingError.value
48
+ ? resolveError(loadingError.value, {
49
+ fallbackKey: "modules.store.load_error",
50
+ })
51
+ : null,
52
+ );
45
53
  /** Modules an operation from this page changed, until the frontend follows. */
46
54
  const awaitingReload = ref(new Set<string>());
47
55
  let followTimer: ReturnType<typeof setTimeout> | undefined;
@@ -151,6 +159,7 @@ export const useModuleStore = () => {
151
159
  return {
152
160
  listing,
153
161
  loadingError,
162
+ loadingNotice,
154
163
  isFollowing,
155
164
  awaitingReload,
156
165
  load,
@@ -1,4 +1,3 @@
1
- import { isString } from "../../../../dms-core/app/utils/type-check";
2
1
  import type { Ref, VNode } from "vue";
3
2
  import ModuleStoreDependencyPicker from "../../build/components/pages/modules/ModuleStoreDependencyPicker.vue";
4
3
  import {
@@ -22,12 +21,6 @@ import type {
22
21
  } from "#dms-core/app/types/module-store";
23
22
  import type { useModuleStore } from "./useModuleStore";
24
23
 
25
- /** What a refused store request carries: the server's error key, if any. */
26
- interface StoreRequestError {
27
- data?: unknown;
28
- message?: string;
29
- }
30
-
31
24
  /** What the frontend banner says while it lags behind the backend. */
32
25
  export interface StoreFrontendNotice {
33
26
  key: "dev" | "production" | "unknown";
@@ -65,18 +58,9 @@ export const useModuleStoreActions = (
65
58
  ) => {
66
59
  const { t } = useI18n();
67
60
  const { processI18n } = useTranslation();
68
- const toast = useToast();
61
+ const { presentError } = useApiErrorPresenter();
69
62
  const { confirm } = useConfirm();
70
63
 
71
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Store requests can reject with arbitrary JavaScript values; the store's Promise contract has no rejection owner type.
72
- function resolveStoreError(error: unknown): string {
73
- // oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- This existing unchecked optional-property view is not validation of a StoreRequestError; inference cannot establish that rejected values have this shape, and parsing would change malformed-error behavior.
74
- const details = error as StoreRequestError | undefined;
75
- const key = isString(details?.data) ? details.data : undefined;
76
- if (!key) return details?.message ?? "";
77
- return t(`modules.store.errors.${key.replace("error.", "")}`);
78
- }
79
-
80
64
  /**
81
65
  * Runs `kind` from a dialog: a refusal is worded as the toast words it and
82
66
  * shown in the dialog, which stays open.
@@ -89,12 +73,15 @@ export const useModuleStoreActions = (
89
73
  try {
90
74
  await moduleStore.run(kind, entry, body);
91
75
  } catch (error) {
92
- throw new ConfirmActionError({
93
- title: t("modules.store.action_failed", {
94
- module: processI18n(entry.title),
95
- }),
96
- description: resolveStoreError(error),
97
- });
76
+ throw new ConfirmActionError(
77
+ {
78
+ fallbackKey: "modules.store.action_failed",
79
+ params: {
80
+ module: processI18n(entry.title),
81
+ },
82
+ },
83
+ { cause: error },
84
+ );
98
85
  }
99
86
  }
100
87
 
@@ -165,11 +152,9 @@ export const useModuleStoreActions = (
165
152
  try {
166
153
  await moduleStore.run(kind, entry);
167
154
  } catch (error) {
168
- toast.add({
169
- color: "error",
170
- icon: "i-ph-warning-circle",
171
- title: t("modules.store.action_failed", { module: title }),
172
- description: resolveStoreError(error),
155
+ presentError(error, {
156
+ fallbackKey: "modules.store.action_failed",
157
+ params: { module: title },
173
158
  });
174
159
  }
175
160
  }
@@ -178,10 +178,10 @@ const skeletonItems = Array.from(
178
178
  while the frontend restarts keeps the tiles and tries again. -->
179
179
  <DmsBanner
180
180
  v-else-if="moduleStore.loadingError.value && !storeListing"
181
- tone="error"
181
+ :tone="moduleStore.loadingNotice.value?.severity ?? 'error'"
182
182
  size="sm"
183
183
  icon="i-ph-plugs"
184
- :title="t('modules.store.load_error')"
184
+ :title="moduleStore.loadingNotice.value?.message"
185
185
  >
186
186
  <template #actions>
187
187
  <UButton
@@ -26,6 +26,8 @@ import { REQUIRED_MESSAGE } from "#dms-core/app/composables/useFormValidation";
26
26
  import { useUnsavedChanges } from "#dms-ui/app/composables/unsaved-changes/useUnsavedChanges";
27
27
  import { useActionConfirm } from "#dms-ui/app/entries/composables/confirm/useActionConfirm";
28
28
 
29
+ const { presentError } = useApiErrorPresenter();
30
+
29
31
  interface RoleEditorHandle {
30
32
  inputOf: (field: RoleEditorField) => HTMLInputElement | undefined;
31
33
  }
@@ -152,7 +154,7 @@ async function runMutation(
152
154
  await mutation();
153
155
  toast.add({ title: t(successKey), color: "success" });
154
156
  } catch (error) {
155
- useApiError(error);
157
+ presentError(error, { fallbackKey: "error.500.description" });
156
158
  }
157
159
  }
158
160
 
@@ -181,7 +183,8 @@ async function save(): Promise<void> {
181
183
  color: "success",
182
184
  });
183
185
  } catch (error) {
184
- if (!(await fieldErrors.applyApiError(error))) useApiError(error);
186
+ if (!(await fieldErrors.applyApiError(error)))
187
+ presentError(error, { fallbackKey: "error.500.description" });
185
188
  } finally {
186
189
  isSaving.value = false;
187
190
  }
@@ -34,6 +34,7 @@ export default defineDmsPlugin(() => {
34
34
  const { $i18n: i18n } = useDmsApp();
35
35
  const { t } = getAppTranslation();
36
36
  const toast = useToast();
37
+ const { presentError } = useApiErrorPresenter();
37
38
  const { logout } = useLogout();
38
39
  const { changeLanguage } = useUserLanguage();
39
40
  const { copy } = useClipboard({ legacy: true });
@@ -51,10 +52,9 @@ export default defineDmsPlugin(() => {
51
52
  try {
52
53
  await copy(window.location.href);
53
54
  toast.add({ title: t("commandPalette.session.urlCopied") });
54
- } catch {
55
- toast.add({
56
- title: t("commandPalette.session.copyFailed"),
57
- color: "error",
55
+ } catch (error) {
56
+ presentError(error, {
57
+ fallbackKey: "commandPalette.session.copyFailed",
58
58
  });
59
59
  }
60
60
  }
@@ -875,10 +875,6 @@
875
875
  "title": "Builder",
876
876
  "description": "Edits the project’s pages, tables and forms, writing only code that compiles."
877
877
  },
878
- "cicd": {
879
- "title": "Publishing",
880
- "description": "Versions, environments and deploys of your project through git."
881
- },
882
878
  "database": {
883
879
  "title": "Database",
884
880
  "description": "Schemas, data browser, schema diagram and an AQL query workspace."
@@ -1048,6 +1044,8 @@
1048
1044
  "footer": {
1049
1045
  "documentation": "Documentation",
1050
1046
  "website": "Website",
1047
+ "github_star": "Star",
1048
+ "github_star_label": "Star AntelopeJS DMS on GitHub",
1051
1049
  "language": "Language"
1052
1050
  }
1053
1051
  },
@@ -875,10 +875,6 @@
875
875
  "title": "Builder",
876
876
  "description": "Modifie les pages, tableaux et formulaires du projet, en n’écrivant que du code qui compile."
877
877
  },
878
- "cicd": {
879
- "title": "Publication",
880
- "description": "Versions, environnements et déploiements de votre projet via git."
881
- },
882
878
  "database": {
883
879
  "title": "Base de données",
884
880
  "description": "Schémas, explorateur de données, diagramme et espace de requêtes AQL."
@@ -1048,6 +1044,8 @@
1048
1044
  "footer": {
1049
1045
  "documentation": "Documentation",
1050
1046
  "website": "Site web",
1047
+ "github_star": "Star",
1048
+ "github_star_label": "Mettre une étoile à AntelopeJS DMS sur GitHub",
1051
1049
  "language": "Langue"
1052
1050
  }
1053
1051
  },
@@ -63,9 +63,11 @@ function claimsValid(
63
63
  function rememberToken(jti: string, exp: number, now: number) {
64
64
  for (const [spent, expiresAt] of usedRenderTokens)
65
65
  if (expiresAt <= now) usedRenderTokens.delete(spent);
66
- while (usedRenderTokens.size >= RENDER_REPLAY_LIMIT)
67
- // SAFETY: The synchronous loop requires at least 10,000 entries in this Map<string, number>, so next() has a string key.
68
- usedRenderTokens.delete(usedRenderTokens.keys().next().value as string);
66
+ while (usedRenderTokens.size >= RENDER_REPLAY_LIMIT) {
67
+ const [oldest] = usedRenderTokens.keys();
68
+ if (oldest === undefined) break;
69
+ usedRenderTokens.delete(oldest);
70
+ }
69
71
  usedRenderTokens.set(jti, exp);
70
72
  }
71
73
 
@@ -45,6 +45,7 @@ const VARIANTS = {
45
45
  } as const;
46
46
 
47
47
  const { t } = useI18n();
48
+ const { resolveError } = useApiErrorPresenter();
48
49
  const { processI18n } = useTranslation();
49
50
 
50
51
  const translate = (text: string | undefined) =>
@@ -52,7 +53,7 @@ const translate = (text: string | undefined) =>
52
53
 
53
54
  const notFoundState = computed(() => {
54
55
  const answered = props.notFound?.emptyState ?? {};
55
- const defaults = notFoundTexts(props.notFound, t);
56
+ const defaults = notFoundTexts(props.notFound, t, resolveError);
56
57
  return {
57
58
  title: translate(answered.title) ?? defaults.title,
58
59
  description: translate(answered.description) ?? defaults.description,
@@ -37,12 +37,8 @@ export function createExportJobHandler(
37
37
  successMessage: target.labels.successMessage
38
38
  ? processI18n(target.labels.successMessage)
39
39
  : undefined,
40
- errorTitle: target.labels.errorTitle
41
- ? processI18n(target.labels.errorTitle)
42
- : undefined,
43
- retry: target.labels.retry
44
- ? processI18n(target.labels.retry)
45
- : undefined,
40
+ errorTitle: target.labels.errorTitle,
41
+ retry: target.labels.retry,
46
42
  }
47
43
  : undefined;
48
44
  const statusUrlTemplate = target.statusUrl;
@@ -121,19 +117,19 @@ export function createActionRequests(
121
117
  const handleApiTarget = async (
122
118
  target: ActionTarget & { type: "api" },
123
119
  url: string,
124
- label: string,
120
+ _label: string,
125
121
  rowData?: Data,
126
122
  selection?: BulkSelection,
127
123
  ) => {
128
124
  try {
129
125
  await requestApiTarget(target, url, rowData, {}, selection);
130
126
  } catch (error: unknown) {
131
- handleApiError(error, label ? processI18n(label, rowData) : undefined);
127
+ handleApiError(error);
132
128
  }
133
129
  };
134
130
  const handleDownloadTarget = async (
135
131
  target: ActionTarget & { type: "download" },
136
- label: string,
132
+ _label: string,
137
133
  rowData?: Data,
138
134
  ) => {
139
135
  try {
@@ -145,7 +141,7 @@ export function createActionRequests(
145
141
  fetchRaw,
146
142
  );
147
143
  } catch (error: unknown) {
148
- handleApiError(error, label ? processI18n(label, rowData) : undefined);
144
+ handleApiError(error);
149
145
  }
150
146
  };
151
147
  return { requestApiTarget, handleApiTarget, handleDownloadTarget };