@riducms/admin 0.1.5 → 0.2.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/package.json +11 -9
- package/src/app.css +63 -4
- package/src/app.svelte +12 -14
- package/src/components/ui/date-value-control/date-value-control-loader.svelte +15 -4
- package/src/components/ui/date-value-control/date-value-control.svelte +17 -17
- package/src/components/ui/select/select-trigger.svelte +1 -1
- package/src/core/forms/embedded-fields.ts +132 -0
- package/src/core/forms/embedded-schema-draft.svelte.ts +335 -0
- package/src/core/forms/field-editor-authoring.ts +47 -0
- package/src/core/forms/field-editor-binding.ts +228 -0
- package/src/core/forms/field-issue-focus.ts +42 -0
- package/src/core/forms/field-occurrence.ts +122 -0
- package/src/core/forms/form-controller.svelte.ts +475 -29
- package/src/core/forms/form-issue-correlation.ts +159 -0
- package/src/core/forms/form-schema.ts +211 -17
- package/src/core/forms/form-validation.ts +183 -17
- package/src/core/forms/live-validation.svelte.ts +317 -0
- package/src/core/forms/plugin-field-authoring.ts +160 -0
- package/src/core/forms/plugin-field-binding.ts +258 -0
- package/src/core/forms/plugin-field-write.ts +57 -0
- package/src/core/i18n/admin-i18n.svelte.ts +8 -1
- package/src/core/i18n/localized-schema.ts +33 -10
- package/src/core/plugins/field-registry.ts +29 -49
- package/src/core/plugins/row-label-registry.ts +25 -5
- package/src/core/runtime/admin-runtime.svelte.ts +25 -13
- package/src/features/auth/account-route.svelte +10 -5
- package/src/features/auth/account-security-route.svelte +1 -1
- package/src/features/collections/collection-list-bulk-editor.svelte +6 -6
- package/src/features/collections/collection-list-controller.svelte.ts +5 -5
- package/src/features/collections/collection-list-filter-popover.svelte +6 -6
- package/src/features/collections/collection-list-route.svelte +13 -14
- package/src/features/collections/list-workspace.ts +22 -8
- package/src/features/documents/document-controller.svelte.ts +8 -2
- package/src/features/documents/document-route.svelte +36 -43
- package/src/features/reference-browser/reference-browser-workflow.svelte.ts +30 -15
- package/src/features/reference-browser/reference-browser.svelte +215 -189
- package/src/features/uploads/upload-metadata-fields.svelte +6 -6
- package/src/features/versions/version-history-route.svelte +1 -1
- package/src/fields/embedded-schema-draft-editor.svelte +74 -0
- package/src/fields/embedded-schema-fields.svelte +44 -0
- package/src/fields/field-clipboard.ts +76 -16
- package/src/fields/field-collapsible.svelte +49 -0
- package/src/fields/field-layout.svelte +2 -18
- package/src/fields/field-messages.svelte +13 -18
- package/src/fields/field-renderer.svelte +53 -14
- package/src/fields/join/join-field.svelte +8 -1
- package/src/fields/live-validation-feedback.svelte +21 -0
- package/src/fields/local-field-editor.svelte +52 -0
- package/src/fields/nested/nested-field.svelte +159 -91
- package/src/fields/nested/scoped-field.ts +7 -9
- package/src/fields/plugin-field.svelte +156 -0
- package/src/fields/point/point-field.svelte +12 -2
- package/src/fields/primitive-list/primitive-list-field.svelte +170 -0
- package/src/fields/primitive-list/primitive-number-input.ts +6 -0
- package/src/fields/radio/radio-field.svelte +13 -9
- package/src/fields/relationship/relationship-field.svelte +36 -12
- package/src/fields/relationship/relationship-query.ts +4 -2
- package/src/fields/relationship/relationship-quick-picker.svelte +38 -10
- package/src/fields/scalar/date-control-value.ts +5 -5
- package/src/fields/scalar/date-value.ts +5 -5
- package/src/fields/scalar/scalar-field.svelte +47 -41
- package/src/fields/select/select-field.svelte +24 -24
- package/src/fields/select/select-value.ts +3 -3
- package/src/fields/text/slug-field.svelte +24 -8
- package/src/fields/text/text-field.svelte +8 -6
- package/src/index.ts +4 -12
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
2
|
+
import { transformEmbeddedPayloads } from "@admin/core/forms/embedded-fields";
|
|
1
3
|
import type { SchemaField } from "@riducms/protocol";
|
|
2
4
|
|
|
3
5
|
export type FieldClipboardKind = "field" | "row";
|
|
@@ -61,7 +63,7 @@ export function compatibleClipboardValue(
|
|
|
61
63
|
payload.signature !== fieldClipboardSignature(field)
|
|
62
64
|
)
|
|
63
65
|
return undefined;
|
|
64
|
-
return
|
|
66
|
+
return cloneFieldForPaste(field, payload.value, kind);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export async function writeFieldClipboard(payload: FieldClipboardPayload) {
|
|
@@ -88,15 +90,49 @@ export async function readFieldClipboard() {
|
|
|
88
90
|
return memoryClipboard;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
/** Clone owned repeating rows and declared payloads; arbitrary JSON retains its identity semantics. */
|
|
94
|
+
export function cloneFieldForPaste(
|
|
95
|
+
field: SchemaField,
|
|
96
|
+
value: unknown,
|
|
97
|
+
kind: FieldClipboardKind = "field"
|
|
98
|
+
): unknown {
|
|
99
|
+
function row(input: unknown): unknown {
|
|
100
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
101
|
+
return cloneJSON(input);
|
|
102
|
+
const source = input as Record<string, unknown>;
|
|
103
|
+
const children =
|
|
104
|
+
field.type === "blocks"
|
|
105
|
+
? (resolveBlockTypes(field.blocks).find((block) => block.slug === source.blockType)
|
|
106
|
+
?.fields ?? [])
|
|
107
|
+
: (field.nested?.fields ?? []);
|
|
108
|
+
return { ...record(children, source), _key: crypto.randomUUID() };
|
|
109
|
+
}
|
|
110
|
+
if (kind === "row") return row(value);
|
|
111
|
+
if ((field.type === "array" || field.type === "blocks") && Array.isArray(value))
|
|
112
|
+
return value.map(row);
|
|
113
|
+
if (
|
|
114
|
+
field.type === "group" &&
|
|
115
|
+
value !== null &&
|
|
116
|
+
typeof value === "object" &&
|
|
117
|
+
!Array.isArray(value)
|
|
118
|
+
) {
|
|
119
|
+
return record(field.nested?.fields ?? [], value as Record<string, unknown>);
|
|
120
|
+
}
|
|
121
|
+
if (field.type === "plugin")
|
|
122
|
+
return transformEmbeddedPayloads(field, cloneJSON(value), field.path, (occurrence) => ({
|
|
123
|
+
...record(occurrence.block.fields, occurrence.payload),
|
|
124
|
+
[occurrence.case.identity]: crypto.randomUUID(),
|
|
125
|
+
}));
|
|
126
|
+
return cloneJSON(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function record(fields: readonly SchemaField[], source: Record<string, unknown>) {
|
|
130
|
+
const copy = cloneJSON(source) as Record<string, unknown>;
|
|
131
|
+
for (const field of fields) {
|
|
132
|
+
if (Object.hasOwn(source, field.name))
|
|
133
|
+
copy[field.name] = cloneFieldForPaste(field, source[field.name]);
|
|
134
|
+
}
|
|
135
|
+
return copy;
|
|
100
136
|
}
|
|
101
137
|
|
|
102
138
|
function cloneJSON(value: unknown) {
|
|
@@ -107,7 +143,7 @@ function fieldShape(field: SchemaField): unknown {
|
|
|
107
143
|
return {
|
|
108
144
|
type: field.type,
|
|
109
145
|
category: field.category,
|
|
110
|
-
select: field.select?.
|
|
146
|
+
select: field.select?.options.map((option) => option.value),
|
|
111
147
|
relationship: field.relationship && {
|
|
112
148
|
collection: field.relationship.collectionSlug,
|
|
113
149
|
targets: field.relationship.targets?.map((target) => target.collectionSlug),
|
|
@@ -123,10 +159,34 @@ function fieldShape(field: SchemaField): unknown {
|
|
|
123
159
|
maxRows: field.nested.maxRows ?? 0,
|
|
124
160
|
fields: field.nested.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
|
|
125
161
|
},
|
|
126
|
-
blocks: field.blocks
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
162
|
+
blocks: field.blocks && {
|
|
163
|
+
minRows: field.blocks.minRows ?? 0,
|
|
164
|
+
maxRows: field.blocks.maxRows ?? 0,
|
|
165
|
+
types: resolveBlockTypes(field.blocks).map((block) => ({
|
|
166
|
+
key: block.slug,
|
|
167
|
+
fields: block.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
|
|
168
|
+
})),
|
|
169
|
+
},
|
|
170
|
+
plugin: field.plugin && {
|
|
171
|
+
key: field.plugin.key,
|
|
172
|
+
config: field.plugin.config,
|
|
173
|
+
embeddedTrees: field.plugin.embeddedTrees?.map((tree) => ({
|
|
174
|
+
version: tree.version,
|
|
175
|
+
key: tree.key,
|
|
176
|
+
root: tree.root,
|
|
177
|
+
children: tree.children,
|
|
178
|
+
tag: tree.tag,
|
|
179
|
+
cases: tree.cases.map((branch) => ({
|
|
180
|
+
tagValue: branch.tagValue,
|
|
181
|
+
payload: branch.payload,
|
|
182
|
+
discriminator: branch.discriminator,
|
|
183
|
+
identity: branch.identity,
|
|
184
|
+
types: resolveBlockTypes(branch).map((block) => ({
|
|
185
|
+
key: block.slug,
|
|
186
|
+
fields: block.fields.map((child) => ({ name: child.name, shape: fieldShape(child) })),
|
|
187
|
+
})),
|
|
188
|
+
})),
|
|
189
|
+
})),
|
|
190
|
+
},
|
|
131
191
|
};
|
|
132
192
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
3
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
4
|
+
import { fieldIssueRevealEvent } from "@admin/core/forms/field-issue-focus";
|
|
5
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
6
|
+
import FieldRenderer from "@admin/fields/field-renderer.svelte";
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
fields,
|
|
10
|
+
form,
|
|
11
|
+
collapsible,
|
|
12
|
+
}: {
|
|
13
|
+
fields: readonly SchemaField[];
|
|
14
|
+
form: FormController;
|
|
15
|
+
collapsible: NonNullable<SchemaField["admin"]["collapsible"]>;
|
|
16
|
+
} = $props();
|
|
17
|
+
const runtime = getAdminRuntime();
|
|
18
|
+
// The disclosure's identity belongs to its keyed layout group.
|
|
19
|
+
// svelte-ignore state_referenced_locally
|
|
20
|
+
let open = $state(!collapsible.initiallyCollapsed);
|
|
21
|
+
function reveal(element: HTMLElement) {
|
|
22
|
+
const show = () => {
|
|
23
|
+
open = true;
|
|
24
|
+
};
|
|
25
|
+
element.addEventListener(fieldIssueRevealEvent, show);
|
|
26
|
+
return () => element.removeEventListener(fieldIssueRevealEvent, show);
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<details
|
|
31
|
+
bind:open
|
|
32
|
+
{@attach reveal}
|
|
33
|
+
class="col-span-1 rounded-[4px] border border-control-border bg-background sm:col-span-12"
|
|
34
|
+
data-field-collapsible={collapsible.id}
|
|
35
|
+
>
|
|
36
|
+
<summary class="cursor-pointer px-5 py-4 text-[13.5px] font-medium text-foreground">
|
|
37
|
+
{runtime.i18n.text(collapsible.label, collapsible.labelTranslations)}
|
|
38
|
+
</summary>
|
|
39
|
+
{#if open}
|
|
40
|
+
<div
|
|
41
|
+
class="grid grid-cols-1 gap-x-4 gap-y-6 border-t border-control-border p-5 sm:grid-cols-12"
|
|
42
|
+
>
|
|
43
|
+
{#each fields as field (field.id)}<FieldRenderer {field} {form} />{/each}
|
|
44
|
+
</div>
|
|
45
|
+
{:else}
|
|
46
|
+
<!-- Addressable boundaries reveal the disclosure before mounting/focusing a field. -->
|
|
47
|
+
{#each fields as field (field.id)}<span hidden data-field-path={field.path}></span>{/each}
|
|
48
|
+
{/if}
|
|
49
|
+
</details>
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import type { SchemaField } from "@riducms/protocol";
|
|
3
3
|
|
|
4
4
|
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
5
|
-
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
6
5
|
import FieldRenderer from "@admin/fields/field-renderer.svelte";
|
|
7
6
|
import { createFieldLayout } from "@admin/fields/field-layout";
|
|
8
7
|
import FieldTabLayout from "@admin/fields/field-tab-layout.svelte";
|
|
8
|
+
import FieldCollapsible from "@admin/fields/field-collapsible.svelte";
|
|
9
9
|
|
|
10
10
|
let {
|
|
11
11
|
fields,
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
form: FormController;
|
|
17
17
|
suppressedTabGroupID?: string;
|
|
18
18
|
} = $props();
|
|
19
|
-
const runtime = getAdminRuntime();
|
|
20
19
|
</script>
|
|
21
20
|
|
|
22
21
|
{#snippet fieldGrid(tabFields: readonly SchemaField[])}
|
|
@@ -25,22 +24,7 @@
|
|
|
25
24
|
{#if group.tabGroup !== undefined}
|
|
26
25
|
<FieldTabLayout fields={group.fields} {form} tabGroup={group.tabGroup} />
|
|
27
26
|
{:else if group.collapsible !== undefined}
|
|
28
|
-
<
|
|
29
|
-
class="col-span-1 rounded-[4px] border border-control-border bg-background sm:col-span-12"
|
|
30
|
-
open={!group.collapsible.initiallyCollapsed}
|
|
31
|
-
data-field-collapsible={group.collapsible.id}
|
|
32
|
-
>
|
|
33
|
-
<summary class="cursor-pointer px-5 py-4 text-[13.5px] font-medium text-foreground">
|
|
34
|
-
{runtime.i18n.text(group.collapsible.label, group.collapsible.labelTranslations)}
|
|
35
|
-
</summary>
|
|
36
|
-
<div
|
|
37
|
-
class="grid grid-cols-1 gap-x-4 gap-y-6 border-t border-control-border p-5 sm:grid-cols-12"
|
|
38
|
-
>
|
|
39
|
-
{#each group.fields as field (field.id)}
|
|
40
|
-
<FieldRenderer {field} {form} />
|
|
41
|
-
{/each}
|
|
42
|
-
</div>
|
|
43
|
-
</details>
|
|
27
|
+
<FieldCollapsible fields={group.fields} {form} collapsible={group.collapsible} />
|
|
44
28
|
{:else if group.row !== undefined}
|
|
45
29
|
<div
|
|
46
30
|
class="col-span-1 grid grid-cols-1 gap-x-4 gap-y-6 sm:col-span-12 sm:grid-cols-12"
|
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ValidationIssue } from "@riducms/protocol";
|
|
3
|
-
import
|
|
3
|
+
import { FieldFeedback } from "@riducms/ui";
|
|
4
|
+
import type { Snippet } from "svelte";
|
|
4
5
|
|
|
5
6
|
let {
|
|
6
|
-
|
|
7
|
+
controlID,
|
|
7
8
|
issues = [],
|
|
8
9
|
description,
|
|
9
10
|
class: className,
|
|
11
|
+
children,
|
|
10
12
|
}: {
|
|
11
|
-
|
|
12
|
-
issues?: readonly ValidationIssue[];
|
|
13
|
-
description?: string;
|
|
14
|
-
class?: string;
|
|
13
|
+
controlID: string;
|
|
14
|
+
issues?: readonly ValidationIssue[] | undefined;
|
|
15
|
+
description?: string | undefined;
|
|
16
|
+
class?: string | undefined;
|
|
17
|
+
children: Snippet;
|
|
15
18
|
} = $props();
|
|
19
|
+
const errors = $derived(issues.map((issue) => issue.message));
|
|
16
20
|
</script>
|
|
17
21
|
|
|
18
|
-
{
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<ul class="grid gap-0.5">
|
|
22
|
-
{#each issues as issue (`${issue.path}:${issue.code}:${issue.message}`)}
|
|
23
|
-
<li class="ridu-field-error">{issue.message}</li>
|
|
24
|
-
{/each}
|
|
25
|
-
</ul>
|
|
26
|
-
</div>
|
|
27
|
-
{:else if description !== undefined}
|
|
28
|
-
<p {id} class={["ridu-field-help", className]}>{description}</p>
|
|
29
|
-
{/if}
|
|
22
|
+
<FieldFeedback {controlID} {description} {errors} class={className}>
|
|
23
|
+
{@render children()}
|
|
24
|
+
</FieldFeedback>
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
withContentLocaleLabel,
|
|
14
14
|
} from "@admin/fields/localized-field-presentation";
|
|
15
15
|
|
|
16
|
+
import PluginField from "@admin/fields/plugin-field.svelte";
|
|
17
|
+
import LocalFieldEditor from "@admin/fields/local-field-editor.svelte";
|
|
16
18
|
import SelectField from "@admin/fields/select/select-field.svelte";
|
|
17
19
|
import RadioField from "@admin/fields/radio/radio-field.svelte";
|
|
18
20
|
import PointField from "@admin/fields/point/point-field.svelte";
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
import ScalarField from "@admin/fields/scalar/scalar-field.svelte";
|
|
23
25
|
import NestedField from "@admin/fields/nested/nested-field.svelte";
|
|
24
26
|
import RelationshipField from "@admin/fields/relationship/relationship-field.svelte";
|
|
27
|
+
import PrimitiveListField from "@admin/fields/primitive-list/primitive-list-field.svelte";
|
|
25
28
|
import TextField from "@admin/fields/text/text-field.svelte";
|
|
29
|
+
import LiveValidationFeedback from "@admin/fields/live-validation-feedback.svelte";
|
|
26
30
|
|
|
27
31
|
interface Props {
|
|
28
32
|
field: SchemaField;
|
|
@@ -48,15 +52,13 @@
|
|
|
48
52
|
referenceBrowser: ReferenceBrowser,
|
|
49
53
|
findDocument: (collection, id, signal) =>
|
|
50
54
|
runtime.client.find(collection, id, { signal, locale: form.contentLocale }),
|
|
51
|
-
async requestPlugin<Result>(path: string, body: unknown, signal?: AbortSignal) {
|
|
52
|
-
const owner = field.admin.component?.plugin ?? field.plugin?.key;
|
|
53
|
-
if (owner === undefined) {
|
|
54
|
-
throw new Error("This field renderer does not have an owning plugin");
|
|
55
|
-
}
|
|
56
|
-
return runtime.client.requestPlugin<Result>(owner, path, body, { signal });
|
|
57
|
-
},
|
|
58
55
|
};
|
|
59
|
-
const
|
|
56
|
+
const editor = $derived(
|
|
57
|
+
field.admin.editor === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: runtime.editors[field.admin.editor.reference as `app:${string}`]
|
|
60
|
+
);
|
|
61
|
+
const plugin = $derived(editor === undefined ? runtime.fields.resolve(field) : undefined);
|
|
60
62
|
const accessPath = $derived(fieldAccessPath(field));
|
|
61
63
|
const visible = $derived(
|
|
62
64
|
field.admin.hidden !== true &&
|
|
@@ -65,7 +67,7 @@
|
|
|
65
67
|
evaluateFieldCondition(field.admin.condition, field.path, (path) => form.get(path)))
|
|
66
68
|
);
|
|
67
69
|
const renderedField = $derived(
|
|
68
|
-
field.
|
|
70
|
+
field.admin.readOnly || form.canWrite(field.path, accessPath)
|
|
69
71
|
? field
|
|
70
72
|
: { ...field, admin: { ...field.admin, readOnly: true } }
|
|
71
73
|
);
|
|
@@ -101,11 +103,12 @@
|
|
|
101
103
|
);
|
|
102
104
|
|
|
103
105
|
function renderField() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
switch (plugin.type) {
|
|
106
|
+
switch (plugin?.type) {
|
|
107
107
|
case "text":
|
|
108
108
|
return TextField;
|
|
109
|
+
case "text-list":
|
|
110
|
+
case "number-list":
|
|
111
|
+
return PrimitiveListField;
|
|
109
112
|
case "select":
|
|
110
113
|
return SelectField;
|
|
111
114
|
case "radio":
|
|
@@ -131,9 +134,45 @@
|
|
|
131
134
|
}
|
|
132
135
|
</script>
|
|
133
136
|
|
|
134
|
-
{#if visible}<div
|
|
137
|
+
{#if visible}<div
|
|
138
|
+
class={columnClass}
|
|
139
|
+
data-live-validation-field={field.path}
|
|
140
|
+
onfocusout={(event) => {
|
|
141
|
+
if (
|
|
142
|
+
event.target instanceof Element &&
|
|
143
|
+
event.target.closest("[data-live-validation-field]") === event.currentTarget &&
|
|
144
|
+
!(event.relatedTarget instanceof Node && event.currentTarget.contains(event.relatedTarget))
|
|
145
|
+
)
|
|
146
|
+
form.liveValidation.flush(field.path);
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
135
149
|
{const RenderField = $derived(renderField())}
|
|
136
|
-
|
|
150
|
+
{#if editor !== undefined}
|
|
151
|
+
{#key form}
|
|
152
|
+
{#key `${form.editorEpoch}:${runtime.manifestRevision}:${field.id}`}
|
|
153
|
+
<LocalFieldEditor
|
|
154
|
+
schema={presentedField}
|
|
155
|
+
{form}
|
|
156
|
+
{editor}
|
|
157
|
+
{authoring}
|
|
158
|
+
i18n={runtime.i18n}
|
|
159
|
+
/>
|
|
160
|
+
{/key}
|
|
161
|
+
{/key}
|
|
162
|
+
{:else if plugin?.extension !== undefined}
|
|
163
|
+
{#key form}
|
|
164
|
+
{#key `${form.editorEpoch}:${runtime.manifestRevision}:${field.id}`}
|
|
165
|
+
<PluginField schema={presentedField} {form} extension={plugin.extension} />
|
|
166
|
+
{/key}
|
|
167
|
+
{/key}
|
|
168
|
+
{:else}
|
|
169
|
+
<RenderField field={presentedField} {form} {authoring} />
|
|
170
|
+
{/if}
|
|
171
|
+
<LiveValidationFeedback
|
|
172
|
+
feedback={form.liveValidation.forField(field.path)}
|
|
173
|
+
i18n={runtime.i18n}
|
|
174
|
+
path={field.path}
|
|
175
|
+
/>
|
|
137
176
|
{#if inheritedFrom !== undefined}<p
|
|
138
177
|
class="mt-1 font-mono text-[10px] text-foreground-faint"
|
|
139
178
|
data-localization-source={inheritedFrom}
|
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
sourceID !== undefined && field.join?.allowCreate !== false && targetOperations?.create === true
|
|
159
159
|
);
|
|
160
160
|
const canManage = $derived(!field.admin.readOnly && (canAttach || canCreate));
|
|
161
|
+
const editingBlocked = $derived(form.editingBlocked);
|
|
161
162
|
let browserOpen = $state(false);
|
|
162
163
|
let pending = $state(false);
|
|
163
164
|
let mutationError = $state<string>();
|
|
@@ -179,6 +180,7 @@
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
async function commit(ids: string[]) {
|
|
183
|
+
if (editingBlocked) return false;
|
|
182
184
|
const source = form.resource;
|
|
183
185
|
if (
|
|
184
186
|
source?.id === undefined ||
|
|
@@ -270,7 +272,12 @@
|
|
|
270
272
|
})}
|
|
271
273
|
</p>
|
|
272
274
|
{#if canManage}
|
|
273
|
-
<Button
|
|
275
|
+
<Button
|
|
276
|
+
variant="outline"
|
|
277
|
+
size="sm"
|
|
278
|
+
disabled={pending || editingBlocked}
|
|
279
|
+
onclick={() => (browserOpen = true)}
|
|
280
|
+
>
|
|
274
281
|
<ListPlusIcon class="size-3" />
|
|
275
282
|
{runtime.i18n.t(pending ? "fields:updating" : "fields:manageRelationships")}
|
|
276
283
|
</Button>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AdminI18n, FieldLiveValidation } from "@riducms/plugin";
|
|
3
|
+
import { Button } from "@riducms/ui";
|
|
4
|
+
let { feedback, i18n, path }: { feedback: FieldLiveValidation; i18n: AdminI18n; path: string } =
|
|
5
|
+
$props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div data-live-validation={feedback.status} data-live-validation-path={path} aria-live="polite">
|
|
9
|
+
{#if feedback.status === "pending"}
|
|
10
|
+
<p class="mt-1 text-xs text-muted-foreground">{i18n.t("fields:liveValidationPending")}</p>
|
|
11
|
+
{:else if feedback.status === "failed"}
|
|
12
|
+
<div class="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
|
|
13
|
+
<p>{i18n.t("fields:liveValidationFailed")}</p>
|
|
14
|
+
<Button variant="ghost" size="sm" onclick={feedback.retry}>
|
|
15
|
+
{i18n.t("fields:liveValidationRetry")}
|
|
16
|
+
</Button>
|
|
17
|
+
</div>
|
|
18
|
+
{:else if feedback.status === "skipped"}
|
|
19
|
+
<p class="mt-1 text-xs text-muted-foreground">{i18n.t("fields:liveValidationSkipped")}</p>
|
|
20
|
+
{/if}
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, type Component } from "svelte";
|
|
3
|
+
import type { SchemaField } from "@riducms/protocol";
|
|
4
|
+
import type { FieldAuthoringHost, AdminI18n } from "@riducms/plugin";
|
|
5
|
+
import type { FieldEditorProps, RegisteredFieldEditor } from "@riducms/plugin/editor";
|
|
6
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
7
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
8
|
+
import { FieldEditorBinding } from "@admin/core/forms/field-editor-binding";
|
|
9
|
+
import { guardEditorAuthoring } from "@admin/core/forms/field-editor-authoring";
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
schema,
|
|
13
|
+
form,
|
|
14
|
+
editor,
|
|
15
|
+
authoring,
|
|
16
|
+
i18n,
|
|
17
|
+
}: {
|
|
18
|
+
schema: SchemaField;
|
|
19
|
+
form: FormController;
|
|
20
|
+
editor: RegisteredFieldEditor;
|
|
21
|
+
authoring: FieldAuthoringHost;
|
|
22
|
+
i18n: AdminI18n;
|
|
23
|
+
} = $props();
|
|
24
|
+
const runtime = getAdminRuntime();
|
|
25
|
+
// Parent keys this host by form, lifetime epoch, manifest revision and occurrence ID.
|
|
26
|
+
// Each capability belongs to this mount; reordering only updates the schema getter.
|
|
27
|
+
// svelte-ignore state_referenced_locally
|
|
28
|
+
const config = editor.decode(schema);
|
|
29
|
+
// Registration correlates props; decode above validates this manifest selection.
|
|
30
|
+
// Keep this erasure at the host boundary so registrations cannot retype components.
|
|
31
|
+
// svelte-ignore state_referenced_locally
|
|
32
|
+
const Editor = editor.component as Component<
|
|
33
|
+
Omit<FieldEditorProps<RegisteredFieldEditor["type"], unknown>, "config"> & { config?: unknown }
|
|
34
|
+
>;
|
|
35
|
+
// svelte-ignore state_referenced_locally
|
|
36
|
+
const field = new FieldEditorBinding(
|
|
37
|
+
form,
|
|
38
|
+
() => schema,
|
|
39
|
+
editor.type,
|
|
40
|
+
() => runtime.manifestRevision
|
|
41
|
+
);
|
|
42
|
+
const guardedAuthoring = guardEditorAuthoring(() => authoring, field.assertActive);
|
|
43
|
+
onDestroy(field.destroy);
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<Editor
|
|
47
|
+
{field}
|
|
48
|
+
{...config === undefined ? {} : { config }}
|
|
49
|
+
form={field.form}
|
|
50
|
+
authoring={guardedAuthoring}
|
|
51
|
+
{i18n}
|
|
52
|
+
/>
|