@antelopejs/dms 0.10.0 → 0.11.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 +69 -0
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -4
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/frontend-modules.js +17 -6
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/manifest.d.ts +6 -0
- package/dist/implementations/dms/manifest.js +10 -7
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/index.js +4 -7
- package/dist/index.js.map +1 -1
- package/dist/store/listing.js +1 -2
- package/dist/store/listing.js.map +1 -1
- package/dist/store/operations.js +1 -2
- package/dist/store/operations.js.map +1 -1
- package/dist/utils/ensure-default-tenant.d.ts +2 -1
- package/dist/utils/ensure-default-tenant.js +13 -1
- package/dist/utils/ensure-default-tenant.js.map +1 -1
- package/dist/utils/export-jobs.js +2 -1
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/build/composables/renderNow.ts +1 -2
- package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +3 -0
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +3 -5
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +61 -26
- package/frontend-vue/layers/dms-core/app/utils/formatter.ts +5 -37
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -0
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +0 -2
- package/frontend-vue/layers/dms-layout/app/app.config.ts +0 -1
- package/frontend-vue/layers/dms-layout/app/assets/css/main.css +11 -1
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/Geist-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Italic-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardPageBody.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +2 -106
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionBar.vue +0 -8
- package/frontend-vue/layers/dms-layout/app/build/components/layout/PageHeaderActionsOutlet.vue +0 -10
- package/frontend-vue/layers/dms-layout/app/build/utils/github-repository.ts +2 -2
- package/frontend-vue/layers/dms-layout/app/composables/layout/useServedHeaderActions.ts +24 -4
- package/frontend-vue/layers/dms-layout/app/composables/page/useModuleStore.ts +10 -2
- package/frontend-vue/layers/dms-layout/app/composables/page/usePageRecordDeletion.ts +162 -0
- package/frontend-vue/layers/dms-layout/app/custom-layouts/DefaultLayout.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/custom-layouts/SettingsLayout.vue +2 -7
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +7 -0
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-navigation.client.ts +9 -9
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-personal-pages.client.ts +9 -9
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-quick-actions.client.ts +7 -7
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +11 -10
- package/frontend-vue/layers/dms-layout/app/theme/actions.ts +5 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +6 -4
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +6 -4
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +5 -1
- package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +11 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/actionError.ts +6 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/numberSuffix.ts +2 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +26 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationRows.ts +22 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +11 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formValidation.ts +36 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/form/repeaterTable.ts +17 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/table/useRelationFilterLabels.ts +30 -6
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +13 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/writeError.ts +21 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +67 -29
- package/frontend-vue/layers/dms-ui/app/build/utils/htmlGrammar.ts +79 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/build/utils/streamHighlight.ts +90 -0
- package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +23 -12
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +1 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +53 -22
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +27 -6
- package/frontend-vue/layers/dms-ui/app/components/form/components/RepeaterTable.vue +14 -7
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +74 -25
- package/frontend-vue/layers/dms-ui/app/components/table-view/KanbanBoard.vue +3 -9
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +16 -1
- package/frontend-vue/layers/dms-ui/app/composables/form/types/field.ts +12 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +47 -7
- package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +19 -9
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +94 -3
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -1
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -1
- package/frontend-vue/package.json +6 -2
- package/package.json +7 -7
- package/skills/dms-module/SKILL.md +1 -1
- package/skills/dms-pages/REFERENCE.md +7 -3
- package/dist/hooks/tenant-instances.d.ts +0 -1
- package/dist/hooks/tenant-instances.js +0 -13
- package/dist/hooks/tenant-instances.js.map +0 -1
- package/frontend-vue/layers/dms-core/app/composables/hydration/isHydratingComponent.ts +0 -29
- package/frontend-vue/layers/dms-layout/app/composables/layout/usePageRecord.ts +0 -50
- package/frontend-vue/layers/dms-layout/app/utils/pageRecord.ts +0 -71
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import type { DataTypeFormatter } from "#dms-core/app/composables/data-types/useDataType";
|
|
2
|
-
import {
|
|
3
|
-
getPluginI18n,
|
|
4
|
-
resolveI18nKey,
|
|
5
|
-
} from "../../../../../dms-core/app/composables/translation/useTranslation";
|
|
2
|
+
import { getAppTranslation } from "../../../../../dms-core/app/composables/translation/useTranslation";
|
|
6
3
|
|
|
7
4
|
/** A formatter writing a number for a locale, with Intl's options. */
|
|
8
5
|
type NumberFormatter = (
|
|
@@ -41,7 +38,7 @@ export function withNumberSuffix(format: NumberFormatter): DataTypeFormatter {
|
|
|
41
38
|
if (!suffix || typeof value !== "number" || typeof formatted !== "string") {
|
|
42
39
|
return formatted;
|
|
43
40
|
}
|
|
44
|
-
const unit =
|
|
41
|
+
const unit = getAppTranslation().processI18n(suffix);
|
|
45
42
|
return `${formatted}${UNIT_SEPARATOR}${unit}`;
|
|
46
43
|
};
|
|
47
44
|
}
|
|
@@ -7,7 +7,7 @@ import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
|
7
7
|
import ULink from "@nuxt/ui/components/Link.vue";
|
|
8
8
|
import { buildRelationBadges } from "./relationBadges";
|
|
9
9
|
import { mapRelationBeforeState, sameRelationValue } from "./relationValue";
|
|
10
|
-
import { relationSelectSource } from "./relationRows";
|
|
10
|
+
import { NotFoundRelation, relationSelectSource } from "./relationRows";
|
|
11
11
|
import {
|
|
12
12
|
isSet,
|
|
13
13
|
readRowField,
|
|
@@ -338,12 +338,31 @@ function renderRelationBadge(label: string, title?: string) {
|
|
|
338
338
|
});
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// An id its `select` route did not answer: "Not found", the id on hover.
|
|
342
|
+
function renderNotFoundRelation(relation: NotFoundRelation) {
|
|
343
|
+
const { t } = getAppTranslation();
|
|
344
|
+
return h(
|
|
345
|
+
"span",
|
|
346
|
+
{ class: "text-muted", title: relation.id },
|
|
347
|
+
t("dms.form.relation.not_found"),
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
341
351
|
function renderRelationBadges(values: unknown[], labelKey: string) {
|
|
342
|
-
const { processI18n } = getAppTranslation();
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
352
|
+
const { processI18n, t } = getAppTranslation();
|
|
353
|
+
const notFound = values.filter(
|
|
354
|
+
(item): item is NotFoundRelation => item instanceof NotFoundRelation,
|
|
355
|
+
);
|
|
356
|
+
const { visible, hidden } = buildRelationBadges(
|
|
357
|
+
values.filter((item) => !(item instanceof NotFoundRelation)),
|
|
358
|
+
labelKey,
|
|
346
359
|
);
|
|
360
|
+
const badges = [
|
|
361
|
+
...visible.map((label) => renderRelationBadge(processI18n(label))),
|
|
362
|
+
...notFound.map((relation) =>
|
|
363
|
+
renderRelationBadge(t("dms.form.relation.not_found"), relation.id),
|
|
364
|
+
),
|
|
365
|
+
];
|
|
347
366
|
if (hidden.length > 0) {
|
|
348
367
|
const hiddenLabels = hidden.map((label) => processI18n(label)).join(", ");
|
|
349
368
|
badges.push(renderRelationBadge(`+${hidden.length}`, hiddenLabels));
|
|
@@ -359,6 +378,7 @@ function renderRelation(value: unknown, options: unknown) {
|
|
|
359
378
|
return renderRelationBadges(value, labelKey);
|
|
360
379
|
}
|
|
361
380
|
|
|
381
|
+
if (value instanceof NotFoundRelation) return renderNotFoundRelation(value);
|
|
362
382
|
if (!value || !isObject(value)) return value;
|
|
363
383
|
|
|
364
384
|
const valueObject = value as Record<string, string>;
|
|
@@ -981,6 +1001,7 @@ const registerCascaderRelationType = (
|
|
|
981
1001
|
},
|
|
982
1002
|
beforeStateMapper: mapRelationBeforeState,
|
|
983
1003
|
isSameValue: sameRelationValue,
|
|
1004
|
+
selectSource: relationSelectSource,
|
|
984
1005
|
});
|
|
985
1006
|
};
|
|
986
1007
|
|
|
@@ -45,12 +45,33 @@ export function relationRowLabel(
|
|
|
45
45
|
return row[source.keyMapping?.label || RELATION_LABEL_KEY];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* An id a relation's `select` route did not answer: a deleted row, or one out
|
|
50
|
+
* of the reader's reach — it cannot tell which, so it is shown as "Not
|
|
51
|
+
* found", never as "Deleted", with the id itself in its tooltip.
|
|
52
|
+
*/
|
|
53
|
+
export class NotFoundRelation {
|
|
54
|
+
constructor(readonly id: string) {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The ids of a read that its rows do not answer. */
|
|
58
|
+
export function notFoundRelationIds(
|
|
59
|
+
source: DataTypeSelectSource,
|
|
60
|
+
ids: string[],
|
|
61
|
+
rows: RelationRow[],
|
|
62
|
+
): string[] {
|
|
63
|
+
const answered = new Set(
|
|
64
|
+
rows.map((row) => String(relationRowValue(source, row))),
|
|
65
|
+
);
|
|
66
|
+
return ids.filter((id) => !answered.has(id));
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
/**
|
|
49
70
|
* Reads the rows of a relation's ids from its `select` route, narrowed with
|
|
50
71
|
* `?ids=` to those rows alone (matched on the mapped value key), whatever the
|
|
51
72
|
* route's page size. The one way the picker, the filter chips and the
|
|
52
73
|
* read-only lists name the ids they hold. A failed read answers no rows: the
|
|
53
|
-
* ids then show as
|
|
74
|
+
* ids then show as not found, as a row out of the reader's reach does.
|
|
54
75
|
*/
|
|
55
76
|
export function useRelationRows() {
|
|
56
77
|
const { $authFetch } = useAuthFetch();
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { type Ref, ref, watch } from "vue";
|
|
2
2
|
import { apiErrorStatus } from "#dms-core/app/composables/useFieldErrors";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
HTTP_FORBIDDEN,
|
|
5
|
+
HTTP_NOT_FOUND,
|
|
6
|
+
} from "#dms-core/app/utils/http-status";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Why a form could not load the record it edits: `forbidden` when the server
|
|
7
10
|
* refused the caller the read (a role that may edit rows but not view them),
|
|
8
11
|
* `missing` when its address lacks the record's id (a page reached without
|
|
9
|
-
* its `?id=`), `
|
|
10
|
-
* a
|
|
12
|
+
* its `?id=`), `not_found` when the server answered 404 (the record was
|
|
13
|
+
* deleted, or never existed), `error` for anything else (a network failure,
|
|
14
|
+
* a server error).
|
|
11
15
|
*/
|
|
12
|
-
export type FormLoadFailure = "forbidden" | "missing" | "error";
|
|
16
|
+
export type FormLoadFailure = "forbidden" | "missing" | "not_found" | "error";
|
|
13
17
|
|
|
14
18
|
/** What a form knows of the record it is about, as far as `missing` goes. */
|
|
15
19
|
export interface FormRecordAddress {
|
|
@@ -51,7 +55,9 @@ export interface FormRecordLoad {
|
|
|
51
55
|
/** The failure a load error stands for; `null` when there is none. */
|
|
52
56
|
export function formLoadFailure(error: unknown): FormLoadFailure | null {
|
|
53
57
|
if (!error) return null;
|
|
54
|
-
|
|
58
|
+
const status = apiErrorStatus(error);
|
|
59
|
+
if (status === HTTP_FORBIDDEN) return "forbidden";
|
|
60
|
+
return status === HTTP_NOT_FOUND ? "not_found" : "error";
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
/** How a form takes the answers of its record's later reads. */
|
|
@@ -22,6 +22,11 @@ export interface FormValidationField {
|
|
|
22
22
|
type?: string;
|
|
23
23
|
/** Holds one value per content language. */
|
|
24
24
|
localized?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The language a localized field falls back to: required as soon as the
|
|
27
|
+
* field holds any language, and where its required error goes.
|
|
28
|
+
*/
|
|
29
|
+
fallbackLocale?: string;
|
|
25
30
|
/** A box to tick: unticked, it is missing. */
|
|
26
31
|
acceptance?: boolean;
|
|
27
32
|
/**
|
|
@@ -63,6 +68,32 @@ function issueField(issue: { path: PropertyKey[] }): PropertyKey | undefined {
|
|
|
63
68
|
return issue.path[0];
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/** Whether a localized value fills some language but not its fallback one. */
|
|
72
|
+
function lacksFallbackLocale(
|
|
73
|
+
field: FormValidationField,
|
|
74
|
+
value: unknown,
|
|
75
|
+
): boolean {
|
|
76
|
+
if (!field.localized || !field.fallbackLocale) return false;
|
|
77
|
+
if (typeof value !== "object" || value === null) return false;
|
|
78
|
+
const languages = value as Record<string, unknown>;
|
|
79
|
+
const isAnyFilled = Object.values(languages).some(
|
|
80
|
+
(text) => !isBlankValue(text, field.type),
|
|
81
|
+
);
|
|
82
|
+
return (
|
|
83
|
+
isAnyFilled && isBlankValue(languages[field.fallbackLocale], field.type)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Where a localized field's required error goes: the language to fill. */
|
|
88
|
+
function requiredPath(
|
|
89
|
+
field: FormValidationField,
|
|
90
|
+
locale: string | undefined,
|
|
91
|
+
): PropertyKey[] {
|
|
92
|
+
if (!field.localized) return [field.id];
|
|
93
|
+
const language = field.fallbackLocale ?? locale;
|
|
94
|
+
return language ? [field.id, language] : [field.id];
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
/** An issue saying the value is not of a branch's type at all. */
|
|
67
98
|
function isTypeMismatch(issue: z.ZodIssue, path: PropertyKey[]): boolean {
|
|
68
99
|
return issue.code === "invalid_type" && issue.path.length === path.length;
|
|
@@ -117,6 +148,10 @@ export async function validateFormState(
|
|
|
117
148
|
for (const field of context.fields) {
|
|
118
149
|
const value = state[field.id];
|
|
119
150
|
if (reported.has(field.id)) continue;
|
|
151
|
+
if (lacksFallbackLocale(field, value)) {
|
|
152
|
+
missing.add(field.id);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
120
155
|
const isBlank = field.acceptance
|
|
121
156
|
? value !== true
|
|
122
157
|
: isBlankValue(value, field.type);
|
|
@@ -159,10 +194,7 @@ export async function validateFormState(
|
|
|
159
194
|
if (!missing.has(field.id)) continue;
|
|
160
195
|
issues.push({
|
|
161
196
|
message: context.requiredMessage,
|
|
162
|
-
path:
|
|
163
|
-
field.localized && context.locale
|
|
164
|
-
? [field.id, context.locale]
|
|
165
|
-
: [field.id],
|
|
197
|
+
path: requiredPath(field, context.locale),
|
|
166
198
|
});
|
|
167
199
|
}
|
|
168
200
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
DataTypeSelectSource,
|
|
4
4
|
} from "#dms-core/app/composables/data-types/useDataType";
|
|
5
5
|
import type { RenderableColumn } from "../data-types/useColumnValueRenderer";
|
|
6
|
+
import { NotFoundRelation } from "../data-types/relationRows";
|
|
6
7
|
|
|
7
8
|
/** A column of a read-only list: a field of its rows. */
|
|
8
9
|
export interface RepeaterTableColumn {
|
|
@@ -49,17 +50,29 @@ export function relationLabelSource(
|
|
|
49
50
|
return source ? { ...source, multiple: !!options?.multiple } : undefined;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/** The rows a relation column's read answered, by value, and the ids it asked. */
|
|
54
|
+
export interface RelationColumnRows {
|
|
55
|
+
rowsByValue: ReadonlyMap<unknown, Record<string, unknown>>;
|
|
56
|
+
readIds: ReadonlySet<string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
/**
|
|
53
60
|
* A relation value as its formatter draws it: the related row in place of
|
|
54
|
-
* each id it holds. An id
|
|
55
|
-
*
|
|
61
|
+
* each id it holds. An id the read asked for and no row answers (deleted, out
|
|
62
|
+
* of reach) is not found; one not asked yet stays as it is, as does a value
|
|
63
|
+
* the server already joined.
|
|
56
64
|
*/
|
|
57
65
|
export function joinRelationValue(
|
|
58
66
|
value: unknown,
|
|
59
|
-
rowsByValue:
|
|
67
|
+
{ rowsByValue, readIds }: RelationColumnRows,
|
|
60
68
|
multiple: boolean,
|
|
61
69
|
): unknown {
|
|
62
|
-
const join = (item: unknown) =>
|
|
70
|
+
const join = (item: unknown) => {
|
|
71
|
+
const row = rowsByValue.get(item);
|
|
72
|
+
if (row) return row;
|
|
73
|
+
const id = String(item);
|
|
74
|
+
return readIds.has(id) ? new NotFoundRelation(id) : item;
|
|
75
|
+
};
|
|
63
76
|
if (multiple && Array.isArray(value)) return value.map(join);
|
|
64
77
|
return join(value);
|
|
65
78
|
}
|
|
@@ -2,6 +2,7 @@ import { onMounted, shallowRef, watch } from "vue";
|
|
|
2
2
|
import type { DataTypeSelectSource } from "#dms-core/app/composables/data-types/useDataType";
|
|
3
3
|
import { unlabeledRelationIds } from "../data-types/relationValue";
|
|
4
4
|
import {
|
|
5
|
+
notFoundRelationIds,
|
|
5
6
|
relationRowLabel,
|
|
6
7
|
relationRowValue,
|
|
7
8
|
useRelationRows,
|
|
@@ -18,15 +19,24 @@ const labelKey = (searchUrl: string, id: string) => `${searchUrl}\n${id}`;
|
|
|
18
19
|
const NO_LABELS: ReadonlySet<string> = new Set();
|
|
19
20
|
const idsOf = (value: unknown) => unlabeledRelationIds(value, NO_LABELS);
|
|
20
21
|
|
|
22
|
+
/** How a chip names one id of a relation filter. */
|
|
23
|
+
export interface RelationFilterLabel {
|
|
24
|
+
label: string;
|
|
25
|
+
/** The id of a row the route did not answer, shown on hover. */
|
|
26
|
+
notFoundId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* The labels of the ids a table's relation filters hold: a chip names the
|
|
23
31
|
* related row ("Acme"), not the raw id it filters on ("cust-060"). Each id is
|
|
24
32
|
* read once from the relation's `select` route, as its picker does; an id the
|
|
25
|
-
* route does not answer (deleted, out of reach)
|
|
33
|
+
* route does not answer (deleted, out of reach) is named "Not found".
|
|
26
34
|
*/
|
|
27
35
|
export function useRelationFilterLabels(sources: () => RelationFilterSource[]) {
|
|
28
36
|
const { readRelationRows } = useRelationRows();
|
|
37
|
+
const { t } = useI18n();
|
|
29
38
|
const labels = shallowRef<Record<string, string>>({});
|
|
39
|
+
const notFound = shallowRef<ReadonlySet<string>>(new Set());
|
|
30
40
|
const requested = new Set<string>();
|
|
31
41
|
|
|
32
42
|
async function loadLabels(source: RelationFilterSource, ids: string[]) {
|
|
@@ -42,6 +52,13 @@ export function useRelationFilterLabels(sources: () => RelationFilterSource[]) {
|
|
|
42
52
|
if (Object.keys(found).length) {
|
|
43
53
|
labels.value = { ...labels.value, ...found };
|
|
44
54
|
}
|
|
55
|
+
const missing = notFoundRelationIds(source, ids, rows);
|
|
56
|
+
if (missing.length) {
|
|
57
|
+
notFound.value = new Set([
|
|
58
|
+
...notFound.value,
|
|
59
|
+
...missing.map((id) => labelKey(source.searchUrl, id)),
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
const pending = () =>
|
|
@@ -63,11 +80,18 @@ export function useRelationFilterLabels(sources: () => RelationFilterSource[]) {
|
|
|
63
80
|
);
|
|
64
81
|
});
|
|
65
82
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The labels of a relation filter's ids, in order: an id not read yet as
|
|
85
|
+
* is, one the route did not answer as "Not found".
|
|
86
|
+
*/
|
|
87
|
+
const labelsOf = (source: RelationFilterSource): RelationFilterLabel[] =>
|
|
88
|
+
idsOf(source.value).map((id) => {
|
|
89
|
+
const key = labelKey(source.searchUrl, id);
|
|
90
|
+
if (notFound.value.has(key)) {
|
|
91
|
+
return { label: t("dms.form.relation.not_found"), notFoundId: id };
|
|
92
|
+
}
|
|
93
|
+
return { label: labels.value[key] ?? id };
|
|
94
|
+
});
|
|
71
95
|
|
|
72
96
|
return { labelsOf };
|
|
73
97
|
}
|
package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts
CHANGED
|
@@ -5,7 +5,10 @@ import {
|
|
|
5
5
|
type FormPageUrls,
|
|
6
6
|
} from "./useTableViewConfig";
|
|
7
7
|
import type { FormProps } from "../../../composables/form/types";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
QueryParamFilters,
|
|
10
|
+
TableViewColumn,
|
|
11
|
+
} from "../../../composables/table-view/types";
|
|
9
12
|
import type { LocationQuery, LocationQueryValue } from "#dms/frontend-module";
|
|
10
13
|
import {
|
|
11
14
|
TableRowAction,
|
|
@@ -33,7 +36,7 @@ import {
|
|
|
33
36
|
type BulkActionOutcome,
|
|
34
37
|
type ConfirmedBulkAction,
|
|
35
38
|
} from "./utils/bulkActions";
|
|
36
|
-
import {
|
|
39
|
+
import { describeTableWriteError } from "./utils/writeError";
|
|
37
40
|
import {
|
|
38
41
|
ConfirmActionError,
|
|
39
42
|
type ConfirmOptions,
|
|
@@ -59,6 +62,8 @@ const getItemId = (
|
|
|
59
62
|
interface TableRowActionsConfig {
|
|
60
63
|
api: ReturnType<typeof $fetch.create>;
|
|
61
64
|
location: string;
|
|
65
|
+
/** The columns, whose labels name the fields a refused write lacks. */
|
|
66
|
+
columns: ReadonlyArray<Pick<TableViewColumn, "accessorKey" | "header">>;
|
|
62
67
|
caption?: string;
|
|
63
68
|
labelKey?: string;
|
|
64
69
|
rowIdKey?: string;
|
|
@@ -146,7 +151,12 @@ export const useTableRowActions = <T extends Data>(
|
|
|
146
151
|
color: Color.error,
|
|
147
152
|
icon: "i-ph-warning-circle",
|
|
148
153
|
title: title || t("dms.form.error_title"),
|
|
149
|
-
description:
|
|
154
|
+
description: describeTableWriteError(
|
|
155
|
+
error,
|
|
156
|
+
t,
|
|
157
|
+
config.columns,
|
|
158
|
+
processI18n,
|
|
159
|
+
),
|
|
150
160
|
});
|
|
151
161
|
};
|
|
152
162
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { I18nTranslate } from "#dms-core/app/composables/translation/useTranslation";
|
|
2
|
+
import type { TableViewColumn } from "../../../../composables/table-view/types/column";
|
|
3
|
+
import { resolveActionError } from "../../confirm/actionError";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Why a table view's write failed (a card moved on the board, a row moved
|
|
7
|
+
* by hand, a row action), for its toast: the server's reason, a field the
|
|
8
|
+
* data API refused named by its column's label ("Loss reason is
|
|
9
|
+
* required."). Never the raw ofetch error line.
|
|
10
|
+
*/
|
|
11
|
+
export function describeTableWriteError(
|
|
12
|
+
error: unknown,
|
|
13
|
+
translate: I18nTranslate,
|
|
14
|
+
columns: ReadonlyArray<Pick<TableViewColumn, "accessorKey" | "header">>,
|
|
15
|
+
processI18n: (text: string) => string,
|
|
16
|
+
): string {
|
|
17
|
+
return resolveActionError(error, translate, (field) => {
|
|
18
|
+
const column = columns.find((entry) => entry.accessorKey === field);
|
|
19
|
+
return column ? processI18n(column.header) : undefined;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
// Read-only highlighting: the code parsed by the editor's
|
|
2
|
-
// languages and cut into styled tokens, without
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
javascriptLanguage,
|
|
8
|
-
typescriptLanguage,
|
|
9
|
-
} from "@codemirror/lang-javascript";
|
|
10
|
-
import { jsonLanguage } from "@codemirror/lang-json";
|
|
11
|
-
import { StandardSQL } from "@codemirror/lang-sql";
|
|
12
|
-
import { StreamLanguage } from "@codemirror/language";
|
|
1
|
+
// Read-only highlighting: the code parsed by the grammars the editor's
|
|
2
|
+
// CodeMirror languages are built on, and cut into styled tokens, without
|
|
3
|
+
// mounting an editor. Each language's grammar loads on demand and alone: a
|
|
4
|
+
// snippet needs neither the editor's view nor its state, which would triple
|
|
5
|
+
// what a page showing a JSON payload downloads. SQL is the exception: its
|
|
6
|
+
// grammar only ships inside `@codemirror/lang-sql`.
|
|
13
7
|
import type { Parser } from "@lezer/common";
|
|
14
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type Highlighter,
|
|
10
|
+
highlightTree,
|
|
11
|
+
tagHighlighter,
|
|
12
|
+
} from "@lezer/highlight";
|
|
15
13
|
import { CODE_PALETTE, type CodeTokenStyle } from "./codePalette";
|
|
16
|
-
import {
|
|
14
|
+
import type { HighlightedRange } from "./streamHighlight";
|
|
17
15
|
|
|
18
16
|
/** A run of code drawn in one style; unstyled text has no `style`. */
|
|
19
17
|
export interface CodeToken {
|
|
@@ -21,13 +19,42 @@ export interface CodeToken {
|
|
|
21
19
|
style?: CodeTokenStyle;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
/** Cuts code of the language it was loaded for into tokens. */
|
|
23
|
+
export type CodeHighlighter = (code: string) => CodeToken[];
|
|
24
|
+
|
|
25
|
+
/** Calls `put` for each styled range of `code`, in order. */
|
|
26
|
+
type RangeReader = (
|
|
27
|
+
code: string,
|
|
28
|
+
highlighter: Highlighter,
|
|
29
|
+
put: HighlightedRange,
|
|
30
|
+
) => void;
|
|
31
|
+
|
|
32
|
+
const readWith =
|
|
33
|
+
(parser: Parser): RangeReader =>
|
|
34
|
+
(code, highlighter, put) =>
|
|
35
|
+
highlightTree(parser.parse(code), highlighter, put);
|
|
36
|
+
|
|
37
|
+
// One dynamic import per language, so a page loads the grammar it shows.
|
|
38
|
+
const READERS: Record<string, () => Promise<RangeReader>> = {
|
|
39
|
+
json: async () => readWith((await import("@lezer/json")).parser),
|
|
40
|
+
javascript: async () => readWith((await import("@lezer/javascript")).parser),
|
|
41
|
+
typescript: async () =>
|
|
42
|
+
readWith(
|
|
43
|
+
(await import("@lezer/javascript")).parser.configure({ dialect: "ts" }),
|
|
44
|
+
),
|
|
45
|
+
html: async () => readWith((await import("./htmlGrammar")).htmlParser),
|
|
46
|
+
sql: async () =>
|
|
47
|
+
readWith(
|
|
48
|
+
(await import("@codemirror/lang-sql")).StandardSQL.language.parser,
|
|
49
|
+
),
|
|
50
|
+
shell: async () => {
|
|
51
|
+
const [{ highlightStream }, { shellParser }] = await Promise.all([
|
|
52
|
+
import("./streamHighlight"),
|
|
53
|
+
import("./shellParser"),
|
|
54
|
+
]);
|
|
55
|
+
return (code, highlighter, put) =>
|
|
56
|
+
highlightStream(shellParser, code, highlighter, put);
|
|
57
|
+
},
|
|
31
58
|
};
|
|
32
59
|
|
|
33
60
|
// Each palette entry's position is its class, read back into its style.
|
|
@@ -40,16 +67,11 @@ function styleOf(classes: string): CodeTokenStyle {
|
|
|
40
67
|
return { color, fontStyle };
|
|
41
68
|
}
|
|
42
69
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
* `resolveCodeLanguage`); a language without a parser is one plain token.
|
|
46
|
-
*/
|
|
47
|
-
export function highlightCode(code: string, language: string): CodeToken[] {
|
|
48
|
-
const parser = PARSERS[language];
|
|
49
|
-
if (!parser || !code) return [{ text: code }];
|
|
70
|
+
function tokensOf(code: string, read: RangeReader): CodeToken[] {
|
|
71
|
+
if (!code) return [{ text: code }];
|
|
50
72
|
const tokens: CodeToken[] = [];
|
|
51
73
|
let cursor = 0;
|
|
52
|
-
|
|
74
|
+
read(code, HIGHLIGHTER, (from, to, classes) => {
|
|
53
75
|
if (from > cursor) tokens.push({ text: code.slice(cursor, from) });
|
|
54
76
|
tokens.push({ text: code.slice(from, to), style: styleOf(classes) });
|
|
55
77
|
cursor = to;
|
|
@@ -57,3 +79,19 @@ export function highlightCode(code: string, language: string): CodeToken[] {
|
|
|
57
79
|
if (cursor < code.length) tokens.push({ text: code.slice(cursor) });
|
|
58
80
|
return tokens;
|
|
59
81
|
}
|
|
82
|
+
|
|
83
|
+
const plain: CodeHighlighter = (code) => [{ text: code }];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Loads the grammar of `language` (a resolved name, see
|
|
87
|
+
* `resolveCodeLanguage`) and resolves to what cuts its code into styled
|
|
88
|
+
* tokens; a language without a grammar shows each code as one plain token.
|
|
89
|
+
*/
|
|
90
|
+
export async function loadCodeHighlighter(
|
|
91
|
+
language: string,
|
|
92
|
+
): Promise<CodeHighlighter> {
|
|
93
|
+
const load = READERS[language];
|
|
94
|
+
if (!load) return plain;
|
|
95
|
+
const read = await load();
|
|
96
|
+
return (code) => tokensOf(code, read);
|
|
97
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// HTML as the editor's language reads it (`@codemirror/lang-html`): scripts,
|
|
2
|
+
// styles, `style` attributes and event handlers parsed in their own
|
|
3
|
+
// languages, from the lezer grammars alone. The nesting mirrors lang-html's
|
|
4
|
+
// defaults, so a snippet colours HTML exactly like the editor. A module of its
|
|
5
|
+
// own because HTML is the one grammar composed of others.
|
|
6
|
+
import { parser as cssParser } from "@lezer/css";
|
|
7
|
+
import { configureNesting, parser } from "@lezer/html";
|
|
8
|
+
import { parser as javascriptParser } from "@lezer/javascript";
|
|
9
|
+
|
|
10
|
+
// What lezer read of a tag's attributes; an absent one is undefined.
|
|
11
|
+
type Attributes = Readonly<Record<string, string | undefined>>;
|
|
12
|
+
|
|
13
|
+
const typescriptParser = javascriptParser.configure({ dialect: "ts" });
|
|
14
|
+
// A JSON script holds one value, read as JavaScript reads it.
|
|
15
|
+
const jsonScriptParser = javascriptParser.configure({
|
|
16
|
+
top: "SingleExpression",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const JSON_SCRIPT_TYPE =
|
|
20
|
+
/^(importmap|speculationrules|application\/(.+\+)?json)$/i;
|
|
21
|
+
const JAVASCRIPT_TYPE =
|
|
22
|
+
/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i;
|
|
23
|
+
const CSS_TYPE = /^(text\/)?(x-)?(stylesheet|css)$/i;
|
|
24
|
+
|
|
25
|
+
const NESTED_TAGS = [
|
|
26
|
+
{
|
|
27
|
+
tag: "script",
|
|
28
|
+
attrs: (attrs: Attributes) =>
|
|
29
|
+
attrs.type === "text/typescript" || attrs.lang === "ts",
|
|
30
|
+
parser: typescriptParser,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
tag: "script",
|
|
34
|
+
attrs: (attrs: Attributes) =>
|
|
35
|
+
attrs.type === "text/babel" || attrs.type === "text/jsx",
|
|
36
|
+
parser: javascriptParser.configure({ dialect: "jsx" }),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
tag: "script",
|
|
40
|
+
attrs: (attrs: Attributes) => attrs.type === "text/typescript-jsx",
|
|
41
|
+
parser: javascriptParser.configure({ dialect: "jsx ts" }),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
tag: "script",
|
|
45
|
+
attrs: (attrs: Attributes) => JSON_SCRIPT_TYPE.test(attrs.type ?? ""),
|
|
46
|
+
parser: jsonScriptParser,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
tag: "script",
|
|
50
|
+
attrs: (attrs: Attributes) =>
|
|
51
|
+
!attrs.type || JAVASCRIPT_TYPE.test(attrs.type),
|
|
52
|
+
parser: javascriptParser,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
tag: "style",
|
|
56
|
+
attrs: (attrs: Attributes) =>
|
|
57
|
+
(!attrs.lang || attrs.lang === "css") &&
|
|
58
|
+
(!attrs.type || CSS_TYPE.test(attrs.type)),
|
|
59
|
+
parser: cssParser,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const EVENT_ATTRIBUTES = (
|
|
64
|
+
"beforeunload copy cut dragstart dragover dragleave dragenter dragend " +
|
|
65
|
+
"drag paste focus blur change click load mousedown mouseenter mouseleave " +
|
|
66
|
+
"mouseup keydown keyup resize scroll unload"
|
|
67
|
+
)
|
|
68
|
+
.split(" ")
|
|
69
|
+
.map((name) => `on${name}`);
|
|
70
|
+
|
|
71
|
+
const NESTED_ATTRIBUTES = [
|
|
72
|
+
{ name: "style", parser: cssParser.configure({ top: "Styles" }) },
|
|
73
|
+
...EVENT_ATTRIBUTES.map((name) => ({ name, parser: javascriptParser })),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/** The HTML grammar, with its embedded languages. */
|
|
77
|
+
export const htmlParser = parser.configure({
|
|
78
|
+
wrap: configureNesting(NESTED_TAGS, NESTED_ATTRIBUTES),
|
|
79
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StreamParser, StringStream } from "@codemirror/
|
|
1
|
+
import type { StreamParser, StringStream } from "@codemirror/streamparser";
|
|
2
2
|
|
|
3
3
|
// A shell snippet as read for highlighting only: the command of each
|
|
4
4
|
// pipeline step, its flags, quoted strings, variables and comments.
|