@riducms/admin 0.1.5 → 0.2.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/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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
2
|
+
import type { SchemaField, ValidationIssue } from "@riducms/protocol";
|
|
3
|
+
import { embeddedOccurrences, isEmbeddedRecord } from "@admin/core/forms/embedded-fields";
|
|
4
|
+
|
|
5
|
+
export interface FieldValueLocation {
|
|
6
|
+
schema?: SchemaField;
|
|
7
|
+
path: string;
|
|
8
|
+
token: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Go and JavaScript may spell the same JSON string differently (notably U+2028/2029). */
|
|
13
|
+
export function canonicalIssueTarget(target: string): string | undefined {
|
|
14
|
+
try {
|
|
15
|
+
const segments: unknown = JSON.parse(target);
|
|
16
|
+
return Array.isArray(segments) && segments.every((segment) => typeof segment === "string")
|
|
17
|
+
? JSON.stringify(segments)
|
|
18
|
+
: undefined;
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Issues describe the submitted snapshot. Follow stable occurrences, never current indexes. */
|
|
25
|
+
export function correlateFormIssues(
|
|
26
|
+
fields: readonly SchemaField[],
|
|
27
|
+
before: Record<string, unknown>,
|
|
28
|
+
after: Record<string, unknown>,
|
|
29
|
+
issues: readonly ValidationIssue[]
|
|
30
|
+
) {
|
|
31
|
+
return correlate(indexFieldValues(fields, before), indexFieldValues(fields, after), issues);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function correlateEmbeddedIssues(
|
|
35
|
+
field: SchemaField,
|
|
36
|
+
before: unknown,
|
|
37
|
+
after: unknown,
|
|
38
|
+
issues: readonly ValidationIssue[]
|
|
39
|
+
) {
|
|
40
|
+
const relevant = issues.filter(
|
|
41
|
+
(issue) => issue.path === field.path || issue.path.startsWith(`${field.path}.`)
|
|
42
|
+
);
|
|
43
|
+
if (relevant.length === 0) return [];
|
|
44
|
+
return correlate(
|
|
45
|
+
indexFieldValues([field], { [field.name]: before }),
|
|
46
|
+
indexFieldValues([field], { [field.name]: after }),
|
|
47
|
+
relevant,
|
|
48
|
+
false
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function indexFieldValues(fields: readonly SchemaField[], values: Record<string, unknown>) {
|
|
53
|
+
const locations: FieldValueLocation[] = [];
|
|
54
|
+
const add = (path: string, token: readonly string[], value: unknown, schema?: SchemaField) =>
|
|
55
|
+
locations.push({
|
|
56
|
+
path,
|
|
57
|
+
token: JSON.stringify(token),
|
|
58
|
+
value,
|
|
59
|
+
...(schema === undefined ? {} : { schema }),
|
|
60
|
+
});
|
|
61
|
+
const record = (
|
|
62
|
+
children: readonly SchemaField[],
|
|
63
|
+
value: unknown,
|
|
64
|
+
path: string,
|
|
65
|
+
token: readonly string[]
|
|
66
|
+
) => {
|
|
67
|
+
if (!isEmbeddedRecord(value)) return;
|
|
68
|
+
for (const child of children)
|
|
69
|
+
visit(child, value[child.name], `${path}.${child.name}`, [...token, child.id]);
|
|
70
|
+
};
|
|
71
|
+
const visit = (field: SchemaField, value: unknown, path: string, token: readonly string[]) => {
|
|
72
|
+
add(path, token, value, field);
|
|
73
|
+
if (field.type === "group") record(field.nested?.fields ?? [], value, path, token);
|
|
74
|
+
if ((field.type === "array" || field.type === "blocks") && Array.isArray(value)) {
|
|
75
|
+
const keys = new Map<string, number>();
|
|
76
|
+
for (const row of value) {
|
|
77
|
+
if (isEmbeddedRecord(row) && typeof row._key === "string")
|
|
78
|
+
keys.set(row._key, (keys.get(row._key) ?? 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
for (const [i, row] of value.entries()) {
|
|
81
|
+
if (!isEmbeddedRecord(row) || typeof row._key !== "string" || keys.get(row._key) !== 1)
|
|
82
|
+
continue;
|
|
83
|
+
const block = resolveBlockTypes(field.blocks).find(
|
|
84
|
+
(candidate) => candidate.slug === row.blockType
|
|
85
|
+
);
|
|
86
|
+
const next = [...token, row._key, block?.slug ?? ""];
|
|
87
|
+
add(`${path}.${i}`, next, row);
|
|
88
|
+
record(block?.fields ?? field.nested?.fields ?? [], row, `${path}.${i}`, next);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (field.plugin?.embeddedTrees !== undefined) {
|
|
92
|
+
const result = embeddedOccurrences(field, value, path);
|
|
93
|
+
if (result.issues.length > 0) return;
|
|
94
|
+
for (const occurrence of result.occurrences) {
|
|
95
|
+
if (occurrence.identity === undefined) continue;
|
|
96
|
+
const next = [
|
|
97
|
+
...token,
|
|
98
|
+
occurrence.tree.key,
|
|
99
|
+
occurrence.case.tagValue,
|
|
100
|
+
occurrence.block.slug,
|
|
101
|
+
occurrence.identity,
|
|
102
|
+
];
|
|
103
|
+
add(occurrence.path, next, occurrence.payload);
|
|
104
|
+
record(occurrence.block.fields, occurrence.payload, occurrence.path, next);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
for (const field of fields) visit(field, values[field.name], field.path, [field.id]);
|
|
109
|
+
return locations;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function correlate(
|
|
113
|
+
before: FieldValueLocation[],
|
|
114
|
+
after: FieldValueLocation[],
|
|
115
|
+
issues: readonly ValidationIssue[],
|
|
116
|
+
useTargets = true
|
|
117
|
+
) {
|
|
118
|
+
const previous = new Map(before.map((location) => [location.path, location]));
|
|
119
|
+
const previousTargets = new Map(before.map((location) => [location.token, location]));
|
|
120
|
+
const current = new Map(after.map((location) => [location.token, location]));
|
|
121
|
+
return issues.flatMap((issue) => {
|
|
122
|
+
if (useTargets && issue.target !== undefined) {
|
|
123
|
+
// The server may transform/reorder its final candidate before validation. Its
|
|
124
|
+
// display path then describes that candidate, not our submitted snapshot.
|
|
125
|
+
const token = canonicalIssueTarget(issue.target);
|
|
126
|
+
if (token === undefined) return [];
|
|
127
|
+
const source = previousTargets.get(token);
|
|
128
|
+
const target = current.get(token);
|
|
129
|
+
if (!source || !target || JSON.stringify(source.value) !== JSON.stringify(target.value))
|
|
130
|
+
return [];
|
|
131
|
+
return [{ ...issue, path: target.path }];
|
|
132
|
+
}
|
|
133
|
+
let path = issue.path;
|
|
134
|
+
while (!previous.has(path) && path.includes(".")) path = path.slice(0, path.lastIndexOf("."));
|
|
135
|
+
const source = previous.get(path);
|
|
136
|
+
if (source === undefined) return [issue];
|
|
137
|
+
const target = current.get(source.token);
|
|
138
|
+
if (target === undefined) return [];
|
|
139
|
+
const suffix = issue.path.slice(source.path.length);
|
|
140
|
+
// An envelope index without a schema-owned identity is ambiguous after edits.
|
|
141
|
+
if (
|
|
142
|
+
/\.\d+(?:\.|$)/.test(suffix) &&
|
|
143
|
+
JSON.stringify(source.value) !== JSON.stringify(target.value)
|
|
144
|
+
)
|
|
145
|
+
return [];
|
|
146
|
+
if (JSON.stringify(at(source.value, suffix)) !== JSON.stringify(at(target.value, suffix)))
|
|
147
|
+
return [];
|
|
148
|
+
return [{ ...issue, path: target.path + suffix }];
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function at(value: unknown, suffix: string) {
|
|
153
|
+
for (const segment of suffix.split(".").filter(Boolean)) {
|
|
154
|
+
if (Array.isArray(value)) value = value[Number(segment)];
|
|
155
|
+
else if (isEmbeddedRecord(value)) value = value[segment];
|
|
156
|
+
else return undefined;
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { resolveBlockTypes } from "@riducms/protocol";
|
|
2
|
+
import { embeddedOccurrences, transformEmbeddedPayloads } from "@admin/core/forms/embedded-fields";
|
|
1
3
|
import type { SchemaField } from "@riducms/protocol";
|
|
2
4
|
|
|
3
5
|
export type FormValues = Record<string, unknown>;
|
|
@@ -31,9 +33,10 @@ export function localizationSource(
|
|
|
31
33
|
path: string,
|
|
32
34
|
values: FormValues,
|
|
33
35
|
original: FormValues,
|
|
34
|
-
sources: Readonly<Record<string, string
|
|
36
|
+
sources: Readonly<Record<string, string>>,
|
|
37
|
+
fields: readonly SchemaField[] = []
|
|
35
38
|
) {
|
|
36
|
-
return sources[localizationProvenancePath(path, values, original)];
|
|
39
|
+
return sources[localizationProvenancePath(path, values, original, fields)];
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export function shouldSubmitLocalizedPath(
|
|
@@ -41,9 +44,10 @@ export function shouldSubmitLocalizedPath(
|
|
|
41
44
|
values: FormValues,
|
|
42
45
|
original: FormValues,
|
|
43
46
|
locale: string | undefined,
|
|
44
|
-
sources: Readonly<Record<string, string
|
|
47
|
+
sources: Readonly<Record<string, string>>,
|
|
48
|
+
fields: readonly SchemaField[] = []
|
|
45
49
|
) {
|
|
46
|
-
const provenancePath = localizationProvenancePath(path, values, original);
|
|
50
|
+
const provenancePath = localizationProvenancePath(path, values, original, fields);
|
|
47
51
|
const source = sources[provenancePath];
|
|
48
52
|
return (
|
|
49
53
|
source === undefined ||
|
|
@@ -60,10 +64,18 @@ export function submissionFormValues(
|
|
|
60
64
|
return submissionRecord(fields, input, "", include);
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
/** Initialize a new record, retaining supplied values and container metadata. */
|
|
68
|
+
export function initialFormValues(
|
|
69
|
+
fields: readonly SchemaField[],
|
|
70
|
+
input: FormValues = {}
|
|
71
|
+
): FormValues {
|
|
72
|
+
const values = cloneFormValues(input);
|
|
65
73
|
for (const field of fields) {
|
|
66
|
-
const defaultValue = initialFieldValue(
|
|
74
|
+
const defaultValue = initialFieldValue(
|
|
75
|
+
field,
|
|
76
|
+
input[field.name],
|
|
77
|
+
Object.hasOwn(input, field.name)
|
|
78
|
+
);
|
|
67
79
|
if (defaultValue.present) values[field.name] = cloneFormValue(defaultValue.value);
|
|
68
80
|
}
|
|
69
81
|
return values;
|
|
@@ -108,9 +120,31 @@ function submissionRecord(
|
|
|
108
120
|
} else if (field.type === "blocks" && Array.isArray(value)) {
|
|
109
121
|
submitted[field.name] = value.map((row, index) => {
|
|
110
122
|
if (!isRecord(row) || typeof row.blockType !== "string") return cloneFormValue(row);
|
|
111
|
-
const block = field.blocks
|
|
112
|
-
|
|
123
|
+
const block = resolveBlockTypes(field.blocks).find(
|
|
124
|
+
(candidate) => candidate.slug === row.blockType
|
|
125
|
+
);
|
|
126
|
+
return block === undefined
|
|
127
|
+
? cloneFormValue(row)
|
|
128
|
+
: submissionRecord(block.fields, row, `${path}.${index}`, include);
|
|
113
129
|
});
|
|
130
|
+
} else if (field.type === "plugin" && field.plugin?.embeddedTrees !== undefined) {
|
|
131
|
+
submitted[field.name] = transformEmbeddedPayloads(
|
|
132
|
+
field,
|
|
133
|
+
cloneFormValue(value),
|
|
134
|
+
path,
|
|
135
|
+
(occurrence) => ({
|
|
136
|
+
...submissionRecord(
|
|
137
|
+
occurrence.block.fields,
|
|
138
|
+
occurrence.payload,
|
|
139
|
+
occurrence.path,
|
|
140
|
+
include
|
|
141
|
+
),
|
|
142
|
+
[occurrence.case.discriminator]: occurrence.block.slug,
|
|
143
|
+
...(occurrence.identity === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { [occurrence.case.identity]: occurrence.identity }),
|
|
146
|
+
})
|
|
147
|
+
);
|
|
114
148
|
} else {
|
|
115
149
|
submitted[field.name] = cloneFormValue(value);
|
|
116
150
|
}
|
|
@@ -120,7 +154,14 @@ function submissionRecord(
|
|
|
120
154
|
return submitted;
|
|
121
155
|
}
|
|
122
156
|
|
|
123
|
-
function localizationProvenancePath(
|
|
157
|
+
function localizationProvenancePath(
|
|
158
|
+
path: string,
|
|
159
|
+
values: FormValues,
|
|
160
|
+
originalValues: FormValues,
|
|
161
|
+
fields: readonly SchemaField[] = []
|
|
162
|
+
) {
|
|
163
|
+
const embeddedPath = embeddedProvenancePath(fields, values, originalValues, path);
|
|
164
|
+
if (embeddedPath !== undefined) return embeddedPath;
|
|
124
165
|
const segments = path.split(".");
|
|
125
166
|
const resolved: string[] = [];
|
|
126
167
|
let current: unknown = values;
|
|
@@ -358,6 +399,58 @@ function reconcileFieldValue(
|
|
|
358
399
|
);
|
|
359
400
|
}
|
|
360
401
|
|
|
402
|
+
if (
|
|
403
|
+
previous.type === "plugin" &&
|
|
404
|
+
next.type === "plugin" &&
|
|
405
|
+
next.plugin?.embeddedTrees !== undefined
|
|
406
|
+
) {
|
|
407
|
+
const reconcilePayloads = (value: unknown, before: unknown, report: DetachedDraftValue[]) => {
|
|
408
|
+
const copy = cloneFormValue(value);
|
|
409
|
+
const fieldPath = joinPath(parentPath, next.name);
|
|
410
|
+
const old = embeddedOccurrences(previous, before, fieldPath);
|
|
411
|
+
const currentOld = embeddedOccurrences(previous, value, fieldPath);
|
|
412
|
+
const candidates = embeddedOccurrences(next, copy, fieldPath);
|
|
413
|
+
// Unknown definitions remain intact for the submission recovery guard.
|
|
414
|
+
if (candidates.issues.length > 0) return copy;
|
|
415
|
+
return transformEmbeddedPayloads(next, copy, fieldPath, (occurrence) => {
|
|
416
|
+
const match = (candidate: (typeof old.occurrences)[number]) =>
|
|
417
|
+
candidate.tree.key === occurrence.tree.key &&
|
|
418
|
+
candidate.case.tagValue === occurrence.case.tagValue &&
|
|
419
|
+
candidate.block.slug === occurrence.block.slug &&
|
|
420
|
+
candidate.identity === occurrence.identity;
|
|
421
|
+
const prior = old.occurrences.find(match);
|
|
422
|
+
const previousOccurrence = currentOld.occurrences.find(match);
|
|
423
|
+
if (previousOccurrence === undefined)
|
|
424
|
+
return {
|
|
425
|
+
...(initializeDefaults ? initialFormValues(occurrence.block.fields) : {}),
|
|
426
|
+
...occurrence.payload,
|
|
427
|
+
};
|
|
428
|
+
const priorSchema = previousOccurrence.block.fields;
|
|
429
|
+
const reconciled = reconcileRecord(
|
|
430
|
+
priorSchema,
|
|
431
|
+
occurrence.block.fields,
|
|
432
|
+
occurrence.payload,
|
|
433
|
+
prior?.payload,
|
|
434
|
+
report,
|
|
435
|
+
occurrence.path,
|
|
436
|
+
initializeDefaults
|
|
437
|
+
);
|
|
438
|
+
return {
|
|
439
|
+
...reconciled.values,
|
|
440
|
+
[occurrence.case.discriminator]: occurrence.block.slug,
|
|
441
|
+
...(occurrence.identity === undefined
|
|
442
|
+
? {}
|
|
443
|
+
: { [occurrence.case.identity]: occurrence.identity }),
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
};
|
|
447
|
+
return {
|
|
448
|
+
currentPresent,
|
|
449
|
+
current: reconcilePayloads(currentValue, originalValue, detached),
|
|
450
|
+
originalPresent,
|
|
451
|
+
original: reconcilePayloads(originalValue, originalValue, []),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
361
454
|
return {
|
|
362
455
|
currentPresent,
|
|
363
456
|
current: cloneFormValue(currentValue),
|
|
@@ -442,13 +535,20 @@ function reconcileBlocksValue(
|
|
|
442
535
|
}
|
|
443
536
|
const currentRows = Array.isArray(currentValue) ? currentValue : [];
|
|
444
537
|
const originalRows = Array.isArray(originalValue) ? originalValue : [];
|
|
445
|
-
const previousBlocks = new Map(
|
|
446
|
-
|
|
538
|
+
const previousBlocks = new Map(
|
|
539
|
+
(resolveBlockTypes(previous.blocks) ?? []).map((block) => [block.slug, block])
|
|
540
|
+
);
|
|
541
|
+
const nextBlocks = new Map(
|
|
542
|
+
(resolveBlockTypes(next.blocks) ?? []).map((block) => [block.slug, block])
|
|
543
|
+
);
|
|
447
544
|
const rowPath = joinPath(parentPath, next.name);
|
|
448
|
-
const keptCurrent:
|
|
545
|
+
const keptCurrent: unknown[] = [];
|
|
449
546
|
|
|
450
547
|
for (const [index, row] of currentRows.entries()) {
|
|
451
|
-
if (!isRecord(row))
|
|
548
|
+
if (!isRecord(row)) {
|
|
549
|
+
keptCurrent.push(cloneFormValue(row));
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
452
552
|
const blockType = String(row.blockType ?? "");
|
|
453
553
|
const previousBlock = previousBlocks.get(blockType);
|
|
454
554
|
const nextBlock = nextBlocks.get(blockType);
|
|
@@ -463,6 +563,7 @@ function reconcileBlocksValue(
|
|
|
463
563
|
value: cloneFormValue(row),
|
|
464
564
|
});
|
|
465
565
|
}
|
|
566
|
+
keptCurrent.push(cloneFormValue(row));
|
|
466
567
|
continue;
|
|
467
568
|
}
|
|
468
569
|
keptCurrent.push(
|
|
@@ -479,11 +580,11 @@ function reconcileBlocksValue(
|
|
|
479
580
|
}
|
|
480
581
|
|
|
481
582
|
const keptOriginal = originalRows.flatMap((row, index) => {
|
|
482
|
-
if (!isRecord(row)) return [];
|
|
583
|
+
if (!isRecord(row)) return [cloneFormValue(row)];
|
|
483
584
|
const blockType = String(row.blockType ?? "");
|
|
484
585
|
const previousBlock = previousBlocks.get(blockType);
|
|
485
586
|
const nextBlock = nextBlocks.get(blockType);
|
|
486
|
-
if (previousBlock === undefined || nextBlock === undefined) return [];
|
|
587
|
+
if (previousBlock === undefined || nextBlock === undefined) return [cloneFormValue(row)];
|
|
487
588
|
return [
|
|
488
589
|
reconcileRecord(
|
|
489
590
|
previousBlock.fields,
|
|
@@ -530,12 +631,40 @@ function fieldDefault(field: SchemaField) {
|
|
|
530
631
|
return { present: true, value: field.select.defaultValues };
|
|
531
632
|
}
|
|
532
633
|
if (field.default === undefined) return { present: false };
|
|
634
|
+
if (field.type === "text-list" || field.type === "number-list")
|
|
635
|
+
return { present: true, value: JSON.parse(field.default) };
|
|
533
636
|
if (field.type === "number") return { present: true, value: Number(field.default) };
|
|
534
637
|
if (field.type === "checkbox") return { present: true, value: field.default === "true" };
|
|
535
638
|
return { present: true, value: field.default };
|
|
536
639
|
}
|
|
537
640
|
|
|
538
|
-
function initialFieldValue(
|
|
641
|
+
function initialFieldValue(
|
|
642
|
+
field: SchemaField,
|
|
643
|
+
value?: unknown,
|
|
644
|
+
present = false
|
|
645
|
+
): { present: boolean; value?: unknown } {
|
|
646
|
+
if (present) {
|
|
647
|
+
if (field.type === "group" && isRecord(value)) {
|
|
648
|
+
value = initialFormValues(field.nested?.fields ?? [], value);
|
|
649
|
+
} else if (field.type === "array" && Array.isArray(value)) {
|
|
650
|
+
value = value.map((row) =>
|
|
651
|
+
isRecord(row) ? initialFormValues(field.nested?.fields ?? [], row) : row
|
|
652
|
+
);
|
|
653
|
+
} else if (field.type === "blocks" && Array.isArray(value)) {
|
|
654
|
+
value = value.map((row) => {
|
|
655
|
+
if (!isRecord(row)) return row;
|
|
656
|
+
const block = resolveBlockTypes(field.blocks).find(
|
|
657
|
+
(candidate) => candidate.slug === row.blockType
|
|
658
|
+
);
|
|
659
|
+
return block === undefined ? row : initialFormValues(block.fields, row);
|
|
660
|
+
});
|
|
661
|
+
} else if (field.type === "plugin" && field.plugin?.embeddedTrees !== undefined) {
|
|
662
|
+
value = transformEmbeddedPayloads(field, cloneFormValue(value), field.path, (occurrence) =>
|
|
663
|
+
initialFormValues(occurrence.block.fields, occurrence.payload)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return { present: true, value };
|
|
667
|
+
}
|
|
539
668
|
const direct = fieldDefault(field);
|
|
540
669
|
if (direct.present) return direct;
|
|
541
670
|
if (field.type === "group") {
|
|
@@ -607,3 +736,68 @@ export function cloneFormValue(value: unknown): unknown {
|
|
|
607
736
|
}
|
|
608
737
|
return value;
|
|
609
738
|
}
|
|
739
|
+
|
|
740
|
+
function embeddedProvenancePath(
|
|
741
|
+
fields: readonly SchemaField[],
|
|
742
|
+
current: FormValues,
|
|
743
|
+
original: FormValues,
|
|
744
|
+
path: string,
|
|
745
|
+
prefix = ""
|
|
746
|
+
): string | undefined {
|
|
747
|
+
for (const field of fields) {
|
|
748
|
+
const fieldPath = joinPath(prefix, field.name);
|
|
749
|
+
if (!path.startsWith(`${fieldPath}.`)) continue;
|
|
750
|
+
const value = current[field.name];
|
|
751
|
+
const before = original[field.name];
|
|
752
|
+
if (field.type === "plugin") {
|
|
753
|
+
const now = embeddedOccurrences(field, value, fieldPath);
|
|
754
|
+
const previous = embeddedOccurrences(field, before, fieldPath);
|
|
755
|
+
const occurrence = now.occurrences.find((candidate) => path.startsWith(`${candidate.path}.`));
|
|
756
|
+
if (occurrence !== undefined && occurrence.identity !== undefined) {
|
|
757
|
+
const match = previous.occurrences.find(
|
|
758
|
+
(candidate) =>
|
|
759
|
+
candidate.tree.key === occurrence.tree.key &&
|
|
760
|
+
candidate.case.tagValue === occurrence.case.tagValue &&
|
|
761
|
+
candidate.block.slug === occurrence.block.slug &&
|
|
762
|
+
candidate.identity === occurrence.identity
|
|
763
|
+
);
|
|
764
|
+
if (match !== undefined) {
|
|
765
|
+
const suffix = path.slice(occurrence.path.length + 1);
|
|
766
|
+
return `${match.path}.${localizationProvenancePath(suffix, occurrence.payload, match.payload, occurrence.block.fields)}`;
|
|
767
|
+
}
|
|
768
|
+
return "";
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (field.type === "group" && isRecord(value))
|
|
772
|
+
return embeddedProvenancePath(
|
|
773
|
+
field.nested?.fields ?? [],
|
|
774
|
+
value,
|
|
775
|
+
isRecord(before) ? before : {},
|
|
776
|
+
path,
|
|
777
|
+
fieldPath
|
|
778
|
+
);
|
|
779
|
+
if ((field.type === "array" || field.type === "blocks") && Array.isArray(value)) {
|
|
780
|
+
for (const [index, row] of value.entries()) {
|
|
781
|
+
if (!isRecord(row) || !path.startsWith(`${fieldPath}.${index}.`)) continue;
|
|
782
|
+
const prior = Array.isArray(before) ? matchingRow(row, before, index) : {};
|
|
783
|
+
const children =
|
|
784
|
+
field.type === "blocks"
|
|
785
|
+
? (resolveBlockTypes(field.blocks).find((block) => block.slug === row.blockType)
|
|
786
|
+
?.fields ?? [])
|
|
787
|
+
: (field.nested?.fields ?? []);
|
|
788
|
+
const resolved = embeddedProvenancePath(
|
|
789
|
+
children,
|
|
790
|
+
row,
|
|
791
|
+
isRecord(prior) ? prior : {},
|
|
792
|
+
path,
|
|
793
|
+
`${fieldPath}.${index}`
|
|
794
|
+
);
|
|
795
|
+
const priorIndex = Array.isArray(before) ? before.indexOf(prior) : -1;
|
|
796
|
+
return resolved === undefined || priorIndex < 0
|
|
797
|
+
? resolved
|
|
798
|
+
: resolved.replace(`${fieldPath}.${index}.`, `${fieldPath}.${priorIndex}.`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return undefined;
|
|
803
|
+
}
|