@rebasepro/admin 0.6.1 → 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.
- package/dist/{CollectionEditorDialog-HNXGxXdO.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
- package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
- package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-C9yhLyja.js} +129 -15
- package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
- package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-Dqx27GD8.js} +61 -39
- package/dist/RouterCollectionsStudioView-Dqx27GD8.js.map +1 -0
- package/dist/collection_editor/extensibility_types.d.ts +135 -0
- package/dist/collection_editor/index.d.ts +6 -0
- package/dist/collection_editor/serializable_types.d.ts +374 -0
- package/dist/collection_editor/serializable_utils.d.ts +50 -0
- package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +2 -1
- package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
- package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
- package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
- package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
- package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
- package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
- package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
- package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
- package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
- package/dist/collection_editor_ui.d.ts +4 -0
- package/dist/collection_editor_ui.js +33 -4
- package/dist/collection_editor_ui.js.map +1 -0
- package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
- package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
- package/dist/components/EntityEditView.d.ts +3 -2
- package/dist/components/RebaseCMS.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +10 -9
- package/dist/form/EntityFormBinding.d.ts +27 -0
- package/dist/form/form_utils.d.ts +8 -0
- package/dist/form/index.d.ts +5 -1
- package/dist/{history-BO6VT85a.js → history-ChHgyyvM.js} +2 -2
- package/dist/{history-BO6VT85a.js.map → history-ChHgyyvM.js.map} +1 -1
- package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
- package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
- package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
- package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
- package/dist/hooks/navigation/utils.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/routes/CustomViewRoute.d.ts +2 -1
- package/dist/{src-BYniefVu.js → src-DOX2nOyh.js} +1586 -1283
- package/dist/src-DOX2nOyh.js.map +1 -0
- package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
- package/dist/types/components/EntityFormProps.d.ts +59 -0
- package/dist/types/fields.d.ts +6 -1
- package/dist/util/dataControllerAdapter.d.ts +30 -0
- package/dist/util/navigation_utils.d.ts +1 -1
- package/dist/util/propertyConfigMapper.d.ts +17 -0
- package/package.json +18 -18
- package/src/collection_editor/extensibility_types.ts +168 -0
- package/src/collection_editor/index.ts +46 -0
- package/src/collection_editor/serializable_types.ts +487 -0
- package/src/collection_editor/serializable_utils.ts +588 -0
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +4 -4
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +134 -70
- package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
- package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
- package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +155 -10
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
- package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
- package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
- package/src/collection_editor/useSafeSnackbarController.ts +9 -0
- package/src/collection_editor_ui.ts +6 -0
- package/src/components/DefaultDrawer.tsx +13 -3
- package/src/components/DrawerNavigationGroup.tsx +2 -2
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -11
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +54 -183
- package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
- package/src/components/EntityEditView.tsx +5 -4
- package/src/components/EntityEditViewFormActions.tsx +32 -26
- package/src/components/HomePage/ContentHomePage.tsx +13 -4
- package/src/components/RebaseCMS.tsx +3 -2
- package/src/components/RebaseNavigation.tsx +71 -7
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SideDialogs.tsx +0 -7
- package/src/components/common/default_entity_actions.tsx +9 -9
- package/src/contexts/BreacrumbsContext.tsx +15 -6
- package/src/editor/selectors/ai-selector.tsx +0 -1
- package/src/form/EntityForm.tsx +191 -428
- package/src/form/EntityFormActions.tsx +33 -47
- package/src/form/EntityFormBinding.tsx +323 -0
- package/src/form/components/FormLayout.tsx +1 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
- package/src/form/form_utils.ts +163 -0
- package/src/form/index.tsx +14 -5
- package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
- package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
- package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
- package/src/hooks/navigation/useAsyncResolver.ts +100 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
- package/src/hooks/navigation/useNavigationResolution.ts +33 -0
- package/src/hooks/navigation/useResolvedCollections.ts +29 -71
- package/src/hooks/navigation/useResolvedViews.tsx +40 -70
- package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
- package/src/hooks/navigation/utils.ts +7 -4
- package/src/index.ts +5 -0
- package/src/routes/CustomViewRoute.tsx +14 -6
- package/src/types/components/EntityFormActionsProps.tsx +1 -1
- package/src/types/components/EntityFormProps.tsx +68 -0
- package/src/types/fields.tsx +7 -1
- package/src/util/dataControllerAdapter.ts +93 -0
- package/src/util/navigation_utils.ts +1 -1
- package/src/util/propertyConfigMapper.ts +134 -0
- package/LICENSE +0 -21
- package/dist/CollectionEditorDialog-HNXGxXdO.js.map +0 -1
- package/dist/CollectionsStudioView-B1mN33GB.js.map +0 -1
- package/dist/PropertyEditView-C2wp8kQ9.js.map +0 -1
- package/dist/components/EntityCollectionView/Board.d.ts +0 -3
- package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
- package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
- package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
- package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
- package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
- package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
- package/dist/src-BYniefVu.js.map +0 -1
- package/src/components/EntityCollectionView/Board.tsx +0 -475
- package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
- package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
- package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
- package/src/components/EntityCollectionView/board_types.ts +0 -113
- package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
- 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 {
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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:
|
|
161
|
-
adminViews:
|
|
131
|
+
views: resolvedData.views,
|
|
132
|
+
adminViews: resolvedData.adminViews,
|
|
162
133
|
loading,
|
|
163
134
|
error,
|
|
164
135
|
refresh
|
|
165
|
-
}), [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
|
217
|
-
const
|
|
218
|
-
const uniqueGroups = [...
|
|
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
|
|
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 => ({
|
|
32
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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 = {
|
package/src/types/fields.tsx
CHANGED
|
@@ -213,7 +213,7 @@ export interface FormContext<M extends Record<string, unknown> = Record<string,
|
|
|
213
213
|
|
|
214
214
|
savingError?: Error;
|
|
215
215
|
|
|
216
|
-
openEntityMode
|
|
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
|
|
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.
|