@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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isString } from "../../../../dms-core/app/utils/type-check";
|
|
2
1
|
import type { Ref, VNode } from "vue";
|
|
3
2
|
import ModuleStoreDependencyPicker from "../../build/components/pages/modules/ModuleStoreDependencyPicker.vue";
|
|
4
3
|
import {
|
|
@@ -22,12 +21,6 @@ import type {
|
|
|
22
21
|
} from "#dms-core/app/types/module-store";
|
|
23
22
|
import type { useModuleStore } from "./useModuleStore";
|
|
24
23
|
|
|
25
|
-
/** What a refused store request carries: the server's error key, if any. */
|
|
26
|
-
interface StoreRequestError {
|
|
27
|
-
data?: unknown;
|
|
28
|
-
message?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
24
|
/** What the frontend banner says while it lags behind the backend. */
|
|
32
25
|
export interface StoreFrontendNotice {
|
|
33
26
|
key: "dev" | "production" | "unknown";
|
|
@@ -65,18 +58,9 @@ export const useModuleStoreActions = (
|
|
|
65
58
|
) => {
|
|
66
59
|
const { t } = useI18n();
|
|
67
60
|
const { processI18n } = useTranslation();
|
|
68
|
-
const
|
|
61
|
+
const { presentError } = useApiErrorPresenter();
|
|
69
62
|
const { confirm } = useConfirm();
|
|
70
63
|
|
|
71
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- Store requests can reject with arbitrary JavaScript values; the store's Promise contract has no rejection owner type.
|
|
72
|
-
function resolveStoreError(error: unknown): string {
|
|
73
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- This existing unchecked optional-property view is not validation of a StoreRequestError; inference cannot establish that rejected values have this shape, and parsing would change malformed-error behavior.
|
|
74
|
-
const details = error as StoreRequestError | undefined;
|
|
75
|
-
const key = isString(details?.data) ? details.data : undefined;
|
|
76
|
-
if (!key) return details?.message ?? "";
|
|
77
|
-
return t(`modules.store.errors.${key.replace("error.", "")}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
64
|
/**
|
|
81
65
|
* Runs `kind` from a dialog: a refusal is worded as the toast words it and
|
|
82
66
|
* shown in the dialog, which stays open.
|
|
@@ -89,12 +73,15 @@ export const useModuleStoreActions = (
|
|
|
89
73
|
try {
|
|
90
74
|
await moduleStore.run(kind, entry, body);
|
|
91
75
|
} catch (error) {
|
|
92
|
-
throw new ConfirmActionError(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
76
|
+
throw new ConfirmActionError(
|
|
77
|
+
{
|
|
78
|
+
fallbackKey: "modules.store.action_failed",
|
|
79
|
+
params: {
|
|
80
|
+
module: processI18n(entry.title),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{ cause: error },
|
|
84
|
+
);
|
|
98
85
|
}
|
|
99
86
|
}
|
|
100
87
|
|
|
@@ -165,11 +152,9 @@ export const useModuleStoreActions = (
|
|
|
165
152
|
try {
|
|
166
153
|
await moduleStore.run(kind, entry);
|
|
167
154
|
} catch (error) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
title: t("modules.store.action_failed", { module: title }),
|
|
172
|
-
description: resolveStoreError(error),
|
|
155
|
+
presentError(error, {
|
|
156
|
+
fallbackKey: "modules.store.action_failed",
|
|
157
|
+
params: { module: title },
|
|
173
158
|
});
|
|
174
159
|
}
|
|
175
160
|
}
|
|
@@ -178,10 +178,10 @@ const skeletonItems = Array.from(
|
|
|
178
178
|
while the frontend restarts keeps the tiles and tries again. -->
|
|
179
179
|
<DmsBanner
|
|
180
180
|
v-else-if="moduleStore.loadingError.value && !storeListing"
|
|
181
|
-
tone="error"
|
|
181
|
+
:tone="moduleStore.loadingNotice.value?.severity ?? 'error'"
|
|
182
182
|
size="sm"
|
|
183
183
|
icon="i-ph-plugs"
|
|
184
|
-
:title="
|
|
184
|
+
:title="moduleStore.loadingNotice.value?.message"
|
|
185
185
|
>
|
|
186
186
|
<template #actions>
|
|
187
187
|
<UButton
|
|
@@ -26,6 +26,8 @@ import { REQUIRED_MESSAGE } from "#dms-core/app/composables/useFormValidation";
|
|
|
26
26
|
import { useUnsavedChanges } from "#dms-ui/app/composables/unsaved-changes/useUnsavedChanges";
|
|
27
27
|
import { useActionConfirm } from "#dms-ui/app/entries/composables/confirm/useActionConfirm";
|
|
28
28
|
|
|
29
|
+
const { presentError } = useApiErrorPresenter();
|
|
30
|
+
|
|
29
31
|
interface RoleEditorHandle {
|
|
30
32
|
inputOf: (field: RoleEditorField) => HTMLInputElement | undefined;
|
|
31
33
|
}
|
|
@@ -152,7 +154,7 @@ async function runMutation(
|
|
|
152
154
|
await mutation();
|
|
153
155
|
toast.add({ title: t(successKey), color: "success" });
|
|
154
156
|
} catch (error) {
|
|
155
|
-
|
|
157
|
+
presentError(error, { fallbackKey: "error.500.description" });
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -181,7 +183,8 @@ async function save(): Promise<void> {
|
|
|
181
183
|
color: "success",
|
|
182
184
|
});
|
|
183
185
|
} catch (error) {
|
|
184
|
-
if (!(await fieldErrors.applyApiError(error)))
|
|
186
|
+
if (!(await fieldErrors.applyApiError(error)))
|
|
187
|
+
presentError(error, { fallbackKey: "error.500.description" });
|
|
185
188
|
} finally {
|
|
186
189
|
isSaving.value = false;
|
|
187
190
|
}
|
|
@@ -34,6 +34,7 @@ export default defineDmsPlugin(() => {
|
|
|
34
34
|
const { $i18n: i18n } = useDmsApp();
|
|
35
35
|
const { t } = getAppTranslation();
|
|
36
36
|
const toast = useToast();
|
|
37
|
+
const { presentError } = useApiErrorPresenter();
|
|
37
38
|
const { logout } = useLogout();
|
|
38
39
|
const { changeLanguage } = useUserLanguage();
|
|
39
40
|
const { copy } = useClipboard({ legacy: true });
|
|
@@ -51,10 +52,9 @@ export default defineDmsPlugin(() => {
|
|
|
51
52
|
try {
|
|
52
53
|
await copy(window.location.href);
|
|
53
54
|
toast.add({ title: t("commandPalette.session.urlCopied") });
|
|
54
|
-
} catch {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
color: "error",
|
|
55
|
+
} catch (error) {
|
|
56
|
+
presentError(error, {
|
|
57
|
+
fallbackKey: "commandPalette.session.copyFailed",
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
}
|
|
@@ -875,10 +875,6 @@
|
|
|
875
875
|
"title": "Builder",
|
|
876
876
|
"description": "Edits the project’s pages, tables and forms, writing only code that compiles."
|
|
877
877
|
},
|
|
878
|
-
"cicd": {
|
|
879
|
-
"title": "Publishing",
|
|
880
|
-
"description": "Versions, environments and deploys of your project through git."
|
|
881
|
-
},
|
|
882
878
|
"database": {
|
|
883
879
|
"title": "Database",
|
|
884
880
|
"description": "Schemas, data browser, schema diagram and an AQL query workspace."
|
|
@@ -875,10 +875,6 @@
|
|
|
875
875
|
"title": "Builder",
|
|
876
876
|
"description": "Modifie les pages, tableaux et formulaires du projet, en n’écrivant que du code qui compile."
|
|
877
877
|
},
|
|
878
|
-
"cicd": {
|
|
879
|
-
"title": "Publication",
|
|
880
|
-
"description": "Versions, environnements et déploiements de votre projet via git."
|
|
881
|
-
},
|
|
882
878
|
"database": {
|
|
883
879
|
"title": "Base de données",
|
|
884
880
|
"description": "Schémas, explorateur de données, diagramme et espace de requêtes AQL."
|
|
@@ -45,6 +45,7 @@ const VARIANTS = {
|
|
|
45
45
|
} as const;
|
|
46
46
|
|
|
47
47
|
const { t } = useI18n();
|
|
48
|
+
const { resolveError } = useApiErrorPresenter();
|
|
48
49
|
const { processI18n } = useTranslation();
|
|
49
50
|
|
|
50
51
|
const translate = (text: string | undefined) =>
|
|
@@ -52,7 +53,7 @@ const translate = (text: string | undefined) =>
|
|
|
52
53
|
|
|
53
54
|
const notFoundState = computed(() => {
|
|
54
55
|
const answered = props.notFound?.emptyState ?? {};
|
|
55
|
-
const defaults = notFoundTexts(props.notFound, t);
|
|
56
|
+
const defaults = notFoundTexts(props.notFound, t, resolveError);
|
|
56
57
|
return {
|
|
57
58
|
title: translate(answered.title) ?? defaults.title,
|
|
58
59
|
description: translate(answered.description) ?? defaults.description,
|
|
@@ -37,12 +37,8 @@ export function createExportJobHandler(
|
|
|
37
37
|
successMessage: target.labels.successMessage
|
|
38
38
|
? processI18n(target.labels.successMessage)
|
|
39
39
|
: undefined,
|
|
40
|
-
errorTitle: target.labels.errorTitle
|
|
41
|
-
|
|
42
|
-
: undefined,
|
|
43
|
-
retry: target.labels.retry
|
|
44
|
-
? processI18n(target.labels.retry)
|
|
45
|
-
: undefined,
|
|
40
|
+
errorTitle: target.labels.errorTitle,
|
|
41
|
+
retry: target.labels.retry,
|
|
46
42
|
}
|
|
47
43
|
: undefined;
|
|
48
44
|
const statusUrlTemplate = target.statusUrl;
|
|
@@ -121,19 +117,19 @@ export function createActionRequests(
|
|
|
121
117
|
const handleApiTarget = async (
|
|
122
118
|
target: ActionTarget & { type: "api" },
|
|
123
119
|
url: string,
|
|
124
|
-
|
|
120
|
+
_label: string,
|
|
125
121
|
rowData?: Data,
|
|
126
122
|
selection?: BulkSelection,
|
|
127
123
|
) => {
|
|
128
124
|
try {
|
|
129
125
|
await requestApiTarget(target, url, rowData, {}, selection);
|
|
130
126
|
} catch (error: unknown) {
|
|
131
|
-
handleApiError(error
|
|
127
|
+
handleApiError(error);
|
|
132
128
|
}
|
|
133
129
|
};
|
|
134
130
|
const handleDownloadTarget = async (
|
|
135
131
|
target: ActionTarget & { type: "download" },
|
|
136
|
-
|
|
132
|
+
_label: string,
|
|
137
133
|
rowData?: Data,
|
|
138
134
|
) => {
|
|
139
135
|
try {
|
|
@@ -145,7 +141,7 @@ export function createActionRequests(
|
|
|
145
141
|
fetchRaw,
|
|
146
142
|
);
|
|
147
143
|
} catch (error: unknown) {
|
|
148
|
-
handleApiError(error
|
|
144
|
+
handleApiError(error);
|
|
149
145
|
}
|
|
150
146
|
};
|
|
151
147
|
return { requestApiTarget, handleApiTarget, handleDownloadTarget };
|
|
@@ -13,7 +13,18 @@ function isGroupPoint(
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function isSeries(entry: ChartSeries | ChartGroupPoint): entry is ChartSeries {
|
|
16
|
-
return "data" in entry;
|
|
16
|
+
return "data" in entry && Array.isArray(entry.data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The series of a fetched answer that carry their points. The answer comes
|
|
21
|
+
* from a route, so one answering another shape leaves its chart empty rather
|
|
22
|
+
* than failing the page the chart sits on.
|
|
23
|
+
*/
|
|
24
|
+
export function drawableSeries(
|
|
25
|
+
series: ReadonlyArray<ChartSeries | ChartGroupPoint>,
|
|
26
|
+
): ChartSeries[] {
|
|
27
|
+
return series.filter(isSeries);
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
/**
|
|
@@ -31,7 +42,7 @@ export function responseSeries(
|
|
|
31
42
|
): ChartSeries[] {
|
|
32
43
|
const entries: Array<ChartSeries | ChartGroupPoint> = series;
|
|
33
44
|
if (entries.length === 0 || !entries.every(isGroupPoint))
|
|
34
|
-
return entries
|
|
45
|
+
return drawableSeries(entries);
|
|
35
46
|
const labelled = xaxisType === "category";
|
|
36
47
|
const data = entries
|
|
37
48
|
.filter(isGroupPoint)
|
|
@@ -1,150 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isObject as isObjectType,
|
|
3
|
-
isString,
|
|
4
|
-
} from "../../../../../dms-core/app/utils/type-check";
|
|
5
|
-
import {
|
|
6
|
-
readTranslationKey,
|
|
7
|
-
type I18nTranslate,
|
|
8
|
-
} from "#dms-core/app/composables/translation/useTranslation";
|
|
9
|
-
import { describeFieldErrors } from "#dms-core/app/composables/useFieldErrors";
|
|
1
|
+
import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
|
|
10
2
|
|
|
11
|
-
/**
|
|
3
|
+
/** Legacy fallback key for requests that could not reach the server. */
|
|
12
4
|
export const ACTION_ERROR_NETWORK_KEY = "error.network.description";
|
|
13
|
-
/**
|
|
5
|
+
/** Legacy fallback key for permission refusals. */
|
|
14
6
|
export const ACTION_ERROR_FORBIDDEN_KEY = "dms.action_error.forbidden";
|
|
15
|
-
/**
|
|
7
|
+
/** Fallback key for actions without a more specific context. */
|
|
16
8
|
export const ACTION_ERROR_GENERIC_KEY = "dms.action_error.generic";
|
|
17
9
|
|
|
18
|
-
|
|
19
|
-
const HTTP_FORBIDDEN = 403;
|
|
20
|
-
const HTTP_SERVER_ERROR = 500;
|
|
21
|
-
|
|
22
|
-
const MAX_MESSAGE_LENGTH = 300;
|
|
23
|
-
|
|
24
|
-
const KEY_PATTERN = /^\$?[\w-]+(\.[\w-]+)+$/;
|
|
25
|
-
|
|
26
|
-
const TECHNICAL_PATTERNS = [
|
|
27
|
-
/^\s*[[{<]/,
|
|
28
|
-
/\n\s*at\s/,
|
|
29
|
-
/^\s*[A-Z]\w*Error\b/,
|
|
30
|
-
/<no response>/,
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const NETWORK_MESSAGES = [/failed to fetch/i, /networkerror/i, /load failed/i];
|
|
34
|
-
|
|
35
|
-
interface FetchErrorLike {
|
|
36
|
-
name?: string;
|
|
37
|
-
message?: string;
|
|
38
|
-
statusCode?: number;
|
|
39
|
-
status?: number;
|
|
40
|
-
response?: { status?: number };
|
|
41
|
-
data?: unknown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// oxlint-disable-next-line anti-slop/no-unsafe-dictionary-type -- Error bodies are untrusted; property values remain unknown until their individual checks.
|
|
45
|
-
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
46
|
-
isObjectType(value) && value !== null;
|
|
47
|
-
|
|
48
|
-
const isFetchError = (error: unknown): error is FetchErrorLike =>
|
|
49
|
-
isObject(error) &&
|
|
50
|
-
(error.name === "FetchError" ||
|
|
51
|
-
"statusCode" in error ||
|
|
52
|
-
"response" in error ||
|
|
53
|
-
"data" in error);
|
|
54
|
-
|
|
55
|
-
const statusOf = (error: FetchErrorLike): number | undefined =>
|
|
56
|
-
error.statusCode ?? error.status ?? error.response?.status;
|
|
57
|
-
|
|
58
|
-
/** The message the server put in the error body, when it sent one. */
|
|
59
|
-
const serverMessageOf = (error: FetchErrorLike): string | undefined => {
|
|
60
|
-
const { data } = error;
|
|
61
|
-
if (isString(data)) return data;
|
|
62
|
-
// oxlint-disable-next-line anti-slop/no-known-value-widening -- The object guard accepts all untrusted error data; only an actual string message is returned.
|
|
63
|
-
if (isObject(data) && isString(data.message)) return data.message;
|
|
64
|
-
return undefined;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
const isTechnical = (text: string) =>
|
|
68
|
-
text.length > MAX_MESSAGE_LENGTH ||
|
|
69
|
-
TECHNICAL_PATTERNS.some((pattern) => pattern.test(text));
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* `text` as the user should read it: a translated i18n key, or plain prose
|
|
73
|
-
* as it came. Undefined when it is not user-facing (an untranslated key, JSON,
|
|
74
|
-
* a stack trace…), for the caller to fall back on a generic message.
|
|
75
|
-
*/
|
|
76
|
-
export const userFacingText = (
|
|
77
|
-
text: string | undefined,
|
|
78
|
-
translate: I18nTranslate,
|
|
79
|
-
): string | undefined => {
|
|
80
|
-
const trimmed = text?.trim();
|
|
81
|
-
if (!trimmed) return undefined;
|
|
82
|
-
const { key } = readTranslationKey(trimmed);
|
|
83
|
-
if (KEY_PATTERN.test(trimmed)) {
|
|
84
|
-
const translated = translate(key, {});
|
|
85
|
-
return translated !== key ? translated : undefined;
|
|
86
|
-
}
|
|
87
|
-
return isTechnical(trimmed) ? undefined : trimmed;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const translateKey = (translate: I18nTranslate, key: string) =>
|
|
91
|
-
translate(key, {});
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The message telling a user why an action failed, from whatever the request
|
|
95
|
-
* threw: the server's own message when it is user-facing (an i18n key, or
|
|
96
|
-
* plain text on a 4xx), else a translated fallback: the request never got
|
|
97
|
-
* through, it was refused for lack of permission, or something went wrong.
|
|
98
|
-
* Never a stack trace, raw JSON or an ofetch error line. A body the data API
|
|
99
|
-
* refused for some fields names them by `labelOf` ("Loss reason is
|
|
100
|
-
* required."): a table view's write passes its column labels.
|
|
101
|
-
*/
|
|
10
|
+
/** @deprecated Capture useApiErrorPresenter().resolveError during setup instead. */
|
|
102
11
|
export function resolveActionError(
|
|
103
|
-
// oxlint-disable-next-line anti-slop/no-unknown-parameters -- The request's catch value is decoded here and may be any thrown JavaScript value.
|
|
104
12
|
error: unknown,
|
|
105
|
-
|
|
106
|
-
|
|
13
|
+
_translate: I18nTranslate,
|
|
14
|
+
_labelOf?: (field: string) => string | undefined,
|
|
107
15
|
): string {
|
|
108
|
-
if (isFetchError(error)) {
|
|
109
|
-
const status = statusOf(error);
|
|
110
|
-
if (status === undefined) {
|
|
111
|
-
return translateKey(translate, ACTION_ERROR_NETWORK_KEY);
|
|
112
|
-
}
|
|
113
|
-
const raw = serverMessageOf(error);
|
|
114
|
-
const isForbidden =
|
|
115
|
-
status === HTTP_UNAUTHORIZED || status === HTTP_FORBIDDEN;
|
|
116
|
-
|
|
117
|
-
if (isForbidden || status >= HTTP_SERVER_ERROR) {
|
|
118
|
-
const translated =
|
|
119
|
-
raw && KEY_PATTERN.test(raw.trim())
|
|
120
|
-
? userFacingText(raw, translate)
|
|
121
|
-
: undefined;
|
|
122
|
-
return (
|
|
123
|
-
translated ??
|
|
124
|
-
translateKey(
|
|
125
|
-
translate,
|
|
126
|
-
isForbidden ? ACTION_ERROR_FORBIDDEN_KEY : ACTION_ERROR_GENERIC_KEY,
|
|
127
|
-
)
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
return (
|
|
131
|
-
describeFieldErrors(error, translate, labelOf) ??
|
|
132
|
-
userFacingText(raw, translate) ??
|
|
133
|
-
translateKey(translate, ACTION_ERROR_GENERIC_KEY)
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const message =
|
|
138
|
-
error instanceof Error
|
|
139
|
-
? error.message
|
|
140
|
-
: isString(error)
|
|
141
|
-
? error
|
|
142
|
-
: undefined;
|
|
143
|
-
if (message && NETWORK_MESSAGES.some((pattern) => pattern.test(message))) {
|
|
144
|
-
return translateKey(translate, ACTION_ERROR_NETWORK_KEY);
|
|
145
|
-
}
|
|
146
16
|
return (
|
|
147
|
-
|
|
148
|
-
|
|
17
|
+
useApiErrorPresenter().resolveError(error, {
|
|
18
|
+
fallbackKey: ACTION_ERROR_GENERIC_KEY,
|
|
19
|
+
})?.message ?? ""
|
|
149
20
|
);
|
|
150
21
|
}
|
|
@@ -3,7 +3,6 @@ import type {
|
|
|
3
3
|
ConfirmDialog,
|
|
4
4
|
} from "#dms-core/app/types/confirm-dialog";
|
|
5
5
|
import { isConfirmFrom } from "#dms-core/app/types/confirm-dialog";
|
|
6
|
-
import { resolveActionError } from "./actionError";
|
|
7
6
|
import type {
|
|
8
7
|
ConfirmOptions,
|
|
9
8
|
ConfirmPartialOutcome,
|
|
@@ -43,8 +42,7 @@ export function useActionConfirm() {
|
|
|
43
42
|
const { confirm } = useConfirm();
|
|
44
43
|
const { $authFetch } = useAuthFetch();
|
|
45
44
|
const { t } = useI18n();
|
|
46
|
-
const
|
|
47
|
-
const consumeReadOnlyRefusal = useReadOnlyRefusal();
|
|
45
|
+
const { presentError } = useApiErrorPresenter();
|
|
48
46
|
const runSecondaryTarget = secondaryTargetRunner($authFetch.raw);
|
|
49
47
|
|
|
50
48
|
const loadDialog = async (
|
|
@@ -62,13 +60,7 @@ export function useActionConfirm() {
|
|
|
62
60
|
try {
|
|
63
61
|
return await $authFetch<ConfirmDialog>(url);
|
|
64
62
|
} catch (error) {
|
|
65
|
-
|
|
66
|
-
toast.add({
|
|
67
|
-
color: Color.error,
|
|
68
|
-
icon: "i-ph-warning-circle",
|
|
69
|
-
title: t("dms.form.error_title"),
|
|
70
|
-
description: resolveActionError(error, t),
|
|
71
|
-
});
|
|
63
|
+
presentError(error, { fallbackKey: "dms.action_error.generic" });
|
|
72
64
|
return undefined;
|
|
73
65
|
}
|
|
74
66
|
};
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
} from "../../utils/urlVariables";
|
|
17
17
|
import { apiErrorStatus } from "#dms-core/app/composables/useFieldErrors";
|
|
18
18
|
import { HTTP_NOT_FOUND } from "#dms-core/app/utils/http-status";
|
|
19
|
-
import { isString } from "#dms-core/app/utils/type-check";
|
|
20
19
|
import { sameFormValue } from "../unsaved-changes/formValue";
|
|
21
20
|
import {
|
|
22
21
|
type FieldValueComparer,
|
|
@@ -135,12 +134,11 @@ function createFormState(props: FormProps) {
|
|
|
135
134
|
/** @internal */
|
|
136
135
|
export function createFormFields(props: FormProps) {
|
|
137
136
|
const context = createFormState(props);
|
|
137
|
+
const { presentError } = useApiErrorPresenter();
|
|
138
138
|
const {
|
|
139
|
-
toast,
|
|
140
139
|
$authFetch,
|
|
141
140
|
getDataType,
|
|
142
141
|
processI18n,
|
|
143
|
-
processApiMessage,
|
|
144
142
|
route,
|
|
145
143
|
initialValues,
|
|
146
144
|
disabledFields,
|
|
@@ -205,15 +203,7 @@ export function createFormFields(props: FormProps) {
|
|
|
205
203
|
};
|
|
206
204
|
|
|
207
205
|
const showFetchErrorToast = (error: Parameters<typeof apiErrorStatus>[0]) => {
|
|
208
|
-
|
|
209
|
-
const err = error as Error & { data?: string };
|
|
210
|
-
toast.add({
|
|
211
|
-
title: processI18n("$dms.form.fetch_error_title"),
|
|
212
|
-
description: isString(err.data)
|
|
213
|
-
? processApiMessage(err.data)
|
|
214
|
-
: processI18n("$dms.form.fetch_error_unknown"),
|
|
215
|
-
color: Color.error,
|
|
216
|
-
});
|
|
206
|
+
presentError(error, { fallbackKey: "dms.form.fetch_error_unknown" });
|
|
217
207
|
};
|
|
218
208
|
|
|
219
209
|
const baseSchema = createBaseValidationSchema(props.schema);
|
|
@@ -51,6 +51,7 @@ export const useTableConfigClipboard = <T extends Data>() => {
|
|
|
51
51
|
injectLocal<ShallowRef<TableSharedData<T>>>("tableSharedData");
|
|
52
52
|
const toast = useToast();
|
|
53
53
|
const { t } = useI18n();
|
|
54
|
+
const { presentError } = useApiErrorPresenter();
|
|
54
55
|
|
|
55
56
|
const buildConfig = (): SerializedTableConfig | null => {
|
|
56
57
|
const data = tableSharedData?.value;
|
|
@@ -97,11 +98,9 @@ export const useTableConfigClipboard = <T extends Data>() => {
|
|
|
97
98
|
title: t("dms.table.export_config_copied"),
|
|
98
99
|
duration: TOAST_DURATION_MS,
|
|
99
100
|
});
|
|
100
|
-
} catch {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
title: t("dms.table.export_config_failed"),
|
|
104
|
-
description: code,
|
|
101
|
+
} catch (error) {
|
|
102
|
+
presentError(error, {
|
|
103
|
+
fallbackKey: "dms.table.export_config_failed",
|
|
105
104
|
});
|
|
106
105
|
}
|
|
107
106
|
};
|
|
@@ -117,11 +116,9 @@ export const useTableConfigClipboard = <T extends Data>() => {
|
|
|
117
116
|
duration: TOAST_DURATION_MS,
|
|
118
117
|
});
|
|
119
118
|
return true;
|
|
120
|
-
} catch {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
title: t("dms.table.import_config_invalid"),
|
|
124
|
-
duration: TOAST_DURATION_MS,
|
|
119
|
+
} catch (error) {
|
|
120
|
+
presentError(error, {
|
|
121
|
+
fallbackKey: "dms.table.import_config_invalid",
|
|
125
122
|
});
|
|
126
123
|
return false;
|
|
127
124
|
}
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/tableRowActionBulkRequests.ts
CHANGED
|
@@ -17,9 +17,8 @@ import {
|
|
|
17
17
|
type ConfirmPartialOutcome,
|
|
18
18
|
} from "../../../composables/confirm/types";
|
|
19
19
|
import { getErrorPayload } from "./tableRowActionValues";
|
|
20
|
-
import type { describeTableWriteError } from "./utils/writeError";
|
|
21
20
|
export type TableRowActionErrorHandler = (
|
|
22
|
-
error:
|
|
21
|
+
error: unknown,
|
|
23
22
|
title?: string,
|
|
24
23
|
) => void;
|
|
25
24
|
const createBulkRequest = (config: TableRowActionsConfig) => {
|
|
@@ -92,7 +91,8 @@ export const createTableRowBulkOutcomes = (
|
|
|
92
91
|
services: TableRowActionServices,
|
|
93
92
|
handleApiError: TableRowActionErrorHandler,
|
|
94
93
|
) => {
|
|
95
|
-
const {
|
|
94
|
+
const { t } = services;
|
|
95
|
+
const { presentError } = useApiErrorPresenter();
|
|
96
96
|
const requestBulkAction = createBulkRequest(config);
|
|
97
97
|
const reportBulkSuccess = createBulkReporter(config, services);
|
|
98
98
|
/**
|
|
@@ -112,7 +112,10 @@ export const createTableRowBulkOutcomes = (
|
|
|
112
112
|
const outcome = await requestBulkAction(ids, bulkConfig, values);
|
|
113
113
|
const shortfall = bulkActionShortfall(bulkConfig.kind, outcome, t);
|
|
114
114
|
if (outcome.processed === 0 && shortfall) {
|
|
115
|
-
throw new ConfirmActionError(
|
|
115
|
+
throw new ConfirmActionError({
|
|
116
|
+
fallbackKey: `dms.table.bulk_refused.${bulkConfig.kind}`,
|
|
117
|
+
params: { count: outcome.skipped },
|
|
118
|
+
});
|
|
116
119
|
}
|
|
117
120
|
reportBulkSuccess(outcome.processed, actionConfig, bulkConfig);
|
|
118
121
|
if (shortfall) return { partial: shortfall };
|
|
@@ -131,16 +134,14 @@ export const createTableRowBulkOutcomes = (
|
|
|
131
134
|
try {
|
|
132
135
|
outcome = await requestBulkAction(ids, bulkConfig);
|
|
133
136
|
} catch (error) {
|
|
134
|
-
handleApiError(error,
|
|
137
|
+
handleApiError(error, bulkConfig.errorKey);
|
|
135
138
|
return false;
|
|
136
139
|
}
|
|
137
140
|
const shortfall = bulkActionShortfall(bulkConfig.kind, outcome, t);
|
|
138
141
|
if (outcome.processed === 0) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
title: shortfall?.title ?? t(bulkConfig.errorKey),
|
|
143
|
-
description: shortfall?.description,
|
|
142
|
+
presentError(null, {
|
|
143
|
+
fallbackKey: `dms.table.bulk_refused.${bulkConfig.kind}`,
|
|
144
|
+
params: { count: outcome.skipped },
|
|
144
145
|
});
|
|
145
146
|
return false;
|
|
146
147
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
|
-
import type { describeTableWriteError } from "./utils/writeError";
|
|
3
2
|
export const DEFAULT_ROW_ID_KEY = "_id";
|
|
4
3
|
|
|
5
4
|
export const getItemId = (
|
|
@@ -9,9 +8,7 @@ export const getItemId = (
|
|
|
9
8
|
return get(item, idKey) ?? item._id ?? item.id;
|
|
10
9
|
};
|
|
11
10
|
|
|
12
|
-
export const getErrorPayload = (
|
|
13
|
-
error: Parameters<typeof describeTableWriteError>[0],
|
|
14
|
-
) => {
|
|
11
|
+
export const getErrorPayload = (error: unknown) => {
|
|
15
12
|
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- Rejections may be arbitrary JavaScript values, not EventError instances. Its owner describes only the fields forwarded here; inference cannot prove them, and validation would change primitive and malformed-error payloads.
|
|
16
13
|
const err = error as EventError;
|
|
17
14
|
return err?.data || err?.message;
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
CARDS_DISPLAY_ID,
|
|
4
4
|
} from "../../../composables/table-view/types";
|
|
5
5
|
import { KANBAN_DISPLAY_BRIDGE_KEY } from "../../../composables/table-view/kanban";
|
|
6
|
-
import {
|
|
6
|
+
import { tableWriteErrorOptions } from "./utils/writeError";
|
|
7
7
|
import { buildTableQuery, isFilterEffective } from "./utils/tableQuery";
|
|
8
8
|
import { useGroupedRows } from "./useGroupedRows";
|
|
9
9
|
import { useTableReorder } from "./useTableReorder";
|
|
@@ -32,10 +32,9 @@ export function useTableViewReorder<T extends Data>(
|
|
|
32
32
|
const { effectiveColumnFilters, effectiveGlobalFilter } = scope.querySorting;
|
|
33
33
|
const { activeTabFilters, t } = scope.tabs;
|
|
34
34
|
const { activeDisplayId } = scope.displayOptions;
|
|
35
|
-
const { toast } = scope.sortRecovery;
|
|
36
35
|
const { processI18n } = scope.arrival;
|
|
37
36
|
const { props } = scope;
|
|
38
|
-
const
|
|
37
|
+
const { presentError } = useApiErrorPresenter();
|
|
39
38
|
const ORDERED_DISPLAY_IDS = new Set([TABLE_DISPLAY_ID, CARDS_DISPLAY_ID]);
|
|
40
39
|
const { reorderState } = useTableReorder<T>({
|
|
41
40
|
reorder: reorderConfig,
|
|
@@ -55,18 +54,16 @@ export function useTableViewReorder<T extends Data>(
|
|
|
55
54
|
),
|
|
56
55
|
refresh: () => refresh(),
|
|
57
56
|
onError: (failure) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
description: describeTableWriteError(
|
|
57
|
+
presentError(
|
|
58
|
+
failure,
|
|
59
|
+
tableWriteErrorOptions(
|
|
62
60
|
failure,
|
|
63
61
|
t,
|
|
64
62
|
allColumns,
|
|
65
63
|
processI18n,
|
|
64
|
+
"dms.table.reorder.error",
|
|
66
65
|
),
|
|
67
|
-
|
|
68
|
-
icon: "i-ph-warning-circle",
|
|
69
|
-
});
|
|
66
|
+
);
|
|
70
67
|
},
|
|
71
68
|
});
|
|
72
69
|
return { reorderState };
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useExpandedRowDetails.ts
CHANGED
|
@@ -18,6 +18,7 @@ export function useExpandedRowDetails<T>(
|
|
|
18
18
|
getRow: (id: string) => Promise<T | undefined>,
|
|
19
19
|
) {
|
|
20
20
|
const entries = ref<Record<string, ExpandedRowDetailEntry<T>>>({});
|
|
21
|
+
const { presentError } = useApiErrorPresenter();
|
|
21
22
|
let generation = 0;
|
|
22
23
|
|
|
23
24
|
const setEntry = (id: string, entry: ExpandedRowDetailEntry<T>) => {
|
|
@@ -30,7 +31,9 @@ export function useExpandedRowDetails<T>(
|
|
|
30
31
|
let row: T | undefined;
|
|
31
32
|
try {
|
|
32
33
|
row = await getRow(id);
|
|
33
|
-
} catch {
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (loadedFor !== generation) return;
|
|
36
|
+
presentError(error, { fallbackKey: "dms.table.load_error_title" });
|
|
34
37
|
row = undefined;
|
|
35
38
|
}
|
|
36
39
|
if (loadedFor !== generation) return;
|