@antelopejs/dms 0.12.1 → 0.13.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.
- package/CHANGELOG.md +40 -0
- package/data/dbip-country-lite.mmdb +0 -0
- package/data/module-store/catalog.json +0 -6
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/ensure-default-tenant.js +7 -2
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/frontend-vue/dms.frontend.ts +2 -0
- package/frontend-vue/layers/dms-auth/app/build/components/AuthFormAlert.vue +1 -1
- package/frontend-vue/layers/dms-auth/app/build/composables/useAuthFormError.ts +11 -14
- package/frontend-vue/layers/dms-auth/app/composables/useOAuthErrorToast.ts +3 -8
- package/frontend-vue/layers/dms-auth/app/custom-pages/auth/accounts.vue +3 -2
- package/frontend-vue/layers/dms-core/app/build/composables/api-error.ts +145 -0
- package/frontend-vue/layers/dms-core/app/build/composables/auth/usePermissionPreview.ts +5 -1
- package/frontend-vue/layers/dms-core/app/composables/auth/useAuthFetch.ts +0 -2
- package/frontend-vue/layers/dms-core/app/composables/page/useSiteLayout.ts +6 -5
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -11
- package/frontend-vue/layers/dms-core/app/composables/useApiErrorPresenter.ts +159 -0
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +14 -29
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +3 -1
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/notification/NotificationInboxDisplay.vue +3 -3
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/profile/ProfilePersonalInfo.vue +4 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityEmail.vue +7 -9
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityPassword.vue +4 -4
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecuritySessions.vue +6 -7
- package/frontend-vue/layers/dms-layout/app/build/components/pages/settings/security/SecurityTwoFactor.vue +2 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts +14 -34
- package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts +3 -11
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +9 -0
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStoreActions.ts +13 -28
- package/frontend-vue/layers/dms-layout/app/custom-pages/modules-store.vue +2 -2
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +5 -2
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +4 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +0 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +0 -4
- package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockStatus.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/actionTargetRequests.ts +6 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/groupPoints.ts +13 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +10 -139
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +2 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formState.ts +2 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useTableConfigClipboard.ts +7 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionBulkRequests.ts +11 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionValues.ts +1 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableViewGrouping.ts +7 -10
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useExpandedRowDetails.ts +4 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRecordLink.ts +4 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +7 -18
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +3 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +9 -5
- package/frontend-vue/layers/dms-ui/app/build/utils/fetchNotFound.ts +4 -3
- package/frontend-vue/layers/dms-ui/app/components/block-preview/BlockPreview.vue +6 -1
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +3 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +24 -19
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +6 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Image.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/ImagePresentation.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/imageItems.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/components/guard/Guard.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +13 -11
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +6 -6
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +14 -25
- package/frontend-vue/layers/dms-ui/app/composables/global/keyboardPlatform.ts +36 -9
- package/frontend-vue/layers/dms-ui/app/composables/useExportJob.ts +13 -59
- package/frontend-vue/layers/dms-ui/app/plugins/kbd-platform.ts +13 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/package.json +1 -3
- package/frontend-vue/scripts/check-api-error-presentation.mjs +248 -0
- package/frontend-vue/scripts/check-invariants.mjs +23 -0
- package/frontend-vue/scripts/codemods/api-error-presenter.mjs +196 -0
- package/frontend-vue/scripts/test-api-error-presentation.mjs +134 -0
- package/package.json +10 -4
- package/skills/dms-module/REFERENCE.md +3 -1
- package/skills/dms-module/SKILL.md +7 -2
- package/skills/dms-project/SKILL.md +9 -3
- package/frontend-vue/layers/dms-core/app/composables/useReadOnlyRefusal.ts +0 -49
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { decodeApiFailure } from "../build/composables/api-error";
|
|
2
|
+
|
|
3
|
+
/** The supported tones for request failures. */
|
|
4
|
+
export type ApiErrorSeverity = "info" | "warning" | "error";
|
|
5
|
+
|
|
6
|
+
/** Localized text and tone for an inline error. */
|
|
7
|
+
export interface ApiErrorNotice {
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly severity: ApiErrorSeverity;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Placeholder values accepted by error translations. */
|
|
13
|
+
export type ApiErrorParams = Readonly<
|
|
14
|
+
Record<string, string | number | boolean>
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
/** Keys are resolved against loaded catalogs and never displayed verbatim. */
|
|
18
|
+
export interface ResolveApiErrorOptions {
|
|
19
|
+
readonly fallbackKey: string;
|
|
20
|
+
readonly params?: ApiErrorParams;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A contextual action whose label is also a loaded translation key. */
|
|
24
|
+
export interface ApiErrorAction {
|
|
25
|
+
readonly labelKey: string;
|
|
26
|
+
readonly onSelect: () => void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An existing progress notification, not the identity of a failure. */
|
|
30
|
+
export type ApiErrorToastId = string | number;
|
|
31
|
+
|
|
32
|
+
/** Toast presentation never overrides the server's translated reason or tone. */
|
|
33
|
+
export interface PresentApiErrorOptions extends ResolveApiErrorOptions {
|
|
34
|
+
readonly actions?: readonly ApiErrorAction[];
|
|
35
|
+
readonly replaceToastId?: ApiErrorToastId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Captured during setup and callable after awaits. */
|
|
39
|
+
export interface ApiErrorPresenter {
|
|
40
|
+
resolveError(
|
|
41
|
+
error: unknown,
|
|
42
|
+
options: ResolveApiErrorOptions,
|
|
43
|
+
): ApiErrorNotice | null;
|
|
44
|
+
presentError(error: unknown, options: PresentApiErrorOptions): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const GENERIC_KEY = "error.500.description";
|
|
48
|
+
const PRESENTED_STATE_KEY = "dms.apiErrors.presentedResponses";
|
|
49
|
+
const FAILURE_DURATION_MS = 5000;
|
|
50
|
+
const ICONS: Record<ApiErrorSeverity, string> = {
|
|
51
|
+
info: "i-ph-info",
|
|
52
|
+
warning: "i-ph-warning",
|
|
53
|
+
error: "i-ph-warning-circle",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function isCompiledMessage(value: unknown): boolean {
|
|
57
|
+
return (
|
|
58
|
+
value !== null &&
|
|
59
|
+
typeof value === "object" &&
|
|
60
|
+
(("type" in value && value.type === 0) || ("t" in value && value.t === 0))
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolves errors without side effects and presents at most one toast per failed
|
|
66
|
+
* Response in this app. Aborts without a failed response are silent.
|
|
67
|
+
*/
|
|
68
|
+
export function useApiErrorPresenter(): ApiErrorPresenter {
|
|
69
|
+
const toast = useToast();
|
|
70
|
+
const { i18n } = getPluginI18n();
|
|
71
|
+
const presented = useDmsState(
|
|
72
|
+
PRESENTED_STATE_KEY,
|
|
73
|
+
() => new WeakMap<object, ApiErrorToastId | null>(),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
function lookup(
|
|
77
|
+
key: string | undefined,
|
|
78
|
+
params: ApiErrorParams = {},
|
|
79
|
+
): string | null {
|
|
80
|
+
if (!key) return null;
|
|
81
|
+
try {
|
|
82
|
+
const message = i18n.tm(key.startsWith("$") ? key.slice(1) : key);
|
|
83
|
+
if (
|
|
84
|
+
typeof message !== "string" &&
|
|
85
|
+
typeof message !== "function" &&
|
|
86
|
+
!isCompiledMessage(message)
|
|
87
|
+
)
|
|
88
|
+
return null;
|
|
89
|
+
return i18n.rt(message, params);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function noticeOf(
|
|
96
|
+
failure: ReturnType<typeof decodeApiFailure>,
|
|
97
|
+
options: ResolveApiErrorOptions,
|
|
98
|
+
): ApiErrorNotice | null {
|
|
99
|
+
if (failure.cancelled) return null;
|
|
100
|
+
const params = { ...failure.params, ...options.params };
|
|
101
|
+
const message =
|
|
102
|
+
lookup(failure.key, params) ??
|
|
103
|
+
lookup(options.fallbackKey, params) ??
|
|
104
|
+
lookup(GENERIC_KEY);
|
|
105
|
+
if (message === null)
|
|
106
|
+
throw new Error(
|
|
107
|
+
"The core error.500.description translation must be loaded",
|
|
108
|
+
);
|
|
109
|
+
return { message, severity: failure.severity };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveError(
|
|
113
|
+
error: unknown,
|
|
114
|
+
options: ResolveApiErrorOptions,
|
|
115
|
+
): ApiErrorNotice | null {
|
|
116
|
+
return noticeOf(decodeApiFailure(error), options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function presentError(error: unknown, options: PresentApiErrorOptions): void {
|
|
120
|
+
const failure = decodeApiFailure(error);
|
|
121
|
+
const notice = noticeOf(failure, options);
|
|
122
|
+
if (import.meta.env.SSR) return;
|
|
123
|
+
const replacement = options.replaceToastId;
|
|
124
|
+
const identity = failure.identity;
|
|
125
|
+
if (!notice || (identity && presented.value.has(identity))) {
|
|
126
|
+
if (
|
|
127
|
+
replacement !== undefined &&
|
|
128
|
+
(!identity || presented.value.get(identity) !== replacement)
|
|
129
|
+
)
|
|
130
|
+
toast.remove(replacement);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const payload = {
|
|
134
|
+
title: notice.message,
|
|
135
|
+
description: undefined,
|
|
136
|
+
color: notice.severity,
|
|
137
|
+
icon: ICONS[notice.severity],
|
|
138
|
+
duration: FAILURE_DURATION_MS,
|
|
139
|
+
progress: true,
|
|
140
|
+
actions: options.actions?.flatMap((action) => {
|
|
141
|
+
const label = lookup(action.labelKey);
|
|
142
|
+
return label === null ? [] : [{ label, onClick: action.onSelect }];
|
|
143
|
+
}),
|
|
144
|
+
};
|
|
145
|
+
if (identity) presented.value.set(identity, replacement ?? null);
|
|
146
|
+
try {
|
|
147
|
+
if (replacement !== undefined) toast.update(replacement, payload);
|
|
148
|
+
else {
|
|
149
|
+
const added = toast.add(payload);
|
|
150
|
+
if (identity) presented.value.set(identity, added.id);
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (identity) presented.value.delete(identity);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { presentError, resolveError };
|
|
159
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { nextTick, reactive } from "vue";
|
|
2
|
-
import {
|
|
3
|
-
type I18nTranslate,
|
|
4
|
-
resolveApiMessage,
|
|
5
|
-
} from "./translation/useTranslation";
|
|
2
|
+
import { type I18nTranslate } from "./translation/useTranslation";
|
|
6
3
|
import { resolveApiErrorMessage } from "./useApiError";
|
|
7
4
|
import { isBoolean, isNumber, isObject, isString } from "../utils/type-check";
|
|
8
5
|
|
|
@@ -19,11 +16,10 @@ import { isBoolean, isNumber, isObject, isString } from "../utils/type-check";
|
|
|
19
16
|
export interface ApiFieldError {
|
|
20
17
|
/** Id of the field, as the form names it (the request body key). */
|
|
21
18
|
field: string;
|
|
22
|
-
/**
|
|
23
|
-
* What to show under the field: an i18n key (bare `error.xxx` or `$key`)
|
|
24
|
-
* or prose, resolved with `resolveApiMessage`.
|
|
25
|
-
*/
|
|
19
|
+
/** Server translation key, resolved by the API error presenter. */
|
|
26
20
|
message: string;
|
|
21
|
+
/** Caller key used only when the server's message is not translated. */
|
|
22
|
+
fallbackKey?: string;
|
|
27
23
|
/** The values of the field the error names (addresses of a list…). */
|
|
28
24
|
values?: string[];
|
|
29
25
|
/**
|
|
@@ -325,7 +321,7 @@ function lookupCode<F extends string>(
|
|
|
325
321
|
if (!entry) return undefined;
|
|
326
322
|
return isString(entry)
|
|
327
323
|
? { field: entry, message: code }
|
|
328
|
-
: { field: entry.field, message: entry.message };
|
|
324
|
+
: { field: entry.field, message: code, fallbackKey: entry.message };
|
|
329
325
|
}
|
|
330
326
|
|
|
331
327
|
function fromBody(body: ReturnType<typeof bodyOf>): ApiFieldError[] {
|
|
@@ -489,11 +485,7 @@ export interface HandleApiErrorOptions {
|
|
|
489
485
|
export function useFieldErrors<F extends string>(
|
|
490
486
|
options: UseFieldErrorsOptions<F>,
|
|
491
487
|
) {
|
|
492
|
-
const {
|
|
493
|
-
const toast = useToast();
|
|
494
|
-
const consumeReadOnlyRefusal = useReadOnlyRefusal();
|
|
495
|
-
const translate = (key: string, params: Parameters<I18nTranslate>[1] = {}) =>
|
|
496
|
-
t(key, params);
|
|
488
|
+
const { presentError, resolveError } = useApiErrorPresenter();
|
|
497
489
|
const fieldIds = Object.keys(options.fields) as F[];
|
|
498
490
|
const errors: Partial<Record<F, string>> = reactive({});
|
|
499
491
|
|
|
@@ -549,21 +541,20 @@ export function useFieldErrors<F extends string>(
|
|
|
549
541
|
async function applyApiError(
|
|
550
542
|
error: Parameters<typeof resolveApiErrorMessage>[0],
|
|
551
543
|
): Promise<boolean> {
|
|
552
|
-
const { fields } = resolveFieldErrors(error, {
|
|
544
|
+
const { fields, hasUnmatched } = resolveFieldErrors(error, {
|
|
553
545
|
fields: fieldIds,
|
|
554
546
|
codes: options.codes,
|
|
555
547
|
});
|
|
556
548
|
if (fields.length === 0) return false;
|
|
557
549
|
clear();
|
|
558
550
|
for (const entry of fields) {
|
|
559
|
-
errors[entry.field as F] =
|
|
560
|
-
|
|
561
|
-
entry.
|
|
562
|
-
|
|
563
|
-
);
|
|
551
|
+
errors[entry.field as F] = resolveError(entry.message, {
|
|
552
|
+
fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
|
|
553
|
+
params: entry.params,
|
|
554
|
+
})?.message;
|
|
564
555
|
}
|
|
565
556
|
await focus(fields[0]!.field as F);
|
|
566
|
-
return
|
|
557
|
+
return !hasUnmatched;
|
|
567
558
|
}
|
|
568
559
|
/* oxlint-enable anti-slop/require-safety-comment-for-type-assertion */
|
|
569
560
|
|
|
@@ -572,13 +563,8 @@ export function useFieldErrors<F extends string>(
|
|
|
572
563
|
error: Parameters<typeof resolveApiErrorMessage>[0],
|
|
573
564
|
toastOptions: HandleApiErrorOptions,
|
|
574
565
|
) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
title: t(toastOptions.toastTitle),
|
|
578
|
-
description: toastOptions.toastDescription
|
|
579
|
-
? t(toastOptions.toastDescription)
|
|
580
|
-
: resolveApiMessage(translate, describeApiError(error)),
|
|
581
|
-
color: "error",
|
|
566
|
+
presentError(error, {
|
|
567
|
+
fallbackKey: toastOptions.toastDescription ?? toastOptions.toastTitle,
|
|
582
568
|
});
|
|
583
569
|
}
|
|
584
570
|
|
|
@@ -591,7 +577,6 @@ export function useFieldErrors<F extends string>(
|
|
|
591
577
|
error: Parameters<typeof resolveApiErrorMessage>[0],
|
|
592
578
|
toastOptions: HandleApiErrorOptions,
|
|
593
579
|
): Promise<boolean> {
|
|
594
|
-
if (consumeReadOnlyRefusal(error)) return false;
|
|
595
580
|
if (await applyApiError(error)) return true;
|
|
596
581
|
toastError(error, toastOptions);
|
|
597
582
|
return false;
|
|
@@ -75,13 +75,15 @@ const { quickActions } = siteLayout;
|
|
|
75
75
|
const route = useDmsRoute();
|
|
76
76
|
const router = useDmsRouter();
|
|
77
77
|
const { $authFetch } = useAuthFetch();
|
|
78
|
+
const { presentError } = useApiErrorPresenter();
|
|
78
79
|
|
|
79
80
|
const { handleCustomButton } = useActionTargets({
|
|
80
81
|
api: $authFetch,
|
|
81
82
|
componentId: "page-header",
|
|
82
83
|
pageId: siteLayout.findMatchingRoute(route.path)?.metadata?.fullId ?? "page",
|
|
83
84
|
refreshCallback: refreshPageBlocks,
|
|
84
|
-
handleApiError: (error, title) =>
|
|
85
|
+
handleApiError: (error, title) =>
|
|
86
|
+
presentError(error, { fallbackKey: title ?? "error.500.description" }),
|
|
85
87
|
});
|
|
86
88
|
|
|
87
89
|
function pressComponentButton(componentId: string, buttonId?: string) {
|
|
@@ -27,7 +27,7 @@ const props = defineProps<NotificationInboxDisplayProps>();
|
|
|
27
27
|
const SKELETON_ROWS = 3;
|
|
28
28
|
|
|
29
29
|
const { t, locale } = useI18n();
|
|
30
|
-
const
|
|
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
|
-
|
|
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();
|
|
@@ -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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
152
|
+
presentError(error, { fallbackKey });
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
function showCodes(
|
package/frontend-vue/layers/dms-layout/app/build/composables/settings/profile/useAccountData.ts
CHANGED
|
@@ -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 {
|
|
67
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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(
|
|
293
|
+
throw new ConfirmTextError(fieldError.message, { cause: error });
|
|
309
294
|
}
|
|
310
295
|
if (fieldError?.field === "password") {
|
|
311
|
-
passwordError.value =
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/frontend-vue/layers/dms-layout/app/build/composables/settings/security/useSecurityFormat.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 */
|
|
@@ -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,
|