@rebasepro/admin 0.6.1 → 0.8.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 (187) hide show
  1. package/dist/{CollectionEditorDialog-HNXGxXdO.js → CollectionEditorDialog-BO_aQ-Z4.js} +236 -155
  2. package/dist/CollectionEditorDialog-BO_aQ-Z4.js.map +1 -0
  3. package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-DKzOpMjR.js} +190 -50
  4. package/dist/PropertyEditView-DKzOpMjR.js.map +1 -0
  5. package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-BQTIExjA.js} +61 -39
  6. package/dist/RouterCollectionsStudioView-BQTIExjA.js.map +1 -0
  7. package/dist/collection_editor/_cms_internals.d.ts +1 -1
  8. package/dist/collection_editor/api/generateCollectionApi.d.ts +3 -2
  9. package/dist/collection_editor/extensibility_types.d.ts +135 -0
  10. package/dist/collection_editor/index.d.ts +6 -0
  11. package/dist/collection_editor/serializable_types.d.ts +370 -0
  12. package/dist/collection_editor/serializable_utils.d.ts +50 -0
  13. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +3 -3
  14. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
  15. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
  16. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
  17. package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
  18. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
  19. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
  20. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
  21. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
  22. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
  23. package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
  24. package/dist/collection_editor_ui.d.ts +4 -0
  25. package/dist/collection_editor_ui.js +33 -4
  26. package/dist/collection_editor_ui.js.map +1 -0
  27. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  28. package/dist/components/EntityCollectionTable/index.d.ts +1 -1
  29. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
  30. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
  31. package/dist/components/EntityEditView.d.ts +3 -2
  32. package/dist/components/RebaseCMS.d.ts +1 -1
  33. package/dist/components/SelectableTable/SelectionStore.d.ts +12 -21
  34. package/dist/components/field_configs.d.ts +1 -1
  35. package/dist/editor/editor.d.ts +0 -6
  36. package/dist/editor.js +1 -3
  37. package/dist/editor.js.map +1 -1
  38. package/dist/form/EntityForm.d.ts +10 -9
  39. package/dist/form/EntityFormBinding.d.ts +27 -0
  40. package/dist/form/components/StorageUploadProgress.d.ts +6 -1
  41. package/dist/form/form_utils.d.ts +8 -0
  42. package/dist/form/index.d.ts +5 -2
  43. package/dist/{history-BO6VT85a.js → history-B3cqBzEV.js} +2 -2
  44. package/dist/{history-BO6VT85a.js.map → history-B3cqBzEV.js.map} +1 -1
  45. package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
  46. package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
  47. package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
  48. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  49. package/dist/hooks/navigation/utils.d.ts +1 -1
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.js +2 -2
  52. package/dist/preview/components/StorageThumbnail.d.ts +3 -1
  53. package/dist/routes/CustomViewRoute.d.ts +2 -1
  54. package/dist/{src-BYniefVu.js → src-cxV1ODRG.js} +1731 -1710
  55. package/dist/src-cxV1ODRG.js.map +1 -0
  56. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  57. package/dist/types/components/EntityFormProps.d.ts +59 -0
  58. package/dist/types/components/PropertyPreviewProps.d.ts +2 -1
  59. package/dist/types/fields.d.ts +10 -7
  60. package/dist/util/dataControllerAdapter.d.ts +30 -0
  61. package/dist/util/navigation_utils.d.ts +1 -1
  62. package/dist/util/propertyConfigMapper.d.ts +17 -0
  63. package/package.json +8 -8
  64. package/src/collection_editor/_cms_internals.ts +1 -1
  65. package/src/collection_editor/api/generateCollectionApi.ts +3 -2
  66. package/src/collection_editor/extensibility_types.ts +168 -0
  67. package/src/collection_editor/index.ts +46 -0
  68. package/src/collection_editor/pgColumnToProperty.ts +2 -2
  69. package/src/collection_editor/serializable_types.ts +474 -0
  70. package/src/collection_editor/serializable_utils.ts +576 -0
  71. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +6 -6
  72. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +170 -89
  73. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
  74. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
  75. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
  76. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
  77. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
  78. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
  79. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +154 -18
  80. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  81. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
  82. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
  83. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
  84. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
  85. package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +35 -1
  86. package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
  87. package/src/collection_editor/ui/collection_editor/utils/supported_fields.tsx +0 -1
  88. package/src/collection_editor/ui/collection_editor/utils/update_property_for_widget.ts +7 -29
  89. package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
  90. package/src/collection_editor/useSafeSnackbarController.ts +9 -0
  91. package/src/collection_editor_ui.ts +6 -0
  92. package/src/components/DefaultDrawer.tsx +14 -4
  93. package/src/components/DrawerNavigationGroup.tsx +2 -2
  94. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +11 -9
  95. package/src/components/EntityCollectionTable/fields/TableMultipleRelationField.tsx +2 -1
  96. package/src/components/EntityCollectionTable/fields/TableRelationField.tsx +2 -1
  97. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +23 -20
  98. package/src/components/EntityCollectionTable/index.tsx +1 -1
  99. package/src/components/EntityCollectionTable/table_bindings.tsx +24 -44
  100. package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
  101. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +13 -12
  102. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
  103. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +66 -184
  104. package/src/components/EntityCollectionView/EntityCollectionView.tsx +8 -25
  105. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -1
  106. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
  107. package/src/components/EntityCollectionView/useBoardDataController.tsx +12 -40
  108. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +3 -3
  109. package/src/components/EntityDetailView.tsx +2 -2
  110. package/src/components/EntityEditView.tsx +7 -6
  111. package/src/components/EntityEditViewFormActions.tsx +35 -29
  112. package/src/components/HomePage/ContentHomePage.tsx +14 -5
  113. package/src/components/RebaseCMS.tsx +3 -3
  114. package/src/components/RebaseNavigation.tsx +74 -9
  115. package/src/components/RelationSelector.tsx +2 -2
  116. package/src/components/SelectableTable/SelectableTable.tsx +13 -5
  117. package/src/components/SelectableTable/SelectionStore.ts +18 -48
  118. package/src/components/SideDialogs.tsx +0 -7
  119. package/src/components/common/default_entity_actions.tsx +22 -11
  120. package/src/components/field_configs.tsx +0 -14
  121. package/src/contexts/BreacrumbsContext.tsx +15 -6
  122. package/src/editor/editor.tsx +4 -7
  123. package/src/editor/selectors/ai-selector.tsx +0 -1
  124. package/src/form/EntityForm.tsx +191 -428
  125. package/src/form/EntityFormActions.tsx +35 -49
  126. package/src/form/EntityFormBinding.tsx +323 -0
  127. package/src/form/components/FormLayout.tsx +1 -1
  128. package/src/form/components/StorageUploadProgress.tsx +9 -2
  129. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +12 -1
  130. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +4 -4
  131. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
  132. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -7
  133. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +19 -15
  134. package/src/form/form_utils.ts +163 -0
  135. package/src/form/index.tsx +14 -6
  136. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
  137. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
  138. package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
  139. package/src/hooks/navigation/useAsyncResolver.ts +100 -0
  140. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
  141. package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
  142. package/src/hooks/navigation/useNavigationResolution.ts +33 -0
  143. package/src/hooks/navigation/useResolvedCollections.ts +29 -71
  144. package/src/hooks/navigation/useResolvedViews.tsx +40 -70
  145. package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
  146. package/src/hooks/navigation/utils.ts +7 -4
  147. package/src/index.ts +6 -1
  148. package/src/preview/PropertyPreview.tsx +1 -16
  149. package/src/preview/components/StorageThumbnail.tsx +24 -7
  150. package/src/routes/CustomViewRoute.tsx +14 -6
  151. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  152. package/src/types/components/EntityFormProps.tsx +68 -0
  153. package/src/types/components/PropertyPreviewProps.tsx +1 -1
  154. package/src/types/fields.tsx +9 -8
  155. package/src/util/dataControllerAdapter.ts +93 -0
  156. package/src/util/navigation_utils.ts +1 -1
  157. package/src/util/previews.ts +20 -0
  158. package/src/util/propertyConfigMapper.ts +134 -0
  159. package/src/util/property_utils.tsx +1 -2
  160. package/dist/CollectionEditorDialog-HNXGxXdO.js.map +0 -1
  161. package/dist/CollectionsStudioView-B1mN33GB.js.map +0 -1
  162. package/dist/PropertyEditView-C2wp8kQ9.js.map +0 -1
  163. package/dist/components/EntityCollectionTable/fields/VirtualTableDateField.d.ts +0 -12
  164. package/dist/components/EntityCollectionTable/fields/VirtualTableInput.d.ts +0 -10
  165. package/dist/components/EntityCollectionTable/fields/VirtualTableNumberInput.d.ts +0 -9
  166. package/dist/components/EntityCollectionTable/fields/VirtualTableSwitch.d.ts +0 -8
  167. package/dist/components/EntityCollectionView/Board.d.ts +0 -3
  168. package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
  169. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
  170. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
  171. package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
  172. package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +0 -11
  173. package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
  174. package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
  175. package/dist/src-BYniefVu.js.map +0 -1
  176. package/src/components/EntityCollectionTable/fields/VirtualTableDateField.tsx +0 -39
  177. package/src/components/EntityCollectionTable/fields/VirtualTableInput.tsx +0 -90
  178. package/src/components/EntityCollectionTable/fields/VirtualTableNumberInput.tsx +0 -83
  179. package/src/components/EntityCollectionTable/fields/VirtualTableSwitch.tsx +0 -32
  180. package/src/components/EntityCollectionView/Board.tsx +0 -475
  181. package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
  182. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
  183. package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
  184. package/src/components/EntityCollectionView/board_types.ts +0 -113
  185. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +0 -141
  186. package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
  187. package/src/hooks/navigation/useNavigationURLs.ts +0 -56
@@ -1,15 +1,13 @@
1
1
  import { useEntitySelectionDialog } from "../../hooks/useEntitySelectionDialog";
2
2
  import type { FieldProps } from "../../types/fields";
3
3
  import type { RelationProperty } from "@rebasepro/types";
4
+ import { Entity, getCollectionDataPath, getDataSourceCapabilities, Relation } from "@rebasepro/types";
4
5
  import React, { useCallback } from "react";
5
- import { Entity, EntityRelation, getDataSourceCapabilities, Relation } from "@rebasepro/types";
6
6
  import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
7
7
  import { EntityPreviewContainer } from "../../components/EntityPreview";
8
- import { IconForView } from "@rebasepro/core";
8
+ import { ErrorView, IconForView } from "@rebasepro/core";
9
9
  import { getIconForProperty } from "../../util/property_utils";
10
- import { getRelationFrom, resolveRelationProperty, normalizeToEntityRelation } from "@rebasepro/common";
11
-
12
- import { ErrorView } from "@rebasepro/core";
10
+ import { getRelationFrom, normalizeToEntityRelation, resolveRelationProperty } from "@rebasepro/common";
13
11
  import { cls } from "@rebasepro/ui";
14
12
  import { RelationPreview } from "../../preview";
15
13
  import { RelationSelector } from "../../components/RelationSelector";
@@ -22,7 +20,7 @@ export function RelationFieldBinding(props: FieldProps<RelationProperty>) {
22
20
  throw Error("RelationFieldBinding expected a property containing a relation");
23
21
  }
24
22
  const collection = context.collection;
25
- const capabilities = collection ? getDataSourceCapabilities(collection.driver) : undefined;
23
+ const capabilities = collection ? getDataSourceCapabilities(collection.engine) : undefined;
26
24
  if (!collection || !capabilities?.supportsRelations || !("relations" in collection) || !collection.relations) {
27
25
  throw Error("RelationFieldBinding expected a collection with relations support");
28
26
  }
@@ -124,7 +122,7 @@ function SingleRelationFieldBinding({
124
122
 
125
123
  const referenceDialogController = useEntitySelectionDialog({
126
124
  multiselect: false,
127
- path: collection.slug,
125
+ path: getCollectionDataPath(collection),
128
126
  collection,
129
127
  onSingleEntitySelected,
130
128
  selectedEntityIds: validValue && normalizedValue ? [normalizedValue.id] : undefined,
@@ -10,6 +10,7 @@ import { FieldHelperText, LabelWithIconAndTooltip } from "../components";
10
10
  import { isReadOnly } from "@rebasepro/common";
11
11
  import { getIconForProperty } from "../../util/property_utils";
12
12
  import { useAuthController, useSnackbarController, useStorageSource, useTranslation } from "@rebasepro/core";
13
+ import { StorageSourceContext } from "@rebasepro/core";
13
14
  import {
14
15
  closestCenter,
15
16
  DndContext,
@@ -62,7 +63,7 @@ export function StorageUploadFieldBinding({
62
63
 
63
64
  const authController = useAuthController();
64
65
 
65
- const storageSource = useStorageSource(context.collection);
66
+ const storageSource = useStorageSource();
66
67
  const disabled = isReadOnly(property) || !!property.ui?.disabled || isSubmitting || context.disabled;
67
68
 
68
69
  const {
@@ -72,7 +73,8 @@ export function StorageUploadFieldBinding({
72
73
  storage,
73
74
  onFileUploadComplete,
74
75
  storagePathBuilder,
75
- multipleFilesSupported
76
+ multipleFilesSupported,
77
+ resolvedStorageSource
76
78
  } = useStorageUploadController({
77
79
  entityValues: context.values,
78
80
  entityId: context.entityId,
@@ -103,19 +105,21 @@ export function StorageUploadFieldBinding({
103
105
  title={property.name ?? propertyKey}
104
106
  className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"}/>}
105
107
 
106
- <StorageUpload
107
- value={internalValue}
108
- name={propertyKey}
109
- disabled={disabled ?? false}
110
- autoFocus={autoFocus ?? false}
111
- property={property}
112
- onChange={setValue}
113
- setInternalValue={setInternalValue}
114
- onFilesAdded={onFilesAdded}
115
- onFileUploadComplete={onFileUploadComplete}
116
- storagePathBuilder={storagePathBuilder}
117
- storage={storage}
118
- multipleFilesSupported={multipleFilesSupported}/>
108
+ <StorageSourceContext.Provider value={resolvedStorageSource}>
109
+ <StorageUpload
110
+ value={internalValue}
111
+ name={propertyKey}
112
+ disabled={disabled ?? false}
113
+ autoFocus={autoFocus ?? false}
114
+ property={property}
115
+ onChange={setValue}
116
+ setInternalValue={setInternalValue}
117
+ onFilesAdded={onFilesAdded}
118
+ onFileUploadComplete={onFileUploadComplete}
119
+ storagePathBuilder={storagePathBuilder}
120
+ storage={storage}
121
+ multipleFilesSupported={multipleFilesSupported}/>
122
+ </StorageSourceContext.Provider>
119
123
 
120
124
  <FieldHelperText includeDescription={includeDescription}
121
125
  showError={showError}
@@ -0,0 +1,163 @@
1
+ import type { EntityCollection, PropertyConfig } from "@rebasepro/types";
2
+ import type { AuthController, Entity, EntityStatus, EntityValues } from "@rebasepro/types";
3
+ import { deepEqual as equal } from "fast-equals";
4
+ import { getIn, setIn } from "@rebasepro/formex";
5
+ import { getDefaultValuesFor } from "@rebasepro/common";
6
+ import { isObject, mergeDeep } from "@rebasepro/utils";
7
+ import { z } from "zod";
8
+
9
+ // extract touched values for nested touched trees and map to current values
10
+ export function extractTouchedValues(values: unknown, touched: Record<string, boolean>): Record<string, unknown> {
11
+ let acc: Record<string, unknown> = {};
12
+ if (!touched || typeof touched !== "object") {
13
+ return acc;
14
+ }
15
+
16
+ Object.entries(touched).forEach(([key, value]) => {
17
+ if (value) {
18
+ acc = setIn(acc, key, getIn(values, key)) as Record<string, unknown>;
19
+ }
20
+ })
21
+
22
+ return acc;
23
+ }
24
+
25
+ /**
26
+ * Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
27
+ * This prevents ghost containers created by `setIn` intermediate path construction
28
+ * (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
29
+ * from falsely triggering the unsaved local changes indicator.
30
+ */
31
+ /**
32
+ * Check if a value is semantically empty (null, undefined, or empty string).
33
+ */
34
+ function isSemanticEmpty(v: unknown): boolean {
35
+ return v === null || v === undefined || v === "";
36
+ }
37
+
38
+ export function removeEmptyContainers(obj: unknown): unknown {
39
+ if (Array.isArray(obj)) {
40
+ const cleaned = obj.map(removeEmptyContainers);
41
+ // Keep arrays even if they contain only nulls/undefined — that's intentional data
42
+ return cleaned;
43
+ }
44
+ if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
45
+ const result: Record<string, unknown> = {};
46
+ for (const key of Object.keys(obj)) {
47
+ const cleaned = removeEmptyContainers((obj as Record<string, unknown>)[key]);
48
+ // Skip empty plain objects
49
+ if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned)
50
+ && Object.getPrototypeOf(cleaned) === Object.prototype
51
+ && Object.keys(cleaned).length === 0) {
52
+ continue;
53
+ }
54
+ result[key] = cleaned;
55
+ }
56
+ // After cleaning, check if all remaining values are semantically empty
57
+ // (null, undefined, or ""). This catches ghost objects like {type: "", value: null}
58
+ // created by oneOf block initialization that aren't meaningful changes.
59
+ if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) {
60
+ return {};
61
+ }
62
+ return result;
63
+ }
64
+ return obj;
65
+ }
66
+
67
+ export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
68
+ const changes: Partial<T> = {};
69
+
70
+ if (!source) {
71
+ return {};
72
+ }
73
+ if (!comparison) {
74
+ return source;
75
+ }
76
+
77
+ const allKeys = Array.from(new Set([...Object.keys(source), ...Object.keys(comparison)]));
78
+
79
+ for (const key of allKeys) {
80
+ const sourceValue = (source as Record<string, unknown>)[key];
81
+ const comparisonValue = (comparison as Record<string, unknown>)[key];
82
+
83
+ if (equal(sourceValue, comparisonValue)) {
84
+ continue;
85
+ }
86
+
87
+ const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
88
+ const comparisonHasKey = comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key);
89
+
90
+ if (comparisonHasKey && !sourceHasKey) {
91
+ (changes as Record<string, unknown>)[key] = undefined;
92
+ } else if (Array.isArray(sourceValue)) {
93
+ const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
94
+ if (sourceValue.length !== comparisonArray.length) {
95
+ (changes as Record<string, unknown>)[key] = sourceValue;
96
+ continue;
97
+ }
98
+ const hasChanges = sourceValue.some((item, index) => !equal(item, comparisonArray[index]));
99
+ if (hasChanges) {
100
+ (changes as Record<string, unknown>)[key] = sourceValue;
101
+ }
102
+ } else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
103
+ const nestedChanges = getChanges(sourceValue, comparisonValue);
104
+ if (Object.keys(nestedChanges).length > 0) {
105
+ (changes as Record<string, unknown>)[key] = nestedChanges;
106
+ }
107
+ } else {
108
+ (changes as Record<string, unknown>)[key] = sourceValue;
109
+ }
110
+ }
111
+
112
+ return changes;
113
+ }
114
+
115
+ export function getInitialEntityValues<M extends Record<string, unknown>>(
116
+ authController: AuthController,
117
+ collection: EntityCollection,
118
+ path: string,
119
+ status: "new" | "existing" | "copy",
120
+ entity: Entity<M> | undefined,
121
+ propertyConfigs?: Record<string, PropertyConfig>
122
+ ): Partial<EntityValues<M>> {
123
+ const properties = collection.properties;
124
+ if ((status === "existing" || status === "copy") && entity) {
125
+ let values: Partial<EntityValues<M>>;
126
+ if (!collection.alwaysApplyDefaultValues) {
127
+ values = entity.values ?? getDefaultValuesFor(properties);
128
+ } else {
129
+ const defaultValues = getDefaultValuesFor(properties);
130
+ values = mergeDeep(defaultValues, entity.values ?? {});
131
+ }
132
+ // When copying, clear ID fields so the database generates new IDs
133
+ if (status === "copy") {
134
+ const result = { ...values };
135
+ for (const [key, property] of Object.entries(properties)) {
136
+ if (property && "isId" in property && property.isId) {
137
+ delete (result as Record<string, unknown>)[key];
138
+ }
139
+ }
140
+ return result;
141
+ }
142
+ return values;
143
+ } else if (status === "new") {
144
+ return getDefaultValuesFor(properties);
145
+ } else {
146
+ console.error({
147
+ status,
148
+ entity
149
+ });
150
+ throw new Error("Form has not been initialised with the correct parameters");
151
+ }
152
+ }
153
+
154
+ export function zodToFormErrors(zodError: z.ZodError): Record<string, string> {
155
+ let errors: Record<string, string> = {};
156
+ for (const issue of zodError.issues) {
157
+ const path = issue.path.join(".");
158
+ if (path && !getIn(errors, path)) {
159
+ errors = setIn(errors, path, issue.message) as Record<string, string>;
160
+ }
161
+ }
162
+ return errors;
163
+ }
@@ -1,8 +1,8 @@
1
- export {
2
- EntityForm,
3
- zodToFormErrors
4
- } from "./EntityForm";
5
- // EntityFormProps is exported from @rebasepro/types
1
+ export { EntityForm } from "./EntityForm";
2
+ export type { EntityFormProps, OnUpdateParams } from "../types/components/EntityFormProps";
3
+
4
+ export { EntityFormBinding } from "./EntityFormBinding";
5
+ export type { EntityFormBindingProps } from "./EntityFormBinding";
6
6
 
7
7
  export { SelectFieldBinding } from "./field_bindings/SelectFieldBinding";
8
8
  export { MultiSelectFieldBinding } from "./field_bindings/MultiSelectFieldBinding";
@@ -12,7 +12,6 @@ export { TextFieldBinding } from "./field_bindings/TextFieldBinding";
12
12
  export { SwitchFieldBinding } from "./field_bindings/SwitchFieldBinding";
13
13
  export { DateTimeFieldBinding } from "./field_bindings/DateTimeFieldBinding";
14
14
  export { ReferenceFieldBinding } from "./field_bindings/ReferenceFieldBinding";
15
- export { ReferenceAsStringFieldBinding } from "./field_bindings/ReferenceAsStringFieldBinding";
16
15
  export { MapFieldBinding } from "./field_bindings/MapFieldBinding";
17
16
  export { KeyValueFieldBinding } from "./field_bindings/KeyValueFieldBinding";
18
17
  export { RepeatFieldBinding } from "./field_bindings/RepeatFieldBinding";
@@ -26,3 +25,12 @@ export * from "./components";
26
25
 
27
26
  export { PropertyFieldBinding } from "./PropertyFieldBinding";
28
27
  export * from "./useClearRestoreValue";
28
+
29
+ // Shared form utilities
30
+ export {
31
+ extractTouchedValues,
32
+ removeEmptyContainers,
33
+ getChanges,
34
+ getInitialEntityValues,
35
+ zodToFormErrors
36
+ } from "./form_utils";
@@ -1,6 +1,6 @@
1
1
  import type { EntityCollection } from "@rebasepro/types";
2
2
  import React, { createContext } from "react";
3
- import { CollectionRegistryController, EntityReference } from "@rebasepro/types";
3
+ import { CollectionRegistryController } from "@rebasepro/types";
4
4
 
5
5
  export const CollectionRegistryContext = createContext<CollectionRegistryController>({
6
6
  getCollection: () => undefined,
@@ -12,7 +12,7 @@ export const CollectionRegistryContext = createContext<CollectionRegistryControl
12
12
  initialised: false
13
13
  });
14
14
 
15
- export function useCollectionRegistryController<DB = Record<string, unknown>, EC extends EntityCollection = EntityCollection<any>>(): CollectionRegistryController<DB, EC> {
15
+ export function useCollectionRegistryController<DB = Record<string, unknown>, EC extends EntityCollection = EntityCollection>(): CollectionRegistryController<DB, EC> {
16
16
  const context = React.useContext(CollectionRegistryContext);
17
17
  if (context === undefined) {
18
18
  throw new Error("useCollectionRegistryController must be used within a CollectionRegistryContext.Provider");
@@ -1,6 +1,6 @@
1
- import type { NavigationStateController, AppView, NavigationResult, RebasePlugin } from "@rebasepro/types";
1
+ import type { NavigationStateController } from "@rebasepro/types";
2
2
  import React, { createContext } from "react";
3
- ;
3
+
4
4
 
5
5
  export const NavigationStateContext = createContext<NavigationStateController>({
6
6
  loading: true,
@@ -1,8 +1,7 @@
1
1
  import type { UrlController } from "@rebasepro/types";
2
2
  import React, { createContext } from "react";
3
- import { NavigateOptions } from "react-router-dom";
4
3
 
5
- ;
4
+
6
5
 
7
6
  export const UrlContext = createContext<UrlController>({
8
7
  basePath: "/",
@@ -0,0 +1,100 @@
1
+ import { useCallback, useEffect, useRef, useState, useMemo } from "react";
2
+
3
+ export type UseAsyncResolverResult<T> = {
4
+ data: T;
5
+ loading: boolean;
6
+ error: Error | undefined;
7
+ refresh: () => void;
8
+ };
9
+
10
+ /**
11
+ * Generic hook that resolves an async value with loading/error/refresh
12
+ * state management, cancellation on unmount or deps change, and
13
+ * ref-based change detection to prevent unnecessary state updates.
14
+ *
15
+ * Extracted from the common pattern in useResolvedCollections and
16
+ * useResolvedViews.
17
+ */
18
+ export function useAsyncResolver<T>({
19
+ resolver,
20
+ initialValue,
21
+ isEqual,
22
+ deps,
23
+ disabled,
24
+ }: {
25
+ /** Async function that resolves the data */
26
+ resolver: () => Promise<T>;
27
+ /** Initial value before first resolution */
28
+ initialValue: T;
29
+ /** Equality check to prevent unnecessary state updates */
30
+ isEqual: (a: T, b: T) => boolean;
31
+ /** Effect dependencies — when these change, resolver re-runs */
32
+ deps: React.DependencyList;
33
+ /** When true, skip resolution */
34
+ disabled?: boolean;
35
+ }): UseAsyncResolverResult<T> {
36
+
37
+ const [data, setData] = useState<T>(initialValue);
38
+ const [loading, setLoading] = useState(true);
39
+ const [error, setError] = useState<Error | undefined>(undefined);
40
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
41
+
42
+ const refresh = useCallback(() => {
43
+ setRefreshTrigger(prev => prev + 1);
44
+ }, []);
45
+
46
+ // Ref for change detection — avoids state updates when data hasn't changed
47
+ const dataRef = useRef<T>(initialValue);
48
+
49
+ // Store resolver in a ref so we always call the latest version
50
+ // without adding it to effect deps (it's typically a new closure each render)
51
+ const resolverRef = useRef(resolver);
52
+ resolverRef.current = resolver;
53
+
54
+ // Same for isEqual
55
+ const isEqualRef = useRef(isEqual);
56
+ isEqualRef.current = isEqual;
57
+
58
+ useEffect(() => {
59
+ if (disabled) return;
60
+
61
+ let cancelled = false;
62
+
63
+ (async () => {
64
+ try {
65
+ const result = await resolverRef.current();
66
+
67
+ if (cancelled) return;
68
+
69
+ // Only update state if data actually changed
70
+ if (!isEqualRef.current(dataRef.current, result)) {
71
+ dataRef.current = result;
72
+ setData(result);
73
+ }
74
+
75
+ setError(undefined);
76
+ } catch (e) {
77
+ if (!cancelled) {
78
+ console.error(e);
79
+ setError(e as Error);
80
+ }
81
+ } finally {
82
+ if (!cancelled) {
83
+ setLoading(false);
84
+ }
85
+ }
86
+ })();
87
+
88
+ return () => {
89
+ cancelled = true;
90
+ };
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, [...deps, refreshTrigger, disabled]);
93
+
94
+ return useMemo(() => ({
95
+ data,
96
+ loading,
97
+ error,
98
+ refresh
99
+ }), [data, loading, error, refresh]);
100
+ }
@@ -1,16 +1,21 @@
1
- import type { EntityCollection } from "@rebasepro/types";
2
- import { useCallback, useRef, useState, useMemo } from "react";
1
+ import type { EntityCollection, DataSourceDefinition } from "@rebasepro/types";
2
+ import { useCallback, useEffect, useRef, useState, useMemo } from "react";
3
3
  import { CollectionRegistry, getParentReferencesFromPath as commonGetParentReferencesFromPath, removeInitialAndTrailingSlashes, getSubcollections } from "@rebasepro/common";
4
4
  import { EntityReference, UserConfigurationPersistence, CollectionRegistryController } from "@rebasepro/types";
5
5
  import { mergeDeep } from "@rebasepro/utils";
6
6
 
7
7
  export function useBuildCollectionRegistryController(props: {
8
- userConfigPersistence?: UserConfigurationPersistence
8
+ userConfigPersistence?: UserConfigurationPersistence,
9
+ dataSources?: Record<string, DataSourceDefinition>
9
10
  }): CollectionRegistryController & {
10
11
  collectionRegistryRef: React.MutableRefObject<CollectionRegistry>;
11
12
  } {
12
- const { userConfigPersistence } = props;
13
- const collectionRegistryRef = useRef<CollectionRegistry>(new CollectionRegistry());
13
+ const { userConfigPersistence, dataSources } = props;
14
+ const collectionRegistryRef = useRef<CollectionRegistry>(new CollectionRegistry(undefined, dataSources));
15
+ // Keep the registry's data sources in sync (used to resolve engine during
16
+ // normalization). Applied synchronously before the async registration
17
+ // effect in useResolvedCollections runs.
18
+ if (dataSources) collectionRegistryRef.current.setDataSources(dataSources);
14
19
  const [initialised, setInitialised] = useState(false);
15
20
 
16
21
  const getCollection = useCallback((
@@ -156,9 +161,11 @@ export function useBuildCollectionRegistryController(props: {
156
161
 
157
162
  // Determine initialised automatically based on whether collections exist,
158
163
  // though the NavigationStateController also plays a role in overall init
159
- if (!initialised && collections.length > 0) {
160
- setInitialised(true);
161
- }
164
+ useEffect(() => {
165
+ if (!initialised && collections.length > 0) {
166
+ setInitialised(true);
167
+ }
168
+ }, [initialised, collections.length]);
162
169
 
163
170
  return useMemo(() => ({
164
171
  collections,
@@ -1,5 +1,5 @@
1
1
  import type { AppView, EntityCollection, NavigationResult, RebasePlugin, NavigationStateController, UrlController, NavigationGroupMapping } from "@rebasepro/types";
2
- import { useCallback, useMemo } from "react";
2
+ import { useCallback, useMemo, useRef } from "react";
3
3
 
4
4
  import { AuthController, RebaseData, CollectionRegistryController, User } from "@rebasepro/types";
5
5
  import type { EntityCollectionsBuilder, AppViewsBuilder, EffectiveRoleController } from "@rebasepro/types";
@@ -100,10 +100,16 @@ export function useBuildNavigationStateController<EC extends EntityCollection, U
100
100
  collectionRegistryController
101
101
  });
102
102
 
103
- // Expose a combined refresh function
103
+ // Expose a combined refresh function with microtask batching
104
+ const pendingRefreshRef = useRef(false);
104
105
  const refreshNavigation = useCallback(() => {
105
- refreshCollections();
106
- refreshViews();
106
+ if (pendingRefreshRef.current) return;
107
+ pendingRefreshRef.current = true;
108
+ queueMicrotask(() => {
109
+ pendingRefreshRef.current = false;
110
+ refreshCollections();
111
+ refreshViews();
112
+ });
107
113
  }, [refreshCollections, refreshViews]);
108
114
 
109
115
  return useMemo(() => ({
@@ -112,8 +118,7 @@ export function useBuildNavigationStateController<EC extends EntityCollection, U
112
118
  topLevelNavigation,
113
119
  loading: collectionsLoading || viewsLoading,
114
120
  navigationLoadingError: collectionsError ?? viewsError,
115
- refreshNavigation,
116
- plugins
121
+ refreshNavigation
117
122
  }), [
118
123
  views,
119
124
  adminViews,
@@ -122,7 +127,6 @@ export function useBuildNavigationStateController<EC extends EntityCollection, U
122
127
  viewsLoading,
123
128
  collectionsError,
124
129
  viewsError,
125
- refreshNavigation,
126
- plugins
130
+ refreshNavigation
127
131
  ]);
128
132
  }
@@ -211,5 +211,38 @@ export async function resolveAppViews(
211
211
  }
212
212
  }
213
213
 
214
+ // Detect duplicate view slugs (dev warning)
215
+ if (process.env.NODE_ENV !== 'production') {
216
+ const slugCounts = new Map<string, number>();
217
+ resolvedViews.forEach(v => {
218
+ slugCounts.set(v.slug, (slugCounts.get(v.slug) ?? 0) + 1);
219
+ });
220
+ slugCounts.forEach((count, slug) => {
221
+ if (count > 1) {
222
+ console.warn(
223
+ `[Rebase] Duplicate view slug "${slug}" detected (${count} views). ` +
224
+ `Last-write-wins. Ensure unique slugs across CMS views and plugins.`
225
+ );
226
+ }
227
+ });
228
+ }
229
+
230
+ // Filter by roles — applies to CMS, plugin, and builder-returned views
231
+ resolvedViews = filterViewsByRole(resolvedViews, authController);
232
+
214
233
  return resolvedViews;
215
234
  }
235
+
236
+ /**
237
+ * Filter views by the `roles` field on AppView.
238
+ * When `roles` is set, the view is only included if the current user
239
+ * has at least one of the listed roles. Views without `roles` (or with
240
+ * an empty array) are always included.
241
+ */
242
+ function filterViewsByRole(views: AppView[], authController: AuthController): AppView[] {
243
+ const userRoles = authController.user?.roles ?? [];
244
+ return views.filter(view => {
245
+ if (!view.roles || view.roles.length === 0) return true;
246
+ return view.roles.some(role => userRoles.includes(role));
247
+ });
248
+ }