@antelopejs/dms 0.10.0 → 0.11.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 +113 -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/implementations/dms/page.js +45 -16
- package/dist/implementations/dms/page.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/app/utils/menu.ts +66 -70
- 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/DashboardSidebar.vue +3 -11
- 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/components/navigation/NavigationMenu.vue +18 -3
- 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 +8 -11
- 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
|
@@ -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.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A CodeMirror stream parser (a language written as a line tokenizer, like
|
|
2
|
+
// the shell's) run for read-only highlighting. `StreamLanguage` would do it
|
|
3
|
+
// inside an editor state, and so pull CodeMirror's view and state into every
|
|
4
|
+
// page showing a snippet: this drives the parser the same way, line by line
|
|
5
|
+
// over `StringStream`, and names its tokens' tags the same way.
|
|
6
|
+
import { type StreamParser, StringStream } from "@codemirror/streamparser";
|
|
7
|
+
import { type Highlighter, type Tag, tags } from "@lezer/highlight";
|
|
8
|
+
|
|
9
|
+
/** Receives one styled range of the code: its offsets and its classes. */
|
|
10
|
+
export type HighlightedRange = (
|
|
11
|
+
from: number,
|
|
12
|
+
to: number,
|
|
13
|
+
classes: string,
|
|
14
|
+
) => void;
|
|
15
|
+
|
|
16
|
+
// CodeMirror's defaults, which `StreamLanguage` hands every stream.
|
|
17
|
+
const TAB_SIZE = 4;
|
|
18
|
+
const INDENT_UNIT = 2;
|
|
19
|
+
// Zero-length tokens a parser may return in a row (each one changing its
|
|
20
|
+
// state) before the stream moves on by itself, as `StreamLanguage` allows.
|
|
21
|
+
const MAX_EMPTY_TOKENS = 10;
|
|
22
|
+
|
|
23
|
+
type TagValue = Tag | readonly Tag[] | ((tag: Tag) => Tag);
|
|
24
|
+
const TAG_TABLE = tags as unknown as Record<string, TagValue | undefined>;
|
|
25
|
+
const tagsByName = new Map<string, Tag[]>();
|
|
26
|
+
|
|
27
|
+
// A token type names tags the way `StreamLanguage` reads them: space
|
|
28
|
+
// separated, each a tag followed by its modifiers (`variableName.special`).
|
|
29
|
+
function tagsOf(type: string): Tag[] {
|
|
30
|
+
const cached = tagsByName.get(type);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const resolved = type.split(" ").flatMap((name) => {
|
|
33
|
+
let found: Tag[] = [];
|
|
34
|
+
for (const part of name.split(".")) {
|
|
35
|
+
const value = TAG_TABLE[part];
|
|
36
|
+
if (typeof value === "function") found = found.map(value);
|
|
37
|
+
else if (value && !found.length)
|
|
38
|
+
found = Array.isArray(value) ? [...value] : [value as Tag];
|
|
39
|
+
}
|
|
40
|
+
return found;
|
|
41
|
+
});
|
|
42
|
+
tagsByName.set(type, resolved);
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Tokenizes `code` with `parser` and calls `put` for each range its
|
|
48
|
+
* highlighter styles, adjacent ranges of one style merged, as
|
|
49
|
+
* `highlightTree` reports a parsed tree.
|
|
50
|
+
*/
|
|
51
|
+
export function highlightStream<State>(
|
|
52
|
+
parser: StreamParser<State>,
|
|
53
|
+
code: string,
|
|
54
|
+
highlighter: Highlighter,
|
|
55
|
+
put: HighlightedRange,
|
|
56
|
+
): void {
|
|
57
|
+
const state = parser.startState?.(INDENT_UNIT) ?? (true as State);
|
|
58
|
+
let pending: { from: number; to: number; classes: string } | null = null;
|
|
59
|
+
const flush = () => {
|
|
60
|
+
if (pending) put(pending.from, pending.to, pending.classes);
|
|
61
|
+
pending = null;
|
|
62
|
+
};
|
|
63
|
+
const add = (from: number, to: number, classes: string | null) => {
|
|
64
|
+
if (pending && (pending.to !== from || pending.classes !== classes))
|
|
65
|
+
flush();
|
|
66
|
+
if (!classes) return;
|
|
67
|
+
if (pending) pending.to = to;
|
|
68
|
+
else pending = { from, to, classes };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let lineStart = 0;
|
|
72
|
+
for (const line of code.split("\n")) {
|
|
73
|
+
if (!line) parser.blankLine?.(state, INDENT_UNIT);
|
|
74
|
+
const stream = new StringStream(line, TAB_SIZE, INDENT_UNIT);
|
|
75
|
+
let emptyTokens = 0;
|
|
76
|
+
while (!stream.eol()) {
|
|
77
|
+
stream.start = stream.pos;
|
|
78
|
+
const type = parser.token(stream, state);
|
|
79
|
+
if (stream.pos === stream.start) {
|
|
80
|
+
if (++emptyTokens < MAX_EMPTY_TOKENS) continue;
|
|
81
|
+
stream.next();
|
|
82
|
+
}
|
|
83
|
+
emptyTokens = 0;
|
|
84
|
+
const classes = type ? highlighter.style(tagsOf(type)) : null;
|
|
85
|
+
add(lineStart + stream.start, lineStart + stream.pos, classes);
|
|
86
|
+
}
|
|
87
|
+
lineStart += line.length + 1;
|
|
88
|
+
}
|
|
89
|
+
flush();
|
|
90
|
+
}
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, onMounted, shallowRef } from "vue";
|
|
2
|
+
import { computed, onMounted, shallowRef, watch } from "vue";
|
|
3
3
|
import DmsCopyButton from "../copy/CopyButton.vue";
|
|
4
4
|
import {
|
|
5
5
|
codeLanguageLabel,
|
|
6
6
|
PLAIN_CODE_LANGUAGE,
|
|
7
7
|
resolveCodeLanguage,
|
|
8
8
|
} from "../../build/utils/codeLanguages";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
CodeHighlighter,
|
|
11
|
+
CodeToken,
|
|
12
|
+
} from "../../build/utils/codeHighlight";
|
|
10
13
|
|
|
11
14
|
// Read-only code in the editor's look: a head with the title, the language
|
|
12
15
|
// and a copy button, then the code highlighted by the editor's languages.
|
|
13
16
|
// Without a title or a language there is no head: the copy button floats in
|
|
14
|
-
// the code's corner. Until
|
|
15
|
-
// code shows as is.
|
|
17
|
+
// the code's corner. Until its language's grammar loads (and on the server)
|
|
18
|
+
// the code shows as is.
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* How long lines break: `false` scrolls them sideways, `true` wraps them
|
|
@@ -63,7 +66,7 @@ const WRAP_CLASSES: Record<"false" | "true" | "anywhere", string> = {
|
|
|
63
66
|
anywhere: "whitespace-pre-wrap [overflow-wrap:anywhere]",
|
|
64
67
|
};
|
|
65
68
|
|
|
66
|
-
const highlight = shallowRef<
|
|
69
|
+
const highlight = shallowRef<CodeHighlighter | null>(null);
|
|
67
70
|
|
|
68
71
|
const language = computed(() => resolveCodeLanguage(props.language));
|
|
69
72
|
// A language given, even `text`, is named in a head; without a title either,
|
|
@@ -79,9 +82,7 @@ const wrapClass = computed(
|
|
|
79
82
|
() => WRAP_CLASSES[String(props.wrap) as keyof typeof WRAP_CLASSES],
|
|
80
83
|
);
|
|
81
84
|
const tokens = computed<CodeToken[]>(() =>
|
|
82
|
-
highlight.value
|
|
83
|
-
? highlight.value(props.code, language.value)
|
|
84
|
-
: [{ text: props.code }],
|
|
85
|
+
highlight.value ? highlight.value(props.code) : [{ text: props.code }],
|
|
85
86
|
);
|
|
86
87
|
const sizeStyle = computed(() => {
|
|
87
88
|
if (props.maxLines)
|
|
@@ -91,10 +92,20 @@ const sizeStyle = computed(() => {
|
|
|
91
92
|
return props.maxHeight ? { maxHeight: props.maxHeight } : undefined;
|
|
92
93
|
});
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// Only the grammar of the language shown loads. A language changed while
|
|
96
|
+
// one loads drops it: the code stays plain until its own grammar is in.
|
|
97
|
+
async function loadHighlighter(): Promise<void> {
|
|
98
|
+
const requested = language.value;
|
|
99
|
+
highlight.value = null;
|
|
100
|
+
const { loadCodeHighlighter } = await import(
|
|
101
|
+
"../../build/utils/codeHighlight"
|
|
102
|
+
);
|
|
103
|
+
const highlighter = await loadCodeHighlighter(requested);
|
|
104
|
+
if (language.value === requested) highlight.value = highlighter;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onMounted(loadHighlighter);
|
|
108
|
+
watch(language, loadHighlighter);
|
|
98
109
|
</script>
|
|
99
110
|
|
|
100
111
|
<template>
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
findCascaderNode,
|
|
7
7
|
formatCascaderPath,
|
|
8
8
|
isCascaderNodeSelectable,
|
|
9
|
+
readCascaderAncestry,
|
|
9
10
|
resolveCascaderKeyMapping,
|
|
11
|
+
buildCascaderPathLabels,
|
|
12
|
+
CASCADER_PATH_SEPARATOR,
|
|
10
13
|
searchCascaderTree,
|
|
11
14
|
type CascaderKeyMapping,
|
|
12
15
|
type CascaderNode,
|
|
@@ -18,6 +21,7 @@ import {
|
|
|
18
21
|
FIELD_TRIGGER_INVALID_CLASS,
|
|
19
22
|
FIELD_TRIGGER_UI,
|
|
20
23
|
} from "../../../build/utils/fieldTrigger";
|
|
24
|
+
import { useRelationRows } from "../../../build/composables/data-types/relationRows";
|
|
21
25
|
|
|
22
26
|
interface CascaderProps {
|
|
23
27
|
searchUrl: string;
|
|
@@ -88,6 +92,36 @@ const { data, status, execute } = await useDmsAsyncData(
|
|
|
88
92
|
);
|
|
89
93
|
|
|
90
94
|
const keyMapping = computed(() => resolveCascaderKeyMapping(props.keyMapping));
|
|
95
|
+
const selectedValues = computed<string[]>(() => {
|
|
96
|
+
if (Array.isArray(props.modelValue)) {
|
|
97
|
+
return props.modelValue.map(String);
|
|
98
|
+
}
|
|
99
|
+
return props.modelValue ? [String(props.modelValue)] : [];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The picked rows and their ancestors, read by `?ids=` level by level: the
|
|
103
|
+
// field names its value before the menu is opened, whatever the tree's size.
|
|
104
|
+
const { readRelationRows } = useRelationRows();
|
|
105
|
+
const { data: ancestry } = await useDmsAsyncData(
|
|
106
|
+
() => `cascader-value-${instanceId}-${selectedValues.value.join(",")}`,
|
|
107
|
+
() =>
|
|
108
|
+
selectedValues.value.length > 0
|
|
109
|
+
? readCascaderAncestry(
|
|
110
|
+
(ids) =>
|
|
111
|
+
readRelationRows(
|
|
112
|
+
{
|
|
113
|
+
searchUrl: props.searchUrl,
|
|
114
|
+
keyMapping: { value: keyMapping.value.value },
|
|
115
|
+
},
|
|
116
|
+
ids,
|
|
117
|
+
),
|
|
118
|
+
keyMapping.value,
|
|
119
|
+
selectedValues.value,
|
|
120
|
+
)
|
|
121
|
+
: Promise.resolve(undefined),
|
|
122
|
+
// The field's label, not the page's data.
|
|
123
|
+
{ lazy: true, refreshOnPageRefresh: false },
|
|
124
|
+
);
|
|
91
125
|
const rows = computed(() => data.value?.results ?? []);
|
|
92
126
|
const isPartial = computed(
|
|
93
127
|
() => (data.value?.total ?? 0) > rows.value.length && rows.value.length > 0,
|
|
@@ -100,12 +134,6 @@ const selectionRules = computed(() => ({
|
|
|
100
134
|
maxDepth: props.maxDepth,
|
|
101
135
|
}));
|
|
102
136
|
|
|
103
|
-
const selectedValues = computed<string[]>(() => {
|
|
104
|
-
if (Array.isArray(props.modelValue)) {
|
|
105
|
-
return props.modelValue.map(String);
|
|
106
|
-
}
|
|
107
|
-
return props.modelValue ? [String(props.modelValue)] : [];
|
|
108
|
-
});
|
|
109
137
|
const selectedSet = computed(() => new Set(selectedValues.value));
|
|
110
138
|
|
|
111
139
|
const open = ref(false);
|
|
@@ -134,14 +162,11 @@ const columns = computed<CascaderNode[][]>(() => {
|
|
|
134
162
|
return result;
|
|
135
163
|
});
|
|
136
164
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const label = (source as Record<string, unknown>)[keyMapping.value.label];
|
|
143
|
-
return label === undefined || label === null ? undefined : String(label);
|
|
144
|
-
}
|
|
165
|
+
/** The picked id no row answers (deleted, out of reach), if it is one. */
|
|
166
|
+
const notFoundValue = computed(() => {
|
|
167
|
+
const value = selectedValues.value[0];
|
|
168
|
+
return value && ancestry.value?.notFound.includes(value) ? value : undefined;
|
|
169
|
+
});
|
|
145
170
|
|
|
146
171
|
const triggerLabel = computed(() => {
|
|
147
172
|
if (selectedValues.value.length === 0) return "";
|
|
@@ -151,9 +176,15 @@ const triggerLabel = computed(() => {
|
|
|
151
176
|
});
|
|
152
177
|
}
|
|
153
178
|
const value = selectedValues.value[0]!;
|
|
179
|
+
if (notFoundValue.value) return t("dms.form.relation.not_found");
|
|
154
180
|
const node = findCascaderNode(roots.value, value);
|
|
155
181
|
if (node) return formatCascaderPath(node);
|
|
156
|
-
|
|
182
|
+
const path = buildCascaderPathLabels(
|
|
183
|
+
ancestry.value?.rows ?? [],
|
|
184
|
+
keyMapping.value,
|
|
185
|
+
value,
|
|
186
|
+
);
|
|
187
|
+
return path.length > 0 ? path.join(CASCADER_PATH_SEPARATOR) : value;
|
|
157
188
|
});
|
|
158
189
|
|
|
159
190
|
const placeholderLabel = computed(() =>
|
|
@@ -339,12 +370,6 @@ watch(searchTerm, () => {
|
|
|
339
370
|
};
|
|
340
371
|
});
|
|
341
372
|
|
|
342
|
-
onMounted(() => {
|
|
343
|
-
if (selectedValues.value.length > 0 || props.initialValue) {
|
|
344
|
-
void execute();
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
373
|
function nodeClasses(node: CascaderNode, isHighlighted: boolean): string[] {
|
|
349
374
|
return [
|
|
350
375
|
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
|
|
@@ -383,7 +408,13 @@ function nodeClasses(node: CascaderNode, isHighlighted: boolean): string[] {
|
|
|
383
408
|
:aria-label="placeholderLabel"
|
|
384
409
|
v-bind="ariaAttrs"
|
|
385
410
|
>
|
|
386
|
-
<span
|
|
411
|
+
<span
|
|
412
|
+
v-if="triggerLabel"
|
|
413
|
+
:class="['truncate', notFoundValue && 'text-muted']"
|
|
414
|
+
:title="notFoundValue ?? triggerLabel"
|
|
415
|
+
>
|
|
416
|
+
{{ triggerLabel }}
|
|
417
|
+
</span>
|
|
387
418
|
<span v-else class="text-dimmed truncate">{{ placeholderLabel }}</span>
|
|
388
419
|
</UButton>
|
|
389
420
|
|