@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
@@ -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
@@ -1,8 +1,8 @@
1
1
  // CMS-specific types take priority over base types when names conflict
2
2
  export * from "./types";
3
- // Editor types only — the full ProseMirror editor is a heavy import (~300 KB)
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 { RebaseEditorProps, JSONContent, EditorAIController } from "./editor";
5
+ export type { RichTextEditorProps, RichTextEditorTextSize, RebaseEditorProps, RebaseEditorTextSize, 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";
@@ -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 = {
@@ -213,7 +213,7 @@ export interface FormContext<M extends Record<string, unknown> = Record<string,
213
213
 
214
214
  savingError?: Error;
215
215
 
216
- openEntityMode: "side_panel" | "full_screen" | "split" | "dialog";
216
+ openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
217
217
 
218
218
  /**
219
219
  * This is the underlying formex controller that powers the form.
@@ -223,6 +223,12 @@ export interface FormContext<M extends Record<string, unknown> = Record<string,
223
223
  formex: FormexController<M>;
224
224
 
225
225
  disabled: boolean;
226
+
227
+ /**
228
+ * Whether the form context is in read-only detail view mode.
229
+ * Custom entity views can use this to adjust their rendering.
230
+ */
231
+ readOnly?: boolean;
226
232
  }
227
233
 
228
234
  /**
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Adapts an `EntityTableController<M>` (from @rebasepro/types) to a
3
+ * `CollectionDataController<T>` (from @rebasepro/ui), bridging the
4
+ * entity-aware data layer to the headless collection view.
5
+ */
6
+
7
+ import { useMemo } from "react";
8
+ import type { Entity, EntityTableController } from "@rebasepro/types";
9
+ import type { CollectionDataController } from "@rebasepro/ui";
10
+
11
+ /**
12
+ * Flatten Entity<M> objects to plain row objects.
13
+ * The headless view works with flat `Record<string, unknown>` rows,
14
+ * while the entity system uses `{ id, path, values: M }`.
15
+ *
16
+ * The flattened row includes `id` and `path` at the top level,
17
+ * plus all values spread.
18
+ */
19
+ function flattenEntities<M extends Record<string, unknown>>(
20
+ entities: Entity<M>[]
21
+ ): Array<M & { id: string | number; path: string }> {
22
+ return entities.map(entity => ({
23
+ id: entity.id,
24
+ path: entity.path,
25
+ ...entity.values
26
+ }));
27
+ }
28
+
29
+ /**
30
+ * React hook that adapts an EntityTableController to a CollectionDataController.
31
+ *
32
+ * Usage:
33
+ * ```tsx
34
+ * const tableController = useDataTableController({ path, collection });
35
+ * const dataController = useCollectionDataController(tableController);
36
+ *
37
+ * <CollectionView dataController={dataController} ... />
38
+ * ```
39
+ */
40
+ export function useCollectionDataController<M extends Record<string, unknown>>(
41
+ tableController: EntityTableController<M>
42
+ ): CollectionDataController<M & { id: string | number; path: string }> {
43
+ const flatData = useMemo(
44
+ () => flattenEntities(tableController.data),
45
+ [tableController.data]
46
+ );
47
+
48
+ return useMemo((): CollectionDataController<M & { id: string | number; path: string }> => ({
49
+ data: flatData,
50
+ loading: tableController.dataLoading,
51
+ noMoreToLoad: tableController.noMoreToLoad,
52
+ error: tableController.dataLoadingError,
53
+
54
+ filterValues: tableController.filterValues as Record<string, [string, unknown]> | undefined,
55
+ setFilterValues: tableController.setFilterValues as
56
+ ((values: Record<string, [string, unknown]> | undefined) => void) | undefined,
57
+
58
+ sortBy: tableController.sortBy,
59
+ setSortBy: tableController.setSortBy,
60
+
61
+ searchString: tableController.searchString,
62
+ setSearchString: tableController.setSearchString,
63
+
64
+ clearFilter: tableController.clearFilter,
65
+
66
+ initialScroll: tableController.initialScroll,
67
+ onScroll: tableController.onScroll,
68
+
69
+ paginationEnabled: tableController.paginationEnabled,
70
+ pageSize: tableController.pageSize,
71
+ itemCount: tableController.itemCount,
72
+ setItemCount: tableController.setItemCount,
73
+ }), [flatData, tableController]);
74
+ }
75
+
76
+ /**
77
+ * Non-hook version for cases where you already have the data
78
+ * and just need to build a static controller.
79
+ */
80
+ export function createStaticDataController<T extends Record<string, unknown>>(
81
+ data: T[],
82
+ options?: {
83
+ loading?: boolean;
84
+ error?: Error;
85
+ }
86
+ ): CollectionDataController<T> {
87
+ return {
88
+ data,
89
+ loading: options?.loading ?? false,
90
+ noMoreToLoad: true,
91
+ error: options?.error,
92
+ };
93
+ }
@@ -31,7 +31,7 @@ export function navigateToEntity({
31
31
  }:
32
32
 
33
33
  {
34
- openEntityMode: "side_panel" | "full_screen" | "split" | "dialog";
34
+ openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
35
35
  collection?: EntityCollection;
36
36
  entityId?: string | number;
37
37
  selectedTab?: string;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Maps a Rebase `Property` (from @rebasepro/types) to a `CollectionPropertyConfig`
3
+ * (from @rebasepro/ui), stripping all entity-specific fields and keeping only
4
+ * the UI-relevant subset.
5
+ *
6
+ * This is the bridge between the entity-aware and headless layers.
7
+ */
8
+
9
+ import type { Property, Properties, EnumValues, EnumValueConfig } from "@rebasepro/types";
10
+ import type { CollectionPropertyConfig, CollectionEnumValueConfig } from "@rebasepro/ui";
11
+
12
+ /**
13
+ * Map Rebase property types to headless-supported types.
14
+ * `vector` and `binary` are not directly renderable, so we fall back to `string`.
15
+ */
16
+ const TYPE_MAP: Record<string, CollectionPropertyConfig["type"]> = {
17
+ string: "string",
18
+ number: "number",
19
+ boolean: "boolean",
20
+ date: "date",
21
+ geopoint: "geopoint",
22
+ reference: "reference",
23
+ relation: "relation",
24
+ array: "array",
25
+ map: "map",
26
+ vector: "string", // vectors display as text
27
+ binary: "string", // binary displays as text
28
+ };
29
+
30
+ /**
31
+ * Convert a single Property to a CollectionPropertyConfig.
32
+ */
33
+ export function mapPropertyToConfig(property: Property): CollectionPropertyConfig {
34
+ const base: CollectionPropertyConfig = {
35
+ type: TYPE_MAP[property.type] ?? "string",
36
+ name: property.name,
37
+ description: property.description,
38
+ columnWidth: property.ui?.columnWidth,
39
+ hideFromCollection: property.ui?.hideFromCollection,
40
+ };
41
+
42
+ // String-specific
43
+ if (property.type === "string") {
44
+ if (property.enum) {
45
+ base.enum = mapEnumValues(property.enum);
46
+ }
47
+ if (property.multiline) base.multiline = true;
48
+ if (property.ui?.previewAsTag) base.previewAsTag = true;
49
+ if (property.url || property.ui?.url) base.url = property.url ?? property.ui?.url;
50
+ if (property.ui?.markdown || property.markdown) base.markdown = true;
51
+ if (property.storage) base.storage = true;
52
+ if (property.email) base.email = true;
53
+ }
54
+
55
+ // Number-specific
56
+ if (property.type === "number" && property.enum) {
57
+ base.enum = mapEnumValues(property.enum);
58
+ }
59
+
60
+ // Array-specific
61
+ if (property.type === "array" && property.of && !Array.isArray(property.of)) {
62
+ base.of = mapPropertyToConfig(property.of);
63
+ }
64
+
65
+ // Map-specific
66
+ if (property.type === "map" && property.properties) {
67
+ base.properties = mapPropertiesToConfigs(property.properties);
68
+ if (property.propertiesOrder) {
69
+ base.propertiesOrder = property.propertiesOrder;
70
+ }
71
+ }
72
+
73
+ // Date-specific
74
+ if (property.type === "date" && property.mode) {
75
+ base.mode = property.mode;
76
+ }
77
+
78
+ // Custom preview component
79
+ if (property.ui?.Preview) {
80
+ // The connected wrapper will handle this by injecting via cellRenderer override
81
+ // We don't copy it here because Preview may reference entity-aware components
82
+ }
83
+
84
+ return base;
85
+ }
86
+
87
+ /**
88
+ * Convert a Properties record to a CollectionPropertyConfig record.
89
+ */
90
+ export function mapPropertiesToConfigs(
91
+ properties: Properties
92
+ ): Record<string, CollectionPropertyConfig> {
93
+ const result: Record<string, CollectionPropertyConfig> = {};
94
+ for (const [key, property] of Object.entries(properties)) {
95
+ result[key] = mapPropertyToConfig(property);
96
+ }
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Convert Rebase EnumValues to CollectionEnumValueConfig format.
102
+ * Handles both array form (EnumValueConfig[]) and record form.
103
+ */
104
+ function mapEnumValues(
105
+ enumValues: EnumValues | undefined
106
+ ): Record<string, CollectionEnumValueConfig> | Map<string | number, CollectionEnumValueConfig> | undefined {
107
+ if (!enumValues) return undefined;
108
+
109
+ // Array form: EnumValueConfig[]
110
+ if (Array.isArray(enumValues)) {
111
+ const result: Record<string, CollectionEnumValueConfig> = {};
112
+ for (const item of enumValues) {
113
+ result[String(item.id)] = normalizeEnumValueConfig(item);
114
+ }
115
+ return result;
116
+ }
117
+
118
+ // Record form: Record<string | number, string | EnumValueConfig>
119
+ const result: Record<string, CollectionEnumValueConfig> = {};
120
+ for (const [key, value] of Object.entries(enumValues)) {
121
+ result[key] = typeof value === "string"
122
+ ? value
123
+ : normalizeEnumValueConfig(value);
124
+ }
125
+ return result;
126
+ }
127
+
128
+ function normalizeEnumValueConfig(config: EnumValueConfig): CollectionEnumValueConfig {
129
+ return {
130
+ label: config.label,
131
+ color: typeof config.color === "string" ? config.color : undefined,
132
+ disabled: config.disabled,
133
+ };
134
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Rebase
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.