@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,5 +1,5 @@
1
1
  import type { EntityCollection, RebasePlugin } from "@rebasepro/types";
2
- import { useCallback, useEffect, useRef, useState, useMemo } from "react";
2
+ import { useRef, useMemo } from "react";
3
3
 
4
4
  import { AuthController, CollectionRegistryController, RebaseData, User } from "@rebasepro/types";
5
5
  import type { EntityCollectionsBuilder } from "@rebasepro/types";
@@ -8,6 +8,7 @@ import { CollectionRegistry } from "@rebasepro/common";
8
8
 
9
9
  import { resolveCollections } from "./useNavigationResolution";
10
10
  import { areCollectionListsEqual } from "./utils";
11
+ import { useAsyncResolver } from "./useAsyncResolver";
11
12
 
12
13
  export type UseResolvedCollectionsProps<EC extends EntityCollection, USER extends User> = {
13
14
  authController: AuthController<USER>;
@@ -50,16 +51,11 @@ export function useResolvedCollections<EC extends EntityCollection, USER extends
50
51
  collectionRegistryController
51
52
  } = props;
52
53
 
53
- const [loading, setLoading] = useState(true);
54
- const [error, setError] = useState<Error | undefined>(undefined);
55
- const [resolvedCollections, setResolvedCollections] = useState<EntityCollection[]>([]);
56
-
57
- // Track the trigger count to allow force-refresh
58
- const [refreshTrigger, setRefreshTrigger] = useState(0);
59
-
60
- const refresh = useCallback(() => {
61
- setRefreshTrigger(prev => prev + 1);
62
- }, []);
54
+ // Stable identity string for the user — avoids re-triggering when the
55
+ // authController object reference changes but uid/roles are the same.
56
+ const userIdentity = authController.user
57
+ ? `${authController.user.uid}:${(authController.user.roles ?? []).sort().join(',')}`
58
+ : null;
63
59
 
64
60
  // Use refs for values that may be new objects each render but shouldn't
65
61
  // re-trigger the effect. The effect reads them at execution time.
@@ -70,74 +66,36 @@ export function useResolvedCollections<EC extends EntityCollection, USER extends
70
66
  const pluginsRef = useRef(plugins);
71
67
  pluginsRef.current = plugins;
72
68
 
69
+ const { data: collections, loading, error, refresh } = useAsyncResolver<EntityCollection[]>({
70
+ resolver: async () => {
71
+ const resolved = await resolveCollections(
72
+ collectionsProp,
73
+ authControllerRef.current,
74
+ dataRef.current,
75
+ pluginsRef.current
76
+ );
73
77
 
74
- // Ref for resolved collections change detection
75
- const resolvedCollectionsRef = useRef<EntityCollection[]>([]);
76
-
77
- const initialLoading = authController.initialLoading;
78
- const user = authController.user;
79
-
80
- useEffect(() => {
81
- if (disabled || initialLoading) return;
82
-
83
- let cancelled = false;
84
-
85
- (async () => {
86
- try {
87
- const resolved = await resolveCollections(
88
- collectionsProp,
89
- authControllerRef.current,
90
- dataRef.current,
91
- pluginsRef.current
92
- );
78
+ const deduped = [...resolved];
93
79
 
94
- if (cancelled) return;
80
+ // Register with the CollectionRegistry; returns true if changed
81
+ const changed = collectionRegistryController.collectionRegistryRef.current.registerMultiple(deduped);
95
82
 
96
- const deduped = [...resolved];
97
-
98
- // Register with the CollectionRegistry; returns true if changed
99
- const changed = collectionRegistryController.collectionRegistryRef.current.registerMultiple(deduped);
100
-
101
- if (changed) {
102
- console.debug("Collections have changed", deduped);
103
- }
104
-
105
- // Only update state if collections actually changed
106
- if (!areCollectionListsEqual(resolvedCollectionsRef.current, deduped)) {
107
- resolvedCollectionsRef.current = deduped;
108
- setResolvedCollections(deduped);
109
- }
110
-
111
- setError(undefined);
112
- } catch (e) {
113
- if (!cancelled) {
114
- console.error(e);
115
- setError(e as Error);
116
- }
117
- } finally {
118
- if (!cancelled) {
119
- setLoading(false);
120
- }
83
+ if (changed) {
84
+ console.debug("Collections have changed", deduped);
121
85
  }
122
- })();
123
86
 
124
- return () => {
125
- cancelled = true;
126
- };
127
- }, [
128
- collectionsProp,
129
- disabled,
130
- collectionRegistryController.collectionRegistryRef,
131
- refreshTrigger,
132
- initialLoading,
133
- user
134
- ]);
87
+ return deduped;
88
+ },
89
+ initialValue: [],
90
+ isEqual: areCollectionListsEqual,
91
+ deps: [collectionsProp, userIdentity, disabled],
92
+ disabled: disabled || authController.initialLoading,
93
+ });
135
94
 
136
95
  return useMemo(() => ({
137
- collections: resolvedCollections,
96
+ collections,
138
97
  loading,
139
98
  error,
140
99
  refresh
141
- }), [resolvedCollections, loading, error, refresh]);
100
+ }), [collections, loading, error, refresh]);
142
101
  }
143
-
@@ -1,6 +1,6 @@
1
1
 
2
2
  import type { AppView, AppViewsBuilder, EffectiveRoleController, RebasePlugin } from "@rebasepro/types";
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { useMemo, useRef } from "react";
4
4
 
5
5
  /**
6
6
  * Compare two view arrays by their slug identity.
@@ -20,6 +20,7 @@ function viewSlugsEqual(a: AppView[] | undefined, b: AppView[] | undefined): boo
20
20
  import type { AuthController, RebaseData, User } from "@rebasepro/types";
21
21
 
22
22
  import { resolveAppViews } from "./useNavigationResolution";
23
+ import { useAsyncResolver } from "./useAsyncResolver";
23
24
 
24
25
  export type UseResolvedViewsProps<USER extends User> = {
25
26
  authController: AuthController<USER>;
@@ -39,6 +40,23 @@ export type UseResolvedViewsResult = {
39
40
  refresh: () => void;
40
41
  };
41
42
 
43
+ /**
44
+ * Combined result type for the async resolver — holds both views and
45
+ * adminViews so they can be resolved together in a single async call.
46
+ */
47
+ type ResolvedViewsData = {
48
+ views: AppView[] | undefined;
49
+ adminViews: AppView[] | undefined;
50
+ };
51
+
52
+ /**
53
+ * Equality check for the combined views data.
54
+ * Compares both views and adminViews by slug identity.
55
+ */
56
+ function areResolvedViewsEqual(a: ResolvedViewsData, b: ResolvedViewsData): boolean {
57
+ return viewSlugsEqual(a.views, b.views) && viewSlugsEqual(a.adminViews, b.adminViews);
58
+ }
59
+
42
60
  /**
43
61
  * Hook that resolves view and admin view props (which may be async builders or arrays)
44
62
  * into concrete AppView[].
@@ -60,21 +78,11 @@ export function useResolvedViews<USER extends User>(
60
78
  effectiveRoleController
61
79
  } = props;
62
80
 
63
- const [loading, setLoading] = useState(true);
64
- const [error, setError] = useState<Error | undefined>(undefined);
65
- const [resolvedViews, setResolvedViews] = useState<AppView[] | undefined>(undefined);
66
- const [resolvedAdminViews, setResolvedAdminViews] = useState<AppView[] | undefined>(undefined);
67
-
68
- // Track the trigger count to allow force-refresh
69
- const [refreshTrigger, setRefreshTrigger] = useState(0);
70
-
71
- const refresh = useCallback(() => {
72
- setRefreshTrigger(prev => prev + 1);
73
- }, []);
74
-
75
- // Refs for change-detection (avoids state updates when views haven't changed)
76
- const viewsRef = useRef<AppView[] | undefined>(undefined);
77
- const adminViewsRef = useRef<AppView[] | undefined>(undefined);
81
+ // Stable identity string for the user — avoids re-triggering when the
82
+ // authController object reference changes but uid/roles are the same.
83
+ const userIdentity = authController.user
84
+ ? `${authController.user.uid}:${(authController.user.roles ?? []).sort().join(',')}`
85
+ : null;
78
86
 
79
87
  // Use refs for values that may be new objects each render but shouldn't
80
88
  // re-trigger the effect. The effect reads them at execution time.
@@ -104,64 +112,26 @@ export function useResolvedViews<USER extends User>(
104
112
  resolvedAuthControllerRef.current = resolvedAuthController;
105
113
 
106
114
  const initialLoading = resolvedAuthController.initialLoading;
107
- const user = resolvedAuthController.user;
108
-
109
- useEffect(() => {
110
- if (initialLoading) return;
111
-
112
- let cancelled = false;
113
-
114
- (async () => {
115
- try {
116
- const [newViews, newAdminViews] = await Promise.all([
117
- resolveAppViews(viewsProp, resolvedAuthControllerRef.current, dataRef.current, pluginsRef.current),
118
- resolveAppViews(adminViewsProp, resolvedAuthControllerRef.current, dataRef.current)
119
- ]);
120
-
121
- // Compare views by slug identity rather than deepEqual.
122
- // Views contain React elements (JSX) whose internal properties
123
- // change on every render, making deepEqual unreliable.
124
- if (!viewSlugsEqual(viewsRef.current, newViews)) {
125
- viewsRef.current = newViews;
126
- setResolvedViews(newViews);
127
- }
128
115
 
129
- if (!viewSlugsEqual(adminViewsRef.current, newAdminViews)) {
130
- adminViewsRef.current = newAdminViews;
131
- setResolvedAdminViews(newAdminViews);
132
- }
133
-
134
- setError(undefined);
135
- } catch (e) {
136
- if (!cancelled) {
137
- console.error("Error resolving views:", e);
138
- setError(e as Error);
139
- }
140
- } finally {
141
- if (!cancelled) {
142
- setLoading(false);
143
- }
144
- }
145
- })();
146
-
147
- return () => {
148
- cancelled = true;
149
- };
150
- }, [
151
- viewsProp,
152
- adminViewsProp,
153
- refreshTrigger,
154
- adminMode,
155
- initialLoading,
156
- user
157
- ]);
116
+ const { data: resolvedData, loading, error, refresh } = useAsyncResolver<ResolvedViewsData>({
117
+ resolver: async () => {
118
+ const [newViews, newAdminViews] = await Promise.all([
119
+ resolveAppViews(viewsProp, resolvedAuthControllerRef.current, dataRef.current, pluginsRef.current),
120
+ resolveAppViews(adminViewsProp, resolvedAuthControllerRef.current, dataRef.current)
121
+ ]);
122
+ return { views: newViews, adminViews: newAdminViews };
123
+ },
124
+ initialValue: { views: undefined, adminViews: undefined },
125
+ isEqual: areResolvedViewsEqual,
126
+ deps: [viewsProp, adminViewsProp, adminMode, userIdentity],
127
+ disabled: initialLoading,
128
+ });
158
129
 
159
130
  return useMemo(() => ({
160
- views: resolvedViews,
161
- adminViews: resolvedAdminViews,
131
+ views: resolvedData.views,
132
+ adminViews: resolvedData.adminViews,
162
133
  loading,
163
134
  error,
164
135
  refresh
165
- }), [resolvedViews, resolvedAdminViews, loading, error, refresh]);
136
+ }), [resolvedData, loading, error, refresh]);
166
137
  }
167
-
@@ -16,7 +16,7 @@ export type UseTopLevelNavigationProps = {
16
16
  viewsOrder?: string[];
17
17
  urlController: UrlController;
18
18
  adminMode?: "content" | "studio" | "settings";
19
- collectionRegistryController: CollectionRegistryController<any> & { collectionRegistryRef: React.MutableRefObject<CollectionRegistry> };
19
+ collectionRegistryController: CollectionRegistryController & { collectionRegistryRef: React.MutableRefObject<CollectionRegistry> };
20
20
  };
21
21
 
22
22
  export type UseTopLevelNavigationResult = {
@@ -178,26 +178,30 @@ export function useTopLevelNavigation(
178
178
  ];
179
179
 
180
180
  const groupOrderValue = (groupName?: string): number => {
181
- if (groupName === NAVIGATION_ADMIN_GROUP_NAME) return 1;
181
+ if (groupName === NAVIGATION_ADMIN_GROUP_NAME || groupName === "Settings" || groupName === "Admin") return 1;
182
182
  return 0;
183
183
  };
184
184
 
185
+ const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
185
186
  navigationEntries = navigationEntries.sort((a, b) => {
186
- return groupOrderValue(a.group) - groupOrderValue(b.group);
187
- });
187
+ // Primary: group priority (admin/settings last)
188
+ const groupDiff = groupOrderValue(a.group) - groupOrderValue(b.group);
189
+ if (groupDiff !== 0) return groupDiff;
188
190
 
189
- const usedViewsOrder = viewsOrder ?? navigationEntriesOrder;
190
- if (usedViewsOrder) {
191
- navigationEntries = navigationEntries.sort((a, b) => {
191
+ // Secondary: explicit view order (if provided)
192
+ if (usedViewsOrder) {
192
193
  const getSortPath = (navEntry: NavigationEntry) => typeof navEntry.slug === "string" ? navEntry.slug : navEntry.slug[0];
193
194
  const aIndex = usedViewsOrder.indexOf(getSortPath(a));
194
195
  const bIndex = usedViewsOrder.indexOf(getSortPath(b));
195
- if (aIndex === -1 && bIndex === -1) return 0;
196
- if (aIndex === -1) return 1;
197
- if (bIndex === -1) return -1;
198
- return aIndex - bIndex;
199
- });
200
- }
196
+ if (aIndex !== -1 || bIndex !== -1) {
197
+ if (aIndex === -1) return 1;
198
+ if (bIndex === -1) return -1;
199
+ return aIndex - bIndex;
200
+ }
201
+ }
202
+
203
+ return 0;
204
+ });
201
205
 
202
206
  const collectedGroupsFromEntries = navigationEntries
203
207
  .map(e => e.group)
@@ -213,9 +217,9 @@ export function useTopLevelNavigation(
213
217
  ];
214
218
 
215
219
  const uniqueGroupsArray = [...new Set(allDefinedGroups)];
216
- const adminGroups = uniqueGroupsArray.filter(g => g === NAVIGATION_ADMIN_GROUP_NAME);
217
- const nonAdminGroups = uniqueGroupsArray.filter(g => g !== NAVIGATION_ADMIN_GROUP_NAME);
218
- const uniqueGroups = [...nonAdminGroups, ...adminGroups] as string[];
220
+ const lastGroups = uniqueGroupsArray.filter(g => g === NAVIGATION_ADMIN_GROUP_NAME || g === "Settings" || g === "Admin");
221
+ const otherGroups = uniqueGroupsArray.filter(g => g !== NAVIGATION_ADMIN_GROUP_NAME && g !== "Settings" && g !== "Admin");
222
+ const uniqueGroups = [...otherGroups, ...lastGroups] as string[];
219
223
 
220
224
  const computedTopLevelNav = {
221
225
  allowDragAndDrop: plugins?.some((plugin: RebasePlugin) => plugin.hooks?.allowDragAndDrop) ?? false,
@@ -1,12 +1,12 @@
1
1
  import type { AppView, EntityCollection, RebasePlugin, NavigationGroupMapping } from "@rebasepro/types";
2
- ;
2
+
3
3
  import { getSubcollections } from "@rebasepro/common";
4
4
  import { deepEqual as equal } from "fast-equals";
5
5
 
6
6
  export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
7
7
  export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
8
8
 
9
- export function getGroup(collectionOrView: EntityCollection<any, any> | AppView) {
9
+ export function getGroup(collectionOrView: EntityCollection | AppView) {
10
10
  const trimmed = collectionOrView.group?.trim();
11
11
  if (!trimmed || trimmed === "") {
12
12
  return NAVIGATION_DEFAULT_GROUP_NAME;
@@ -28,8 +28,11 @@ export function computeNavigationGroups({
28
28
 
29
29
  // Deep clone the input groups upfront to avoid mutating the caller's data
30
30
  let result = navigationGroupMappings
31
- ? navigationGroupMappings.map(g => ({ name: g.name,
32
- entries: [...g.entries] }))
31
+ ? navigationGroupMappings.map(g => ({
32
+ name: g.name,
33
+ entries: [...g.entries],
34
+ ...(g.collapsedByDefault !== undefined && { collapsedByDefault: g.collapsedByDefault })
35
+ }))
33
36
  : navigationGroupMappings;
34
37
 
35
38
  // Merge plugin navigation entries
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export * from "./types";
3
3
  // Editor types only — the full ProseMirror editor (RichTextEditor) is a heavy import (~300 KB)
4
4
  // and is available as a separate entry point: @rebasepro/admin/editor
5
- export type { RichTextEditorProps, RichTextEditorTextSize, RebaseEditorProps, RebaseEditorTextSize, JSONContent, EditorAIController } from "./editor";
5
+ export type { RichTextEditorProps, RichTextEditorTextSize, JSONContent, EditorAIController } from "./editor";
6
6
  export * from "./form";
7
7
  export * from "./preview";
8
8
  export * from "./routes";
@@ -92,6 +92,11 @@ export {
92
92
  getEntityTitlePropertyKey
93
93
  } from "./util";
94
94
 
95
+ // Headless collection view adapters
96
+ // Map entity types to the data-agnostic CollectionView from @rebasepro/ui
97
+ export { mapPropertyToConfig, mapPropertiesToConfigs } from "./util/propertyConfigMapper";
98
+ export { useCollectionDataController, createStaticDataController } from "./util/dataControllerAdapter";
99
+
95
100
  // Data import/export — merged from former standalone packages
96
101
  export * from "./data_import";
97
102
  export * from "./data_export";
@@ -82,6 +82,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
82
82
  content = <StorageThumbnail
83
83
  interactive={interactive}
84
84
  storeUrl={property.storage?.storeUrl ?? false}
85
+ storageSourceKey={stringProperty.storage.storageSource}
85
86
  size={props.size}
86
87
  fill={fill}
87
88
  storagePathOrDownloadUrl={filePath}/>;
@@ -107,22 +108,6 @@ export const PropertyPreview = React.memo(function PropertyPreview<P extends Pro
107
108
  propertyKey={propertyKey}
108
109
  size={props.size}
109
110
  />;
110
- } else if (stringProperty.reference) {
111
- if (typeof stringProperty.reference.path === "string") {
112
- content = <ReferencePreview
113
- disabled={!stringProperty.reference.path}
114
- previewProperties={stringProperty.reference?.ui?.previewProperties}
115
- includeId={stringProperty.reference.includeId}
116
- includeEntityLink={stringProperty.reference.includeEntityLink}
117
- size={props.size}
118
- reference={new EntityReference({ id: value,
119
- path: stringProperty.reference.path })}
120
- textOnly={props.textOnly}
121
- />;
122
- } else {
123
- content = <EmptyValue/>;
124
- }
125
-
126
111
  } else {
127
112
  content = <StringPropertyPreview {...props}
128
113
  property={stringProperty}
@@ -2,7 +2,8 @@ import React, { useEffect } from "react";
2
2
 
3
3
  import { renderSkeletonImageThumbnail } from "../property_previews/SkeletonPropertyComponent";
4
4
  import { UrlComponentPreview } from "./UrlComponentPreview";
5
- import { ErrorView, useStorageSource } from "@rebasepro/core";
5
+ import { ErrorView, useStorageSource, useStorageSources } from "@rebasepro/core";
6
+ import { resolveStorageSource } from "@rebasepro/common";
6
7
  import { DownloadConfig, FileType } from "@rebasepro/types";
7
8
  import type { PreviewSize } from "../../types/components/PropertyPreviewProps";
8
9
  import { Skeleton } from "@rebasepro/ui";
@@ -12,6 +13,8 @@ type StorageThumbnailProps = {
12
13
  size: PreviewSize;
13
14
  interactive?: boolean;
14
15
  fill?: boolean;
16
+ /** Key of the storage source backing this property (`StorageConfig.storageSource`). */
17
+ storageSourceKey?: string;
15
18
  };
16
19
 
17
20
  /**
@@ -24,7 +27,8 @@ function areEqual(prevProps: StorageThumbnailProps, nextProps: StorageThumbnailP
24
27
  prevProps.storagePathOrDownloadUrl === nextProps.storagePathOrDownloadUrl &&
25
28
  prevProps.storeUrl === nextProps.storeUrl &&
26
29
  prevProps.interactive === nextProps.interactive &&
27
- prevProps.fill === nextProps.fill;
30
+ prevProps.fill === nextProps.fill &&
31
+ prevProps.storageSourceKey === nextProps.storageSourceKey;
28
32
  }
29
33
 
30
34
  const URL_CACHE: Record<string, DownloadConfig> = {};
@@ -34,13 +38,26 @@ export function StorageThumbnailInternal({
34
38
  interactive,
35
39
  storagePathOrDownloadUrl,
36
40
  size,
37
- fill
41
+ fill,
42
+ storageSourceKey
38
43
  }: StorageThumbnailProps) {
39
44
 
40
45
  const [error, setError] = React.useState<Error | undefined>(undefined);
41
- const storage = useStorageSource();
46
+ const defaultStorage = useStorageSource();
47
+ const storageSources = useStorageSources();
48
+ // Resolve the per-property backend so previews of `storeUrl: false`
49
+ // properties pointing at a named source hit the right backend.
50
+ const storage = resolveStorageSource({
51
+ sourceKey: storageSourceKey,
52
+ sources: storageSources.sources,
53
+ defaultSource: defaultStorage
54
+ });
42
55
 
43
- const [downloadConfig, setDownloadConfig] = React.useState<DownloadConfig>(URL_CACHE[storagePathOrDownloadUrl]);
56
+ // Cache key is namespaced by source so the same path on two backends
57
+ // does not collide.
58
+ const cacheKey = `${storageSourceKey ?? ""}::${storagePathOrDownloadUrl}`;
59
+
60
+ const [downloadConfig, setDownloadConfig] = React.useState<DownloadConfig>(URL_CACHE[cacheKey]);
44
61
 
45
62
  useEffect(() => {
46
63
  if (!storagePathOrDownloadUrl)
@@ -50,13 +67,13 @@ export function StorageThumbnailInternal({
50
67
  .then(function (downloadConfig) {
51
68
  if (!unmounted) {
52
69
  setDownloadConfig(downloadConfig);
53
- URL_CACHE[storagePathOrDownloadUrl] = downloadConfig;
70
+ URL_CACHE[cacheKey] = downloadConfig;
54
71
  }
55
72
  }).catch(setError);
56
73
  return () => {
57
74
  unmounted = true;
58
75
  };
59
- }, [storagePathOrDownloadUrl]);
76
+ }, [storagePathOrDownloadUrl, cacheKey]);
60
77
 
61
78
  if (!storagePathOrDownloadUrl) return null;
62
79
 
@@ -1,8 +1,6 @@
1
+ import React, { useEffect, useRef } from "react";
1
2
  import type { AppView } from "@rebasepro/types";
2
- ;
3
- import { useEffect } from "react";
4
- import { useBreadcrumbsController } from "../index";
5
- import { useUrlController } from "../index";
3
+ import { useBreadcrumbsController, useUrlController } from "../index";
6
4
 
7
5
  export function CustomViewRoute({ view }: {
8
6
  view: AppView
@@ -11,8 +9,14 @@ export function CustomViewRoute({ view }: {
11
9
  const breadcrumbs = useBreadcrumbsController();
12
10
  const urlController = useUrlController();
13
11
 
12
+ // Use a ref to avoid breadcrumbs identity in the dep array —
13
+ // breadcrumbs.set() creates a new context value each call, which would
14
+ // re-trigger this effect and cause an infinite render loop.
15
+ const breadcrumbsRef = useRef(breadcrumbs);
16
+ breadcrumbsRef.current = breadcrumbs;
17
+
14
18
  useEffect(() => {
15
- breadcrumbs.set({
19
+ breadcrumbsRef.current.set({
16
20
  breadcrumbs: [{
17
21
  title: view.name,
18
22
  url: urlController.buildAppUrlPath(view.slug)
@@ -20,5 +24,9 @@ export function CustomViewRoute({ view }: {
20
24
  });
21
25
  }, [view.slug, urlController]);
22
26
 
23
- return view.view;
27
+ if (typeof view.view === 'function') {
28
+ const ViewComponent = view.view;
29
+ return <ViewComponent />;
30
+ }
31
+ return <>{view.view}</>;
24
32
  }
@@ -13,7 +13,7 @@ export interface EntityFormActionsProps {
13
13
  disabled: boolean;
14
14
  status: "new" | "existing" | "copy";
15
15
  pluginActions: React.ReactNode[];
16
- openEntityMode: "side_panel" | "full_screen" | "split" | "dialog";
16
+ openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
17
17
  showDefaultActions?: boolean;
18
18
  navigateBack: () => void;
19
19
  formContext: FormContext
@@ -4,6 +4,11 @@ import { FormContext } from "../fields";
4
4
  import { FormexController } from "./formex";
5
5
  import { EntityFormActionsProps } from "./EntityFormActionsProps";
6
6
 
7
+ /**
8
+ * Props for the headless EntityForm component.
9
+ * This form can be used without CMS context — all backend concerns
10
+ * (save, caching, analytics, plugin slots) are provided via callbacks.
11
+ */
7
12
  export type EntityFormProps<M extends Record<string, unknown>> = {
8
13
  path: string;
9
14
  fullIdPath?: string;
@@ -44,6 +49,69 @@ export type EntityFormProps<M extends Record<string, unknown>> = {
44
49
  Builder?: React.ComponentType<EntityCustomViewParams<M>>;
45
50
 
46
51
  children?: React.ReactNode;
52
+
53
+ // --- Headless callbacks (replace internal CMS hooks) ---
54
+
55
+ /**
56
+ * Called on form submit with the values to save.
57
+ * The EntityFormBinding wrapper provides this automatically.
58
+ * When using EntityForm standalone, provide your own save logic.
59
+ */
60
+ onSubmit?: (values: M, formex: FormexController<M>) => Promise<Entity<M> | void> | void;
61
+
62
+ /**
63
+ * Called when form values change (deferred).
64
+ * The EntityFormBinding wrapper uses this for entity caching.
65
+ */
66
+ onValuesChangeDeferred?: (values: M, controller: FormexController<M>) => void;
67
+
68
+ /**
69
+ * Called when the form is reset.
70
+ */
71
+ onReset?: () => void;
72
+
73
+ /**
74
+ * Validate that a field value is unique.
75
+ * The EntityFormBinding wrapper provides this via the data layer.
76
+ */
77
+ uniqueFieldValidator?: (params: { name: string; value: unknown }) => Promise<boolean>;
78
+
79
+ // --- Slots (replace useSlot) ---
80
+
81
+ /** Content rendered before the form fields */
82
+ beforeFields?: React.ReactNode;
83
+ /** Content rendered after the form fields */
84
+ afterFields?: React.ReactNode;
85
+ /** Plugin-provided action elements */
86
+ pluginActions?: React.ReactNode[];
87
+
88
+ // --- Local changes (managed externally in binding) ---
89
+
90
+ /**
91
+ * Initial values for the form, after applying local changes.
92
+ * If not provided, computed from entity + collection defaults.
93
+ */
94
+ computedInitialValues?: Partial<M>;
95
+ /**
96
+ * Whether there are unsaved local changes in the cache.
97
+ */
98
+ hasLocalChanges?: boolean;
99
+ /**
100
+ * The local changes data for the manual-apply menu.
101
+ */
102
+ localChangesData?: Partial<M>;
103
+ /**
104
+ * Whether manual-apply local changes mode is enabled.
105
+ */
106
+ manualApplyLocalChanges?: boolean;
107
+ /**
108
+ * Cache key for the local changes menu.
109
+ */
110
+ localChangesCacheKey?: string;
111
+ /**
112
+ * Callback when local changes are cleared.
113
+ */
114
+ onClearLocalChanges?: () => void;
47
115
  };
48
116
 
49
117
  export type OnUpdateParams = {
@@ -15,7 +15,7 @@ export interface PropertyPreviewProps<P extends Property | Property, CustomProps
15
15
  */
16
16
  propertyKey?: string;
17
17
 
18
- value: any;
18
+ value: InferPropertyType<P> | any;
19
19
 
20
20
  /**
21
21
  * Property this display is related to, now strongly typed to P