@antelopejs/dms 0.12.1 → 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.
- package/CHANGELOG.md +28 -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 +242 -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 +93 -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
|
@@ -49,6 +49,7 @@ export function useTableRecordLink<T>(
|
|
|
49
49
|
options: TableRecordLinkOptions<T>,
|
|
50
50
|
): TableRecordLink {
|
|
51
51
|
const { action, urlScope } = options;
|
|
52
|
+
const { presentError } = useApiErrorPresenter();
|
|
52
53
|
|
|
53
54
|
let linkedId: string | undefined;
|
|
54
55
|
|
|
@@ -62,7 +63,9 @@ export function useTableRecordLink<T>(
|
|
|
62
63
|
if (outcome === "missingPanel") return options.showMissing(id);
|
|
63
64
|
writeRecordId(urlScope);
|
|
64
65
|
if (linkedId === id) linkedId = undefined;
|
|
65
|
-
|
|
66
|
+
if (outcome === "notifyFailed")
|
|
67
|
+
presentError(found.error, { fallbackKey: "dms.table.load_error_title" });
|
|
68
|
+
else options.notify(outcome);
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
const follow = (query: LocationQuery) => {
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useActionConfirm } from "../confirm/useActionConfirm";
|
|
2
2
|
import { useActionTargets } from "../actions/useActionTargets";
|
|
3
|
-
import {
|
|
3
|
+
import { tableWriteErrorOptions } from "./utils/writeError";
|
|
4
4
|
import type {
|
|
5
5
|
TableRowActionsConfig,
|
|
6
6
|
TableRowActionServices,
|
|
@@ -14,7 +14,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
14
14
|
config: TableRowActionsConfig,
|
|
15
15
|
) => {
|
|
16
16
|
const toast = useToast();
|
|
17
|
-
const
|
|
17
|
+
const { presentError } = useApiErrorPresenter();
|
|
18
18
|
const { t, locale } = useI18n();
|
|
19
19
|
const { confirm } = useConfirm();
|
|
20
20
|
const { confirmAction } = useActionConfirm();
|
|
@@ -39,22 +39,11 @@ export const useTableRowActions = <T extends Data>(
|
|
|
39
39
|
* what failed, the description is the server's message when it is meant
|
|
40
40
|
* for users, else a translated fallback (never a raw error line).
|
|
41
41
|
*/
|
|
42
|
-
const handleApiError = (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
toast.add({
|
|
48
|
-
color: Color.error,
|
|
49
|
-
icon: "i-ph-warning-circle",
|
|
50
|
-
title: title || t("dms.form.error_title"),
|
|
51
|
-
description: describeTableWriteError(
|
|
52
|
-
error,
|
|
53
|
-
t,
|
|
54
|
-
config.columns,
|
|
55
|
-
processI18n,
|
|
56
|
-
),
|
|
57
|
-
});
|
|
42
|
+
const handleApiError = (error: unknown, title?: string) => {
|
|
43
|
+
presentError(
|
|
44
|
+
error,
|
|
45
|
+
tableWriteErrorOptions(error, t, config.columns, processI18n, title),
|
|
46
|
+
);
|
|
58
47
|
};
|
|
59
48
|
|
|
60
49
|
const createFormContainer = createTableRowFormContainer<T>(config, services);
|
|
@@ -106,6 +106,8 @@ export interface DeepLinkedRow<T> {
|
|
|
106
106
|
* error): the row may still exist, the link just could not be followed.
|
|
107
107
|
*/
|
|
108
108
|
isFailed?: boolean;
|
|
109
|
+
/** Original request failure retained for presentation. */
|
|
110
|
+
error?: unknown;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
/**
|
|
@@ -125,7 +127,7 @@ export async function resolveDeepLinkedRow<T>(
|
|
|
125
127
|
return { row: row ?? undefined, isMissing: false };
|
|
126
128
|
} catch (error: unknown) {
|
|
127
129
|
if (readFetchNotFound(error) !== null) return { isMissing: true };
|
|
128
|
-
return { isMissing: false, isFailed: true };
|
|
130
|
+
return { isMissing: false, isFailed: true, error };
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -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 {
|
|
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
|
|
12
|
-
error:
|
|
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
|
-
|
|
17
|
-
|
|
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 {
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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) =>
|
|
101
|
+
handleApiError: (error, title) =>
|
|
102
|
+
presentError(error, { fallbackKey: title ?? "error.500.description" }),
|
|
101
103
|
});
|
|
102
104
|
|
|
103
105
|
const hasFetchedContent = computed(
|
|
@@ -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,7 +112,7 @@ const props = withDefaults(defineProps<ConfirmModalProps>(), {
|
|
|
113
112
|
const emit = defineEmits<ConfirmModalEmits>();
|
|
114
113
|
const slots = defineSlots<ConfirmModalSlots>();
|
|
115
114
|
const { t } = useI18n();
|
|
116
|
-
const
|
|
115
|
+
const { resolveError } = useApiErrorPresenter();
|
|
117
116
|
|
|
118
117
|
const BodyRender: FunctionalComponent = () => props.body?.();
|
|
119
118
|
|
|
@@ -124,8 +123,6 @@ const DEFAULT_ICONS: Partial<Record<ConfirmColor, string>> = {
|
|
|
124
123
|
};
|
|
125
124
|
const FALLBACK_ICON = "i-ph-question";
|
|
126
125
|
|
|
127
|
-
const { processApiMessage } = useTranslation();
|
|
128
|
-
|
|
129
126
|
const values = ref<ConfirmValues>(
|
|
130
127
|
Object.fromEntries(
|
|
131
128
|
props.fields.map((field) => [field.id, field.defaultValue]),
|
|
@@ -170,7 +167,7 @@ const typedText = ref("");
|
|
|
170
167
|
const isPending = ref(false);
|
|
171
168
|
const isSecondaryPending = ref(false);
|
|
172
169
|
|
|
173
|
-
const notice = ref<ConfirmNotice & { tone:
|
|
170
|
+
const notice = ref<ConfirmNotice & { tone: ApiErrorSeverity }>();
|
|
174
171
|
|
|
175
172
|
const isSettled = ref(false);
|
|
176
173
|
|
|
@@ -246,13 +243,12 @@ const isPartialOutcome = (
|
|
|
246
243
|
): value is ConfirmPartialOutcome =>
|
|
247
244
|
isObject(value) && value !== null && "partial" in value;
|
|
248
245
|
|
|
249
|
-
function
|
|
250
|
-
error:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
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
|
+
});
|
|
256
252
|
}
|
|
257
253
|
|
|
258
254
|
/**
|
|
@@ -268,9 +264,12 @@ function showFieldErrors(
|
|
|
268
264
|
});
|
|
269
265
|
if (resolved.fields.length === 0) return false;
|
|
270
266
|
fieldErrors.value = Object.fromEntries(
|
|
271
|
-
resolved.fields.map(({ field, message }) => [
|
|
267
|
+
resolved.fields.map(({ field, message, params, fallbackKey }) => [
|
|
272
268
|
field,
|
|
273
|
-
|
|
269
|
+
resolveError(message, {
|
|
270
|
+
fallbackKey: fallbackKey ?? "dms.field_errors.invalid",
|
|
271
|
+
params,
|
|
272
|
+
})?.message,
|
|
274
273
|
]),
|
|
275
274
|
);
|
|
276
275
|
return !resolved.hasUnmatched;
|
|
@@ -293,11 +292,15 @@ async function runConfirm(
|
|
|
293
292
|
close(true);
|
|
294
293
|
}
|
|
295
294
|
} catch (error) {
|
|
296
|
-
if (consumeReadOnlyRefusal(error)) return;
|
|
297
295
|
if (error instanceof ConfirmTextError && props.confirmText) {
|
|
298
|
-
typedError.value = error
|
|
296
|
+
typedError.value = resolveError(error, {
|
|
297
|
+
fallbackKey: "dms.field_errors.invalid",
|
|
298
|
+
})?.message;
|
|
299
299
|
} else if (!showFieldErrors(error)) {
|
|
300
|
-
|
|
300
|
+
const resolved = resolveConfirmationFailure(error);
|
|
301
|
+
notice.value = resolved
|
|
302
|
+
? { title: resolved.message, tone: resolved.severity }
|
|
303
|
+
: undefined;
|
|
301
304
|
}
|
|
302
305
|
} finally {
|
|
303
306
|
isPending.value = false;
|
|
@@ -343,8 +346,10 @@ async function handleSecondary() {
|
|
|
343
346
|
try {
|
|
344
347
|
await action.run();
|
|
345
348
|
} catch (error) {
|
|
346
|
-
|
|
347
|
-
notice.value =
|
|
349
|
+
const resolved = resolveConfirmationFailure(error);
|
|
350
|
+
notice.value = resolved
|
|
351
|
+
? { title: resolved.message, tone: resolved.severity }
|
|
352
|
+
: undefined;
|
|
348
353
|
} finally {
|
|
349
354
|
isSecondaryPending.value = false;
|
|
350
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
|
|
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
|
-
|
|
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
|
},
|
|
@@ -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 (
|
|
160
|
+
} catch (error) {
|
|
160
161
|
const uploadingFile = uploadingFiles.value.find((u) => u.id === uploadId);
|
|
161
162
|
if (uploadingFile) {
|
|
162
|
-
uploadingFile.error =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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 {
|
|
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,7 +89,7 @@ const { getDataType } = useDataTypes();
|
|
|
89
89
|
const { t } = useI18n();
|
|
90
90
|
const { processText } = useComposedText();
|
|
91
91
|
const toast = useToast();
|
|
92
|
-
const
|
|
92
|
+
const { presentError } = useApiErrorPresenter();
|
|
93
93
|
|
|
94
94
|
const rowIdKey = computed(() => props.rowIdKey ?? DEFAULT_ROW_ID_KEY);
|
|
95
95
|
const pageSize = computed(
|
|
@@ -296,15 +296,17 @@ const awaitNewerMoves = async (itemId: string, current: Promise<void>) => {
|
|
|
296
296
|
}
|
|
297
297
|
};
|
|
298
298
|
|
|
299
|
-
const notifyMoveError = (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
);
|
|
308
310
|
};
|
|
309
311
|
|
|
310
312
|
const onColumnChange = async (
|
|
@@ -115,15 +115,15 @@ export interface ConfirmPartialOutcome {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Thrown by `onConfirm` with a
|
|
119
|
-
*
|
|
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
|
|
122
|
+
readonly params?: ApiErrorParams;
|
|
123
123
|
|
|
124
|
-
constructor(
|
|
125
|
-
super(
|
|
126
|
-
this.
|
|
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,7 +92,7 @@ function createFormNotices(
|
|
|
93
92
|
context: ReturnType<typeof createFormFields>,
|
|
94
93
|
options: UseFormOptions,
|
|
95
94
|
) {
|
|
96
|
-
const
|
|
95
|
+
const { presentError, resolveError } = useApiErrorPresenter();
|
|
97
96
|
const {
|
|
98
97
|
props,
|
|
99
98
|
toast,
|
|
@@ -144,13 +143,6 @@ function createFormNotices(
|
|
|
144
143
|
});
|
|
145
144
|
};
|
|
146
145
|
|
|
147
|
-
const resolveSubmitErrorDescription = (error: EventError) => {
|
|
148
|
-
if (props.errorMessage) return processI18n(props.errorMessage);
|
|
149
|
-
const text = apiErrorText(error);
|
|
150
|
-
if (text) return processApiMessage(text);
|
|
151
|
-
return processI18n("$dms.form.error_unknown");
|
|
152
|
-
};
|
|
153
|
-
|
|
154
146
|
/** Puts a refused submit's field errors under its fields, if any. */
|
|
155
147
|
const showServerFieldErrors = (
|
|
156
148
|
error: Parameters<typeof resolveFormFieldErrors>[0],
|
|
@@ -166,10 +158,16 @@ function createFormNotices(
|
|
|
166
158
|
name: entry.field,
|
|
167
159
|
message: entry.values?.length
|
|
168
160
|
? t("dms.field_errors.with_values", {
|
|
169
|
-
message:
|
|
161
|
+
message: resolveError(entry.message, {
|
|
162
|
+
fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
|
|
163
|
+
params: entry.params,
|
|
164
|
+
})?.message,
|
|
170
165
|
values: entry.values.join(", "),
|
|
171
166
|
})
|
|
172
|
-
:
|
|
167
|
+
: (resolveError(entry.message, {
|
|
168
|
+
fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
|
|
169
|
+
params: entry.params,
|
|
170
|
+
})?.message ?? ""),
|
|
173
171
|
values: entry.values,
|
|
174
172
|
path: entry.path,
|
|
175
173
|
})),
|
|
@@ -177,18 +175,15 @@ function createFormNotices(
|
|
|
177
175
|
};
|
|
178
176
|
|
|
179
177
|
const showSubmitErrorToast = (error: EventError) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
title: processI18n("$dms.form.error_title"),
|
|
183
|
-
description: resolveSubmitErrorDescription(error),
|
|
184
|
-
color: Color.error,
|
|
178
|
+
presentError(error, {
|
|
179
|
+
fallbackKey: props.errorMessage || "dms.form.error_unknown",
|
|
185
180
|
});
|
|
186
181
|
};
|
|
187
182
|
|
|
188
183
|
return {
|
|
189
184
|
...context,
|
|
190
185
|
options,
|
|
191
|
-
|
|
186
|
+
presentError,
|
|
192
187
|
showSubmitSuccessToast,
|
|
193
188
|
showSubmitNotice,
|
|
194
189
|
showServerFieldErrors,
|
|
@@ -281,8 +276,7 @@ function createSavedForm(context: ReturnType<typeof createFormNotices>) {
|
|
|
281
276
|
function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
|
|
282
277
|
const {
|
|
283
278
|
props,
|
|
284
|
-
|
|
285
|
-
processI18n,
|
|
279
|
+
presentError,
|
|
286
280
|
buildUrlContext,
|
|
287
281
|
showSubmitSuccessToast,
|
|
288
282
|
showSubmitNotice,
|
|
@@ -310,11 +304,7 @@ function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
|
|
|
310
304
|
};
|
|
311
305
|
|
|
312
306
|
const showUnresolvedTargetToast = () => {
|
|
313
|
-
|
|
314
|
-
title: processI18n("$dms.form.error_title"),
|
|
315
|
-
description: processI18n("$dms.form.error_no_target"),
|
|
316
|
-
color: Color.error,
|
|
317
|
-
});
|
|
307
|
+
presentError(null, { fallbackKey: "dms.form.error_no_target" });
|
|
318
308
|
};
|
|
319
309
|
|
|
320
310
|
/** Sends through submit events, marking accepted data clean before success. */
|
|
@@ -352,7 +342,6 @@ function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
|
|
|
352
342
|
const reportSubmitError = (
|
|
353
343
|
error: Parameters<typeof resolveFormFieldErrors>[0],
|
|
354
344
|
): void => {
|
|
355
|
-
if (context.consumeReadOnlyRefusal(error)) return;
|
|
356
345
|
if (!showServerFieldErrors(error)) {
|
|
357
346
|
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- Rejection values are passed unchanged to the existing error formatter; there is no Error-instance validation at this boundary.
|
|
358
347
|
showSubmitErrorToast(error as EventError);
|