@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
|
@@ -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
|
|
|
@@ -19,7 +19,10 @@ import DmsForm from "../Form.vue";
|
|
|
19
19
|
import { dmsOwnDrawer } from "../../../build/composables/containers/context";
|
|
20
20
|
import { FIELD_TRIGGER_ICON } from "../../../build/utils/fieldTrigger";
|
|
21
21
|
import { unlabeledRelationIds } from "../../../build/composables/data-types/relationValue";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
notFoundRelationIds,
|
|
24
|
+
useRelationRows,
|
|
25
|
+
} from "../../../build/composables/data-types/relationRows";
|
|
23
26
|
import type { FormProps } from "../../../composables/form/types";
|
|
24
27
|
import { sameFormValue } from "../../../build/composables/unsaved-changes/formValue";
|
|
25
28
|
import { FORM_ENTRY_CONTEXT_KEY } from "../../../build/composables/form/formEntryContext";
|
|
@@ -385,19 +388,36 @@ const labeledIds = computed(
|
|
|
385
388
|
// keeps showing as is rather than being asked again.
|
|
386
389
|
const requestedIds = new Set<string>();
|
|
387
390
|
|
|
391
|
+
// The ids the route did not answer (deleted, out of reach), shown as "Not
|
|
392
|
+
// found" with the id on hover.
|
|
393
|
+
const notFoundIds: Ref<ReadonlySet<string>> = shallowRef(new Set());
|
|
394
|
+
|
|
388
395
|
async function loadValueItems(ids: string[]) {
|
|
389
396
|
for (const id of ids) requestedIds.add(id);
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
);
|
|
394
|
-
|
|
397
|
+
const source = { searchUrl: props.searchUrl, keyMapping: props.keyMapping };
|
|
398
|
+
const rows = await readRelationRows(source, ids);
|
|
399
|
+
const missing = notFoundRelationIds(source, ids, rows);
|
|
400
|
+
if (!rows.length && !missing.length) return;
|
|
401
|
+
notFoundIds.value = new Set([...notFoundIds.value, ...missing]);
|
|
395
402
|
valueItems.value = [
|
|
396
403
|
...valueItems.value,
|
|
397
404
|
...rows.map((item) => mapData(item as Record<string, string>)),
|
|
405
|
+
// Kept pickable, so the value can still be deselected.
|
|
406
|
+
...missing.map((id) => ({
|
|
407
|
+
label: t("dms.form.relation.not_found"),
|
|
408
|
+
value: id,
|
|
409
|
+
})),
|
|
398
410
|
];
|
|
399
411
|
}
|
|
400
412
|
|
|
413
|
+
const notFoundTitle = computed(() => {
|
|
414
|
+
const ids = [props.modelValue]
|
|
415
|
+
.flat()
|
|
416
|
+
.map(String)
|
|
417
|
+
.filter((id) => notFoundIds.value.has(id));
|
|
418
|
+
return ids.length ? ids.join(", ") : undefined;
|
|
419
|
+
});
|
|
420
|
+
|
|
401
421
|
// The picker shows a value through its item: an id no item carries yet is
|
|
402
422
|
// labelled (and given its avatar) from its row before the picker is opened.
|
|
403
423
|
onMounted(() => {
|
|
@@ -558,6 +578,7 @@ const addFormOptions = computed(() => {
|
|
|
558
578
|
:ignore-filter="true"
|
|
559
579
|
:ui="{ content: 'z-50' }"
|
|
560
580
|
:aria-label="t('dms.a11y.show_options')"
|
|
581
|
+
:title="notFoundTitle"
|
|
561
582
|
@update:open="onOpen"
|
|
562
583
|
@update:model-value="handleUpdateModelValue"
|
|
563
584
|
>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
useColumnValueRenderer,
|
|
12
12
|
} from "../../../build/composables/data-types/useColumnValueRenderer";
|
|
13
13
|
import {
|
|
14
|
+
type RelationColumnRows,
|
|
14
15
|
type RelationLabelSource,
|
|
15
16
|
type RepeaterTableColumn,
|
|
16
17
|
joinRelationValue,
|
|
@@ -53,16 +54,22 @@ function useColumnRelationRows(columnId: string, source: RelationLabelSource) {
|
|
|
53
54
|
);
|
|
54
55
|
const { data } = useDmsAsyncData(
|
|
55
56
|
`repeater-table-relation-${instanceId}-${columnId}`,
|
|
56
|
-
() =>
|
|
57
|
+
async () => {
|
|
58
|
+
const readIds = ids.value;
|
|
59
|
+
return { readIds, rows: await readRelationRows(source, readIds) };
|
|
60
|
+
},
|
|
57
61
|
// Names for the cells, not the page's data.
|
|
58
62
|
{ lazy: true, refreshOnPageRefresh: false, watch: [ids] },
|
|
59
63
|
);
|
|
60
|
-
return computed(
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
return computed<RelationColumnRows>(() => ({
|
|
65
|
+
rowsByValue: new Map(
|
|
66
|
+
(data.value?.rows ?? []).map((row) => [
|
|
67
|
+
relationRowValue(source, row),
|
|
68
|
+
row,
|
|
69
|
+
]),
|
|
70
|
+
),
|
|
71
|
+
readIds: new Set(data.value?.readIds),
|
|
72
|
+
}));
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
const NO_LABELS: ReadonlySet<string> = new Set();
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import {
|
|
3
|
-
CASCADER_FETCH_LIMIT,
|
|
4
3
|
CASCADER_PATH_SEPARATOR,
|
|
4
|
+
cascaderValueId,
|
|
5
5
|
cascaderValuePath,
|
|
6
|
+
cascaderValueReads,
|
|
7
|
+
readCascaderAncestry,
|
|
6
8
|
resolveCascaderKeyMapping,
|
|
7
9
|
type CascaderKeyMapping,
|
|
8
10
|
} from "../../utils/cascader";
|
|
9
11
|
import { MAX_RELATION_BADGES } from "../../build/composables/data-types/relationBadges";
|
|
12
|
+
import { useRelationRows } from "../../build/composables/data-types/relationRows";
|
|
10
13
|
|
|
11
14
|
const DEFAULT_VALUE_KEY = "_id";
|
|
12
15
|
|
|
@@ -17,20 +20,20 @@ interface CascaderPathProps {
|
|
|
17
20
|
keyMapping?: Partial<CascaderKeyMapping>;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* A picked value as the cell names it: its path, or "Not found" with its id
|
|
25
|
+
* on hover when its row is gone or out of reach.
|
|
26
|
+
*/
|
|
21
27
|
interface CascaderBadge {
|
|
22
28
|
label: string;
|
|
23
29
|
title: string;
|
|
30
|
+
isNotFound: boolean;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const props = defineProps<CascaderPathProps>();
|
|
27
34
|
|
|
28
|
-
const {
|
|
29
|
-
|
|
30
|
-
interface CascaderResponse {
|
|
31
|
-
results: Record<string, unknown>[];
|
|
32
|
-
total: number;
|
|
33
|
-
}
|
|
35
|
+
const { t } = useI18n();
|
|
36
|
+
const { readRelationRows } = useRelationRows();
|
|
34
37
|
|
|
35
38
|
const keyMapping = computed(() =>
|
|
36
39
|
resolveCascaderKeyMapping({
|
|
@@ -39,33 +42,72 @@ const keyMapping = computed(() =>
|
|
|
39
42
|
}),
|
|
40
43
|
);
|
|
41
44
|
|
|
45
|
+
const values = computed<unknown[]>(() =>
|
|
46
|
+
Array.isArray(props.value) ? props.value : [props.value],
|
|
47
|
+
);
|
|
48
|
+
const reads = computed(() =>
|
|
49
|
+
cascaderValueReads(values.value, keyMapping.value),
|
|
50
|
+
);
|
|
51
|
+
const ids = computed(() => reads.value.idsToRead);
|
|
52
|
+
|
|
53
|
+
// The ids a list did not join (and the parents of those it did), with their
|
|
54
|
+
// ancestors, read by `?ids=` level by level.
|
|
42
55
|
const { data } = await useDmsAsyncData(
|
|
43
|
-
`cascader-path-${props.searchUrl}`,
|
|
56
|
+
() => `cascader-path-${props.searchUrl}-${ids.value.join(",")}`,
|
|
44
57
|
() =>
|
|
45
|
-
props.searchUrl
|
|
46
|
-
?
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
props.searchUrl && ids.value.length > 0
|
|
59
|
+
? readCascaderAncestry(
|
|
60
|
+
(pending) =>
|
|
61
|
+
readRelationRows(
|
|
62
|
+
{
|
|
63
|
+
searchUrl: props.searchUrl!,
|
|
64
|
+
keyMapping: { value: keyMapping.value.value },
|
|
65
|
+
},
|
|
66
|
+
pending,
|
|
67
|
+
),
|
|
68
|
+
keyMapping.value,
|
|
69
|
+
ids.value,
|
|
70
|
+
)
|
|
49
71
|
: Promise.resolve(undefined),
|
|
50
72
|
// The labels of a path, not the page's data.
|
|
51
73
|
{ lazy: true, refreshOnPageRefresh: false },
|
|
52
74
|
);
|
|
53
75
|
|
|
54
|
-
const
|
|
55
|
-
|
|
76
|
+
const pathRows = computed(() => [
|
|
77
|
+
...reads.value.joinedRows,
|
|
78
|
+
...(data.value?.rows ?? []),
|
|
79
|
+
]);
|
|
56
80
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
)
|
|
81
|
+
const notFound = computed(() => new Set(data.value?.notFound));
|
|
82
|
+
|
|
83
|
+
function badgeOf(item: unknown): CascaderBadge | undefined {
|
|
84
|
+
const id = cascaderValueId(item, keyMapping.value);
|
|
85
|
+
if (id !== undefined && notFound.value.has(id)) {
|
|
86
|
+
return {
|
|
87
|
+
label: t("dms.form.relation.not_found"),
|
|
88
|
+
title: id,
|
|
89
|
+
isNotFound: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const path = cascaderValuePath(item, keyMapping.value, pathRows.value);
|
|
93
|
+
if (path.length === 0) return undefined;
|
|
94
|
+
return {
|
|
95
|
+
label: path.at(-1) ?? "",
|
|
96
|
+
title: path.join(CASCADER_PATH_SEPARATOR),
|
|
97
|
+
isNotFound: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const single = computed(() => {
|
|
102
|
+
const badge = badgeOf(props.value);
|
|
103
|
+
if (!badge || badge.isNotFound) return badge;
|
|
104
|
+
return { ...badge, label: badge.title };
|
|
105
|
+
});
|
|
60
106
|
|
|
61
107
|
const badges = computed<CascaderBadge[]>(() =>
|
|
62
108
|
(Array.isArray(props.value) ? props.value : [])
|
|
63
|
-
.map(
|
|
64
|
-
.filter((
|
|
65
|
-
.map((path) => ({
|
|
66
|
-
label: path.at(-1) ?? "",
|
|
67
|
-
title: path.join(CASCADER_PATH_SEPARATOR),
|
|
68
|
-
})),
|
|
109
|
+
.map(badgeOf)
|
|
110
|
+
.filter((badge): badge is CascaderBadge => !!badge),
|
|
69
111
|
);
|
|
70
112
|
const visibleBadges = computed(() =>
|
|
71
113
|
badges.value.slice(0, MAX_RELATION_BADGES),
|
|
@@ -82,6 +124,7 @@ const hiddenBadges = computed(() => badges.value.slice(MAX_RELATION_BADGES));
|
|
|
82
124
|
v-for="badge in visibleBadges"
|
|
83
125
|
:key="badge.title"
|
|
84
126
|
:label="badge.label"
|
|
127
|
+
:class="badge.isNotFound && 'text-muted'"
|
|
85
128
|
:title="badge.title"
|
|
86
129
|
color="neutral"
|
|
87
130
|
variant="subtle"
|
|
@@ -97,5 +140,11 @@ const hiddenBadges = computed(() => badges.value.slice(MAX_RELATION_BADGES));
|
|
|
97
140
|
size="sm"
|
|
98
141
|
/>
|
|
99
142
|
</span>
|
|
100
|
-
<span
|
|
143
|
+
<span
|
|
144
|
+
v-else-if="single"
|
|
145
|
+
:class="['truncate', single.isNotFound && 'text-muted']"
|
|
146
|
+
:title="single.title"
|
|
147
|
+
>
|
|
148
|
+
{{ single.label }}
|
|
149
|
+
</span>
|
|
101
150
|
</template>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { readSubline } from "../../build/composables/data-types/cellHelpers";
|
|
24
24
|
import { useComposedText } from "#dms-core/app/composables/translation/useComposedText";
|
|
25
25
|
import { useCardFields } from "../../build/composables/table-view/useCardFields";
|
|
26
|
+
import { describeTableWriteError } from "../../build/composables/table-view/utils/writeError";
|
|
26
27
|
|
|
27
28
|
const DEFAULT_COLUMN_PAGE_SIZE = 10;
|
|
28
29
|
const DEFAULT_COLUMN_MAX_HEIGHT = "60vh";
|
|
@@ -83,7 +84,7 @@ const emit = defineEmits<{
|
|
|
83
84
|
const groupByField = defineModel<string>("groupByField", { required: true });
|
|
84
85
|
|
|
85
86
|
const { $authFetch } = useAuthFetch();
|
|
86
|
-
const { processI18n
|
|
87
|
+
const { processI18n } = useTranslation();
|
|
87
88
|
const { getDataType } = useDataTypes();
|
|
88
89
|
const { t } = useI18n();
|
|
89
90
|
const { processText } = useComposedText();
|
|
@@ -321,18 +322,11 @@ const awaitNewerMoves = async (itemId: string, current: Promise<void>) => {
|
|
|
321
322
|
}
|
|
322
323
|
};
|
|
323
324
|
|
|
324
|
-
interface MoveErrorPayload {
|
|
325
|
-
data?: { message?: string };
|
|
326
|
-
message?: string;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
325
|
const notifyMoveError = (error: unknown) => {
|
|
330
|
-
const err = error as MoveErrorPayload;
|
|
331
|
-
const message = err?.data?.message || err?.message;
|
|
332
326
|
toast.add({
|
|
333
327
|
color: Color.error,
|
|
334
328
|
title: t("dms.table.kanban.move_error"),
|
|
335
|
-
description:
|
|
329
|
+
description: describeTableWriteError(error, t, props.columns, processI18n),
|
|
336
330
|
});
|
|
337
331
|
};
|
|
338
332
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts" generic="T extends Data">
|
|
2
2
|
import { get } from "@nuxt/ui/runtime/utils/index.js";
|
|
3
|
+
import DmsBlockStatus from "../../build/components/blocks/BlockStatus.vue";
|
|
4
|
+
import { readFetchNotFound } from "../../build/utils/fetchNotFound";
|
|
3
5
|
import type {
|
|
4
6
|
ColumnSizingState,
|
|
5
7
|
ExpandedState,
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
KANBAN_DISPLAY_BRIDGE_KEY,
|
|
54
56
|
} from "../../composables/table-view/kanban";
|
|
55
57
|
import { useTableRowActions } from "../../build/composables/table-view/useTableViewRowActions";
|
|
58
|
+
import { describeTableWriteError } from "../../build/composables/table-view/utils/writeError";
|
|
56
59
|
import {
|
|
57
60
|
buildTableQuery,
|
|
58
61
|
isFilterEffective,
|
|
@@ -1064,6 +1067,11 @@ const listLoadError = computed(() => {
|
|
|
1064
1067
|
: "error.500";
|
|
1065
1068
|
});
|
|
1066
1069
|
|
|
1070
|
+
// A list answering 404 names a resource that does not exist (the workspace
|
|
1071
|
+
// its rows belong to was deleted): no retry fixes it, so the table gives way
|
|
1072
|
+
// to the not-found state blocks show, with what the 404 said.
|
|
1073
|
+
const listNotFound = computed(() => readFetchNotFound(settledError.value));
|
|
1074
|
+
|
|
1067
1075
|
// useDmsAsyncData keeps the previous result when a refetch fails, so rows kept
|
|
1068
1076
|
// on screen would masquerade as the newly requested page/filter. Hide them
|
|
1069
1077
|
// while the latest query is in error; the error panel takes their place.
|
|
@@ -1384,7 +1392,7 @@ const { reorderState } = useTableReorder<T>({
|
|
|
1384
1392
|
onError: (failure) =>
|
|
1385
1393
|
toast.add({
|
|
1386
1394
|
title: t("dms.table.reorder.error"),
|
|
1387
|
-
description:
|
|
1395
|
+
description: describeTableWriteError(failure, t, allColumns, processI18n),
|
|
1388
1396
|
color: Color.error,
|
|
1389
1397
|
icon: "i-ph-warning-circle",
|
|
1390
1398
|
}),
|
|
@@ -1569,6 +1577,7 @@ const {
|
|
|
1569
1577
|
} = useTableRowActions<T>({
|
|
1570
1578
|
api: $authFetch,
|
|
1571
1579
|
location,
|
|
1580
|
+
columns: allColumns,
|
|
1572
1581
|
caption,
|
|
1573
1582
|
labelKey,
|
|
1574
1583
|
rowIdKey: props.rowIdKey,
|
|
@@ -2391,7 +2400,13 @@ onMounted(() => {
|
|
|
2391
2400
|
</script>
|
|
2392
2401
|
|
|
2393
2402
|
<template>
|
|
2403
|
+
<DmsBlockStatus
|
|
2404
|
+
v-if="listNotFound"
|
|
2405
|
+
state="not-found"
|
|
2406
|
+
:not-found="listNotFound"
|
|
2407
|
+
/>
|
|
2394
2408
|
<UTable
|
|
2409
|
+
v-else
|
|
2395
2410
|
ref="tableRoot"
|
|
2396
2411
|
v-bind="tableProps"
|
|
2397
2412
|
v-model:pagination="paginationState"
|
|
@@ -18,6 +18,12 @@ export interface FormFieldReadonly {
|
|
|
18
18
|
link?: FormFieldLink;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Holds while the field `field` holds one of the values `in` lists. */
|
|
22
|
+
export interface FieldCondition {
|
|
23
|
+
field: string;
|
|
24
|
+
in: unknown[];
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
export interface FormField {
|
|
22
28
|
id: string;
|
|
23
29
|
label?: string;
|
|
@@ -36,6 +42,12 @@ export interface FormField {
|
|
|
36
42
|
/** "Module default · Use default" while it holds something else. */
|
|
37
43
|
offersDefault?: boolean;
|
|
38
44
|
localized?: boolean;
|
|
45
|
+
/** Shown only while the condition holds: hidden, neither required nor sent. */
|
|
46
|
+
visibleWhen?: FieldCondition;
|
|
47
|
+
/** Read-only while the condition holds: its value is not sent. */
|
|
48
|
+
disabledWhen?: FieldCondition;
|
|
49
|
+
/** A localized field's fallback language: filled whenever any language is. */
|
|
50
|
+
fallbackLocale?: string;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
export interface FieldGroup {
|