@riducms/admin 0.1.4 → 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,5 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
3
|
+
|
|
2
4
|
import type { SchemaField } from "@riducms/protocol";
|
|
5
|
+
import { fieldControlARIA } from "@riducms/ui";
|
|
3
6
|
import { DragDropProvider, type DragDropEvents } from "@dnd-kit-svelte/svelte";
|
|
4
7
|
import ChevronDownIcon from "~icons/lucide/chevron-down";
|
|
5
8
|
import CirclePlusIcon from "~icons/lucide/circle-plus";
|
|
@@ -28,10 +31,12 @@
|
|
|
28
31
|
import { initialFormValues } from "@admin/core/forms/form-schema";
|
|
29
32
|
import { immutableRowLabelSnapshot } from "@admin/core/plugins/row-label-registry";
|
|
30
33
|
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
34
|
+
import { focusFieldIssue, fieldIssueRevealEvent } from "@admin/core/forms/field-issue-focus";
|
|
31
35
|
import FieldLayout from "@admin/fields/field-layout.svelte";
|
|
32
36
|
import FieldMessages from "@admin/fields/field-messages.svelte";
|
|
33
37
|
import {
|
|
34
38
|
compatibleClipboardValue,
|
|
39
|
+
cloneFieldForPaste,
|
|
35
40
|
createFieldClipboardPayload,
|
|
36
41
|
readFieldClipboard,
|
|
37
42
|
writeFieldClipboard,
|
|
@@ -44,18 +49,32 @@
|
|
|
44
49
|
const customRowLabel = $derived(runtime.rowLabels.resolve(field));
|
|
45
50
|
const rows = $derived((form.get(field.path) as Record<string, unknown>[] | undefined) ?? []);
|
|
46
51
|
const issues = $derived(form.issuesFor(field.path).filter((issue) => issue.path === field.path));
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
const controlARIA = $derived(
|
|
53
|
+
fieldControlARIA(field.id, field.admin.description !== undefined, issues.length > 0)
|
|
54
|
+
);
|
|
55
|
+
const minRows = $derived(
|
|
56
|
+
(field.type === "blocks" ? field.blocks?.minRows : field.nested?.minRows) ?? 0
|
|
57
|
+
);
|
|
58
|
+
const maxRows = $derived(
|
|
59
|
+
(field.type === "blocks" ? field.blocks?.maxRows : field.nested?.maxRows) ?? 0
|
|
60
|
+
);
|
|
61
|
+
const unknownRows = $derived(
|
|
62
|
+
field.type === "blocks" &&
|
|
63
|
+
rows.some(
|
|
64
|
+
(row) => !resolveBlockTypes(field.blocks).some((block) => block.slug === row.blockType)
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
const readOnly = $derived(field.admin.readOnly === true || unknownRows);
|
|
68
|
+
const editingBlocked = $derived(readOnly || form.editingBlocked);
|
|
69
|
+
const canAdd = $derived(!editingBlocked && (maxRows === 0 || rows.length < maxRows));
|
|
52
70
|
let collapsed = $state(new Set<string>());
|
|
53
71
|
let clipboardMessage = $state("");
|
|
54
72
|
let blockPickerOpen = $state(false);
|
|
55
73
|
let blockQuery = $state("");
|
|
74
|
+
let insertAfter = $state<string>();
|
|
56
75
|
const visibleBlockTypes = $derived(
|
|
57
|
-
(field.blocks
|
|
58
|
-
block.
|
|
76
|
+
(resolveBlockTypes(field.blocks) ?? []).filter((block) =>
|
|
77
|
+
block.labels.singular
|
|
59
78
|
.toLocaleLowerCase(runtime.i18n.language)
|
|
60
79
|
.includes(blockQuery.trim().toLocaleLowerCase(runtime.i18n.language))
|
|
61
80
|
)
|
|
@@ -63,7 +82,10 @@
|
|
|
63
82
|
|
|
64
83
|
function changeBlockPickerOpen(open: boolean) {
|
|
65
84
|
blockPickerOpen = open;
|
|
66
|
-
if (!open)
|
|
85
|
+
if (!open) {
|
|
86
|
+
blockQuery = "";
|
|
87
|
+
insertAfter = undefined;
|
|
88
|
+
}
|
|
67
89
|
}
|
|
68
90
|
|
|
69
91
|
function rowField(child: SchemaField, index: number, rowKey: string) {
|
|
@@ -79,7 +101,7 @@
|
|
|
79
101
|
function createRow(blockType?: string) {
|
|
80
102
|
const children =
|
|
81
103
|
field.type === "blocks"
|
|
82
|
-
? (field.blocks
|
|
104
|
+
? (resolveBlockTypes(field.blocks).find((block) => block.slug === blockType)?.fields ?? [])
|
|
83
105
|
: (field.nested?.fields ?? []);
|
|
84
106
|
return {
|
|
85
107
|
...initialFormValues(children),
|
|
@@ -89,25 +111,32 @@
|
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
function add(blockType?: string) {
|
|
92
|
-
if (
|
|
114
|
+
if (editingBlocked || !canAdd) return;
|
|
93
115
|
if (field.type === "blocks" && blockType === undefined) {
|
|
94
116
|
blockPickerOpen = true;
|
|
95
117
|
return;
|
|
96
118
|
}
|
|
97
|
-
|
|
119
|
+
const next = [...rows];
|
|
120
|
+
const index =
|
|
121
|
+
insertAfter === undefined
|
|
122
|
+
? rows.length
|
|
123
|
+
: rows.findIndex((row) => row._key === insertAfter) + 1;
|
|
124
|
+
next.splice(index, 0, createRow(blockType));
|
|
125
|
+
updateRows(next);
|
|
126
|
+
insertAfter = undefined;
|
|
98
127
|
blockPickerOpen = false;
|
|
99
128
|
blockQuery = "";
|
|
100
129
|
}
|
|
101
130
|
|
|
102
131
|
function addBelow(index: number) {
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
if (editingBlocked || !canAdd) return;
|
|
133
|
+
if (field.type === "blocks") {
|
|
134
|
+
insertAfter = String(rows[index]?._key);
|
|
135
|
+
blockPickerOpen = true;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
109
138
|
const next = [...rows];
|
|
110
|
-
next.splice(index + 1, 0,
|
|
139
|
+
next.splice(index + 1, 0, createRow());
|
|
111
140
|
updateRows(next);
|
|
112
141
|
}
|
|
113
142
|
|
|
@@ -117,7 +146,7 @@
|
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
async function pasteField() {
|
|
120
|
-
if (
|
|
149
|
+
if (editingBlocked) return;
|
|
121
150
|
const value = compatibleClipboardValue(await readFieldClipboard(), field, "field");
|
|
122
151
|
if (value === undefined) {
|
|
123
152
|
clipboardMessage = runtime.i18n.t("errors:clipboardField");
|
|
@@ -162,7 +191,7 @@
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
async function pasteRow(index: number) {
|
|
165
|
-
if (
|
|
194
|
+
if (editingBlocked || !canAdd) return;
|
|
166
195
|
const value = compatibleClipboardValue(await readFieldClipboard(), field, "row");
|
|
167
196
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
168
197
|
clipboardMessage = runtime.i18n.t("errors:clipboardRow");
|
|
@@ -172,7 +201,7 @@
|
|
|
172
201
|
row._key = crypto.randomUUID();
|
|
173
202
|
if (
|
|
174
203
|
field.type === "blocks" &&
|
|
175
|
-
!field.blocks
|
|
204
|
+
!resolveBlockTypes(field.blocks).some((block) => block.slug === row.blockType)
|
|
176
205
|
) {
|
|
177
206
|
clipboardMessage = runtime.i18n.t("errors:clipboardBlockType");
|
|
178
207
|
return;
|
|
@@ -184,22 +213,24 @@
|
|
|
184
213
|
}
|
|
185
214
|
|
|
186
215
|
function updateRows(next: Record<string, unknown>[]) {
|
|
187
|
-
if (
|
|
188
|
-
form.
|
|
189
|
-
form.set(field.path, next);
|
|
216
|
+
if (editingBlocked) return;
|
|
217
|
+
form.setRows(field.path, next);
|
|
190
218
|
}
|
|
191
219
|
|
|
192
220
|
function duplicate(index: number) {
|
|
193
|
-
if (
|
|
194
|
-
const copy =
|
|
195
|
-
copy._key = crypto.randomUUID();
|
|
221
|
+
if (editingBlocked || !canAdd) return;
|
|
222
|
+
const copy = cloneFieldForPaste(field, rows[index], "row") as Record<string, unknown>;
|
|
196
223
|
const next = [...rows];
|
|
197
224
|
next.splice(index + 1, 0, copy);
|
|
198
225
|
updateRows(next);
|
|
199
226
|
}
|
|
200
227
|
|
|
201
228
|
function rowLabel(row: Record<string, unknown>, index: number) {
|
|
202
|
-
const key =
|
|
229
|
+
const key =
|
|
230
|
+
field.type === "blocks"
|
|
231
|
+
? resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.admin
|
|
232
|
+
?.rowLabel
|
|
233
|
+
: field.nested?.rowLabel;
|
|
203
234
|
const configured = key === undefined ? undefined : row[key];
|
|
204
235
|
if (configured !== undefined && configured !== null && String(configured).trim() !== "") {
|
|
205
236
|
return String(configured);
|
|
@@ -215,7 +246,8 @@
|
|
|
215
246
|
|
|
216
247
|
function visibleRowLabel(row: Record<string, unknown>, index: number) {
|
|
217
248
|
if (field.type !== "blocks") return rowLabel(row, index);
|
|
218
|
-
const key = field.
|
|
249
|
+
const key = resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.admin
|
|
250
|
+
?.rowLabel;
|
|
219
251
|
const configured = key === undefined ? undefined : row[key];
|
|
220
252
|
return configured === undefined || configured === null || String(configured).trim() === ""
|
|
221
253
|
? runtime.i18n.t("fields:untitled", {
|
|
@@ -226,7 +258,20 @@
|
|
|
226
258
|
|
|
227
259
|
function blockLabel(row: Record<string, unknown>) {
|
|
228
260
|
if (field.type !== "blocks") return undefined;
|
|
229
|
-
return
|
|
261
|
+
return (
|
|
262
|
+
resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.labels
|
|
263
|
+
.singular ?? (typeof row.blockType === "string" ? row.blockType : undefined)
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function revealRow(key: string) {
|
|
268
|
+
return (element: HTMLElement) => {
|
|
269
|
+
const reveal = () => {
|
|
270
|
+
if (collapsed.has(key)) toggleCollapsed(key);
|
|
271
|
+
};
|
|
272
|
+
element.addEventListener(fieldIssueRevealEvent, reveal);
|
|
273
|
+
return () => element.removeEventListener(fieldIssueRevealEvent, reveal);
|
|
274
|
+
};
|
|
230
275
|
}
|
|
231
276
|
|
|
232
277
|
function toggleCollapsed(key: string) {
|
|
@@ -245,7 +290,9 @@
|
|
|
245
290
|
|
|
246
291
|
function fieldsFor(row: Record<string, unknown>) {
|
|
247
292
|
if (field.type !== "blocks") return field.nested?.fields ?? [];
|
|
248
|
-
return
|
|
293
|
+
return (
|
|
294
|
+
resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)?.fields ?? []
|
|
295
|
+
);
|
|
249
296
|
}
|
|
250
297
|
|
|
251
298
|
function fieldsForRow(row: Record<string, unknown>, index: number) {
|
|
@@ -254,7 +301,7 @@
|
|
|
254
301
|
}
|
|
255
302
|
|
|
256
303
|
function reorder(event: Parameters<DragDropEvents["dragend"]>[0]) {
|
|
257
|
-
if (
|
|
304
|
+
if (editingBlocked) return;
|
|
258
305
|
const source = String(event.operation.source?.id ?? "");
|
|
259
306
|
const target = String(event.operation.target?.id ?? "");
|
|
260
307
|
const from = rows.findIndex((row) => row._key === source);
|
|
@@ -279,7 +326,7 @@
|
|
|
279
326
|
<DropdownMenuItem onSelect={copyField}>
|
|
280
327
|
{runtime.i18n.t("fields:copyField")}
|
|
281
328
|
</DropdownMenuItem>
|
|
282
|
-
<DropdownMenuItem disabled={
|
|
329
|
+
<DropdownMenuItem disabled={editingBlocked} onSelect={pasteField}>
|
|
283
330
|
{runtime.i18n.t("fields:pasteField")}
|
|
284
331
|
</DropdownMenuItem>
|
|
285
332
|
</DropdownMenuContent>
|
|
@@ -296,25 +343,26 @@
|
|
|
296
343
|
data-field-path={field.path}
|
|
297
344
|
data-invalid={issues.length > 0}
|
|
298
345
|
aria-labelledby={`${field.id}-label`}
|
|
299
|
-
|
|
346
|
+
{...controlARIA}
|
|
300
347
|
>
|
|
301
|
-
<
|
|
302
|
-
<div class="
|
|
303
|
-
<
|
|
304
|
-
{field.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
348
|
+
<FieldMessages controlID={field.id} {issues} description={field.admin.description}>
|
|
349
|
+
<div class="flex items-start justify-between gap-3 border-b border-control-border pb-2.5">
|
|
350
|
+
<div class="min-w-0">
|
|
351
|
+
<h2 id={`${field.id}-label`} class="text-[17px] font-medium text-foreground">
|
|
352
|
+
{field.admin.label}{#if field.required}<span
|
|
353
|
+
class="ridu-field-required"
|
|
354
|
+
aria-hidden="true"
|
|
355
|
+
>
|
|
356
|
+
*
|
|
357
|
+
</span>{/if}
|
|
358
|
+
</h2>
|
|
359
|
+
{#if field.admin.readOnly}<span class="ridu-field-status mt-1">
|
|
360
|
+
{runtime.i18n.t("fields:readOnly")}
|
|
309
361
|
</span>{/if}
|
|
310
|
-
</
|
|
311
|
-
{
|
|
312
|
-
{runtime.i18n.t("fields:readOnly")}
|
|
313
|
-
</span>{/if}
|
|
362
|
+
</div>
|
|
363
|
+
{@render fieldActions()}
|
|
314
364
|
</div>
|
|
315
|
-
|
|
316
|
-
</div>
|
|
317
|
-
<FieldMessages id="{field.id}-message" {issues} description={field.admin.description} />
|
|
365
|
+
</FieldMessages>
|
|
318
366
|
<FieldLayout fields={inheritedFields} {form} />
|
|
319
367
|
{#if clipboardMessage !== ""}<p
|
|
320
368
|
class="font-mono text-[10px] text-foreground-faint"
|
|
@@ -329,42 +377,47 @@
|
|
|
329
377
|
data-field-path={field.path}
|
|
330
378
|
data-invalid={issues.length > 0}
|
|
331
379
|
aria-labelledby={`${field.id}-label`}
|
|
332
|
-
|
|
380
|
+
{...controlARIA}
|
|
333
381
|
>
|
|
334
|
-
<
|
|
335
|
-
<div class="
|
|
336
|
-
<
|
|
337
|
-
{field.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
382
|
+
<FieldMessages controlID={field.id} {issues} description={field.admin.description}>
|
|
383
|
+
<div class="flex flex-wrap items-end justify-between gap-3">
|
|
384
|
+
<div class="min-w-0">
|
|
385
|
+
<h2 id={`${field.id}-label`} class="text-[19px] leading-7 font-medium text-foreground">
|
|
386
|
+
{field.admin.label}{#if field.required}<span
|
|
387
|
+
class="ridu-field-required"
|
|
388
|
+
aria-hidden="true"
|
|
389
|
+
>
|
|
390
|
+
*
|
|
391
|
+
</span>{/if}
|
|
392
|
+
</h2>
|
|
393
|
+
{#if field.admin.readOnly}<span class="ridu-field-status mt-1">
|
|
394
|
+
{runtime.i18n.t("fields:readOnly")}
|
|
342
395
|
</span>{/if}
|
|
343
|
-
</
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
</Button>
|
|
356
|
-
{/if}
|
|
357
|
-
{@render fieldActions()}
|
|
396
|
+
</div>
|
|
397
|
+
<div class="flex items-center gap-1">
|
|
398
|
+
{#if rows.length > 0}
|
|
399
|
+
<Button size="xs" variant="ghost" class="px-1.5" onclick={collapseAll}>
|
|
400
|
+
{runtime.i18n.t("fields:collapseAll")}
|
|
401
|
+
</Button>
|
|
402
|
+
<Button size="xs" variant="ghost" class="px-1.5" onclick={showAll}>
|
|
403
|
+
{runtime.i18n.t("fields:showAll")}
|
|
404
|
+
</Button>
|
|
405
|
+
{/if}
|
|
406
|
+
{@render fieldActions()}
|
|
407
|
+
</div>
|
|
358
408
|
</div>
|
|
359
|
-
</
|
|
360
|
-
<
|
|
409
|
+
</FieldMessages>
|
|
410
|
+
{#if unknownRows}<p role="alert" class="text-sm text-destructive">
|
|
411
|
+
{runtime.i18n.t("errors:unknownBlockRecovery")}
|
|
412
|
+
</p>{/if}
|
|
361
413
|
<DragDropProvider onDragEnd={reorder}>
|
|
362
414
|
<div class="grid gap-3">
|
|
363
|
-
{#each rows as row, index (
|
|
415
|
+
{#each rows as row, index (form.rowMountKey(row))}
|
|
364
416
|
{const rowKey = String(row._key)}
|
|
365
417
|
{const rowCollapsed = $derived(collapsed.has(rowKey))}
|
|
418
|
+
{const rowIssues = $derived(form.issuesFor(`${field.path}.${index}`))}
|
|
366
419
|
{const rowSnapshot = $derived(immutableRowLabelSnapshot(row))}
|
|
367
|
-
<SortableRow id={String(row._key)} {index} disabled={
|
|
420
|
+
<SortableRow id={String(row._key)} {index} disabled={editingBlocked}>
|
|
368
421
|
{#snippet children(sortable)}
|
|
369
422
|
<div class="flex min-h-10 items-center justify-between gap-2 bg-control px-3 py-2">
|
|
370
423
|
<Button
|
|
@@ -374,7 +427,7 @@
|
|
|
374
427
|
aria-label={runtime.i18n.t("fields:drag", {
|
|
375
428
|
label: rowLabel(row, index),
|
|
376
429
|
})}
|
|
377
|
-
disabled={
|
|
430
|
+
disabled={editingBlocked}
|
|
378
431
|
{@attach sortable.handleRef}
|
|
379
432
|
>
|
|
380
433
|
<GripVerticalIcon class="size-3.5 text-foreground-faint" />
|
|
@@ -403,7 +456,9 @@
|
|
|
403
456
|
row={rowSnapshot}
|
|
404
457
|
rowNumber={index + 1}
|
|
405
458
|
i18n={runtime.i18n}
|
|
406
|
-
|
|
459
|
+
{...customRowLabel.config === undefined
|
|
460
|
+
? {}
|
|
461
|
+
: { config: customRowLabel.config }}
|
|
407
462
|
/>
|
|
408
463
|
{/if}
|
|
409
464
|
</div>
|
|
@@ -437,7 +492,7 @@
|
|
|
437
492
|
</DropdownMenuTrigger>
|
|
438
493
|
<DropdownMenuContent>
|
|
439
494
|
<DropdownMenuItem
|
|
440
|
-
disabled={
|
|
495
|
+
disabled={editingBlocked || index === 0}
|
|
441
496
|
onSelect={() => {
|
|
442
497
|
const next = [...rows];
|
|
443
498
|
[next[index - 1], next[index]] = [next[index], next[index - 1]];
|
|
@@ -447,7 +502,7 @@
|
|
|
447
502
|
{runtime.i18n.t("fields:moveUp")}
|
|
448
503
|
</DropdownMenuItem>
|
|
449
504
|
<DropdownMenuItem
|
|
450
|
-
disabled={
|
|
505
|
+
disabled={editingBlocked || index === rows.length - 1}
|
|
451
506
|
onSelect={() => {
|
|
452
507
|
const next = [...rows];
|
|
453
508
|
[next[index], next[index + 1]] = [next[index + 1], next[index]];
|
|
@@ -457,7 +512,7 @@
|
|
|
457
512
|
{runtime.i18n.t("fields:moveDown")}
|
|
458
513
|
</DropdownMenuItem>
|
|
459
514
|
<DropdownMenuItem
|
|
460
|
-
disabled={
|
|
515
|
+
disabled={editingBlocked || !canAdd}
|
|
461
516
|
onSelect={() => addBelow(index)}
|
|
462
517
|
>
|
|
463
518
|
{runtime.i18n.t("fields:addBelow")}
|
|
@@ -467,13 +522,13 @@
|
|
|
467
522
|
{runtime.i18n.t("fields:copyRow")}
|
|
468
523
|
</DropdownMenuItem>
|
|
469
524
|
<DropdownMenuItem
|
|
470
|
-
disabled={
|
|
525
|
+
disabled={editingBlocked || !canAdd}
|
|
471
526
|
onSelect={() => pasteRow(index)}
|
|
472
527
|
>
|
|
473
528
|
{runtime.i18n.t("fields:pasteRow")}
|
|
474
529
|
</DropdownMenuItem>
|
|
475
530
|
<DropdownMenuItem
|
|
476
|
-
disabled={
|
|
531
|
+
disabled={editingBlocked || !canAdd}
|
|
477
532
|
onSelect={() => duplicate(index)}
|
|
478
533
|
>
|
|
479
534
|
{runtime.i18n.t("fields:duplicate")}
|
|
@@ -481,7 +536,8 @@
|
|
|
481
536
|
<DropdownMenuSeparator />
|
|
482
537
|
<DropdownMenuItem
|
|
483
538
|
class="text-destructive data-[highlighted]:text-destructive"
|
|
484
|
-
disabled={
|
|
539
|
+
disabled={editingBlocked ||
|
|
540
|
+
rows.length <= Math.max(minRows, field.required ? 1 : 0)}
|
|
485
541
|
onSelect={() =>
|
|
486
542
|
updateRows(rows.filter((_, rowIndex) => rowIndex !== index))}
|
|
487
543
|
>
|
|
@@ -491,7 +547,19 @@
|
|
|
491
547
|
</DropdownMenu>
|
|
492
548
|
</div>
|
|
493
549
|
</div>
|
|
550
|
+
{#if rowIssues.length > 0}
|
|
551
|
+
<button
|
|
552
|
+
type="button"
|
|
553
|
+
class="px-4 py-2 text-start text-sm text-destructive underline"
|
|
554
|
+
onclick={() => focusFieldIssue(rowIssues[0].path)}
|
|
555
|
+
>
|
|
556
|
+
{runtime.i18n.t("fields:rowErrors", { count: rowIssues.length })}: {rowIssues[0]
|
|
557
|
+
.message}
|
|
558
|
+
</button>
|
|
559
|
+
{/if}
|
|
494
560
|
<div
|
|
561
|
+
{@attach revealRow(rowKey)}
|
|
562
|
+
data-field-path={`${field.path}.${index}`}
|
|
495
563
|
id={`${field.id}-${rowKey}-content`}
|
|
496
564
|
class={[
|
|
497
565
|
"grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none",
|
|
@@ -504,7 +572,7 @@
|
|
|
504
572
|
>
|
|
505
573
|
<div class="min-h-0 overflow-hidden">
|
|
506
574
|
<div class="grid gap-5 p-4">
|
|
507
|
-
<FieldLayout fields={fieldsForRow(row, index)} {form} />
|
|
575
|
+
{#if !rowCollapsed}<FieldLayout fields={fieldsForRow(row, index)} {form} />{/if}
|
|
508
576
|
</div>
|
|
509
577
|
</div>
|
|
510
578
|
</div>
|
|
@@ -517,7 +585,7 @@
|
|
|
517
585
|
variant="ghost"
|
|
518
586
|
size="sm"
|
|
519
587
|
class="w-fit px-0 text-foreground-faint hover:bg-transparent hover:text-foreground"
|
|
520
|
-
disabled={
|
|
588
|
+
disabled={editingBlocked || !canAdd}
|
|
521
589
|
onclick={() => add()}
|
|
522
590
|
>
|
|
523
591
|
<CirclePlusIcon class="size-5" />
|
|
@@ -527,7 +595,7 @@
|
|
|
527
595
|
? runtime.i18n.t("fields:addRow")
|
|
528
596
|
: runtime.i18n.t("fields:add", { label: field.nested.rowLabels.singular })}
|
|
529
597
|
</Button>
|
|
530
|
-
{#if
|
|
598
|
+
{#if minRows > 0 || maxRows > 0}
|
|
531
599
|
<p class="font-mono text-[10px] text-foreground-faint">
|
|
532
600
|
{runtime.i18n.t("fields:rowLimits", {
|
|
533
601
|
count: rows.length,
|
|
@@ -579,11 +647,11 @@
|
|
|
579
647
|
</p>
|
|
580
648
|
{:else}
|
|
581
649
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
582
|
-
{#each visibleBlockTypes as block (block.
|
|
650
|
+
{#each visibleBlockTypes as block (block.slug)}
|
|
583
651
|
<button
|
|
584
652
|
type="button"
|
|
585
653
|
class="group overflow-hidden rounded-[3px] border border-control-border bg-background text-start transition-colors hover:border-control-border-hover focus-visible:border-ring focus-visible:outline-none"
|
|
586
|
-
onclick={() => add(block.
|
|
654
|
+
onclick={() => add(block.slug)}
|
|
587
655
|
>
|
|
588
656
|
<span
|
|
589
657
|
class="flex aspect-video items-center justify-center border-b border-control-border bg-control text-foreground-faint transition-colors group-hover:bg-control-hover group-hover:text-foreground-muted"
|
|
@@ -591,7 +659,7 @@
|
|
|
591
659
|
<LayoutTemplateIcon class="size-8" />
|
|
592
660
|
</span>
|
|
593
661
|
<span class="block px-3 py-2.5 text-[13px] font-medium text-foreground">
|
|
594
|
-
{block.
|
|
662
|
+
{block.labels.singular}
|
|
595
663
|
</span>
|
|
596
664
|
</button>
|
|
597
665
|
{/each}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapBlockTypes } from "@riducms/protocol";
|
|
1
2
|
import type { SchemaField } from "@riducms/protocol";
|
|
2
3
|
|
|
3
4
|
const canonicalAccessPath = Symbol.for("@riducms/admin/canonicalAccessPath");
|
|
@@ -49,15 +50,12 @@ function scopeField(
|
|
|
49
50
|
...(field.blocks === undefined
|
|
50
51
|
? {}
|
|
51
52
|
: {
|
|
52
|
-
blocks: {
|
|
53
|
-
...
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
),
|
|
59
|
-
})),
|
|
60
|
-
},
|
|
53
|
+
blocks: mapBlockTypes(field.blocks, (block) => ({
|
|
54
|
+
...block,
|
|
55
|
+
fields: block.fields.map((child) =>
|
|
56
|
+
scopeField(child, `${path}.${child.name}`, instance, readOnly)
|
|
57
|
+
),
|
|
58
|
+
})),
|
|
61
59
|
}),
|
|
62
60
|
};
|
|
63
61
|
return scoped;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy, type Component } from "svelte";
|
|
3
|
+
import type {
|
|
4
|
+
EmbeddedSchemaDraft,
|
|
5
|
+
EmbeddedSchemaDraftEditorProps,
|
|
6
|
+
EmbeddedSchemaFormScope,
|
|
7
|
+
FieldAuthoringHost,
|
|
8
|
+
PluginFieldProps,
|
|
9
|
+
ResolvedPluginField,
|
|
10
|
+
} from "@riducms/plugin";
|
|
11
|
+
import type { SchemaField, FieldType } from "@riducms/protocol";
|
|
12
|
+
import type { FormController } from "@admin/core/forms/form-controller.svelte";
|
|
13
|
+
import { PluginFieldBinding } from "@admin/core/forms/plugin-field-binding";
|
|
14
|
+
import { guardPluginAuthoring } from "@admin/core/forms/plugin-field-authoring";
|
|
15
|
+
import { getAdminRuntime } from "@admin/core/runtime/admin-runtime.svelte";
|
|
16
|
+
import { localizeSchemaCollection } from "@admin/core/i18n/localized-schema";
|
|
17
|
+
import { embeddedOccurrences } from "@admin/core/forms/embedded-fields";
|
|
18
|
+
import {
|
|
19
|
+
createEmbeddedSchemaDraft,
|
|
20
|
+
copyEmbeddedSchemaPayload,
|
|
21
|
+
type HostedSchemaDraft,
|
|
22
|
+
} from "@admin/core/forms/embedded-schema-draft.svelte";
|
|
23
|
+
import EmbeddedSchemaFields from "@admin/fields/embedded-schema-fields.svelte";
|
|
24
|
+
import EmbeddedSchemaDraftEditor from "@admin/fields/embedded-schema-draft-editor.svelte";
|
|
25
|
+
import ReferenceBrowser from "@admin/features/reference-browser/reference-browser-loader.svelte";
|
|
26
|
+
let {
|
|
27
|
+
schema,
|
|
28
|
+
form,
|
|
29
|
+
extension,
|
|
30
|
+
}: { schema: SchemaField; form: FormController; extension: ResolvedPluginField } = $props();
|
|
31
|
+
const runtime = getAdminRuntime();
|
|
32
|
+
// This host is keyed by form, baseline, manifest and logical field identity.
|
|
33
|
+
// svelte-ignore state_referenced_locally
|
|
34
|
+
const config = extension.registration.decodeConfig(schema);
|
|
35
|
+
// svelte-ignore state_referenced_locally
|
|
36
|
+
const field = new PluginFieldBinding(
|
|
37
|
+
form,
|
|
38
|
+
() => schema,
|
|
39
|
+
extension.registration,
|
|
40
|
+
() => runtime.manifestRevision
|
|
41
|
+
);
|
|
42
|
+
// Registration validates correlation; component erasure is recovered only at this render boundary.
|
|
43
|
+
// svelte-ignore state_referenced_locally
|
|
44
|
+
const Editor = extension.registration.component as Component<
|
|
45
|
+
Omit<PluginFieldProps<unknown, unknown, FieldType>, "config"> & { config?: unknown }
|
|
46
|
+
>;
|
|
47
|
+
onDestroy(field.destroy);
|
|
48
|
+
const drafts = new Map<EmbeddedSchemaDraft, HostedSchemaDraft>();
|
|
49
|
+
const draftAliases = new WeakMap<EmbeddedSchemaDraft, EmbeddedSchemaDraft>();
|
|
50
|
+
onDestroy(() => {
|
|
51
|
+
for (const draft of drafts.keys()) draft.discard();
|
|
52
|
+
});
|
|
53
|
+
const authoring: FieldAuthoringHost = {
|
|
54
|
+
schemaForm,
|
|
55
|
+
schemaDraftEditor,
|
|
56
|
+
beginSchemaDraft: (scope) => {
|
|
57
|
+
const session = createEmbeddedSchemaDraft(
|
|
58
|
+
form,
|
|
59
|
+
() => field.schema,
|
|
60
|
+
scope,
|
|
61
|
+
runtime.i18n,
|
|
62
|
+
() => !field.stale
|
|
63
|
+
);
|
|
64
|
+
const discard = session.draft.discard;
|
|
65
|
+
session.draft.discard = () => {
|
|
66
|
+
discard();
|
|
67
|
+
drafts.delete(session.draft);
|
|
68
|
+
};
|
|
69
|
+
drafts.set(session.draft, session);
|
|
70
|
+
return session.draft;
|
|
71
|
+
},
|
|
72
|
+
schemaIssues: (scope) => {
|
|
73
|
+
const occurrence = embeddedIndex.get(JSON.stringify([scope.treeKey, scope.identity]));
|
|
74
|
+
return occurrence === undefined ? [] : form.issuesFor(occurrence.path);
|
|
75
|
+
},
|
|
76
|
+
copySchemaPayload: (scope, payload) => copyEmbeddedSchemaPayload(field.schema, scope, payload),
|
|
77
|
+
get collections() {
|
|
78
|
+
return (
|
|
79
|
+
runtime.manifest?.collections.map((collection) =>
|
|
80
|
+
localizeSchemaCollection(collection, runtime.i18n)
|
|
81
|
+
) ?? []
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
get documentRevision() {
|
|
85
|
+
return runtime.documentRevision;
|
|
86
|
+
},
|
|
87
|
+
get locale() {
|
|
88
|
+
return form.contentLocale;
|
|
89
|
+
},
|
|
90
|
+
referenceBrowser: ReferenceBrowser,
|
|
91
|
+
findDocument: (collection, id, signal) =>
|
|
92
|
+
runtime.client.find(collection, id, { signal, locale: form.contentLocale }),
|
|
93
|
+
async requestPlugin<Result>(path: string, body: unknown, signal?: AbortSignal) {
|
|
94
|
+
const owner = extension.owner;
|
|
95
|
+
if (owner === undefined) {
|
|
96
|
+
throw new Error("This field renderer does not have an owning plugin");
|
|
97
|
+
}
|
|
98
|
+
return runtime.client.requestPlugin<Result>(owner, path, body, { signal });
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const embedded = $derived(embeddedOccurrences(field.schema, field.rawValue, field.schema.path));
|
|
102
|
+
const embeddedIndex = $derived(
|
|
103
|
+
new Map(
|
|
104
|
+
embedded.occurrences.map((occurrence) => [
|
|
105
|
+
JSON.stringify([occurrence.tree.key, occurrence.identity]),
|
|
106
|
+
occurrence,
|
|
107
|
+
])
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const guardedAuthoring = guardPluginAuthoring(authoring, field, (draft, raw) => {
|
|
112
|
+
draftAliases.set(draft, raw);
|
|
113
|
+
});
|
|
114
|
+
function draftOptions(options: EmbeddedSchemaDraftEditorProps) {
|
|
115
|
+
field.assertActive();
|
|
116
|
+
const session = drafts.get(draftAliases.get(options.draft) ?? options.draft);
|
|
117
|
+
if (!session) throw new Error("This draft belongs to another field host.");
|
|
118
|
+
return {
|
|
119
|
+
session,
|
|
120
|
+
options: {
|
|
121
|
+
...options,
|
|
122
|
+
draft: session.draft,
|
|
123
|
+
onApply: (payload: Record<string, unknown>) => {
|
|
124
|
+
field.assertEditable();
|
|
125
|
+
if (options.draft.stale) throw new Error("This embedded schema draft is stale.");
|
|
126
|
+
options.onApply(payload);
|
|
127
|
+
},
|
|
128
|
+
onCancel: () => {
|
|
129
|
+
field.assertActive();
|
|
130
|
+
options.onCancel();
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
{#snippet schemaForm(scope: EmbeddedSchemaFormScope)}
|
|
138
|
+
<EmbeddedSchemaFields
|
|
139
|
+
field={field.schema}
|
|
140
|
+
{form}
|
|
141
|
+
{scope}
|
|
142
|
+
occurrence={embeddedIndex.get(JSON.stringify([scope.treeKey, scope.identity]))}
|
|
143
|
+
issues={embedded.issues}
|
|
144
|
+
/>
|
|
145
|
+
{/snippet}
|
|
146
|
+
{#snippet schemaDraftEditor(options: EmbeddedSchemaDraftEditorProps)}
|
|
147
|
+
{const current = draftOptions(options)}
|
|
148
|
+
<EmbeddedSchemaDraftEditor session={current.session} options={current.options} />
|
|
149
|
+
{/snippet}
|
|
150
|
+
<Editor
|
|
151
|
+
{field}
|
|
152
|
+
{...config === undefined ? {} : { config }}
|
|
153
|
+
form={field.form}
|
|
154
|
+
authoring={guardedAuthoring}
|
|
155
|
+
i18n={runtime.i18n}
|
|
156
|
+
/>
|