@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.
- package/CHANGELOG.md +66 -0
- package/README.md +1 -1
- package/data/dbip-country-lite.mmdb +0 -0
- package/data/module-store/catalog.json +0 -6
- package/dist/config/internal/state.d.ts +5 -0
- package/dist/config/internal/state.js.map +1 -1
- package/dist/frontend-module.js +1 -0
- package/dist/frontend-module.js.map +1 -1
- package/dist/implementations/dms/page/quick-action-access.js +4 -1
- package/dist/implementations/dms/page/quick-action-access.js.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/store/operations.js +3 -1
- package/dist/store/operations.js.map +1 -1
- package/dist/store/runtime.d.ts +1 -0
- package/dist/store/runtime.js +9 -0
- package/dist/store/runtime.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 -11
- 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-auth/app/custom-pages/auth/login.vue +3 -1
- package/frontend-vue/layers/dms-auth/server/oauth.ts +5 -3
- 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 +2 -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 -10
- package/frontend-vue/layers/dms-core/app/composables/useApiErrorPresenter.ts +159 -0
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +14 -26
- package/frontend-vue/layers/dms-core/app/utils/composedText.ts +11 -4
- package/frontend-vue/layers/dms-core/app/utils/row-action-rule-evaluator.ts +8 -36
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +1 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +1 -0
- package/frontend-vue/layers/dms-core/shared/types/runtime-config.d.ts +6 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardSidePanel.vue +3 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +14 -0
- 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 +6 -6
- 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/build/utils/github-repository.ts +1 -1
- 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 +2 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +2 -4
- package/frontend-vue/layers/dms-layout/server/render-token.ts +5 -3
- 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 +51 -0
- 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 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/defaultPrimitiveTypes.ts +6 -8
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formState.ts +2 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/page/blockAnchors.ts +2 -3
- 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 +9 -9
- 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 +8 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +53 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +7 -16
- 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/Chart.vue +4 -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 -16
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +2 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +2 -4
- 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 -9
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +11 -1
- 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 -21
- 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 +3 -5
- 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 +23 -7
- package/skills/dms-module/REFERENCE.md +3 -1
- package/skills/dms-module/SKILL.md +7 -2
- package/skills/dms-project/SKILL.md +9 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
|
|
2
2
|
import type { TableViewColumn } from "../../../../composables/table-view/types/column";
|
|
3
|
-
import {
|
|
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(
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
NESTED_CHART_INJECT_KEY,
|
|
44
44
|
} from "../../composables/chart/types";
|
|
45
45
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
46
|
+
import { responseSeries } from "../../build/composables/chart/groupPoints";
|
|
46
47
|
|
|
47
48
|
interface Props extends DefaultComponentProps {
|
|
48
49
|
type: ChartType;
|
|
@@ -164,7 +165,9 @@ const seriesFromCtx = computed<ChartSeries[]>(() => {
|
|
|
164
165
|
|
|
165
166
|
const fetchSeries = computed<ChartSeries[]>(() => {
|
|
166
167
|
if (!data.value) return [];
|
|
167
|
-
return localized(
|
|
168
|
+
return localized(
|
|
169
|
+
responseSeries(data.value.series ?? [], props.title ?? "", props.xaxisType),
|
|
170
|
+
);
|
|
168
171
|
});
|
|
169
172
|
|
|
170
173
|
const finalSeries = computed<ChartSeries[]>(() => {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type ValuePrecision,
|
|
22
22
|
} from "../../composables/chart/types";
|
|
23
23
|
import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
|
|
24
|
+
import { drawableSeries } from "../../build/composables/chart/groupPoints";
|
|
24
25
|
|
|
25
26
|
interface Props extends DefaultComponentProps {
|
|
26
27
|
title: string;
|
|
@@ -93,13 +94,13 @@ function localized(entry: ChartSeries): ChartSeries {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const cardSeries = computed<ChartSeries[]>(() =>
|
|
96
|
-
(data.value?.series ?? []).map(localized),
|
|
97
|
+
drawableSeries(data.value?.series ?? []).map(localized),
|
|
97
98
|
);
|
|
98
99
|
|
|
99
100
|
const cardComparisonSeries = computed<ChartSeries[]>(() =>
|
|
100
101
|
comparison.isOff.value
|
|
101
102
|
? []
|
|
102
|
-
: (data.value?.comparisonSeries ?? []).map((entry) => ({
|
|
103
|
+
: drawableSeries(data.value?.comparisonSeries ?? []).map((entry) => ({
|
|
103
104
|
...localized(entry),
|
|
104
105
|
color: entry.color ?? comparisonColor.value,
|
|
105
106
|
})),
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
ConfirmTextError,
|
|
24
24
|
type ConfirmValues,
|
|
25
25
|
} from "../../composables/confirm/types";
|
|
26
|
-
import { resolveActionError } from "../../build/composables/confirm/actionError";
|
|
27
26
|
import { isConfirmTextMatch } from "../../build/composables/confirm/confirmTextMatch";
|
|
28
27
|
import type { ConfirmDialogField } from "#dms-core/app/types/confirm-dialog";
|
|
29
28
|
import { resolveFieldErrors } from "#dms-core/app/composables/useFieldErrors";
|
|
@@ -113,6 +112,7 @@ const props = withDefaults(defineProps<ConfirmModalProps>(), {
|
|
|
113
112
|
const emit = defineEmits<ConfirmModalEmits>();
|
|
114
113
|
const slots = defineSlots<ConfirmModalSlots>();
|
|
115
114
|
const { t } = useI18n();
|
|
115
|
+
const { resolveError } = useApiErrorPresenter();
|
|
116
116
|
|
|
117
117
|
const BodyRender: FunctionalComponent = () => props.body?.();
|
|
118
118
|
|
|
@@ -123,8 +123,6 @@ const DEFAULT_ICONS: Partial<Record<ConfirmColor, string>> = {
|
|
|
123
123
|
};
|
|
124
124
|
const FALLBACK_ICON = "i-ph-question";
|
|
125
125
|
|
|
126
|
-
const { processApiMessage } = useTranslation();
|
|
127
|
-
|
|
128
126
|
const values = ref<ConfirmValues>(
|
|
129
127
|
Object.fromEntries(
|
|
130
128
|
props.fields.map((field) => [field.id, field.defaultValue]),
|
|
@@ -169,7 +167,7 @@ const typedText = ref("");
|
|
|
169
167
|
const isPending = ref(false);
|
|
170
168
|
const isSecondaryPending = ref(false);
|
|
171
169
|
|
|
172
|
-
const notice = ref<ConfirmNotice & { tone:
|
|
170
|
+
const notice = ref<ConfirmNotice & { tone: ApiErrorSeverity }>();
|
|
173
171
|
|
|
174
172
|
const isSettled = ref(false);
|
|
175
173
|
|
|
@@ -245,13 +243,12 @@ const isPartialOutcome = (
|
|
|
245
243
|
): value is ConfirmPartialOutcome =>
|
|
246
244
|
isObject(value) && value !== null && "partial" in value;
|
|
247
245
|
|
|
248
|
-
function
|
|
249
|
-
error:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
return { title: resolveActionError(error, t) };
|
|
246
|
+
function resolveConfirmationFailure(error: unknown): ApiErrorNotice | null {
|
|
247
|
+
const action = error instanceof ConfirmActionError ? error : undefined;
|
|
248
|
+
return resolveError(error, {
|
|
249
|
+
fallbackKey: action?.message ?? "dms.action_error.generic",
|
|
250
|
+
params: action?.params,
|
|
251
|
+
});
|
|
255
252
|
}
|
|
256
253
|
|
|
257
254
|
/**
|
|
@@ -267,9 +264,12 @@ function showFieldErrors(
|
|
|
267
264
|
});
|
|
268
265
|
if (resolved.fields.length === 0) return false;
|
|
269
266
|
fieldErrors.value = Object.fromEntries(
|
|
270
|
-
resolved.fields.map(({ field, message }) => [
|
|
267
|
+
resolved.fields.map(({ field, message, params, fallbackKey }) => [
|
|
271
268
|
field,
|
|
272
|
-
|
|
269
|
+
resolveError(message, {
|
|
270
|
+
fallbackKey: fallbackKey ?? "dms.field_errors.invalid",
|
|
271
|
+
params,
|
|
272
|
+
})?.message,
|
|
273
273
|
]),
|
|
274
274
|
);
|
|
275
275
|
return !resolved.hasUnmatched;
|
|
@@ -293,9 +293,14 @@ async function runConfirm(
|
|
|
293
293
|
}
|
|
294
294
|
} catch (error) {
|
|
295
295
|
if (error instanceof ConfirmTextError && props.confirmText) {
|
|
296
|
-
typedError.value = error
|
|
296
|
+
typedError.value = resolveError(error, {
|
|
297
|
+
fallbackKey: "dms.field_errors.invalid",
|
|
298
|
+
})?.message;
|
|
297
299
|
} else if (!showFieldErrors(error)) {
|
|
298
|
-
|
|
300
|
+
const resolved = resolveConfirmationFailure(error);
|
|
301
|
+
notice.value = resolved
|
|
302
|
+
? { title: resolved.message, tone: resolved.severity }
|
|
303
|
+
: undefined;
|
|
299
304
|
}
|
|
300
305
|
} finally {
|
|
301
306
|
isPending.value = false;
|
|
@@ -341,7 +346,10 @@ async function handleSecondary() {
|
|
|
341
346
|
try {
|
|
342
347
|
await action.run();
|
|
343
348
|
} catch (error) {
|
|
344
|
-
|
|
349
|
+
const resolved = resolveConfirmationFailure(error);
|
|
350
|
+
notice.value = resolved
|
|
351
|
+
? { title: resolved.message, tone: resolved.severity }
|
|
352
|
+
: undefined;
|
|
345
353
|
} finally {
|
|
346
354
|
isSecondaryPending.value = false;
|
|
347
355
|
}
|
|
@@ -60,7 +60,7 @@ const invalid = computed(() => fieldColor.value === "error");
|
|
|
60
60
|
|
|
61
61
|
const instanceId = useId();
|
|
62
62
|
const { $authFetch } = useAuthFetch();
|
|
63
|
-
const
|
|
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
|
},
|
|
@@ -164,8 +164,7 @@ function trackRangePick(value: Parameters<typeof forwardEmit>[1]): void {
|
|
|
164
164
|
report(undefined);
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
-
|
|
168
|
-
const ends = value as Partial<StrictDateRange>;
|
|
167
|
+
const ends: { start?: unknown; end?: unknown } = value;
|
|
169
168
|
if (!ends.start) return;
|
|
170
169
|
isHalfPicked = !ends.end;
|
|
171
170
|
if (!isHalfPicked) report(undefined);
|
|
@@ -180,8 +179,7 @@ const isPickerOpen = ref(false);
|
|
|
180
179
|
function isPickComplete(value: Parameters<typeof forwardEmit>[1]): boolean {
|
|
181
180
|
if (!value || props.multiple) return false;
|
|
182
181
|
if (!props.range) return true;
|
|
183
|
-
|
|
184
|
-
const ends = value as Partial<StrictDateRange>;
|
|
182
|
+
const ends: { start?: unknown; end?: unknown } = value;
|
|
185
183
|
return !!ends.start && !!ends.end;
|
|
186
184
|
}
|
|
187
185
|
|
|
@@ -41,6 +41,7 @@ const emit = defineEmits<{
|
|
|
41
41
|
|
|
42
42
|
const { $authFetch } = useAuthFetch();
|
|
43
43
|
const { t } = useI18n();
|
|
44
|
+
const { presentError, resolveError } = useApiErrorPresenter();
|
|
44
45
|
|
|
45
46
|
const { emitFormChange, color, highlight, ariaAttrs } = useFormField();
|
|
46
47
|
|
|
@@ -156,11 +157,14 @@ const processFile = async (file: File) => {
|
|
|
156
157
|
}
|
|
157
158
|
|
|
158
159
|
emitValue();
|
|
159
|
-
} catch (
|
|
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,6 +89,7 @@ const { getDataType } = useDataTypes();
|
|
|
89
89
|
const { t } = useI18n();
|
|
90
90
|
const { processText } = useComposedText();
|
|
91
91
|
const toast = useToast();
|
|
92
|
+
const { presentError } = useApiErrorPresenter();
|
|
92
93
|
|
|
93
94
|
const rowIdKey = computed(() => props.rowIdKey ?? DEFAULT_ROW_ID_KEY);
|
|
94
95
|
const pageSize = computed(
|
|
@@ -295,14 +296,17 @@ const awaitNewerMoves = async (itemId: string, current: Promise<void>) => {
|
|
|
295
296
|
}
|
|
296
297
|
};
|
|
297
298
|
|
|
298
|
-
const notifyMoveError = (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
299
|
+
const notifyMoveError = (error: unknown) => {
|
|
300
|
+
presentError(
|
|
301
|
+
error,
|
|
302
|
+
tableWriteErrorOptions(
|
|
303
|
+
error,
|
|
304
|
+
t,
|
|
305
|
+
props.columns,
|
|
306
|
+
processI18n,
|
|
307
|
+
"dms.table.kanban.move_error",
|
|
308
|
+
),
|
|
309
|
+
);
|
|
306
310
|
};
|
|
307
311
|
|
|
308
312
|
const onColumnChange = async (
|
|
@@ -122,8 +122,18 @@ export interface TopListCardResponse {
|
|
|
122
122
|
items: TopListItem[];
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** One measured group, as a data source of shape `series` answers it. */
|
|
126
|
+
export interface ChartGroupPoint {
|
|
127
|
+
x: string | number;
|
|
128
|
+
y: number | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* What a bare chart's data source answers: one point per group, the shape the
|
|
133
|
+
* block registry declares as `series`, or named series.
|
|
134
|
+
*/
|
|
125
135
|
export interface ChartResponse {
|
|
126
|
-
series: ChartSeries[];
|
|
136
|
+
series: ChartSeries[] | ChartGroupPoint[];
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
export type ValueFormat = "number" | "currency" | "percent" | "compact";
|
|
@@ -115,15 +115,15 @@ export interface ConfirmPartialOutcome {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Thrown by `onConfirm` with a
|
|
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,6 +92,7 @@ function createFormNotices(
|
|
|
93
92
|
context: ReturnType<typeof createFormFields>,
|
|
94
93
|
options: UseFormOptions,
|
|
95
94
|
) {
|
|
95
|
+
const { presentError, resolveError } = useApiErrorPresenter();
|
|
96
96
|
const {
|
|
97
97
|
props,
|
|
98
98
|
toast,
|
|
@@ -143,13 +143,6 @@ function createFormNotices(
|
|
|
143
143
|
});
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
const resolveSubmitErrorDescription = (error: EventError) => {
|
|
147
|
-
if (props.errorMessage) return processI18n(props.errorMessage);
|
|
148
|
-
const text = apiErrorText(error);
|
|
149
|
-
if (text) return processApiMessage(text);
|
|
150
|
-
return processI18n("$dms.form.error_unknown");
|
|
151
|
-
};
|
|
152
|
-
|
|
153
146
|
/** Puts a refused submit's field errors under its fields, if any. */
|
|
154
147
|
const showServerFieldErrors = (
|
|
155
148
|
error: Parameters<typeof resolveFormFieldErrors>[0],
|
|
@@ -165,10 +158,16 @@ function createFormNotices(
|
|
|
165
158
|
name: entry.field,
|
|
166
159
|
message: entry.values?.length
|
|
167
160
|
? t("dms.field_errors.with_values", {
|
|
168
|
-
message:
|
|
161
|
+
message: resolveError(entry.message, {
|
|
162
|
+
fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
|
|
163
|
+
params: entry.params,
|
|
164
|
+
})?.message,
|
|
169
165
|
values: entry.values.join(", "),
|
|
170
166
|
})
|
|
171
|
-
:
|
|
167
|
+
: (resolveError(entry.message, {
|
|
168
|
+
fallbackKey: entry.fallbackKey ?? "dms.field_errors.invalid",
|
|
169
|
+
params: entry.params,
|
|
170
|
+
})?.message ?? ""),
|
|
172
171
|
values: entry.values,
|
|
173
172
|
path: entry.path,
|
|
174
173
|
})),
|
|
@@ -176,16 +175,15 @@ function createFormNotices(
|
|
|
176
175
|
};
|
|
177
176
|
|
|
178
177
|
const showSubmitErrorToast = (error: EventError) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
description: resolveSubmitErrorDescription(error),
|
|
182
|
-
color: Color.error,
|
|
178
|
+
presentError(error, {
|
|
179
|
+
fallbackKey: props.errorMessage || "dms.form.error_unknown",
|
|
183
180
|
});
|
|
184
181
|
};
|
|
185
182
|
|
|
186
183
|
return {
|
|
187
184
|
...context,
|
|
188
185
|
options,
|
|
186
|
+
presentError,
|
|
189
187
|
showSubmitSuccessToast,
|
|
190
188
|
showSubmitNotice,
|
|
191
189
|
showServerFieldErrors,
|
|
@@ -278,8 +276,7 @@ function createSavedForm(context: ReturnType<typeof createFormNotices>) {
|
|
|
278
276
|
function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
|
|
279
277
|
const {
|
|
280
278
|
props,
|
|
281
|
-
|
|
282
|
-
processI18n,
|
|
279
|
+
presentError,
|
|
283
280
|
buildUrlContext,
|
|
284
281
|
showSubmitSuccessToast,
|
|
285
282
|
showSubmitNotice,
|
|
@@ -307,11 +304,7 @@ function createFormSubmission(context: ReturnType<typeof createSavedForm>) {
|
|
|
307
304
|
};
|
|
308
305
|
|
|
309
306
|
const showUnresolvedTargetToast = () => {
|
|
310
|
-
|
|
311
|
-
title: processI18n("$dms.form.error_title"),
|
|
312
|
-
description: processI18n("$dms.form.error_no_target"),
|
|
313
|
-
color: Color.error,
|
|
314
|
-
});
|
|
307
|
+
presentError(null, { fallbackKey: "dms.form.error_no_target" });
|
|
315
308
|
};
|
|
316
309
|
|
|
317
310
|
/** Sends through submit events, marking accepted data clean before success. */
|