@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.
- package/README.md +7 -3
- package/dist/{CollectionEditorDialog-jA-PLh6N.js → CollectionEditorDialog-iGgS4rLX.js} +233 -152
- package/dist/CollectionEditorDialog-iGgS4rLX.js.map +1 -0
- package/dist/{PropertyEditView-DEUddmg_.js → PropertyEditView-C9yhLyja.js} +129 -15
- package/dist/PropertyEditView-C9yhLyja.js.map +1 -0
- package/dist/{CollectionsStudioView-UKouBPOm.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/editor/editor.d.ts +10 -4
- package/dist/editor.js +6 -4
- package/dist/editor.js.map +1 -1
- package/dist/form/EntityForm.d.ts +10 -9
- package/dist/form/EntityFormBinding.d.ts +27 -0
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +2 -2
- package/dist/form/form_utils.d.ts +8 -0
- package/dist/form/index.d.ts +5 -1
- package/dist/{history-BL49TIDb.js → history-ChHgyyvM.js} +2 -2
- package/dist/{history-BL49TIDb.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 +3 -1
- package/dist/index.js +2 -2
- package/dist/routes/CustomViewRoute.d.ts +2 -1
- package/dist/{src-BJ8fkM8y.js → src-DOX2nOyh.js} +1634 -1330
- 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/AddKanbanColumnAction.tsx +2 -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/ArrayContainer.tsx +5 -5
- package/src/components/DefaultDrawer.tsx +13 -3
- package/src/components/DrawerNavigationGroup.tsx +2 -2
- package/src/components/DrawerNavigationItem.tsx +4 -4
- package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +14 -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/HomePage/NavigationCard.tsx +8 -14
- package/src/components/HomePage/NavigationGroup.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -5
- 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/SelectableTable/filters/StringNumberFilterField.tsx +28 -21
- package/src/components/SideDialogs.tsx +0 -7
- package/src/components/common/default_entity_actions.tsx +18 -10
- package/src/contexts/BreacrumbsContext.tsx +15 -6
- package/src/editor/editor.tsx +14 -8
- 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/MarkdownEditorFieldBinding.tsx +4 -4
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +1 -1
- 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 +7 -2
- 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-jA-PLh6N.js.map +0 -1
- package/dist/CollectionsStudioView-UKouBPOm.js.map +0 -1
- package/dist/PropertyEditView-DEUddmg_.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-BJ8fkM8y.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
|
@@ -123,7 +123,7 @@ type ActionsViewProps<M extends Record<string, unknown>> = {
|
|
|
123
123
|
status: "new" | "existing" | "copy",
|
|
124
124
|
sideDialogContext: SideDialogController,
|
|
125
125
|
pluginActions?: React.ReactNode[],
|
|
126
|
-
openEntityMode
|
|
126
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
127
127
|
navigateBack: () => void;
|
|
128
128
|
formContext: FormContext,
|
|
129
129
|
formex: FormexController<Record<string, unknown>>;
|
|
@@ -164,6 +164,7 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
164
164
|
{formActions.length > 0 && <div className="grow flex overflow-auto no-scrollbar">
|
|
165
165
|
{formActions.map((action, index) => {
|
|
166
166
|
|
|
167
|
+
|
|
167
168
|
const props = {
|
|
168
169
|
view: "form",
|
|
169
170
|
entity,
|
|
@@ -208,17 +209,19 @@ function buildBottomActions<M extends Record<string, unknown>>({
|
|
|
208
209
|
{t("back_to_detail") ?? "Back to details"}
|
|
209
210
|
</Button>
|
|
210
211
|
)}
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
212
|
+
<Tooltip title={hasErrors ? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving") : undefined}>
|
|
213
|
+
<Button variant={canClose ? "text" : "filled"}
|
|
214
|
+
color="primary"
|
|
215
|
+
type="submit"
|
|
216
|
+
disabled={disabled || formex.isSubmitting}
|
|
217
|
+
onClick={() => {
|
|
218
|
+
sideDialogContext.setPendingClose(false);
|
|
219
|
+
}}>
|
|
220
|
+
{status === "existing" && t("save")}
|
|
221
|
+
{status === "copy" && t("create_copy")}
|
|
222
|
+
{status === "new" && t("create")}
|
|
223
|
+
</Button>
|
|
224
|
+
</Tooltip>
|
|
222
225
|
{canClose && <LoadingButton variant="filled"
|
|
223
226
|
color="primary"
|
|
224
227
|
type="submit"
|
|
@@ -256,19 +259,22 @@ function buildSideActions<M extends Record<string, unknown>>({
|
|
|
256
259
|
const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
|
|
257
260
|
return <div
|
|
258
261
|
className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className)}>
|
|
259
|
-
<
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
262
|
+
<Tooltip title={hasErrors ? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving") : undefined}>
|
|
263
|
+
<LoadingButton fullWidth={true}
|
|
264
|
+
variant="filled"
|
|
265
|
+
color="primary"
|
|
266
|
+
type="submit"
|
|
267
|
+
loading={formex.isSubmitting}
|
|
268
|
+
startIcon={hasErrors ? <AlertCircleIcon/> : undefined}
|
|
269
|
+
disabled={disabled || formex.isSubmitting}
|
|
270
|
+
onClick={() => {
|
|
271
|
+
sideDialogContext.setPendingClose?.(false);
|
|
272
|
+
}}>
|
|
273
|
+
{status === "existing" && t("save")}
|
|
274
|
+
{status === "copy" && t("create_copy")}
|
|
275
|
+
{status === "new" && t("create")}
|
|
276
|
+
</LoadingButton>
|
|
277
|
+
</Tooltip>
|
|
272
278
|
|
|
273
279
|
<Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
|
|
274
280
|
{status === "existing" ? t("discard") : t("clear")}
|
|
@@ -293,7 +299,7 @@ function buildSideActions<M extends Record<string, unknown>>({
|
|
|
293
299
|
openEntityMode,
|
|
294
300
|
navigateBack,
|
|
295
301
|
formContext
|
|
296
|
-
} satisfies EntityActionClickProps<
|
|
302
|
+
} satisfies EntityActionClickProps<Record<string, unknown>>;
|
|
297
303
|
const isEnabled = !action.isEnabled || action.isEnabled(props);
|
|
298
304
|
return (
|
|
299
305
|
<EntityActionButton key={action.key ?? action.name ?? index} action={action} enabled={isEnabled} props={props}/>
|
|
@@ -63,14 +63,23 @@ export function ContentHomePage({
|
|
|
63
63
|
onNavigationEntriesUpdate = () => {}
|
|
64
64
|
} = navigationStateController.topLevelNavigation || {};
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
66
|
+
// Build a set of Studio-only dev view slugs so we can distinguish them from
|
|
67
|
+
// CMS custom views added via <RebaseCMS views={...}> or plugins.
|
|
68
|
+
const studioViewSlugs = useMemo(() => {
|
|
69
|
+
const slugs = new Set<string>();
|
|
70
|
+
(registry.studioConfig?.devViews ?? []).forEach(v => slugs.add(v.slug));
|
|
71
|
+
if (registry.studioConfig && registry.cmsConfig?.collectionEditor) slugs.add("schema");
|
|
72
|
+
return slugs;
|
|
73
|
+
}, [registry.studioConfig, registry.cmsConfig?.collectionEditor]);
|
|
74
|
+
|
|
75
|
+
// Studio mode shows Studio dev views + admin entries (Users/Roles).
|
|
76
|
+
// Content mode shows collections, CMS custom views, and admin entries — but not Studio dev views.
|
|
68
77
|
const rawNavigationEntries = useMemo(() => {
|
|
69
78
|
if (adminModeController.mode === "studio") {
|
|
70
79
|
return unFilteredNavigationEntries.filter(e => e.type === "view" || e.type === "admin");
|
|
71
80
|
}
|
|
72
|
-
return unFilteredNavigationEntries.filter(e => e.type !== "view");
|
|
73
|
-
}, [unFilteredNavigationEntries, adminModeController.mode]);
|
|
81
|
+
return unFilteredNavigationEntries.filter(e => e.type !== "view" || !studioViewSlugs.has(e.slug));
|
|
82
|
+
}, [unFilteredNavigationEntries, adminModeController.mode, studioViewSlugs]);
|
|
74
83
|
|
|
75
84
|
const groupOrderFromNavController = useMemo(() => {
|
|
76
85
|
const entryGroups = new Set(rawNavigationEntries.map(e => e.group).filter(Boolean));
|
|
@@ -25,10 +25,8 @@ export const NavigationCard = React.memo(function NavigationCard({
|
|
|
25
25
|
return (
|
|
26
26
|
<Card
|
|
27
27
|
className={cls(
|
|
28
|
-
"group h-full p-4 cursor-pointer transition-
|
|
29
|
-
"
|
|
30
|
-
"hover:shadow-md hover:shadow-black/[0.04]",
|
|
31
|
-
"hover:border-surface-300 dark:hover:border-primary/20",
|
|
28
|
+
"group h-full p-4 cursor-pointer transition-colors duration-150 ease-in-out",
|
|
29
|
+
"hover:bg-primary/5 dark:hover:bg-primary/5",
|
|
32
30
|
shrink && "w-full max-w-full min-h-0 scale-75"
|
|
33
31
|
)}
|
|
34
32
|
onClick={() => {
|
|
@@ -39,8 +37,8 @@ export const NavigationCard = React.memo(function NavigationCard({
|
|
|
39
37
|
<div className="flex flex-col h-full">
|
|
40
38
|
{/* Header: title + icon left, actions right */}
|
|
41
39
|
<div className="flex items-center w-full justify-between mb-1">
|
|
42
|
-
<div className="flex items-center gap-
|
|
43
|
-
<div className="flex items-center justify-center w-
|
|
40
|
+
<div className="flex items-center gap-3">
|
|
41
|
+
<div className="flex items-center justify-center w-5 h-5 text-surface-400 dark:text-surface-500 transition-colors duration-150 group-hover:text-primary dark:group-hover:text-primary">
|
|
44
42
|
{icon}
|
|
45
43
|
</div>
|
|
46
44
|
<Typography variant="subtitle1"
|
|
@@ -60,7 +58,7 @@ export const NavigationCard = React.memo(function NavigationCard({
|
|
|
60
58
|
</div>
|
|
61
59
|
|
|
62
60
|
{/* Description */}
|
|
63
|
-
<div className="
|
|
61
|
+
<div className="pl-8">
|
|
64
62
|
{description && <Typography variant="caption"
|
|
65
63
|
color="secondary"
|
|
66
64
|
component="div">
|
|
@@ -69,17 +67,13 @@ export const NavigationCard = React.memo(function NavigationCard({
|
|
|
69
67
|
</div>
|
|
70
68
|
|
|
71
69
|
{additionalContent && (
|
|
72
|
-
<div className="pl-
|
|
70
|
+
<div className="pl-8 pointer-events-none">
|
|
73
71
|
{additionalContent}
|
|
74
72
|
</div>
|
|
75
73
|
)}
|
|
76
74
|
|
|
77
|
-
{/*
|
|
78
|
-
<div className="
|
|
79
|
-
<div className={"transition-transform duration-200 group-hover:translate-x-0.5"}>
|
|
80
|
-
<ArrowRightIcon className="text-primary" size={iconSize.small}/>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
75
|
+
{/* Spacer pushes content above to align at the top across grid siblings */}
|
|
76
|
+
<div className="grow"/>
|
|
83
77
|
|
|
84
78
|
</div>
|
|
85
79
|
|
|
@@ -40,7 +40,7 @@ export function NavigationGroup({
|
|
|
40
40
|
color="secondary"
|
|
41
41
|
className={cls(
|
|
42
42
|
"px-4 py-1 rounded",
|
|
43
|
-
"font-
|
|
43
|
+
"font-medium text-[10px] uppercase tracking-[0.08em] text-primary/50 dark:text-primary/70"
|
|
44
44
|
)}
|
|
45
45
|
>
|
|
46
46
|
{currentGroupName}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cardClickableMixin, cardMixin, cls, Typography } from "@rebasepro/ui";
|
|
2
2
|
|
|
3
3
|
import { Link } from "react-router-dom";
|
|
4
4
|
|
|
@@ -35,11 +35,7 @@ export function SmallNavigationCard({
|
|
|
35
35
|
</Typography>
|
|
36
36
|
</div>
|
|
37
37
|
|
|
38
|
-
<div className={"p-2"}>
|
|
39
|
-
<ArrowRightIcon className="text-primary"/>
|
|
40
|
-
</div>
|
|
41
38
|
</Link>
|
|
42
39
|
|
|
43
40
|
</>);
|
|
44
41
|
}
|
|
45
|
-
|
|
@@ -10,11 +10,12 @@ import type { RebaseCMSConfig } from "@rebasepro/types";
|
|
|
10
10
|
* is auto-wired as a native feature (slots, provider, Studio view) without
|
|
11
11
|
* needing any external plugin.
|
|
12
12
|
*/
|
|
13
|
-
export function RebaseCMS({ collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings }: RebaseCMSConfig) {
|
|
13
|
+
export function RebaseCMS({ collections, views, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings }: RebaseCMSConfig) {
|
|
14
14
|
const dispatch = useRebaseRegistryDispatch();
|
|
15
15
|
|
|
16
16
|
useLayoutEffect(() => {
|
|
17
17
|
dispatch.registerCMS({ collections,
|
|
18
|
+
views,
|
|
18
19
|
homePage,
|
|
19
20
|
entityViews,
|
|
20
21
|
entityActions,
|
|
@@ -22,7 +23,7 @@ plugins,
|
|
|
22
23
|
collectionEditor,
|
|
23
24
|
navigationGroupMappings });
|
|
24
25
|
return () => dispatch.unregisterCMS();
|
|
25
|
-
}, [dispatch, collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings]);
|
|
26
|
+
}, [dispatch, collections, views, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings]);
|
|
26
27
|
|
|
27
28
|
return null;
|
|
28
29
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useMemo, lazy, Suspense } from "react";
|
|
1
|
+
import React, { useMemo, useRef, useEffect, lazy, Suspense } from "react";
|
|
2
2
|
import {
|
|
3
3
|
useRebaseRegistry,
|
|
4
4
|
useRebaseContext,
|
|
@@ -7,10 +7,14 @@ import {
|
|
|
7
7
|
useAdminModeController,
|
|
8
8
|
useBuildLocalConfigurationPersistence,
|
|
9
9
|
useRebaseClient,
|
|
10
|
+
useData,
|
|
11
|
+
useDataSources,
|
|
10
12
|
StudioBridgeRegistryProvider,
|
|
11
13
|
useBridgeRegistration,
|
|
12
|
-
CustomizationControllerContext
|
|
14
|
+
CustomizationControllerContext,
|
|
15
|
+
RebaseDataContext
|
|
13
16
|
} from "@rebasepro/core";
|
|
17
|
+
import { buildRoutedRebaseData, resolveDataSource } from "@rebasepro/common";
|
|
14
18
|
import { CircularProgressCenter } from "@rebasepro/ui";
|
|
15
19
|
import type { AppView, CollectionEditorOptions, EntityCustomView, EntityAction, EntityCollection, RebasePlugin } from "@rebasepro/types";
|
|
16
20
|
import type { CollectionRegistryController } from "@rebasepro/types";
|
|
@@ -35,8 +39,8 @@ import { ConfigControllerProvider } from "../collection_editor/ConfigControllerP
|
|
|
35
39
|
|
|
36
40
|
// Lazy-load the schema view — only fetched when studio schema tool is active
|
|
37
41
|
const CollectionsStudioView = lazy(() =>
|
|
38
|
-
import("../collection_editor/ui/collection_editor/
|
|
39
|
-
.then(m => ({ default: m.
|
|
42
|
+
import("../collection_editor/ui/collection_editor/RouterCollectionsStudioView")
|
|
43
|
+
.then(m => ({ default: m.RouterCollectionsStudioView }))
|
|
40
44
|
);
|
|
41
45
|
|
|
42
46
|
export interface RebaseNavigationProps {
|
|
@@ -86,7 +90,24 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
86
90
|
// ── Build the navigation controllers ──────────────────────────────
|
|
87
91
|
const collectionsBuilder = useMemo(() => Array.isArray(cmsCollections) ? () => [...cmsCollections] : cmsCollections, [cmsCollections]);
|
|
88
92
|
|
|
89
|
-
const
|
|
93
|
+
const dataSources = useDataSources();
|
|
94
|
+
const collectionRegistryController = useBuildCollectionRegistryController({ userConfigPersistence, dataSources: dataSources.registry });
|
|
95
|
+
|
|
96
|
+
// ── Multi-data-source routing ─────────────────────────────────────
|
|
97
|
+
// Combine the default data source (server transport) with the registered
|
|
98
|
+
// direct/custom sources from <Rebase> and route each collection by its
|
|
99
|
+
// resolved data-source key, looked up by path against the collection
|
|
100
|
+
// registry. A stable resolver ref keeps the routed data instance
|
|
101
|
+
// referentially stable across registry rebuilds, so data-effect
|
|
102
|
+
// dependencies don't thrash.
|
|
103
|
+
const defaultData = useData();
|
|
104
|
+
const getCollectionRef = useRef(collectionRegistryController.getCollection);
|
|
105
|
+
getCollectionRef.current = collectionRegistryController.getCollection;
|
|
106
|
+
const routedData = useMemo(() => buildRoutedRebaseData({
|
|
107
|
+
defaultData,
|
|
108
|
+
sources: dataSources.sources,
|
|
109
|
+
resolveKey: (slugOrPath) => resolveDataSource(getCollectionRef.current(slugOrPath), dataSources.registry).key
|
|
110
|
+
}), [defaultData, dataSources]);
|
|
90
111
|
|
|
91
112
|
const urlController = useBuildUrlController({
|
|
92
113
|
basePath: "/",
|
|
@@ -102,6 +123,26 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
102
123
|
[cmsCollections]
|
|
103
124
|
);
|
|
104
125
|
|
|
126
|
+
// ── Dev-only data-source sanity check ─────────────────────────────
|
|
127
|
+
// Warn about data sources declared with a direct/custom transport that
|
|
128
|
+
// have no client-side driver — these silently fall back to the default
|
|
129
|
+
// (server) source, a common source of misrouting. Unambiguous: it never
|
|
130
|
+
// false-positives on server-mediated sources (which intentionally have
|
|
131
|
+
// no client driver).
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (process.env.NODE_ENV === "production") return;
|
|
134
|
+
const { registry, sources } = dataSources;
|
|
135
|
+
const missing = Object.values(registry)
|
|
136
|
+
.filter((d) => (d.transport === "direct" || d.transport === "custom") && !sources[d.key])
|
|
137
|
+
.map((d) => d.key);
|
|
138
|
+
if (missing.length > 0) {
|
|
139
|
+
console.warn(
|
|
140
|
+
`[Rebase] These data source(s) declare a direct/custom transport but have no client-side driver and will fall back to the default data source: ${missing.map(k => `"${k}"`).join(", ")}. ` +
|
|
141
|
+
`Provide a \`driver\` for them in \`dataSources\` on <Rebase>.`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
}, [dataSources]);
|
|
145
|
+
|
|
105
146
|
const internalConfigController = useLocalCollectionsConfigController(
|
|
106
147
|
rebaseClient,
|
|
107
148
|
resolvedCollections,
|
|
@@ -120,6 +161,7 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
120
161
|
return {
|
|
121
162
|
slug: "schema",
|
|
122
163
|
name: "Edit collections",
|
|
164
|
+
group: "Database",
|
|
123
165
|
icon: "LayoutList",
|
|
124
166
|
nestedRoutes: true,
|
|
125
167
|
view: (
|
|
@@ -136,13 +178,30 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
136
178
|
return base;
|
|
137
179
|
}, [registry.studioConfig?.devViews, schemaView]);
|
|
138
180
|
|
|
181
|
+
// Merge CMS-registered views with Studio dev views.
|
|
182
|
+
// Order: CMS views (developer's primary content) → Studio dev views (tooling).
|
|
183
|
+
// Plugin views are merged later inside resolveAppViews.
|
|
184
|
+
const cmsViews = registry.cmsConfig?.views;
|
|
185
|
+
const mergedViews = useMemo(() => {
|
|
186
|
+
if (!cmsViews) return devViews;
|
|
187
|
+
if (Array.isArray(cmsViews) && cmsViews.length === 0) return devViews;
|
|
188
|
+
// When cmsViews is a builder function, wrap it to append devViews after resolution
|
|
189
|
+
if (typeof cmsViews === "function") {
|
|
190
|
+
return async (params: Parameters<typeof cmsViews>[0]) => {
|
|
191
|
+
const resolved = await cmsViews(params) ?? [];
|
|
192
|
+
return [...resolved, ...devViews];
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return [...cmsViews, ...devViews];
|
|
196
|
+
}, [cmsViews, devViews]);
|
|
197
|
+
|
|
139
198
|
const navigationStateController = useBuildNavigationStateController({
|
|
140
199
|
plugins: registry.cmsConfig?.plugins ?? EMPTY_PLUGINS,
|
|
141
200
|
collections: collectionsBuilder,
|
|
142
|
-
views:
|
|
201
|
+
views: mergedViews,
|
|
143
202
|
navigationGroupMappings: registry.cmsConfig?.navigationGroupMappings,
|
|
144
203
|
authController: context.authController!,
|
|
145
|
-
data:
|
|
204
|
+
data: routedData,
|
|
146
205
|
collectionRegistryController,
|
|
147
206
|
urlController,
|
|
148
207
|
adminMode: adminModeController?.mode
|
|
@@ -175,7 +234,11 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
175
234
|
}, [parentCustomizationController, registry.cmsConfig?.entityViews, registry.cmsConfig?.entityActions]);
|
|
176
235
|
|
|
177
236
|
// ── Inner content with all context providers ──────────────────────
|
|
237
|
+
// Re-provide RebaseDataContext with the routed data so that every CMS
|
|
238
|
+
// consumer (list/entity views, references, board, import/export, and
|
|
239
|
+
// `context.data`) is routed to the correct driver by collection path.
|
|
178
240
|
const navigationContent = (
|
|
241
|
+
<RebaseDataContext.Provider value={routedData}>
|
|
179
242
|
<CustomizationControllerContext.Provider value={enrichedCustomizationController}>
|
|
180
243
|
<StudioBridgeRegistryProvider>
|
|
181
244
|
<CollectionRegistryContext.Provider value={collectionRegistryController}>
|
|
@@ -194,6 +257,7 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
194
257
|
</CollectionRegistryContext.Provider>
|
|
195
258
|
</StudioBridgeRegistryProvider>
|
|
196
259
|
</CustomizationControllerContext.Provider>
|
|
260
|
+
</RebaseDataContext.Provider>
|
|
197
261
|
);
|
|
198
262
|
|
|
199
263
|
// ── Wrap with ConfigControllerProvider when collection editor is enabled ──
|
|
@@ -2,7 +2,7 @@ import { createSelectionStore } from "./SelectionStore";
|
|
|
2
2
|
import type { Property } from "@rebasepro/types";
|
|
3
3
|
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { CollectionSize, Entity, EntityRelation, EntityTableController, FilterValues, SelectedCellProps } from "@rebasepro/types";
|
|
5
|
-
import { CellRendererParams,
|
|
5
|
+
import { CellRendererParams, TableView, VirtualTableColumn, VirtualTableFilterValues, OnRowClickParams, VirtualTableWhereFilterOp } from "@rebasepro/ui";
|
|
6
6
|
import { enumToObjectEntries } from "@rebasepro/common";
|
|
7
7
|
import { DEFAULT_PAGE_SIZE, EntityCollectionTableController, OnCellValueChange, OnColumnResizeParams } from "@rebasepro/core";
|
|
8
8
|
import { FilterFormFieldProps } from "@rebasepro/ui";
|
|
@@ -244,7 +244,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
244
244
|
<div className="h-full w-full flex flex-col bg-white dark:bg-surface-900"
|
|
245
245
|
ref={ref}>
|
|
246
246
|
|
|
247
|
-
|
|
247
|
+
<TableView
|
|
248
248
|
data={data as unknown as Record<string, unknown>[]}
|
|
249
249
|
columns={columns}
|
|
250
250
|
cellRenderer={(props) => cellRenderer(props as unknown as CellRendererParams<Entity<M>>)}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
2
1
|
import React, { useState } from "react";
|
|
3
2
|
import { EnumValuesChip } from "../../../preview";
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
IconButton,
|
|
5
|
+
MultiSelect,
|
|
6
|
+
MultiSelectItem,
|
|
7
|
+
Select,
|
|
8
|
+
SelectItem,
|
|
9
|
+
TextField,
|
|
10
|
+
VirtualTableWhereFilterOp,
|
|
11
|
+
XIcon
|
|
12
|
+
} from "@rebasepro/ui";
|
|
6
13
|
import { EnumValueConfig } from "@rebasepro/types";
|
|
7
14
|
import { useTranslation } from "@rebasepro/core";
|
|
8
15
|
|
|
@@ -33,14 +40,14 @@ const operationLabels = {
|
|
|
33
40
|
const multipleSelectOperations = ["array-contains-any", "in", "not-in"];
|
|
34
41
|
|
|
35
42
|
export function StringNumberFilterField({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}: StringNumberFilterFieldProps) {
|
|
43
|
+
name,
|
|
44
|
+
value,
|
|
45
|
+
setValue,
|
|
46
|
+
type,
|
|
47
|
+
isArray,
|
|
48
|
+
enumValues,
|
|
49
|
+
title
|
|
50
|
+
}: StringNumberFilterFieldProps) {
|
|
44
51
|
const { t } = useTranslation();
|
|
45
52
|
|
|
46
53
|
const possibleOperations: (keyof typeof operationLabels)[] = isArray
|
|
@@ -110,13 +117,13 @@ export function StringNumberFilterField({
|
|
|
110
117
|
<div className="flex w-full">
|
|
111
118
|
<div className={"w-[100px]"}>
|
|
112
119
|
<Select value={operation}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
size={"medium"}
|
|
121
|
+
fullWidth={true}
|
|
122
|
+
position={"item-aligned"}
|
|
123
|
+
onValueChange={(value) => {
|
|
124
|
+
updateFilter(value as VirtualTableWhereFilterOp | "is-null", internalValue);
|
|
125
|
+
}}
|
|
126
|
+
renderValue={(op) => operationLabels[op as keyof typeof operationLabels]}>
|
|
120
127
|
{possibleOperations.map((op) => (
|
|
121
128
|
<SelectItem key={op} value={op}>
|
|
122
129
|
{operationLabels[op]}
|
|
@@ -174,7 +181,7 @@ export function StringNumberFilterField({
|
|
|
174
181
|
}}>
|
|
175
182
|
{enumValues.map((enumConfig) => (
|
|
176
183
|
<SelectItem key={`select_item_${name}_${enumConfig.id}`}
|
|
177
|
-
|
|
184
|
+
value={String(enumConfig.id)}>
|
|
178
185
|
<EnumValuesChip
|
|
179
186
|
enumKey={String(enumConfig.id)}
|
|
180
187
|
enumValues={enumValues}
|
|
@@ -191,7 +198,7 @@ export function StringNumberFilterField({
|
|
|
191
198
|
value={Array.isArray(internalValue) ? internalValue.map(e => String(e)) : []}
|
|
192
199
|
disabled={isNullOperation}
|
|
193
200
|
onValueChange={(value) => {
|
|
194
|
-
updateFilter(operation, type === "number" ? value.map(v => parseInt(v)) : value)
|
|
201
|
+
updateFilter(operation, type === "number" ? (value ?? []).map(v => parseInt(v)) : value)
|
|
195
202
|
}}
|
|
196
203
|
multiple={multiple}
|
|
197
204
|
endAdornment={internalValue && <IconButton
|
|
@@ -202,7 +209,7 @@ export function StringNumberFilterField({
|
|
|
202
209
|
>
|
|
203
210
|
{enumValues.map((enumConfig) => (
|
|
204
211
|
<MultiSelectItem key={`select_value_${name}_${enumConfig.id}`}
|
|
205
|
-
|
|
212
|
+
value={String(enumConfig.id)}>
|
|
206
213
|
<EnumValuesChip
|
|
207
214
|
enumKey={String(enumConfig.id)}
|
|
208
215
|
enumValues={enumValues}
|
|
@@ -179,13 +179,6 @@ function SideDialogView({
|
|
|
179
179
|
style={{ zIndex: 45 + panelIndex * 10 }}
|
|
180
180
|
onOpenChange={(open) => {
|
|
181
181
|
if (!open) {
|
|
182
|
-
// Check if any suggestion menu is visible in DOM
|
|
183
|
-
const suggestionMenu = document.querySelector("[data-suggestion-menu=\"true\"]");
|
|
184
|
-
if (suggestionMenu && window.getComputedStyle(suggestionMenu).visibility !== "hidden") {
|
|
185
|
-
// Don't close the sheet if a suggestion menu is visible
|
|
186
|
-
// Let Tiptap handle closing the menu first
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
182
|
onCloseRequest();
|
|
190
183
|
}
|
|
191
184
|
}}
|
|
@@ -36,7 +36,7 @@ export const editEntityAction: EntityAction = {
|
|
|
36
36
|
|
|
37
37
|
highlightEntity?.(entity);
|
|
38
38
|
|
|
39
|
-
context
|
|
39
|
+
context?.analyticsController?.onAnalyticsEvent?.("entity_click", {
|
|
40
40
|
path: entity.path,
|
|
41
41
|
entityId: entity.id
|
|
42
42
|
});
|
|
@@ -62,7 +62,7 @@ export const editEntityAction: EntityAction = {
|
|
|
62
62
|
path: newFullIdPath,
|
|
63
63
|
sideEntityController,
|
|
64
64
|
onClose: () => unhighlightEntity?.(entity),
|
|
65
|
-
navigation: context
|
|
65
|
+
navigation: context?.urlController!,
|
|
66
66
|
selectedTab: defaultSelectedView
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -92,7 +92,7 @@ export const copyEntityAction: EntityAction = {
|
|
|
92
92
|
throw new Error("INTERNAL: copyEntityAction: sideEntityController is undefined");
|
|
93
93
|
}
|
|
94
94
|
highlightEntity?.(entity);
|
|
95
|
-
context
|
|
95
|
+
context?.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
|
|
96
96
|
path: entity.path,
|
|
97
97
|
entityId: entity.id
|
|
98
98
|
});
|
|
@@ -106,7 +106,7 @@ export const copyEntityAction: EntityAction = {
|
|
|
106
106
|
copy: true,
|
|
107
107
|
sideEntityController,
|
|
108
108
|
onClose: () => unhighlightEntity?.(entity),
|
|
109
|
-
navigation: context
|
|
109
|
+
navigation: context?.urlController!
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
return Promise.resolve(undefined);
|
|
@@ -125,12 +125,13 @@ export const deleteEntityAction: EntityAction = {
|
|
|
125
125
|
context,
|
|
126
126
|
selectionController,
|
|
127
127
|
onCollectionChange,
|
|
128
|
-
navigateBack
|
|
128
|
+
navigateBack,
|
|
129
|
+
openEntityMode
|
|
129
130
|
}): Promise<void> {
|
|
130
131
|
if (!entity) {
|
|
131
132
|
throw new Error("INTERNAL: deleteEntityAction: Entity is undefined");
|
|
132
133
|
}
|
|
133
|
-
if (!context
|
|
134
|
+
if (!context?.dialogsController) {
|
|
134
135
|
throw new Error("INTERNAL: deleteEntityAction: context.dialogsController is undefined");
|
|
135
136
|
}
|
|
136
137
|
const { closeDialog } = context.dialogsController.open({
|
|
@@ -145,12 +146,19 @@ export const deleteEntityAction: EntityAction = {
|
|
|
145
146
|
callbacks={collection.callbacks}
|
|
146
147
|
open={open}
|
|
147
148
|
onEntityDelete={() => {
|
|
148
|
-
context
|
|
149
|
+
context?.analyticsController?.onAnalyticsEvent?.("single_entity_deleted", {
|
|
149
150
|
path
|
|
150
151
|
});
|
|
151
152
|
selectionController?.setSelectedEntities(selectionController.selectedEntities.filter(e => e.id !== entity.id));
|
|
152
153
|
onCollectionChange?.();
|
|
153
|
-
navigateBack
|
|
154
|
+
// In full-screen mode, navigateBack would go to the deleted entity's
|
|
155
|
+
// detail URL, which no longer exists. Navigate to the parent collection instead.
|
|
156
|
+
if (openEntityMode === "full_screen" && context?.urlController) {
|
|
157
|
+
const collectionUrl = context.urlController.buildUrlCollectionPath(path);
|
|
158
|
+
context.urlController.navigate(collectionUrl, { replace: true });
|
|
159
|
+
} else {
|
|
160
|
+
navigateBack?.();
|
|
161
|
+
}
|
|
154
162
|
}}
|
|
155
163
|
onClose={closeDialog}/>;
|
|
156
164
|
}
|
|
@@ -250,7 +258,7 @@ export const resetPasswordAction: EntityAction = {
|
|
|
250
258
|
if (!entity) {
|
|
251
259
|
throw new Error("INTERNAL: resetPasswordAction: Entity is undefined");
|
|
252
260
|
}
|
|
253
|
-
if (!context
|
|
261
|
+
if (!context?.dialogsController) {
|
|
254
262
|
throw new Error("INTERNAL: resetPasswordAction: context.dialogsController is undefined");
|
|
255
263
|
}
|
|
256
264
|
|
|
@@ -264,7 +272,7 @@ export const resetPasswordAction: EntityAction = {
|
|
|
264
272
|
roles: entity.values?.roles as string[] || []
|
|
265
273
|
};
|
|
266
274
|
|
|
267
|
-
const { closeDialog } = context
|
|
275
|
+
const { closeDialog } = context!.dialogsController.open({
|
|
268
276
|
key: "reset_password_dialog_" + entity.id,
|
|
269
277
|
Component: ({ open }) => (
|
|
270
278
|
<ResetPasswordActionDialog
|
|
@@ -22,14 +22,23 @@ export const BreadcrumbsProvider: React.FC<BreadcrumbsProviderProps> = ({ childr
|
|
|
22
22
|
const set = useCallback((props: {
|
|
23
23
|
breadcrumbs: BreadcrumbEntry[];
|
|
24
24
|
}) => {
|
|
25
|
-
setBreadcrumbs(prev =>
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
setBreadcrumbs(prev => {
|
|
26
|
+
const next = props.breadcrumbs.map(newEntry => {
|
|
27
|
+
const prevEntry = newEntry.id ? prev.find(p => p.id === newEntry.id) : undefined;
|
|
28
|
+
if (prevEntry && newEntry.count === null && typeof prevEntry.count === "number") {
|
|
29
|
+
return { ...newEntry,
|
|
29
30
|
count: prevEntry.count };
|
|
31
|
+
}
|
|
32
|
+
return newEntry;
|
|
33
|
+
});
|
|
34
|
+
// Bail out if nothing changed — return same reference to skip re-render
|
|
35
|
+
if (prev.length === next.length && prev.every((p, i) =>
|
|
36
|
+
p.title === next[i].title && p.url === next[i].url && p.id === next[i].id && p.count === next[i].count
|
|
37
|
+
)) {
|
|
38
|
+
return prev;
|
|
30
39
|
}
|
|
31
|
-
return
|
|
32
|
-
})
|
|
40
|
+
return next;
|
|
41
|
+
});
|
|
33
42
|
}, []);
|
|
34
43
|
|
|
35
44
|
const updateCount = useCallback((id: string, count: number | null | undefined) => {
|