@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
@@ -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: "side_panel" | "full_screen" | "split" | "dialog";
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
- <Button variant={canClose ? "text" : "filled"}
212
- color="primary"
213
- type="submit"
214
- disabled={disabled || formex.isSubmitting}
215
- onClick={() => {
216
- sideDialogContext.setPendingClose(false);
217
- }}>
218
- {status === "existing" && t("save")}
219
- {status === "copy" && t("create_copy")}
220
- {status === "new" && t("create")}
221
- </Button>
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
- <LoadingButton fullWidth={true}
260
- variant="filled"
261
- color="primary"
262
- type="submit"
263
- startIcon={hasErrors ? <AlertCircleIcon/> : undefined}
264
- disabled={disabled || formex.isSubmitting}
265
- onClick={() => {
266
- sideDialogContext.setPendingClose?.(false);
267
- }}>
268
- {status === "existing" && t("save")}
269
- {status === "copy" && t("create_copy")}
270
- {status === "new" && t("create")}
271
- </LoadingButton>
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<any>;
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
- // Studio mode shows view-type entries (devViews) + admin entries (Users/Roles).
67
- // Content mode shows collections, admin entries (Users/Roles), and custom entries — but not studio views.
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-all duration-150 ease-in-out",
29
- "border-surface-200 dark:border-surface-700/40",
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-4">
43
- <div className="flex items-center justify-center w-6 h-6 rounded-lg bg-primary/8 dark:bg-primary/10 text-primary/70 dark:text-primary/60 transition-colors duration-200 group-hover:bg-primary/12 dark:group-hover:bg-primary/15 group-hover:text-primary dark:group-hover:text-primary/80">
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="grow pl-[40px]">
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-[40px] pointer-events-none">
70
+ <div className="pl-8 pointer-events-none">
73
71
  {additionalContent}
74
72
  </div>
75
73
  )}
76
74
 
77
- {/* Arrow */}
78
- <div className="self-end mt-1">
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-semibold text-[11px] uppercase tracking-wider text-surface-400 dark:text-surface-400"
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 { ArrowRightIcon, cardClickableMixin, cardMixin, cls, Typography } from "@rebasepro/ui";
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/CollectionsStudioView")
39
- .then(m => ({ default: m.CollectionsStudioView }))
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 collectionRegistryController = useBuildCollectionRegistryController({ userConfigPersistence });
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: devViews,
201
+ views: mergedViews,
143
202
  navigationGroupMappings: registry.cmsConfig?.navigationGroupMappings,
144
203
  authController: context.authController!,
145
- data: context.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, VirtualTable, VirtualTableColumn, VirtualTableFilterValues, OnRowClickParams, VirtualTableWhereFilterOp } from "@rebasepro/ui";
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
- <VirtualTable
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 { VirtualTableWhereFilterOp } from "@rebasepro/ui";
5
- import { IconButton, MultiSelect, MultiSelectItem, Select, SelectItem, TextField, XIcon } from "@rebasepro/ui";
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
- name,
37
- value,
38
- setValue,
39
- type,
40
- isArray,
41
- enumValues,
42
- title
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
- size={"medium"}
114
- fullWidth={true}
115
- position={"item-aligned"}
116
- onValueChange={(value) => {
117
- updateFilter(value as VirtualTableWhereFilterOp | "is-null", internalValue);
118
- }}
119
- renderValue={(op) => operationLabels[op as keyof typeof operationLabels]}>
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
- value={String(enumConfig.id)}>
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
- value={String(enumConfig.id)}>
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.analyticsController?.onAnalyticsEvent?.("entity_click", {
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.urlController!,
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.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
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.urlController!
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.dialogsController) {
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.analyticsController?.onAnalyticsEvent?.("single_entity_deleted", {
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.dialogsController) {
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.dialogsController.open({
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 => props.breadcrumbs.map(newEntry => {
26
- const prevEntry = newEntry.id ? prev.find(p => p.id === newEntry.id) : undefined;
27
- if (prevEntry && newEntry.count === null && typeof prevEntry.count === "number") {
28
- return { ...newEntry,
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 newEntry;
32
- }));
40
+ return next;
41
+ });
33
42
  }, []);
34
43
 
35
44
  const updateCount = useCallback((id: string, count: number | null | undefined) => {