@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.
Files changed (66) hide show
  1. package/package.json +11 -9
  2. package/src/app.css +63 -4
  3. package/src/app.svelte +12 -14
  4. package/src/components/ui/date-value-control/date-value-control-loader.svelte +15 -4
  5. package/src/components/ui/date-value-control/date-value-control.svelte +17 -17
  6. package/src/components/ui/select/select-trigger.svelte +1 -1
  7. package/src/core/forms/embedded-fields.ts +132 -0
  8. package/src/core/forms/embedded-schema-draft.svelte.ts +335 -0
  9. package/src/core/forms/field-editor-authoring.ts +47 -0
  10. package/src/core/forms/field-editor-binding.ts +228 -0
  11. package/src/core/forms/field-issue-focus.ts +42 -0
  12. package/src/core/forms/field-occurrence.ts +122 -0
  13. package/src/core/forms/form-controller.svelte.ts +475 -29
  14. package/src/core/forms/form-issue-correlation.ts +159 -0
  15. package/src/core/forms/form-schema.ts +211 -17
  16. package/src/core/forms/form-validation.ts +183 -17
  17. package/src/core/forms/live-validation.svelte.ts +317 -0
  18. package/src/core/forms/plugin-field-authoring.ts +160 -0
  19. package/src/core/forms/plugin-field-binding.ts +258 -0
  20. package/src/core/forms/plugin-field-write.ts +57 -0
  21. package/src/core/i18n/admin-i18n.svelte.ts +8 -1
  22. package/src/core/i18n/localized-schema.ts +33 -10
  23. package/src/core/plugins/field-registry.ts +29 -49
  24. package/src/core/plugins/row-label-registry.ts +25 -5
  25. package/src/core/runtime/admin-runtime.svelte.ts +25 -13
  26. package/src/features/auth/account-route.svelte +10 -5
  27. package/src/features/auth/account-security-route.svelte +1 -1
  28. package/src/features/collections/collection-list-bulk-editor.svelte +6 -6
  29. package/src/features/collections/collection-list-controller.svelte.ts +5 -5
  30. package/src/features/collections/collection-list-filter-popover.svelte +6 -6
  31. package/src/features/collections/collection-list-route.svelte +13 -14
  32. package/src/features/collections/list-workspace.ts +22 -8
  33. package/src/features/documents/document-controller.svelte.ts +8 -2
  34. package/src/features/documents/document-route.svelte +36 -43
  35. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +30 -15
  36. package/src/features/reference-browser/reference-browser.svelte +215 -189
  37. package/src/features/uploads/upload-metadata-fields.svelte +6 -6
  38. package/src/features/versions/version-history-route.svelte +1 -1
  39. package/src/fields/embedded-schema-draft-editor.svelte +74 -0
  40. package/src/fields/embedded-schema-fields.svelte +44 -0
  41. package/src/fields/field-clipboard.ts +76 -16
  42. package/src/fields/field-collapsible.svelte +49 -0
  43. package/src/fields/field-layout.svelte +2 -18
  44. package/src/fields/field-messages.svelte +13 -18
  45. package/src/fields/field-renderer.svelte +53 -14
  46. package/src/fields/join/join-field.svelte +8 -1
  47. package/src/fields/live-validation-feedback.svelte +21 -0
  48. package/src/fields/local-field-editor.svelte +52 -0
  49. package/src/fields/nested/nested-field.svelte +159 -91
  50. package/src/fields/nested/scoped-field.ts +7 -9
  51. package/src/fields/plugin-field.svelte +156 -0
  52. package/src/fields/point/point-field.svelte +12 -2
  53. package/src/fields/primitive-list/primitive-list-field.svelte +170 -0
  54. package/src/fields/primitive-list/primitive-number-input.ts +6 -0
  55. package/src/fields/radio/radio-field.svelte +13 -9
  56. package/src/fields/relationship/relationship-field.svelte +36 -12
  57. package/src/fields/relationship/relationship-query.ts +4 -2
  58. package/src/fields/relationship/relationship-quick-picker.svelte +38 -10
  59. package/src/fields/scalar/date-control-value.ts +5 -5
  60. package/src/fields/scalar/date-value.ts +5 -5
  61. package/src/fields/scalar/scalar-field.svelte +47 -41
  62. package/src/fields/select/select-field.svelte +24 -24
  63. package/src/fields/select/select-value.ts +3 -3
  64. package/src/fields/text/slug-field.svelte +24 -8
  65. package/src/fields/text/text-field.svelte +8 -6
  66. 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
- export function initialFormValues(fields: readonly SchemaField[]): FormValues {
64
- const values: FormValues = {};
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(field);
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?.types.find((candidate) => candidate.key === row.blockType);
112
- return submissionRecord(block?.fields ?? [], row, `${path}.${index}`, include);
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(path: string, values: FormValues, originalValues: FormValues) {
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((previous.blocks?.types ?? []).map((block) => [block.key, block]));
446
- const nextBlocks = new Map((next.blocks?.types ?? []).map((block) => [block.key, block]));
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: FormValues[] = [];
545
+ const keptCurrent: unknown[] = [];
449
546
 
450
547
  for (const [index, row] of currentRows.entries()) {
451
- if (!isRecord(row)) continue;
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(field: SchemaField): { present: boolean; value?: unknown } {
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
+ }