@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChartGroupPoint,
|
|
3
|
+
ChartResponse,
|
|
4
|
+
ChartSeries,
|
|
5
|
+
} from "../../../composables/chart/types";
|
|
6
|
+
|
|
7
|
+
type XAxisType = "category" | "datetime" | "numeric";
|
|
8
|
+
|
|
9
|
+
function isGroupPoint(
|
|
10
|
+
entry: ChartSeries | ChartGroupPoint,
|
|
11
|
+
): entry is ChartGroupPoint {
|
|
12
|
+
return "x" in entry && "y" in entry && !("data" in entry);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isSeries(entry: ChartSeries | ChartGroupPoint): entry is ChartSeries {
|
|
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);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The series a bare chart draws from its data source's answer.
|
|
32
|
+
*
|
|
33
|
+
* One point per group becomes one series called `name`. On a category axis
|
|
34
|
+
* each group key becomes its label: Apex lays numeric keys out on a number
|
|
35
|
+
* line whatever axis type it is given, which bunches `202512` and `202601`
|
|
36
|
+
* apart and shifts the bars of a numeric breakdown off their labels.
|
|
37
|
+
*/
|
|
38
|
+
export function responseSeries(
|
|
39
|
+
series: ChartResponse["series"],
|
|
40
|
+
name: string,
|
|
41
|
+
xaxisType: XAxisType = "category",
|
|
42
|
+
): ChartSeries[] {
|
|
43
|
+
const entries: Array<ChartSeries | ChartGroupPoint> = series;
|
|
44
|
+
if (entries.length === 0 || !entries.every(isGroupPoint))
|
|
45
|
+
return drawableSeries(entries);
|
|
46
|
+
const labelled = xaxisType === "category";
|
|
47
|
+
const data = entries
|
|
48
|
+
.filter(isGroupPoint)
|
|
49
|
+
.map((point) => (labelled ? { ...point, x: String(point.x) } : point));
|
|
50
|
+
return [{ name, data }];
|
|
51
|
+
}
|
|
@@ -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,7 +42,7 @@ export function useActionConfirm() {
|
|
|
43
42
|
const { confirm } = useConfirm();
|
|
44
43
|
const { $authFetch } = useAuthFetch();
|
|
45
44
|
const { t } = useI18n();
|
|
46
|
-
const
|
|
45
|
+
const { presentError } = useApiErrorPresenter();
|
|
47
46
|
const runSecondaryTarget = secondaryTargetRunner($authFetch.raw);
|
|
48
47
|
|
|
49
48
|
const loadDialog = async (
|
|
@@ -61,12 +60,7 @@ export function useActionConfirm() {
|
|
|
61
60
|
try {
|
|
62
61
|
return await $authFetch<ConfirmDialog>(url);
|
|
63
62
|
} catch (error) {
|
|
64
|
-
|
|
65
|
-
color: Color.error,
|
|
66
|
-
icon: "i-ph-warning-circle",
|
|
67
|
-
title: t("dms.form.error_title"),
|
|
68
|
-
description: resolveActionError(error, t),
|
|
69
|
-
});
|
|
63
|
+
presentError(error, { fallbackKey: "dms.action_error.generic" });
|
|
70
64
|
return undefined;
|
|
71
65
|
}
|
|
72
66
|
};
|
package/frontend-vue/layers/dms-ui/app/build/composables/data-types/defaultPrimitiveTypes.ts
CHANGED
|
@@ -37,16 +37,14 @@ function renderBoolean(value: CellValue) {
|
|
|
37
37
|
]);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function dateBoundsOf(value: CellValue): [unknown, unknown] {
|
|
41
|
+
const bounds: { start?: unknown; end?: unknown } = value ?? {};
|
|
42
|
+
return [bounds.start, bounds.end];
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
function formatDateBetween(value: CellValue, locale: string): string {
|
|
41
46
|
const { t } = getAppTranslation();
|
|
42
|
-
const [rawStart, rawEnd] = Array.isArray(value)
|
|
43
|
-
? value
|
|
44
|
-
: [
|
|
45
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- The open formatter value has no date-range schema; preserve optional start access on the original input.
|
|
46
|
-
(value as { start?: unknown })?.start,
|
|
47
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- The open formatter value has no date-range schema; preserve optional end access on the original input.
|
|
48
|
-
(value as { end?: unknown })?.end,
|
|
49
|
-
];
|
|
47
|
+
const [rawStart, rawEnd] = Array.isArray(value) ? value : dateBoundsOf(value);
|
|
50
48
|
const start = formatDate(rawStart, locale) ?? t("dms.date.undefined");
|
|
51
49
|
const end = formatDate(rawEnd, locale) ?? t("dms.date.undefined");
|
|
52
50
|
return `${start} - ${end}`;
|
|
@@ -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);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { isVNode, type ComponentPublicInstance, type VNode } from "vue";
|
|
2
2
|
|
|
3
3
|
const blocks = new Map<string, ComponentPublicInstance>();
|
|
4
4
|
|
|
@@ -28,8 +28,7 @@ function firstElementOf(vnode: VNode | null | undefined): Element | null {
|
|
|
28
28
|
if (vnode.component) return firstElementOf(vnode.component.subTree);
|
|
29
29
|
if (!Array.isArray(vnode.children)) return null;
|
|
30
30
|
for (const child of vnode.children) {
|
|
31
|
-
|
|
32
|
-
const element = firstElementOf(child as VNode);
|
|
31
|
+
const element = isVNode(child) ? firstElementOf(child) : null;
|
|
33
32
|
if (element) return element;
|
|
34
33
|
}
|
|
35
34
|
return null;
|
|
@@ -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,9 +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;
|
|
37
|
+
const { presentError } = useApiErrorPresenter();
|
|
38
38
|
const ORDERED_DISPLAY_IDS = new Set([TABLE_DISPLAY_ID, CARDS_DISPLAY_ID]);
|
|
39
39
|
const { reorderState } = useTableReorder<T>({
|
|
40
40
|
reorder: reorderConfig,
|
|
@@ -53,18 +53,18 @@ export function useTableViewReorder<T extends Data>(
|
|
|
53
53
|
ORDERED_DISPLAY_IDS.has(activeDisplayId.value),
|
|
54
54
|
),
|
|
55
55
|
refresh: () => refresh(),
|
|
56
|
-
onError: (failure) =>
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
onError: (failure) => {
|
|
57
|
+
presentError(
|
|
58
|
+
failure,
|
|
59
|
+
tableWriteErrorOptions(
|
|
60
60
|
failure,
|
|
61
61
|
t,
|
|
62
62
|
allColumns,
|
|
63
63
|
processI18n,
|
|
64
|
+
"dms.table.reorder.error",
|
|
64
65
|
),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}),
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
68
|
});
|
|
69
69
|
return { reorderState };
|
|
70
70
|
}
|
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;
|
|
@@ -49,18 +49,23 @@ 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
|
|
|
55
56
|
const settle = async (id: string) => {
|
|
56
57
|
const found = await options.resolve(id);
|
|
57
58
|
const outcome = deepLinkOutcome(found, action);
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
if (outcome === "open") {
|
|
60
|
+
if (found.row) options.open(found.row);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
60
63
|
if (outcome === "missingPanel") return options.showMissing(id);
|
|
61
64
|
writeRecordId(urlScope);
|
|
62
65
|
if (linkedId === id) linkedId = undefined;
|
|
63
|
-
|
|
66
|
+
if (outcome === "notifyFailed")
|
|
67
|
+
presentError(found.error, { fallbackKey: "dms.table.load_error_title" });
|
|
68
|
+
else options.notify(outcome);
|
|
64
69
|
};
|
|
65
70
|
|
|
66
71
|
const follow = (query: LocationQuery) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
2
2
|
import { computed } from "vue";
|
|
3
|
+
import { stringifyQuery, withQuery } from "ufo";
|
|
3
4
|
import { interpolateUrl } from "#dms-core/app/utils/url-interpolation";
|
|
4
5
|
import { isString } from "#dms-core/app/utils/type-check";
|
|
5
6
|
import type {
|
|
@@ -58,6 +59,48 @@ const DESCENDING = "desc";
|
|
|
58
59
|
*/
|
|
59
60
|
export const LIVE_HELD_AT_QUERY_KEY = "liveHeldAt";
|
|
60
61
|
const NO_ROUTE_VALUES: ReplaceUrlVariablesContext = { routeQuery: {} };
|
|
62
|
+
const MAX_BATCH_COUNT_QUERIES = 50;
|
|
63
|
+
const MAX_BATCH_COUNT_TARGET_BYTES = 8_192;
|
|
64
|
+
|
|
65
|
+
function encodeCountQuery({ id, query }: TableCountQuery): string {
|
|
66
|
+
const values = Object.fromEntries(
|
|
67
|
+
Object.entries(query)
|
|
68
|
+
.filter(([, value]) => value !== undefined)
|
|
69
|
+
.map(([key, value]) => [
|
|
70
|
+
key,
|
|
71
|
+
Array.isArray(value) ? value.map(String) : String(value),
|
|
72
|
+
]),
|
|
73
|
+
);
|
|
74
|
+
return `${encodeURIComponent(id)}?${stringifyQuery(values)}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function countBatchTargetBytes(location: string, entries: string[]): number {
|
|
78
|
+
const url = new URL(location, "http://dms.invalid");
|
|
79
|
+
const target = withQuery(url.pathname + url.search, { query: entries });
|
|
80
|
+
return new TextEncoder().encode(target).byteLength;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function packCountQueries(
|
|
84
|
+
location: string,
|
|
85
|
+
queries: TableCountQuery[],
|
|
86
|
+
): string[][] {
|
|
87
|
+
const batches: string[][] = [];
|
|
88
|
+
let batch: string[] = [];
|
|
89
|
+
for (const entry of queries.map(encodeCountQuery)) {
|
|
90
|
+
if (
|
|
91
|
+
batch.length > 0 &&
|
|
92
|
+
(batch.length >= MAX_BATCH_COUNT_QUERIES ||
|
|
93
|
+
countBatchTargetBytes(location, [...batch, entry]) >
|
|
94
|
+
MAX_BATCH_COUNT_TARGET_BYTES)
|
|
95
|
+
) {
|
|
96
|
+
batches.push(batch);
|
|
97
|
+
batch = [];
|
|
98
|
+
}
|
|
99
|
+
batch.push(entry);
|
|
100
|
+
}
|
|
101
|
+
batches.push(batch);
|
|
102
|
+
return batches;
|
|
103
|
+
}
|
|
61
104
|
|
|
62
105
|
type SourceCapabilities = TableViewSourceConfig["capabilities"];
|
|
63
106
|
|
|
@@ -347,11 +390,16 @@ export function useTableRows<T extends Record<string, unknown>>(
|
|
|
347
390
|
signal?: AbortSignal,
|
|
348
391
|
): Promise<Record<string, number>> => {
|
|
349
392
|
if (!source) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
393
|
+
const endpoint = `${location}/count/batch`;
|
|
394
|
+
const counts = await Promise.all(
|
|
395
|
+
packCountQueries(endpoint, queries).map((entries) =>
|
|
396
|
+
api<Record<string, number>>(endpoint, {
|
|
397
|
+
query: { query: entries },
|
|
398
|
+
signal,
|
|
399
|
+
}),
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
return Object.assign({}, ...counts);
|
|
355
403
|
}
|
|
356
404
|
const totals = await Promise.all(
|
|
357
405
|
queries.map(async ({ id, query }) => {
|
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,6 +14,7 @@ export const useTableRowActions = <T extends Data>(
|
|
|
14
14
|
config: TableRowActionsConfig,
|
|
15
15
|
) => {
|
|
16
16
|
const toast = useToast();
|
|
17
|
+
const { presentError } = useApiErrorPresenter();
|
|
17
18
|
const { t, locale } = useI18n();
|
|
18
19
|
const { confirm } = useConfirm();
|
|
19
20
|
const { confirmAction } = useActionConfirm();
|
|
@@ -38,21 +39,11 @@ export const useTableRowActions = <T extends Data>(
|
|
|
38
39
|
* what failed, the description is the server's message when it is meant
|
|
39
40
|
* for users, else a translated fallback (never a raw error line).
|
|
40
41
|
*/
|
|
41
|
-
const handleApiError = (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
color: Color.error,
|
|
47
|
-
icon: "i-ph-warning-circle",
|
|
48
|
-
title: title || t("dms.form.error_title"),
|
|
49
|
-
description: describeTableWriteError(
|
|
50
|
-
error,
|
|
51
|
-
t,
|
|
52
|
-
config.columns,
|
|
53
|
-
processI18n,
|
|
54
|
-
),
|
|
55
|
-
});
|
|
42
|
+
const handleApiError = (error: unknown, title?: string) => {
|
|
43
|
+
presentError(
|
|
44
|
+
error,
|
|
45
|
+
tableWriteErrorOptions(error, t, config.columns, processI18n, title),
|
|
46
|
+
);
|
|
56
47
|
};
|
|
57
48
|
|
|
58
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
|
|