@rebasepro/admin 0.6.0 → 0.7.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 (149) hide show
  1. package/README.md +7 -3
  2. package/dist/{CollectionEditorDialog-jA-PLh6N.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
  3. package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
  4. package/dist/{PropertyEditView-DEUddmg_.js → PropertyEditView-C9yhLyja.js} +129 -15
  5. package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
  6. package/dist/{CollectionsStudioView-UKouBPOm.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
  7. package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
  8. package/dist/collection_editor/extensibility_types.d.ts +135 -0
  9. package/dist/collection_editor/index.d.ts +6 -0
  10. package/dist/collection_editor/serializable_types.d.ts +374 -0
  11. package/dist/collection_editor/serializable_utils.d.ts +50 -0
  12. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
  13. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
  14. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
  15. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
  16. package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
  17. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
  18. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
  19. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
  20. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
  21. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
  22. package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
  23. package/dist/collection_editor_ui.d.ts +4 -0
  24. package/dist/collection_editor_ui.js +33 -4
  25. package/dist/collection_editor_ui.js.map +1 -0
  26. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
  27. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
  28. package/dist/components/EntityEditView.d.ts +3 -2
  29. package/dist/components/RebaseCMS.d.ts +1 -1
  30. package/dist/editor/editor.d.ts +10 -4
  31. package/dist/editor.js +6 -4
  32. package/dist/editor.js.map +1 -1
  33. package/dist/form/EntityForm.d.ts +10 -9
  34. package/dist/form/EntityFormBinding.d.ts +27 -0
  35. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +2 -2
  36. package/dist/form/form_utils.d.ts +8 -0
  37. package/dist/form/index.d.ts +5 -1
  38. package/dist/{history-BL49TIDb.js → history-ChHgyyvM.js} +2 -2
  39. package/dist/{history-BL49TIDb.js.map → history-ChHgyyvM.js.map} +1 -1
  40. package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
  41. package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
  42. package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
  43. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  44. package/dist/hooks/navigation/utils.d.ts +1 -1
  45. package/dist/index.d.ts +3 -1
  46. package/dist/index.js +2 -2
  47. package/dist/routes/CustomViewRoute.d.ts +2 -1
  48. package/dist/{src-BJ8fkM8y.js → src-DOX2nOyh.js} +1634 -1330
  49. package/dist/src-DOX2nOyh.js.map +1 -0
  50. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  51. package/dist/types/components/EntityFormProps.d.ts +59 -0
  52. package/dist/types/fields.d.ts +6 -1
  53. package/dist/util/dataControllerAdapter.d.ts +30 -0
  54. package/dist/util/navigation_utils.d.ts +1 -1
  55. package/dist/util/propertyConfigMapper.d.ts +17 -0
  56. package/package.json +18 -18
  57. package/src/collection_editor/extensibility_types.ts +168 -0
  58. package/src/collection_editor/index.ts +46 -0
  59. package/src/collection_editor/serializable_types.ts +487 -0
  60. package/src/collection_editor/serializable_utils.ts +588 -0
  61. package/src/collection_editor/ui/AddKanbanColumnAction.tsx +2 -0
  62. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
  63. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
  64. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
  65. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
  66. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
  67. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
  68. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
  69. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
  70. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +155 -10
  71. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  72. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
  73. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
  74. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
  75. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
  76. package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
  77. package/src/collection_editor/useSafeSnackbarController.ts +9 -0
  78. package/src/collection_editor_ui.ts +6 -0
  79. package/src/components/ArrayContainer.tsx +5 -5
  80. package/src/components/DefaultDrawer.tsx +13 -3
  81. package/src/components/DrawerNavigationGroup.tsx +2 -2
  82. package/src/components/DrawerNavigationItem.tsx +4 -4
  83. package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
  84. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +14 -11
  85. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
  86. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
  87. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
  88. package/src/components/EntityEditView.tsx +5 -4
  89. package/src/components/EntityEditViewFormActions.tsx +32 -26
  90. package/src/components/HomePage/ContentHomePage.tsx +13 -4
  91. package/src/components/HomePage/NavigationCard.tsx +8 -14
  92. package/src/components/HomePage/NavigationGroup.tsx +1 -1
  93. package/src/components/HomePage/SmallNavigationCard.tsx +1 -5
  94. package/src/components/RebaseCMS.tsx +3 -2
  95. package/src/components/RebaseNavigation.tsx +71 -7
  96. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  97. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +28 -21
  98. package/src/components/SideDialogs.tsx +0 -7
  99. package/src/components/common/default_entity_actions.tsx +18 -10
  100. package/src/contexts/BreacrumbsContext.tsx +15 -6
  101. package/src/editor/editor.tsx +14 -8
  102. package/src/editor/selectors/ai-selector.tsx +0 -1
  103. package/src/form/EntityForm.tsx +191 -428
  104. package/src/form/EntityFormActions.tsx +33 -47
  105. package/src/form/EntityFormBinding.tsx +323 -0
  106. package/src/form/components/FormLayout.tsx +1 -1
  107. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +4 -4
  108. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
  109. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +1 -1
  110. package/src/form/form_utils.ts +163 -0
  111. package/src/form/index.tsx +14 -5
  112. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
  113. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
  114. package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
  115. package/src/hooks/navigation/useAsyncResolver.ts +100 -0
  116. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
  117. package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
  118. package/src/hooks/navigation/useNavigationResolution.ts +33 -0
  119. package/src/hooks/navigation/useResolvedCollections.ts +29 -71
  120. package/src/hooks/navigation/useResolvedViews.tsx +40 -70
  121. package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
  122. package/src/hooks/navigation/utils.ts +7 -4
  123. package/src/index.ts +7 -2
  124. package/src/routes/CustomViewRoute.tsx +14 -6
  125. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  126. package/src/types/components/EntityFormProps.tsx +68 -0
  127. package/src/types/fields.tsx +7 -1
  128. package/src/util/dataControllerAdapter.ts +93 -0
  129. package/src/util/navigation_utils.ts +1 -1
  130. package/src/util/propertyConfigMapper.ts +134 -0
  131. package/LICENSE +0 -21
  132. package/dist/CollectionEditorDialog-jA-PLh6N.js.map +0 -1
  133. package/dist/CollectionsStudioView-UKouBPOm.js.map +0 -1
  134. package/dist/PropertyEditView-DEUddmg_.js.map +0 -1
  135. package/dist/components/EntityCollectionView/Board.d.ts +0 -3
  136. package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
  137. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
  138. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
  139. package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
  140. package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
  141. package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
  142. package/dist/src-BJ8fkM8y.js.map +0 -1
  143. package/src/components/EntityCollectionView/Board.tsx +0 -475
  144. package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
  145. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
  146. package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
  147. package/src/components/EntityCollectionView/board_types.ts +0 -113
  148. package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
  149. package/src/hooks/navigation/useNavigationURLs.ts +0 -56
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Conversion utilities between the full Rebase types and their
3
+ * JSON-serializable counterparts.
4
+ *
5
+ * - `toSerializableCollection` / `toSerializableProperty` — strip non-serializable fields
6
+ * - `fromSerializableCollection` / `fromSerializableProperty` — reconstruct original types
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type {
12
+ EntityCollection,
13
+ Property,
14
+ StringProperty,
15
+ NumberProperty,
16
+ BooleanProperty,
17
+ DateProperty,
18
+ GeopointProperty,
19
+ ReferenceProperty,
20
+ RelationProperty,
21
+ ArrayProperty,
22
+ MapProperty,
23
+ VectorProperty,
24
+ BinaryProperty,
25
+ Properties,
26
+ StorageConfig,
27
+ BaseUIConfig,
28
+ } from "@rebasepro/types";
29
+
30
+ import type {
31
+ SerializableCollection,
32
+ SerializableProperty,
33
+ SerializableProperties,
34
+ SerializableStorageConfig,
35
+ SerializableBaseUIConfig,
36
+ SerializableBaseProperty,
37
+ SerializableStringProperty,
38
+ SerializableNumberProperty,
39
+ SerializableBooleanProperty,
40
+ SerializableDateProperty,
41
+ SerializableGeopointProperty,
42
+ SerializableReferenceProperty,
43
+ SerializableRelationProperty,
44
+ SerializableArrayProperty,
45
+ SerializableMapProperty,
46
+ SerializableVectorProperty,
47
+ SerializableBinaryProperty,
48
+ SerializableStringValidation,
49
+ SerializableDateValidation,
50
+ } from "./serializable_types";
51
+
52
+ // ═══════════════════════════════════════════════════════════════════════
53
+ // PROPERTY CONVERSION: Original → Serializable
54
+ // ═══════════════════════════════════════════════════════════════════════
55
+
56
+ /**
57
+ * Strip non-serializable fields from a `BaseUIConfig`.
58
+ * Removes `Field` and `Preview` (ComponentRef).
59
+ */
60
+ function toSerializableUIConfig(ui: BaseUIConfig | undefined): SerializableBaseUIConfig | undefined {
61
+ if (!ui) return undefined;
62
+ const { Field, Preview, ...rest } = ui as BaseUIConfig & Record<string, unknown>;
63
+ // Only return if there are remaining fields
64
+ const result: Record<string, unknown> = {};
65
+ for (const [key, value] of Object.entries(rest)) {
66
+ if (key !== "Field" && key !== "Preview" && value !== undefined) {
67
+ result[key] = value;
68
+ }
69
+ }
70
+ return Object.keys(result).length > 0 ? result as SerializableBaseUIConfig : undefined;
71
+ }
72
+
73
+ /**
74
+ * Convert a `StorageConfig` to its serializable form.
75
+ * Drops function variants of `storagePath`, `fileName`, and all function fields.
76
+ */
77
+ function toSerializableStorageConfig(storage: StorageConfig | undefined): SerializableStorageConfig | undefined {
78
+ if (!storage) return undefined;
79
+
80
+ // storagePath must be a string for serialization
81
+ const storagePath = typeof storage.storagePath === "string" ? storage.storagePath : undefined;
82
+ if (!storagePath) return undefined;
83
+
84
+ const result: SerializableStorageConfig = { storagePath };
85
+
86
+ if (storage.acceptedFiles) result.acceptedFiles = storage.acceptedFiles;
87
+ if (storage.imageResize) result.imageResize = storage.imageResize;
88
+ if (storage.metadata) result.metadata = storage.metadata;
89
+ if (typeof storage.fileName === "string") result.fileName = storage.fileName;
90
+ if (storage.includeBucketUrl !== undefined) result.includeBucketUrl = storage.includeBucketUrl;
91
+ if (storage.storeUrl !== undefined) result.storeUrl = storage.storeUrl;
92
+ if (storage.maxSize !== undefined) result.maxSize = storage.maxSize;
93
+ // processFile, postProcess, previewUrl are intentionally dropped (functions)
94
+
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Convert a RegExp or string `matches` validation to a plain string.
100
+ */
101
+ function serializeMatches(matches: string | RegExp | undefined): string | undefined {
102
+ if (matches === undefined) return undefined;
103
+ if (typeof matches === "string") return matches;
104
+ return matches.source;
105
+ }
106
+
107
+ /**
108
+ * Resolve a relation target to a string slug.
109
+ * Functions are called to extract the target; EntityCollection objects
110
+ * use their slug.
111
+ */
112
+ function resolveRelationTarget(target: RelationProperty["target"]): string | undefined {
113
+ if (!target) return undefined;
114
+ if (typeof target === "string") return target;
115
+ if (typeof target === "function") {
116
+ try {
117
+ const resolved = target();
118
+ if (typeof resolved === "string") return resolved;
119
+ if (resolved && typeof resolved === "object" && "slug" in resolved) {
120
+ return (resolved as EntityCollection).slug;
121
+ }
122
+ } catch {
123
+ // If the lazy resolver throws (e.g., circular dependency not yet ready), return undefined
124
+ }
125
+ }
126
+ return undefined;
127
+ }
128
+
129
+ /**
130
+ * Strip non-serializable base property fields.
131
+ * Returns a clean object with only the common serializable fields from BaseProperty.
132
+ * Validation is intentionally excluded — each property type handles its own.
133
+ */
134
+ function toSerializableBaseFields(property: Property): Omit<SerializableBaseProperty, "validation"> {
135
+ const result: Partial<Omit<SerializableBaseProperty, "validation">> = {
136
+ name: property.name,
137
+ };
138
+
139
+ if (property.description) result.description = property.description;
140
+ if (property.propertyConfig) result.propertyConfig = property.propertyConfig;
141
+ if (property.columnName) result.columnName = property.columnName;
142
+ if (property.defaultValue !== undefined) result.defaultValue = property.defaultValue;
143
+ if (property.conditions) result.conditions = property.conditions;
144
+ if (property.metadata) result.metadata = property.metadata;
145
+ // dynamicProps and callbacks are intentionally dropped (functions)
146
+
147
+ const ui = toSerializableUIConfig(property.ui);
148
+ if (ui) result.ui = ui;
149
+
150
+ return result as Omit<SerializableBaseProperty, "validation">;
151
+ }
152
+
153
+ /**
154
+ * Convert a single `Property` to its JSON-serializable form.
155
+ * Strips functions, converts RegExp to string, Date to ISO string.
156
+ */
157
+ export function toSerializableProperty(property: Property): SerializableProperty {
158
+ const base = toSerializableBaseFields(property);
159
+
160
+ switch (property.type) {
161
+ case "string": {
162
+ const sp = property as StringProperty;
163
+ const result: SerializableStringProperty = {
164
+ ...base,
165
+ type: "string",
166
+ };
167
+ if (sp.columnType) result.columnType = sp.columnType;
168
+ if (sp.isId !== undefined) result.isId = sp.isId;
169
+ if (sp.enum) result.enum = sp.enum;
170
+ if (sp.multiline) result.multiline = sp.multiline;
171
+ if (sp.markdown) result.markdown = sp.markdown;
172
+ if (sp.userSelect) result.userSelect = sp.userSelect;
173
+ if (sp.url) result.url = sp.url;
174
+ if (sp.email) result.email = sp.email;
175
+ if (sp.previewAsTag) result.previewAsTag = sp.previewAsTag;
176
+
177
+ // Convert validation.matches from RegExp to string
178
+ if (sp.validation) {
179
+ const { matches, ...restValidation } = sp.validation;
180
+ const serializedValidation: SerializableStringValidation = { ...restValidation };
181
+ const matchStr = serializeMatches(matches);
182
+ if (matchStr !== undefined) serializedValidation.matches = matchStr;
183
+ result.validation = serializedValidation;
184
+ }
185
+
186
+ // Storage: convert to serializable form
187
+ const storage = toSerializableStorageConfig(sp.storage);
188
+ if (storage) result.storage = storage;
189
+
190
+ // Reference sub-property
191
+ if (sp.reference) {
192
+ result.reference = toSerializableProperty(sp.reference) as SerializableReferenceProperty;
193
+ }
194
+
195
+ return result;
196
+ }
197
+
198
+ case "number": {
199
+ const np = property as NumberProperty;
200
+ const result: SerializableNumberProperty = {
201
+ ...base,
202
+ type: "number",
203
+ };
204
+ if (np.columnType) result.columnType = np.columnType;
205
+ if (np.validation) result.validation = np.validation;
206
+ if (np.isId !== undefined) result.isId = np.isId;
207
+ if (np.enum) result.enum = np.enum;
208
+ return result;
209
+ }
210
+
211
+ case "boolean": {
212
+ const bp = property as BooleanProperty;
213
+ const result: SerializableBooleanProperty = {
214
+ ...base,
215
+ type: "boolean",
216
+ };
217
+ if (bp.validation) result.validation = bp.validation;
218
+ return result;
219
+ }
220
+
221
+ case "date": {
222
+ const dp = property as DateProperty;
223
+ const result: SerializableDateProperty = {
224
+ ...base,
225
+ type: "date",
226
+ };
227
+ if (dp.columnType) result.columnType = dp.columnType;
228
+ if (dp.mode) result.mode = dp.mode;
229
+ if (dp.timezone) result.timezone = dp.timezone;
230
+ if (dp.autoValue) result.autoValue = dp.autoValue;
231
+ if (dp.clearable) result.clearable = dp.clearable;
232
+
233
+ // Convert Date validation min/max to ISO strings
234
+ if (dp.validation) {
235
+ const serializedValidation: SerializableDateValidation = {};
236
+ if (dp.validation.required !== undefined) serializedValidation.required = dp.validation.required;
237
+ if (dp.validation.requiredMessage) serializedValidation.requiredMessage = dp.validation.requiredMessage;
238
+ if (dp.validation.unique) serializedValidation.unique = dp.validation.unique;
239
+ if (dp.validation.uniqueInArray) serializedValidation.uniqueInArray = dp.validation.uniqueInArray;
240
+ if (dp.validation.min instanceof Date) {
241
+ serializedValidation.min = dp.validation.min.toISOString();
242
+ } else if (typeof dp.validation.min === "string") {
243
+ serializedValidation.min = dp.validation.min;
244
+ }
245
+ if (dp.validation.max instanceof Date) {
246
+ serializedValidation.max = dp.validation.max.toISOString();
247
+ } else if (typeof dp.validation.max === "string") {
248
+ serializedValidation.max = dp.validation.max;
249
+ }
250
+ result.validation = serializedValidation;
251
+ }
252
+ return result;
253
+ }
254
+
255
+ case "geopoint": {
256
+ const gp = property as GeopointProperty;
257
+ const result: SerializableGeopointProperty = {
258
+ ...base,
259
+ type: "geopoint",
260
+ };
261
+ if (gp.validation) result.validation = gp.validation;
262
+ return result;
263
+ }
264
+
265
+ case "reference": {
266
+ const rp = property as ReferenceProperty;
267
+ const result: SerializableReferenceProperty = {
268
+ ...base,
269
+ type: "reference",
270
+ };
271
+ if (rp.isId !== undefined) result.isId = rp.isId;
272
+ if (rp.path) result.path = rp.path;
273
+ if (rp.fixedFilter) result.fixedFilter = rp.fixedFilter;
274
+ if (rp.includeId !== undefined) result.includeId = rp.includeId;
275
+ if (rp.includeEntityLink !== undefined) result.includeEntityLink = rp.includeEntityLink;
276
+ return result;
277
+ }
278
+
279
+ case "relation": {
280
+ const rl = property as RelationProperty;
281
+ const result: SerializableRelationProperty = {
282
+ ...base,
283
+ type: "relation",
284
+ };
285
+ if (rl.isId !== undefined) result.isId = rl.isId;
286
+ // Resolve target to string
287
+ const target = resolveRelationTarget(rl.target);
288
+ if (target) result.target = target;
289
+ if (rl.cardinality) result.cardinality = rl.cardinality;
290
+ if (rl.direction) result.direction = rl.direction;
291
+ if (rl.inverseRelationName) result.inverseRelationName = rl.inverseRelationName;
292
+ if (rl.localKey) result.localKey = rl.localKey;
293
+ if (rl.foreignKeyOnTarget) result.foreignKeyOnTarget = rl.foreignKeyOnTarget;
294
+ if (rl.through) result.through = rl.through;
295
+ if (rl.joinPath) result.joinPath = rl.joinPath;
296
+ if (rl.onUpdate) result.onUpdate = rl.onUpdate;
297
+ if (rl.onDelete) result.onDelete = rl.onDelete;
298
+ if (rl.relationName) result.relationName = rl.relationName;
299
+ if (rl.fixedFilter) result.fixedFilter = rl.fixedFilter;
300
+ if (rl.includeId !== undefined) result.includeId = rl.includeId;
301
+ if (rl.includeEntityLink !== undefined) result.includeEntityLink = rl.includeEntityLink;
302
+ if (rl.widget) result.widget = rl.widget;
303
+ // overrides and relation (resolved) are dropped
304
+ return result;
305
+ }
306
+
307
+ case "array": {
308
+ const ap = property as ArrayProperty;
309
+ const result: SerializableArrayProperty = {
310
+ ...base,
311
+ type: "array",
312
+ };
313
+ if (ap.columnType) result.columnType = ap.columnType;
314
+ if (ap.validation) result.validation = ap.validation;
315
+ if (ap.sortable !== undefined) result.sortable = ap.sortable;
316
+ if (ap.canAddElements !== undefined) result.canAddElements = ap.canAddElements;
317
+
318
+ // Recursively serialize the "of" property
319
+ if (ap.of) {
320
+ if (Array.isArray(ap.of)) {
321
+ result.of = ap.of.map(toSerializableProperty);
322
+ } else {
323
+ result.of = toSerializableProperty(ap.of);
324
+ }
325
+ }
326
+
327
+ // Recursively serialize oneOf properties
328
+ if (ap.oneOf) {
329
+ result.oneOf = {
330
+ properties: toSerializableProperties(ap.oneOf.properties),
331
+ };
332
+ if (ap.oneOf.propertiesOrder) result.oneOf.propertiesOrder = ap.oneOf.propertiesOrder;
333
+ if (ap.oneOf.typeField) result.oneOf.typeField = ap.oneOf.typeField;
334
+ if (ap.oneOf.valueField) result.oneOf.valueField = ap.oneOf.valueField;
335
+ }
336
+ return result;
337
+ }
338
+
339
+ case "map": {
340
+ const mp = property as MapProperty;
341
+ const result: SerializableMapProperty = {
342
+ ...base,
343
+ type: "map",
344
+ };
345
+ if (mp.columnType) result.columnType = mp.columnType;
346
+ if (mp.validation) result.validation = mp.validation;
347
+ if (mp.propertiesOrder) result.propertiesOrder = mp.propertiesOrder;
348
+ if (mp.previewProperties) result.previewProperties = mp.previewProperties;
349
+ if (mp.keyValue) result.keyValue = mp.keyValue;
350
+
351
+ // Recursively serialize nested properties
352
+ if (mp.properties) {
353
+ result.properties = toSerializableProperties(mp.properties);
354
+ }
355
+ return result;
356
+ }
357
+
358
+ case "vector": {
359
+ const vp = property as VectorProperty;
360
+ const result: SerializableVectorProperty = {
361
+ ...base,
362
+ type: "vector",
363
+ dimensions: vp.dimensions,
364
+ };
365
+ if (vp.validation) result.validation = vp.validation;
366
+ return result;
367
+ }
368
+
369
+ case "binary": {
370
+ const bp = property as BinaryProperty;
371
+ const result: SerializableBinaryProperty = {
372
+ ...base,
373
+ type: "binary",
374
+ };
375
+ if (bp.validation) result.validation = bp.validation;
376
+ return result;
377
+ }
378
+
379
+ default:
380
+ // Exhaustive check: if new types are added, this will produce a compile error
381
+ return base as SerializableProperty;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Convert a `Properties` record to its serializable form.
387
+ */
388
+ export function toSerializableProperties(properties: Properties): SerializableProperties {
389
+ const result: SerializableProperties = {};
390
+ for (const [key, property] of Object.entries(properties)) {
391
+ result[key] = toSerializableProperty(property);
392
+ }
393
+ return result;
394
+ }
395
+
396
+ // ═══════════════════════════════════════════════════════════════════════
397
+ // COLLECTION CONVERSION: Original → Serializable
398
+ // ═══════════════════════════════════════════════════════════════════════
399
+
400
+ /**
401
+ * Convert an `EntityCollection` to its JSON-serializable form.
402
+ *
403
+ * Strips all non-serializable fields (functions, React nodes, class instances)
404
+ * while preserving the structural schema that the collection editor works with.
405
+ *
406
+ * The result is safe for `JSON.stringify()` and database storage.
407
+ */
408
+ export function toSerializableCollection(collection: EntityCollection): SerializableCollection {
409
+ const result: SerializableCollection = {
410
+ slug: collection.slug,
411
+ name: collection.name,
412
+ properties: toSerializableProperties(collection.properties),
413
+ };
414
+
415
+ // String fields
416
+ if (collection.singularName) result.singularName = collection.singularName;
417
+ if (collection.description) result.description = collection.description;
418
+ if (collection.group) result.group = collection.group;
419
+ if (collection.driver) result.driver = collection.driver;
420
+ if (collection.databaseId) result.databaseId = collection.databaseId;
421
+ if (collection.titleProperty) result.titleProperty = collection.titleProperty as string;
422
+ if (collection.ownerId) result.ownerId = collection.ownerId;
423
+ if (collection.metadata) result.metadata = collection.metadata;
424
+ if (collection.table) result.table = collection.table;
425
+ if ("schema" in collection && collection.schema) result.schema = collection.schema as string;
426
+ if (collection.orderProperty) result.orderProperty = collection.orderProperty as string;
427
+
428
+ // Icon: only keep string variant
429
+ if (typeof collection.icon === "string") result.icon = collection.icon;
430
+
431
+ // Array fields
432
+ if (collection.propertiesOrder) result.propertiesOrder = collection.propertiesOrder as string[];
433
+ if (collection.previewProperties) result.previewProperties = collection.previewProperties;
434
+ if (collection.listProperties) result.listProperties = collection.listProperties;
435
+ if (collection.enabledViews) result.enabledViews = collection.enabledViews;
436
+ if (collection.disableDefaultActions) result.disableDefaultActions = collection.disableDefaultActions;
437
+ if (collection.securityRules) result.securityRules = collection.securityRules;
438
+
439
+ // Enum-like fields
440
+ if (collection.openEntityMode) result.openEntityMode = collection.openEntityMode;
441
+ if (collection.defaultEntityAction) result.defaultEntityAction = collection.defaultEntityAction;
442
+ if (collection.defaultViewMode) result.defaultViewMode = collection.defaultViewMode;
443
+ if (collection.defaultSize) result.defaultSize = collection.defaultSize;
444
+ if (collection.localChangesBackup !== undefined) result.localChangesBackup = collection.localChangesBackup;
445
+
446
+ // Kanban
447
+ if (collection.kanban) result.kanban = collection.kanban as SerializableCollection["kanban"];
448
+
449
+ // Filters and sorting
450
+ if (collection.fixedFilter) result.fixedFilter = collection.fixedFilter as SerializableCollection["fixedFilter"];
451
+ if (collection.defaultFilter) result.defaultFilter = collection.defaultFilter as SerializableCollection["defaultFilter"];
452
+ if (collection.filterPresets) result.filterPresets = collection.filterPresets as SerializableCollection["filterPresets"];
453
+ if (collection.sort) result.sort = collection.sort as SerializableCollection["sort"];
454
+
455
+ // Numeric / boolean flags
456
+ if (collection.pagination !== undefined) result.pagination = collection.pagination;
457
+ if (collection.selectionEnabled !== undefined) result.selectionEnabled = collection.selectionEnabled;
458
+ if (collection.inlineEditing !== undefined) result.inlineEditing = collection.inlineEditing;
459
+ if (collection.hideFromNavigation !== undefined) result.hideFromNavigation = collection.hideFromNavigation;
460
+ if (collection.hideIdFromForm !== undefined) result.hideIdFromForm = collection.hideIdFromForm;
461
+ if (collection.hideIdFromCollection !== undefined) result.hideIdFromCollection = collection.hideIdFromCollection;
462
+ if (collection.formAutoSave !== undefined) result.formAutoSave = collection.formAutoSave;
463
+ if (collection.alwaysApplyDefaultValues !== undefined) result.alwaysApplyDefaultValues = collection.alwaysApplyDefaultValues;
464
+ if (collection.includeJsonView !== undefined) result.includeJsonView = collection.includeJsonView;
465
+ if (collection.history !== undefined) result.history = collection.history;
466
+ if (collection.sideDialogWidth !== undefined) result.sideDialogWidth = collection.sideDialogWidth;
467
+
468
+ // Exportable: only keep boolean variant
469
+ if (typeof collection.exportable === "boolean") result.exportable = collection.exportable;
470
+
471
+ // Auth: only keep boolean variant
472
+ if (typeof collection.auth === "boolean") result.auth = collection.auth;
473
+
474
+ // defaultSelectedView: only keep string variant
475
+ if (typeof collection.defaultSelectedView === "string") result.defaultSelectedView = collection.defaultSelectedView;
476
+
477
+ return result;
478
+ }
479
+
480
+ // ═══════════════════════════════════════════════════════════════════════
481
+ // PROPERTY CONVERSION: Serializable → Original
482
+ // ═══════════════════════════════════════════════════════════════════════
483
+
484
+ /**
485
+ * Convert a serializable property back to the original `Property` type.
486
+ *
487
+ * This is a mostly pass-through operation since serializable properties
488
+ * are a subset of the original types. The main conversions are:
489
+ * - `validation.matches` (string) stays as string (compatible with Property)
490
+ * - `validation.min/max` on dates: ISO strings → Date objects
491
+ */
492
+ export function fromSerializableProperty(serialized: SerializableProperty): Property {
493
+ switch (serialized.type) {
494
+ case "date": {
495
+ const sp = serialized as SerializableDateProperty;
496
+ const { validation: dateValidation, ...dateRest } = sp;
497
+ const result: Record<string, unknown> = { ...dateRest, type: "date" };
498
+ // Convert ISO string dates back to Date objects
499
+ if (dateValidation) {
500
+ const convertedValidation: Record<string, unknown> = { ...dateValidation };
501
+ if (typeof dateValidation.min === "string") {
502
+ convertedValidation.min = new Date(dateValidation.min);
503
+ }
504
+ if (typeof dateValidation.max === "string") {
505
+ convertedValidation.max = new Date(dateValidation.max);
506
+ }
507
+ result.validation = convertedValidation;
508
+ }
509
+ return result as unknown as DateProperty;
510
+ }
511
+
512
+ case "array": {
513
+ const sp = serialized as SerializableArrayProperty;
514
+ const result = { ...sp } as unknown as ArrayProperty;
515
+ // Recursively convert "of" property
516
+ if (sp.of) {
517
+ if (Array.isArray(sp.of)) {
518
+ result.of = sp.of.map(fromSerializableProperty);
519
+ } else {
520
+ result.of = fromSerializableProperty(sp.of);
521
+ }
522
+ }
523
+ // Recursively convert oneOf properties
524
+ if (sp.oneOf) {
525
+ result.oneOf = {
526
+ ...sp.oneOf,
527
+ properties: fromSerializableProperties(sp.oneOf.properties),
528
+ };
529
+ }
530
+ return result;
531
+ }
532
+
533
+ case "map": {
534
+ const sp = serialized as SerializableMapProperty;
535
+ const result = { ...sp } as unknown as MapProperty;
536
+ // Recursively convert nested properties
537
+ if (sp.properties) {
538
+ result.properties = fromSerializableProperties(sp.properties);
539
+ }
540
+ return result;
541
+ }
542
+
543
+ case "string": {
544
+ const sp = serialized as SerializableStringProperty;
545
+ const result = { ...sp } as unknown as StringProperty;
546
+ // Convert reference sub-property
547
+ if (sp.reference) {
548
+ result.reference = fromSerializableProperty(sp.reference) as ReferenceProperty;
549
+ }
550
+ return result;
551
+ }
552
+
553
+ default:
554
+ // For all other types, the serializable form is already a valid Property
555
+ return serialized as unknown as Property;
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Convert a `SerializableProperties` record back to `Properties`.
561
+ */
562
+ export function fromSerializableProperties(serialized: SerializableProperties): Properties {
563
+ const result: Properties = {};
564
+ for (const [key, property] of Object.entries(serialized)) {
565
+ result[key] = fromSerializableProperty(property);
566
+ }
567
+ return result;
568
+ }
569
+
570
+ // ═══════════════════════════════════════════════════════════════════════
571
+ // COLLECTION CONVERSION: Serializable → Original
572
+ // ═══════════════════════════════════════════════════════════════════════
573
+
574
+ /**
575
+ * Convert a `SerializableCollection` back to an `EntityCollection`.
576
+ *
577
+ * The result will NOT contain any of the non-serializable fields
578
+ * (callbacks, entityActions, etc.) — those must be re-attached by the
579
+ * consumer if needed.
580
+ */
581
+ export function fromSerializableCollection(serialized: SerializableCollection): EntityCollection {
582
+ const { properties, ...rest } = serialized;
583
+
584
+ return {
585
+ ...rest,
586
+ properties: fromSerializableProperties(properties),
587
+ } as EntityCollection;
588
+ }
@@ -15,6 +15,7 @@ import {
15
15
  Dialog,
16
16
  DialogActions,
17
17
  DialogContent,
18
+ DialogTitle,
18
19
  IconButton,
19
20
  PlusIcon,
20
21
  TextField,
@@ -159,6 +160,7 @@ export function AddKanbanColumnAction({
159
160
  </div>
160
161
 
161
162
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
163
+ <DialogTitle hidden>{t("studio_add_kanban_column_title")}</DialogTitle>
162
164
  <DialogContent className="max-w-md">
163
165
  <Typography variant="h6" className="mb-4">
164
166
  {t("studio_add_kanban_column_title")}
@@ -2,7 +2,7 @@
2
2
  import { AIIcon } from "@rebasepro/core";
3
3
  import { useCollectionRegistryController } from "../../_cms_internals";
4
4
  import React, { useState } from "react";
5
- import { useSnackbarController } from "@rebasepro/core";
5
+ import { useSafeSnackbarController } from "../../useSafeSnackbarController";
6
6
  import { EntityCollection } from "@rebasepro/types";
7
7
  import {
8
8
  Button,
@@ -76,7 +76,7 @@ export function AICollectionGeneratorPopover({
76
76
 
77
77
  const collectionRegistry = useCollectionRegistryController();
78
78
  const configController = useCollectionsConfigController();
79
- const snackbarController = useSnackbarController();
79
+ const snackbarController = useSafeSnackbarController();
80
80
 
81
81
  const existingCollections = collectionRegistry.collections ?? [];
82
82
 
@@ -117,7 +117,7 @@ export function AICollectionGeneratorPopover({
117
117
  mode,
118
118
  operationsCount: result.operations?.length
119
119
  });
120
- snackbarController.open({
120
+ snackbarController?.open({
121
121
  type: "success",
122
122
  message: existingCollection
123
123
  ? "Collection updated with AI suggestions"
@@ -133,7 +133,7 @@ export function AICollectionGeneratorPopover({
133
133
  mode,
134
134
  error: errorMessage
135
135
  });
136
- snackbarController.open({
136
+ snackbarController?.open({
137
137
  type: "error",
138
138
  message: errorMessage
139
139
  });