@riducms/admin 0.1.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.
Files changed (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/THIRD_PARTY_NOTICES.md +217 -0
  4. package/package.json +62 -0
  5. package/src/app/admin-core-view-route.svelte +89 -0
  6. package/src/app/admin-layout.svelte +457 -0
  7. package/src/app/admin-navigation-progress.svelte +204 -0
  8. package/src/app/admin-page-title.svelte +94 -0
  9. package/src/app/admin-provider-layer.svelte +26 -0
  10. package/src/app/admin-router-root.svelte +9 -0
  11. package/src/app/admin-routes.svelte +116 -0
  12. package/src/app/deferred-redirect.svelte +23 -0
  13. package/src/app/loading-boundary.svelte +70 -0
  14. package/src/app/not-found-route.svelte +29 -0
  15. package/src/app-meta.ts +2 -0
  16. package/src/app.css +467 -0
  17. package/src/app.svelte +88 -0
  18. package/src/assets/ridu-logo-arch.svg +3 -0
  19. package/src/assets/ridu-word-logo.svg +7 -0
  20. package/src/components/brand/ridu-logo.svelte +24 -0
  21. package/src/components/code-block/code-block.svelte +107 -0
  22. package/src/components/json-tree/json-tree-node.svelte +77 -0
  23. package/src/components/json-tree/json-viewer.svelte +113 -0
  24. package/src/components/ui/banner/banner.svelte +41 -0
  25. package/src/components/ui/banner/index.ts +5 -0
  26. package/src/components/ui/bulk-action-bar/bulk-action-bar.svelte +50 -0
  27. package/src/components/ui/bulk-action-bar/index.ts +1 -0
  28. package/src/components/ui/button/index.ts +8 -0
  29. package/src/components/ui/calendar/calendar-caption.svelte +72 -0
  30. package/src/components/ui/calendar/calendar-cell.svelte +20 -0
  31. package/src/components/ui/calendar/calendar-day.svelte +27 -0
  32. package/src/components/ui/calendar/calendar-grid-row.svelte +13 -0
  33. package/src/components/ui/calendar/calendar-grid.svelte +17 -0
  34. package/src/components/ui/calendar/calendar-head-cell.svelte +20 -0
  35. package/src/components/ui/calendar/calendar-header.svelte +20 -0
  36. package/src/components/ui/calendar/calendar-heading.svelte +17 -0
  37. package/src/components/ui/calendar/calendar-month-select.svelte +45 -0
  38. package/src/components/ui/calendar/calendar-month.svelte +16 -0
  39. package/src/components/ui/calendar/calendar-months.svelte +20 -0
  40. package/src/components/ui/calendar/calendar-nav.svelte +20 -0
  41. package/src/components/ui/calendar/calendar-next-button.svelte +30 -0
  42. package/src/components/ui/calendar/calendar-prev-button.svelte +30 -0
  43. package/src/components/ui/calendar/calendar-year-select.svelte +44 -0
  44. package/src/components/ui/calendar/calendar.svelte +118 -0
  45. package/src/components/ui/calendar/index.ts +1 -0
  46. package/src/components/ui/checkbox/checkbox.svelte +35 -0
  47. package/src/components/ui/checkbox/index.ts +1 -0
  48. package/src/components/ui/confirmation-dialog/confirmation-dialog.svelte +37 -0
  49. package/src/components/ui/confirmation-dialog/index.ts +1 -0
  50. package/src/components/ui/date-value-control/date-value-calendar.svelte +37 -0
  51. package/src/components/ui/date-value-control/date-value-control-loader.svelte +36 -0
  52. package/src/components/ui/date-value-control/date-value-control.svelte +177 -0
  53. package/src/components/ui/date-value-control/index.ts +3 -0
  54. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  55. package/src/components/ui/dialog/dialog-content.svelte +52 -0
  56. package/src/components/ui/dialog/dialog-description.svelte +20 -0
  57. package/src/components/ui/dialog/dialog-footer.svelte +20 -0
  58. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  59. package/src/components/ui/dialog/dialog-overlay.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  62. package/src/components/ui/dialog/dialog.svelte +7 -0
  63. package/src/components/ui/dialog/index.ts +8 -0
  64. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +27 -0
  65. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +20 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +16 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  69. package/src/components/ui/dropdown-menu/index.ts +5 -0
  70. package/src/components/ui/empty-state/empty-state.svelte +34 -0
  71. package/src/components/ui/empty-state/index.ts +1 -0
  72. package/src/components/ui/input/index.ts +1 -0
  73. package/src/components/ui/kbd/index.ts +1 -0
  74. package/src/components/ui/kbd/kbd.svelte +24 -0
  75. package/src/components/ui/pagination/index.ts +1 -0
  76. package/src/components/ui/pagination/pagination.svelte +88 -0
  77. package/src/components/ui/popover/index.ts +3 -0
  78. package/src/components/ui/popover/popover-content.svelte +29 -0
  79. package/src/components/ui/popover/popover-trigger.svelte +11 -0
  80. package/src/components/ui/popover/popover.svelte +7 -0
  81. package/src/components/ui/radio-group/index.ts +3 -0
  82. package/src/components/ui/radio-group/radio-card-item.svelte +38 -0
  83. package/src/components/ui/radio-group/radio-group-item.svelte +17 -0
  84. package/src/components/ui/radio-group/radio-group.svelte +19 -0
  85. package/src/components/ui/select/index.ts +4 -0
  86. package/src/components/ui/select/select-content.svelte +38 -0
  87. package/src/components/ui/select/select-item.svelte +40 -0
  88. package/src/components/ui/select/select-scroll-down-button.svelte +23 -0
  89. package/src/components/ui/select/select-scroll-up-button.svelte +23 -0
  90. package/src/components/ui/select/select-trigger.svelte +29 -0
  91. package/src/components/ui/select/select.svelte +11 -0
  92. package/src/components/ui/sheet/index.ts +6 -0
  93. package/src/components/ui/sheet/sheet-close.svelte +7 -0
  94. package/src/components/ui/sheet/sheet-content.svelte +77 -0
  95. package/src/components/ui/sheet/sheet-description.svelte +17 -0
  96. package/src/components/ui/sheet/sheet-overlay.svelte +36 -0
  97. package/src/components/ui/sheet/sheet-title.svelte +17 -0
  98. package/src/components/ui/sheet/sheet-trigger.svelte +7 -0
  99. package/src/components/ui/sheet/sheet.svelte +7 -0
  100. package/src/components/ui/skeleton/index.ts +1 -0
  101. package/src/components/ui/skeleton/skeleton.svelte +31 -0
  102. package/src/components/ui/slider/index.ts +3 -0
  103. package/src/components/ui/slider/slider-content.svelte +59 -0
  104. package/src/components/ui/slider/slider.svelte +35 -0
  105. package/src/components/ui/sonner/index.ts +1 -0
  106. package/src/components/ui/sonner/sonner.svelte +55 -0
  107. package/src/components/ui/status-indicator/index.ts +5 -0
  108. package/src/components/ui/status-indicator/status-indicator.svelte +54 -0
  109. package/src/components/ui/table/index.ts +6 -0
  110. package/src/components/ui/table/table-body.svelte +20 -0
  111. package/src/components/ui/table/table-cell.svelte +23 -0
  112. package/src/components/ui/table/table-head.svelte +23 -0
  113. package/src/components/ui/table/table-header.svelte +20 -0
  114. package/src/components/ui/table/table-row.svelte +23 -0
  115. package/src/components/ui/table/table.svelte +22 -0
  116. package/src/components/ui/tabs/index.ts +8 -0
  117. package/src/components/ui/tabs/tabs-content.svelte +17 -0
  118. package/src/components/ui/tabs/tabs-list.svelte +40 -0
  119. package/src/components/ui/tabs/tabs-trigger.svelte +23 -0
  120. package/src/components/ui/tabs/tabs.svelte +19 -0
  121. package/src/components/ui/textarea/index.ts +1 -0
  122. package/src/components/ui/tooltip/index.ts +4 -0
  123. package/src/components/ui/tooltip/tooltip-content.svelte +50 -0
  124. package/src/components/ui/tooltip/tooltip-provider.svelte +7 -0
  125. package/src/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  126. package/src/components/ui/tooltip/tooltip.svelte +7 -0
  127. package/src/core/api/admin-client.ts +45 -0
  128. package/src/core/forms/field-condition.ts +63 -0
  129. package/src/core/forms/form-controller.svelte.ts +369 -0
  130. package/src/core/forms/form-draft-recovery.ts +80 -0
  131. package/src/core/forms/form-schema.ts +609 -0
  132. package/src/core/forms/form-validation.ts +750 -0
  133. package/src/core/i18n/admin-i18n.svelte.ts +364 -0
  134. package/src/core/i18n/localized-schema.ts +111 -0
  135. package/src/core/notifications/notification-center.svelte.ts +69 -0
  136. package/src/core/notifications/validation-toast.svelte +18 -0
  137. package/src/core/plugins/field-registry.ts +60 -0
  138. package/src/core/plugins/row-label-registry.ts +62 -0
  139. package/src/core/preferences/preference-write-queue.ts +107 -0
  140. package/src/core/routing/admin-paths.ts +137 -0
  141. package/src/core/runtime/admin-runtime.svelte.ts +625 -0
  142. package/src/features/auth/account-menu.svelte +159 -0
  143. package/src/features/auth/account-navigation.svelte +44 -0
  144. package/src/features/auth/account-route.svelte +395 -0
  145. package/src/features/auth/account-security-controller.svelte.ts +119 -0
  146. package/src/features/auth/account-security-route.svelte +460 -0
  147. package/src/features/auth/auth-brand.svelte +17 -0
  148. package/src/features/auth/auth-frame.svelte +18 -0
  149. package/src/features/auth/create-first-user-controller.svelte.ts +220 -0
  150. package/src/features/auth/create-first-user-route.svelte +174 -0
  151. package/src/features/auth/forgot-password-route.svelte +78 -0
  152. package/src/features/auth/login-route.svelte +221 -0
  153. package/src/features/auth/request-verification-route.svelte +73 -0
  154. package/src/features/auth/reset-password-route.svelte +107 -0
  155. package/src/features/auth/verify-email-route.svelte +60 -0
  156. package/src/features/collections/collection-list-bulk-actions.svelte +143 -0
  157. package/src/features/collections/collection-list-bulk-editor.svelte +157 -0
  158. package/src/features/collections/collection-list-controller.svelte.ts +717 -0
  159. package/src/features/collections/collection-list-filter-popover.svelte +238 -0
  160. package/src/features/collections/collection-list-preferences-controller.svelte.ts +393 -0
  161. package/src/features/collections/collection-list-results.svelte +410 -0
  162. package/src/features/collections/collection-list-route.svelte +951 -0
  163. package/src/features/collections/collection-list-workspace-toolbar.svelte +409 -0
  164. package/src/features/collections/collection-table-skeleton.svelte +44 -0
  165. package/src/features/collections/list-workspace.ts +265 -0
  166. package/src/features/dashboard/dashboard-route.svelte +141 -0
  167. package/src/features/development/development-tools.svelte +217 -0
  168. package/src/features/documents/document-api-view-loader.svelte +31 -0
  169. package/src/features/documents/document-api-view.svelte +547 -0
  170. package/src/features/documents/document-controller.svelte.ts +1252 -0
  171. package/src/features/documents/document-field-partition.ts +18 -0
  172. package/src/features/documents/document-field-sections.svelte +46 -0
  173. package/src/features/documents/document-heading.ts +20 -0
  174. package/src/features/documents/document-route-dirty.ts +43 -0
  175. package/src/features/documents/document-route-loader.svelte +22 -0
  176. package/src/features/documents/document-route.svelte +1124 -0
  177. package/src/features/documents/live-preview-panel.svelte +390 -0
  178. package/src/features/documents/live-preview.ts +61 -0
  179. package/src/features/documents/upload-document-preview-loader.svelte +40 -0
  180. package/src/features/documents/upload-document-preview.svelte +525 -0
  181. package/src/features/localization/content-locale-switcher.svelte +93 -0
  182. package/src/features/navigation/admin-breadcrumbs.svelte +124 -0
  183. package/src/features/navigation/admin-command-menu.svelte +245 -0
  184. package/src/features/navigation/admin-resource-groups.ts +72 -0
  185. package/src/features/reference-browser/reference-browser-loader.svelte +15 -0
  186. package/src/features/reference-browser/reference-browser-workflow.svelte.ts +691 -0
  187. package/src/features/reference-browser/reference-browser.svelte +844 -0
  188. package/src/features/uploads/bulk-upload-route.svelte +624 -0
  189. package/src/features/uploads/bulk-upload.ts +39 -0
  190. package/src/features/uploads/upload-metadata-fields.svelte +112 -0
  191. package/src/features/versions/version-diff.ts +72 -0
  192. package/src/features/versions/version-history-route.svelte +581 -0
  193. package/src/fields/field-clipboard.ts +132 -0
  194. package/src/fields/field-layout.svelte +60 -0
  195. package/src/fields/field-layout.ts +57 -0
  196. package/src/fields/field-messages.svelte +29 -0
  197. package/src/fields/field-renderer.svelte +152 -0
  198. package/src/fields/field-shell.svelte +30 -0
  199. package/src/fields/field-tab-layout.svelte +99 -0
  200. package/src/fields/join/join-field.svelte +358 -0
  201. package/src/fields/localized-field-presentation.ts +17 -0
  202. package/src/fields/nested/nested-field.svelte +602 -0
  203. package/src/fields/nested/scoped-field.ts +78 -0
  204. package/src/fields/nested/sortable-row.svelte +32 -0
  205. package/src/fields/point/point-field.svelte +52 -0
  206. package/src/fields/radio/radio-field.svelte +34 -0
  207. package/src/fields/relationship/relationship-field-controller.svelte.ts +245 -0
  208. package/src/fields/relationship/relationship-field.svelte +258 -0
  209. package/src/fields/relationship/relationship-lookup-controller.svelte.ts +129 -0
  210. package/src/fields/relationship/relationship-query.ts +88 -0
  211. package/src/fields/relationship/relationship-quick-picker-controller.svelte.ts +198 -0
  212. package/src/fields/relationship/relationship-quick-picker.svelte +327 -0
  213. package/src/fields/relationship/relationship-value.ts +47 -0
  214. package/src/fields/scalar/date-control-value.ts +60 -0
  215. package/src/fields/scalar/date-value.ts +26 -0
  216. package/src/fields/scalar/scalar-field.svelte +137 -0
  217. package/src/fields/select/select-field.svelte +132 -0
  218. package/src/fields/select/select-value.ts +14 -0
  219. package/src/fields/text/slug-field.svelte +87 -0
  220. package/src/fields/text/slug.ts +38 -0
  221. package/src/fields/text/text-field.svelte +41 -0
  222. package/src/fields/ui/ui-field.svelte +12 -0
  223. package/src/fields/virtual/virtual-field.svelte +28 -0
  224. package/src/index.ts +49 -0
  225. package/src/main.ts +12 -0
  226. package/src/vite-env.d.ts +9 -0
@@ -0,0 +1,609 @@
1
+ import type { SchemaField } from "@riducms/protocol";
2
+
3
+ export type FormValues = Record<string, unknown>;
4
+
5
+ export interface DetachedDraftValue {
6
+ fieldId: string;
7
+ label: string;
8
+ path: string;
9
+ reason: "removed" | "incompatible" | "removed-block";
10
+ value: unknown;
11
+ }
12
+
13
+ export interface FormSchemaState {
14
+ values: FormValues;
15
+ original: FormValues;
16
+ }
17
+
18
+ export interface FormSchemaReconciliation extends FormSchemaState {
19
+ detached: DetachedDraftValue[];
20
+ }
21
+
22
+ export interface FormDraftRecovery extends FormSchemaReconciliation {
23
+ restoredFields: number;
24
+ }
25
+
26
+ export interface FormSchemaReconciliationOptions {
27
+ initializeDefaults?: boolean;
28
+ }
29
+
30
+ export function localizationSource(
31
+ path: string,
32
+ values: FormValues,
33
+ original: FormValues,
34
+ sources: Readonly<Record<string, string>>
35
+ ) {
36
+ return sources[localizationProvenancePath(path, values, original)];
37
+ }
38
+
39
+ export function shouldSubmitLocalizedPath(
40
+ path: string,
41
+ values: FormValues,
42
+ original: FormValues,
43
+ locale: string | undefined,
44
+ sources: Readonly<Record<string, string>>
45
+ ) {
46
+ const provenancePath = localizationProvenancePath(path, values, original);
47
+ const source = sources[provenancePath];
48
+ return (
49
+ source === undefined ||
50
+ source === locale ||
51
+ !deepEqual(readPath(values, path), readPath(original, provenancePath))
52
+ );
53
+ }
54
+
55
+ export function submissionFormValues(
56
+ fields: readonly SchemaField[],
57
+ input: FormValues,
58
+ include: (path: string, canonicalPath: string) => boolean = () => true
59
+ ): FormValues {
60
+ return submissionRecord(fields, input, "", include);
61
+ }
62
+
63
+ export function initialFormValues(fields: readonly SchemaField[]): FormValues {
64
+ const values: FormValues = {};
65
+ for (const field of fields) {
66
+ const defaultValue = initialFieldValue(field);
67
+ if (defaultValue.present) values[field.name] = cloneFormValue(defaultValue.value);
68
+ }
69
+ return values;
70
+ }
71
+
72
+ export function documentFormValues(
73
+ fields: readonly SchemaField[],
74
+ document: Readonly<FormValues>
75
+ ): FormValues {
76
+ const values: FormValues = {};
77
+ for (const field of fields) {
78
+ if (Object.hasOwn(document, field.name)) {
79
+ values[field.name] = cloneFormValue(document[field.name]);
80
+ }
81
+ }
82
+ return values;
83
+ }
84
+
85
+ function submissionRecord(
86
+ fields: readonly SchemaField[],
87
+ input: unknown,
88
+ prefix: string,
89
+ include: (path: string, canonicalPath: string) => boolean
90
+ ): FormValues {
91
+ const values = isRecord(input) ? input : {};
92
+ const submitted: FormValues = {};
93
+ for (const field of fields) {
94
+ const path = joinPath(prefix, field.name);
95
+ if (
96
+ field.category === "presentation" ||
97
+ !include(path, field.path) ||
98
+ !Object.hasOwn(values, field.name)
99
+ )
100
+ continue;
101
+ const value = values[field.name];
102
+ if (field.type === "group") {
103
+ submitted[field.name] = submissionRecord(field.nested?.fields ?? [], value, path, include);
104
+ } else if (field.type === "array" && Array.isArray(value)) {
105
+ submitted[field.name] = value.map((row, index) =>
106
+ submissionRecord(field.nested?.fields ?? [], row, `${path}.${index}`, include)
107
+ );
108
+ } else if (field.type === "blocks" && Array.isArray(value)) {
109
+ submitted[field.name] = value.map((row, index) => {
110
+ 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);
113
+ });
114
+ } else {
115
+ submitted[field.name] = cloneFormValue(value);
116
+ }
117
+ }
118
+ copyReservedValue(values, submitted, "_key");
119
+ copyReservedValue(values, submitted, "blockType");
120
+ return submitted;
121
+ }
122
+
123
+ function localizationProvenancePath(path: string, values: FormValues, originalValues: FormValues) {
124
+ const segments = path.split(".");
125
+ const resolved: string[] = [];
126
+ let current: unknown = values;
127
+ let original: unknown = originalValues;
128
+ for (const segment of segments) {
129
+ if (Array.isArray(current) && /^\d+$/.test(segment)) {
130
+ const row = current[Number(segment)];
131
+ const key = isRecord(row) ? row._key : undefined;
132
+ const originalIndex =
133
+ typeof key === "string" && Array.isArray(original)
134
+ ? original.findIndex((candidate) => isRecord(candidate) && candidate._key === key)
135
+ : Number(segment);
136
+ const index = originalIndex >= 0 ? originalIndex : Number(segment);
137
+ resolved.push(String(index));
138
+ current = row;
139
+ original = Array.isArray(original) ? original[index] : undefined;
140
+ continue;
141
+ }
142
+ resolved.push(segment);
143
+ current = Array.isArray(current)
144
+ ? current[Number(segment)]
145
+ : isRecord(current)
146
+ ? current[segment]
147
+ : undefined;
148
+ original = Array.isArray(original)
149
+ ? original[Number(segment)]
150
+ : isRecord(original)
151
+ ? original[segment]
152
+ : undefined;
153
+ }
154
+ return resolved.join(".");
155
+ }
156
+
157
+ function readPath(values: unknown, path: string) {
158
+ let current = values;
159
+ for (const segment of path.split(".")) {
160
+ current = Array.isArray(current)
161
+ ? current[Number(segment)]
162
+ : isRecord(current)
163
+ ? current[segment]
164
+ : undefined;
165
+ }
166
+ return current;
167
+ }
168
+
169
+ export function reconcileFormSchema(
170
+ state: FormSchemaState,
171
+ previousFields: readonly SchemaField[],
172
+ nextFields: readonly SchemaField[],
173
+ options: FormSchemaReconciliationOptions = {}
174
+ ): FormSchemaReconciliation {
175
+ const detached: DetachedDraftValue[] = [];
176
+ const reconciled = reconcileRecord(
177
+ previousFields,
178
+ nextFields,
179
+ state.values,
180
+ state.original,
181
+ detached,
182
+ "",
183
+ options.initializeDefaults === true
184
+ );
185
+ return { ...reconciled, detached };
186
+ }
187
+
188
+ export function recoverFormDraft(
189
+ latest: FormSchemaState,
190
+ draft: FormSchemaState,
191
+ previousFields: readonly SchemaField[],
192
+ nextFields: readonly SchemaField[]
193
+ ): FormDraftRecovery {
194
+ const reconciled = reconcileFormSchema(draft, previousFields, nextFields);
195
+ const values = cloneFormValues(latest.values);
196
+ let restoredFields = 0;
197
+
198
+ for (const field of nextFields) {
199
+ const local = reconciled.values[field.name];
200
+ const original = reconciled.original[field.name];
201
+ if (deepEqual(local, original)) continue;
202
+ values[field.name] = cloneFormValue(local);
203
+ restoredFields += 1;
204
+ }
205
+
206
+ return {
207
+ values,
208
+ original: cloneFormValues(latest.original),
209
+ detached: reconciled.detached,
210
+ restoredFields,
211
+ };
212
+ }
213
+
214
+ function reconcileRecord(
215
+ previousFields: readonly SchemaField[],
216
+ nextFields: readonly SchemaField[],
217
+ currentInput: unknown,
218
+ originalInput: unknown,
219
+ detached: DetachedDraftValue[],
220
+ parentPath: string,
221
+ initializeDefaults: boolean
222
+ ) {
223
+ const current = isRecord(currentInput) ? currentInput : {};
224
+ const original = isRecord(originalInput) ? originalInput : {};
225
+ const values: FormValues = {};
226
+ const originals: FormValues = {};
227
+ const previousByID = new Map(previousFields.map((field) => [field.id, field]));
228
+ const nextIDs = new Set(nextFields.map((field) => field.id));
229
+
230
+ for (const next of nextFields) {
231
+ const previous = previousByID.get(next.id);
232
+ if (previous === undefined) {
233
+ if (initializeDefaults) {
234
+ const defaultValue = initialFieldValue(next);
235
+ if (defaultValue.present) {
236
+ values[next.name] = cloneFormValue(defaultValue.value);
237
+ originals[next.name] = cloneFormValue(defaultValue.value);
238
+ }
239
+ }
240
+ continue;
241
+ }
242
+
243
+ const currentPresent = Object.hasOwn(current, previous.name);
244
+ const originalPresent = Object.hasOwn(original, previous.name);
245
+ const currentValue = current[previous.name];
246
+ const originalValue = original[previous.name];
247
+ if (!compatibleFields(previous, next)) {
248
+ if (currentPresent && !deepEqual(currentValue, originalValue)) {
249
+ detached.push(detachedValue(previous, currentValue, "incompatible", parentPath));
250
+ }
251
+ if (initializeDefaults) {
252
+ const defaultValue = initialFieldValue(next);
253
+ if (defaultValue.present) {
254
+ values[next.name] = cloneFormValue(defaultValue.value);
255
+ originals[next.name] = cloneFormValue(defaultValue.value);
256
+ }
257
+ }
258
+ continue;
259
+ }
260
+
261
+ const reconciled = reconcileFieldValue(
262
+ previous,
263
+ next,
264
+ currentPresent,
265
+ currentValue,
266
+ originalPresent,
267
+ originalValue,
268
+ detached,
269
+ parentPath,
270
+ initializeDefaults
271
+ );
272
+ if (reconciled.currentPresent) values[next.name] = reconciled.current;
273
+ if (reconciled.originalPresent) originals[next.name] = reconciled.original;
274
+ }
275
+
276
+ for (const previous of previousFields) {
277
+ if (nextIDs.has(previous.id) || !Object.hasOwn(current, previous.name)) continue;
278
+ const currentValue = current[previous.name];
279
+ if (!deepEqual(currentValue, original[previous.name])) {
280
+ detached.push(detachedValue(previous, currentValue, "removed", parentPath));
281
+ }
282
+ }
283
+
284
+ copyReservedValue(current, values, "_key");
285
+ copyReservedValue(original, originals, "_key");
286
+ copyReservedValue(current, values, "blockType");
287
+ copyReservedValue(original, originals, "blockType");
288
+ return { values, original: originals };
289
+ }
290
+
291
+ function reconcileFieldValue(
292
+ previous: SchemaField,
293
+ next: SchemaField,
294
+ currentPresent: boolean,
295
+ currentValue: unknown,
296
+ originalPresent: boolean,
297
+ originalValue: unknown,
298
+ detached: DetachedDraftValue[],
299
+ parentPath: string,
300
+ initializeDefaults: boolean
301
+ ) {
302
+ if (initializeDefaults && !currentPresent && !originalPresent) {
303
+ const initial = initialFieldValue(next);
304
+ if (initial.present) {
305
+ return {
306
+ currentPresent: true,
307
+ current: cloneFormValue(initial.value),
308
+ originalPresent: true,
309
+ original: cloneFormValue(initial.value),
310
+ };
311
+ }
312
+ }
313
+
314
+ if (previous.type === "group" && next.type === "group") {
315
+ if (!currentPresent && !originalPresent) return absentFieldValue();
316
+ const reconciled = reconcileRecord(
317
+ previous.nested?.fields ?? [],
318
+ next.nested?.fields ?? [],
319
+ currentValue,
320
+ originalValue,
321
+ detached,
322
+ joinPath(parentPath, next.name),
323
+ initializeDefaults
324
+ );
325
+ return {
326
+ currentPresent,
327
+ current: reconciled.values,
328
+ originalPresent,
329
+ original: reconciled.original,
330
+ };
331
+ }
332
+
333
+ if (previous.type === "array" && next.type === "array") {
334
+ return reconcileArrayValue(
335
+ previous,
336
+ next,
337
+ currentPresent,
338
+ currentValue,
339
+ originalPresent,
340
+ originalValue,
341
+ detached,
342
+ parentPath,
343
+ initializeDefaults
344
+ );
345
+ }
346
+
347
+ if (previous.type === "blocks" && next.type === "blocks") {
348
+ return reconcileBlocksValue(
349
+ previous,
350
+ next,
351
+ currentPresent,
352
+ currentValue,
353
+ originalPresent,
354
+ originalValue,
355
+ detached,
356
+ parentPath,
357
+ initializeDefaults
358
+ );
359
+ }
360
+
361
+ return {
362
+ currentPresent,
363
+ current: cloneFormValue(currentValue),
364
+ originalPresent,
365
+ original: cloneFormValue(originalValue),
366
+ };
367
+ }
368
+
369
+ function reconcileArrayValue(
370
+ previous: SchemaField,
371
+ next: SchemaField,
372
+ currentPresent: boolean,
373
+ currentValue: unknown,
374
+ originalPresent: boolean,
375
+ originalValue: unknown,
376
+ detached: DetachedDraftValue[],
377
+ parentPath: string,
378
+ initializeDefaults: boolean
379
+ ) {
380
+ if (!Array.isArray(currentValue) && !Array.isArray(originalValue)) {
381
+ return {
382
+ currentPresent,
383
+ current: cloneFormValue(currentValue),
384
+ originalPresent,
385
+ original: cloneFormValue(originalValue),
386
+ };
387
+ }
388
+ const currentRows = Array.isArray(currentValue) ? currentValue : [];
389
+ const originalRows = Array.isArray(originalValue) ? originalValue : [];
390
+ const previousChildren = previous.nested?.fields ?? [];
391
+ const nextChildren = next.nested?.fields ?? [];
392
+ const rowPath = joinPath(parentPath, next.name);
393
+
394
+ return {
395
+ currentPresent,
396
+ current: currentRows.map((row, index) => {
397
+ const originalRow = matchingRow(row, originalRows, index);
398
+ return reconcileRecord(
399
+ previousChildren,
400
+ nextChildren,
401
+ row,
402
+ originalRow,
403
+ detached,
404
+ `${rowPath}.${index}`,
405
+ initializeDefaults
406
+ ).values;
407
+ }),
408
+ originalPresent,
409
+ original: originalRows.map(
410
+ (row, index) =>
411
+ reconcileRecord(
412
+ previousChildren,
413
+ nextChildren,
414
+ row,
415
+ row,
416
+ [],
417
+ `${rowPath}.${index}`,
418
+ initializeDefaults
419
+ ).values
420
+ ),
421
+ };
422
+ }
423
+
424
+ function reconcileBlocksValue(
425
+ previous: SchemaField,
426
+ next: SchemaField,
427
+ currentPresent: boolean,
428
+ currentValue: unknown,
429
+ originalPresent: boolean,
430
+ originalValue: unknown,
431
+ detached: DetachedDraftValue[],
432
+ parentPath: string,
433
+ initializeDefaults: boolean
434
+ ) {
435
+ if (!Array.isArray(currentValue) && !Array.isArray(originalValue)) {
436
+ return {
437
+ currentPresent,
438
+ current: cloneFormValue(currentValue),
439
+ originalPresent,
440
+ original: cloneFormValue(originalValue),
441
+ };
442
+ }
443
+ const currentRows = Array.isArray(currentValue) ? currentValue : [];
444
+ 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]));
447
+ const rowPath = joinPath(parentPath, next.name);
448
+ const keptCurrent: FormValues[] = [];
449
+
450
+ for (const [index, row] of currentRows.entries()) {
451
+ if (!isRecord(row)) continue;
452
+ const blockType = String(row.blockType ?? "");
453
+ const previousBlock = previousBlocks.get(blockType);
454
+ const nextBlock = nextBlocks.get(blockType);
455
+ const originalRow = matchingRow(row, originalRows, index);
456
+ if (previousBlock === undefined || nextBlock === undefined) {
457
+ if (!deepEqual(row, originalRow)) {
458
+ detached.push({
459
+ fieldId: previous.id,
460
+ label: previous.admin.label,
461
+ path: `${rowPath}.${index}`,
462
+ reason: "removed-block",
463
+ value: cloneFormValue(row),
464
+ });
465
+ }
466
+ continue;
467
+ }
468
+ keptCurrent.push(
469
+ reconcileRecord(
470
+ previousBlock.fields,
471
+ nextBlock.fields,
472
+ row,
473
+ originalRow,
474
+ detached,
475
+ `${rowPath}.${index}`,
476
+ initializeDefaults
477
+ ).values
478
+ );
479
+ }
480
+
481
+ const keptOriginal = originalRows.flatMap((row, index) => {
482
+ if (!isRecord(row)) return [];
483
+ const blockType = String(row.blockType ?? "");
484
+ const previousBlock = previousBlocks.get(blockType);
485
+ const nextBlock = nextBlocks.get(blockType);
486
+ if (previousBlock === undefined || nextBlock === undefined) return [];
487
+ return [
488
+ reconcileRecord(
489
+ previousBlock.fields,
490
+ nextBlock.fields,
491
+ row,
492
+ row,
493
+ [],
494
+ `${rowPath}.${index}`,
495
+ initializeDefaults
496
+ ).values,
497
+ ];
498
+ });
499
+
500
+ return {
501
+ currentPresent,
502
+ current: keptCurrent,
503
+ originalPresent,
504
+ original: keptOriginal,
505
+ };
506
+ }
507
+
508
+ function matchingRow(row: unknown, candidates: unknown[], fallbackIndex: number) {
509
+ if (isRecord(row) && typeof row._key === "string") {
510
+ const matched = candidates.find(
511
+ (candidate) => isRecord(candidate) && candidate._key === row._key
512
+ );
513
+ if (matched !== undefined) return matched;
514
+ }
515
+ return candidates[fallbackIndex];
516
+ }
517
+
518
+ function compatibleFields(previous: SchemaField, next: SchemaField) {
519
+ if (previous.type !== next.type) return false;
520
+ if (previous.type === "select") {
521
+ return Boolean(previous.select?.hasMany) === Boolean(next.select?.hasMany);
522
+ }
523
+ if (previous.type === "plugin") return previous.plugin?.key === next.plugin?.key;
524
+ return true;
525
+ }
526
+
527
+ function fieldDefault(field: SchemaField) {
528
+ if (field.type === "select" && field.select?.hasMany === true) {
529
+ if ((field.select.defaultValues?.length ?? 0) === 0) return { present: false };
530
+ return { present: true, value: field.select.defaultValues };
531
+ }
532
+ if (field.default === undefined) return { present: false };
533
+ if (field.type === "number") return { present: true, value: Number(field.default) };
534
+ if (field.type === "checkbox") return { present: true, value: field.default === "true" };
535
+ return { present: true, value: field.default };
536
+ }
537
+
538
+ function initialFieldValue(field: SchemaField): { present: boolean; value?: unknown } {
539
+ const direct = fieldDefault(field);
540
+ if (direct.present) return direct;
541
+ if (field.type === "group") {
542
+ const nested = initialFormValues(field.nested?.fields ?? []);
543
+ return Object.keys(nested).length === 0 ? { present: false } : { present: true, value: nested };
544
+ }
545
+ if (field.type === "array" && (field.nested?.minRows ?? 0) > 0) {
546
+ return {
547
+ present: true,
548
+ value: Array.from({ length: field.nested?.minRows ?? 0 }, () => ({
549
+ ...initialFormValues(field.nested?.fields ?? []),
550
+ _key: crypto.randomUUID(),
551
+ })),
552
+ };
553
+ }
554
+ return { present: false };
555
+ }
556
+
557
+ function detachedValue(
558
+ field: SchemaField,
559
+ value: unknown,
560
+ reason: DetachedDraftValue["reason"],
561
+ parentPath: string
562
+ ) {
563
+ return {
564
+ fieldId: field.id,
565
+ label: field.admin.label,
566
+ path: joinPath(parentPath, field.name),
567
+ reason,
568
+ value: cloneFormValue(value),
569
+ };
570
+ }
571
+
572
+ function absentFieldValue() {
573
+ return {
574
+ currentPresent: false,
575
+ current: undefined,
576
+ originalPresent: false,
577
+ original: undefined,
578
+ };
579
+ }
580
+
581
+ function copyReservedValue(source: FormValues, target: FormValues, name: string) {
582
+ if (Object.hasOwn(source, name)) target[name] = cloneFormValue(source[name]);
583
+ }
584
+
585
+ function joinPath(parent: string, child: string) {
586
+ return parent === "" ? child : `${parent}.${child}`;
587
+ }
588
+
589
+ function deepEqual(left: unknown, right: unknown) {
590
+ return JSON.stringify(left) === JSON.stringify(right);
591
+ }
592
+
593
+ function isRecord(value: unknown): value is FormValues {
594
+ return typeof value === "object" && value !== null && !Array.isArray(value);
595
+ }
596
+
597
+ export function cloneFormValues(values: FormValues): FormValues {
598
+ return cloneFormValue(values) as FormValues;
599
+ }
600
+
601
+ export function cloneFormValue(value: unknown): unknown {
602
+ if (Array.isArray(value)) return value.map(cloneFormValue);
603
+ if (isRecord(value)) {
604
+ return Object.fromEntries(
605
+ Object.entries(value).map(([name, child]) => [name, cloneFormValue(child)])
606
+ );
607
+ }
608
+ return value;
609
+ }