@rebasepro/admin 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/{CollectionEditorDialog-HNXGxXdO.js → CollectionEditorDialog-BO_aQ-Z4.js} +236 -155
  2. package/dist/CollectionEditorDialog-BO_aQ-Z4.js.map +1 -0
  3. package/dist/{PropertyEditView-C2wp8kQ9.js → PropertyEditView-DKzOpMjR.js} +190 -50
  4. package/dist/PropertyEditView-DKzOpMjR.js.map +1 -0
  5. package/dist/{CollectionsStudioView-B1mN33GB.js → RouterCollectionsStudioView-BQTIExjA.js} +61 -39
  6. package/dist/RouterCollectionsStudioView-BQTIExjA.js.map +1 -0
  7. package/dist/collection_editor/_cms_internals.d.ts +1 -1
  8. package/dist/collection_editor/api/generateCollectionApi.d.ts +3 -2
  9. package/dist/collection_editor/extensibility_types.d.ts +135 -0
  10. package/dist/collection_editor/index.d.ts +6 -0
  11. package/dist/collection_editor/serializable_types.d.ts +370 -0
  12. package/dist/collection_editor/serializable_utils.d.ts +50 -0
  13. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +3 -3
  14. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +12 -1
  15. package/dist/collection_editor/ui/collection_editor/CollectionStudioView.d.ts +13 -1
  16. package/dist/collection_editor/ui/collection_editor/CollectionsStudioView.d.ts +24 -2
  17. package/dist/collection_editor/ui/collection_editor/DisplaySettingsForm.d.ts +2 -1
  18. package/dist/collection_editor/ui/collection_editor/GeneralSettingsForm.d.ts +4 -1
  19. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +12 -1
  20. package/dist/collection_editor/ui/collection_editor/RouterCollectionStudioView.d.ts +15 -0
  21. package/dist/collection_editor/ui/collection_editor/RouterCollectionsStudioView.d.ts +14 -0
  22. package/dist/collection_editor/useJsonCollectionsConfigController.d.ts +37 -0
  23. package/dist/collection_editor/useSafeSnackbarController.d.ts +2 -0
  24. package/dist/collection_editor_ui.d.ts +4 -0
  25. package/dist/collection_editor_ui.js +33 -4
  26. package/dist/collection_editor_ui.js.map +1 -0
  27. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  28. package/dist/components/EntityCollectionTable/index.d.ts +1 -1
  29. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +3 -3
  30. package/dist/components/EntityCollectionView/hooks/useKanbanDragAndDrop.d.ts +3 -3
  31. package/dist/components/EntityEditView.d.ts +3 -2
  32. package/dist/components/RebaseCMS.d.ts +1 -1
  33. package/dist/components/SelectableTable/SelectionStore.d.ts +12 -21
  34. package/dist/components/field_configs.d.ts +1 -1
  35. package/dist/editor/editor.d.ts +0 -6
  36. package/dist/editor.js +1 -3
  37. package/dist/editor.js.map +1 -1
  38. package/dist/form/EntityForm.d.ts +10 -9
  39. package/dist/form/EntityFormBinding.d.ts +27 -0
  40. package/dist/form/components/StorageUploadProgress.d.ts +6 -1
  41. package/dist/form/form_utils.d.ts +8 -0
  42. package/dist/form/index.d.ts +5 -2
  43. package/dist/{history-BO6VT85a.js → history-B3cqBzEV.js} +2 -2
  44. package/dist/{history-BO6VT85a.js.map → history-B3cqBzEV.js.map} +1 -1
  45. package/dist/hooks/navigation/contexts/CollectionRegistryContext.d.ts +1 -1
  46. package/dist/hooks/navigation/useAsyncResolver.d.ts +26 -0
  47. package/dist/hooks/navigation/useBuildCollectionRegistryController.d.ts +2 -0
  48. package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
  49. package/dist/hooks/navigation/utils.d.ts +1 -1
  50. package/dist/index.d.ts +3 -1
  51. package/dist/index.js +2 -2
  52. package/dist/preview/components/StorageThumbnail.d.ts +3 -1
  53. package/dist/routes/CustomViewRoute.d.ts +2 -1
  54. package/dist/{src-BYniefVu.js → src-cxV1ODRG.js} +1731 -1710
  55. package/dist/src-cxV1ODRG.js.map +1 -0
  56. package/dist/types/components/EntityFormActionsProps.d.ts +1 -1
  57. package/dist/types/components/EntityFormProps.d.ts +59 -0
  58. package/dist/types/components/PropertyPreviewProps.d.ts +2 -1
  59. package/dist/types/fields.d.ts +10 -7
  60. package/dist/util/dataControllerAdapter.d.ts +30 -0
  61. package/dist/util/navigation_utils.d.ts +1 -1
  62. package/dist/util/propertyConfigMapper.d.ts +17 -0
  63. package/package.json +8 -8
  64. package/src/collection_editor/_cms_internals.ts +1 -1
  65. package/src/collection_editor/api/generateCollectionApi.ts +3 -2
  66. package/src/collection_editor/extensibility_types.ts +168 -0
  67. package/src/collection_editor/index.ts +46 -0
  68. package/src/collection_editor/pgColumnToProperty.ts +2 -2
  69. package/src/collection_editor/serializable_types.ts +474 -0
  70. package/src/collection_editor/serializable_utils.ts +576 -0
  71. package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +6 -6
  72. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +170 -89
  73. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +95 -78
  74. package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +23 -20
  75. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +79 -27
  76. package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +9 -7
  77. package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +22 -5
  78. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +3 -3
  79. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +154 -18
  80. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  81. package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +1 -0
  82. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +44 -0
  83. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +41 -0
  84. package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +4 -3
  85. package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +35 -1
  86. package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
  87. package/src/collection_editor/ui/collection_editor/utils/supported_fields.tsx +0 -1
  88. package/src/collection_editor/ui/collection_editor/utils/update_property_for_widget.ts +7 -29
  89. package/src/collection_editor/useJsonCollectionsConfigController.tsx +286 -0
  90. package/src/collection_editor/useSafeSnackbarController.ts +9 -0
  91. package/src/collection_editor_ui.ts +6 -0
  92. package/src/components/DefaultDrawer.tsx +14 -4
  93. package/src/components/DrawerNavigationGroup.tsx +2 -2
  94. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +11 -9
  95. package/src/components/EntityCollectionTable/fields/TableMultipleRelationField.tsx +2 -1
  96. package/src/components/EntityCollectionTable/fields/TableRelationField.tsx +2 -1
  97. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +23 -20
  98. package/src/components/EntityCollectionTable/index.tsx +1 -1
  99. package/src/components/EntityCollectionTable/table_bindings.tsx +24 -44
  100. package/src/components/EntityCollectionView/EntityBoardCard.tsx +6 -6
  101. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +13 -12
  102. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +40 -190
  103. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +66 -184
  104. package/src/components/EntityCollectionView/EntityCollectionView.tsx +8 -25
  105. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -1
  106. package/src/components/EntityCollectionView/hooks/useKanbanDragAndDrop.ts +6 -6
  107. package/src/components/EntityCollectionView/useBoardDataController.tsx +12 -40
  108. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +3 -3
  109. package/src/components/EntityDetailView.tsx +2 -2
  110. package/src/components/EntityEditView.tsx +7 -6
  111. package/src/components/EntityEditViewFormActions.tsx +35 -29
  112. package/src/components/HomePage/ContentHomePage.tsx +14 -5
  113. package/src/components/RebaseCMS.tsx +3 -3
  114. package/src/components/RebaseNavigation.tsx +74 -9
  115. package/src/components/RelationSelector.tsx +2 -2
  116. package/src/components/SelectableTable/SelectableTable.tsx +13 -5
  117. package/src/components/SelectableTable/SelectionStore.ts +18 -48
  118. package/src/components/SideDialogs.tsx +0 -7
  119. package/src/components/common/default_entity_actions.tsx +22 -11
  120. package/src/components/field_configs.tsx +0 -14
  121. package/src/contexts/BreacrumbsContext.tsx +15 -6
  122. package/src/editor/editor.tsx +4 -7
  123. package/src/editor/selectors/ai-selector.tsx +0 -1
  124. package/src/form/EntityForm.tsx +191 -428
  125. package/src/form/EntityFormActions.tsx +35 -49
  126. package/src/form/EntityFormBinding.tsx +323 -0
  127. package/src/form/components/FormLayout.tsx +1 -1
  128. package/src/form/components/StorageUploadProgress.tsx +9 -2
  129. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +12 -1
  130. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +4 -4
  131. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -2
  132. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -7
  133. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +19 -15
  134. package/src/form/form_utils.ts +163 -0
  135. package/src/form/index.tsx +14 -6
  136. package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +2 -2
  137. package/src/hooks/navigation/contexts/NavigationStateContext.tsx +2 -2
  138. package/src/hooks/navigation/contexts/UrlContext.tsx +1 -2
  139. package/src/hooks/navigation/useAsyncResolver.ts +100 -0
  140. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +15 -8
  141. package/src/hooks/navigation/useBuildNavigationStateController.tsx +12 -8
  142. package/src/hooks/navigation/useNavigationResolution.ts +33 -0
  143. package/src/hooks/navigation/useResolvedCollections.ts +29 -71
  144. package/src/hooks/navigation/useResolvedViews.tsx +40 -70
  145. package/src/hooks/navigation/useTopLevelNavigation.ts +20 -16
  146. package/src/hooks/navigation/utils.ts +7 -4
  147. package/src/index.ts +6 -1
  148. package/src/preview/PropertyPreview.tsx +1 -16
  149. package/src/preview/components/StorageThumbnail.tsx +24 -7
  150. package/src/routes/CustomViewRoute.tsx +14 -6
  151. package/src/types/components/EntityFormActionsProps.tsx +1 -1
  152. package/src/types/components/EntityFormProps.tsx +68 -0
  153. package/src/types/components/PropertyPreviewProps.tsx +1 -1
  154. package/src/types/fields.tsx +9 -8
  155. package/src/util/dataControllerAdapter.ts +93 -0
  156. package/src/util/navigation_utils.ts +1 -1
  157. package/src/util/previews.ts +20 -0
  158. package/src/util/propertyConfigMapper.ts +134 -0
  159. package/src/util/property_utils.tsx +1 -2
  160. package/dist/CollectionEditorDialog-HNXGxXdO.js.map +0 -1
  161. package/dist/CollectionsStudioView-B1mN33GB.js.map +0 -1
  162. package/dist/PropertyEditView-C2wp8kQ9.js.map +0 -1
  163. package/dist/components/EntityCollectionTable/fields/VirtualTableDateField.d.ts +0 -12
  164. package/dist/components/EntityCollectionTable/fields/VirtualTableInput.d.ts +0 -10
  165. package/dist/components/EntityCollectionTable/fields/VirtualTableNumberInput.d.ts +0 -9
  166. package/dist/components/EntityCollectionTable/fields/VirtualTableSwitch.d.ts +0 -8
  167. package/dist/components/EntityCollectionView/Board.d.ts +0 -3
  168. package/dist/components/EntityCollectionView/BoardColumn.d.ts +0 -42
  169. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +0 -9
  170. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +0 -14
  171. package/dist/components/EntityCollectionView/board_types.d.ts +0 -105
  172. package/dist/form/field_bindings/ReferenceAsStringFieldBinding.d.ts +0 -11
  173. package/dist/hooks/navigation/useNavigationRegistry.d.ts +0 -12
  174. package/dist/hooks/navigation/useNavigationURLs.d.ts +0 -11
  175. package/dist/src-BYniefVu.js.map +0 -1
  176. package/src/components/EntityCollectionTable/fields/VirtualTableDateField.tsx +0 -39
  177. package/src/components/EntityCollectionTable/fields/VirtualTableInput.tsx +0 -90
  178. package/src/components/EntityCollectionTable/fields/VirtualTableNumberInput.tsx +0 -83
  179. package/src/components/EntityCollectionTable/fields/VirtualTableSwitch.tsx +0 -32
  180. package/src/components/EntityCollectionView/Board.tsx +0 -475
  181. package/src/components/EntityCollectionView/BoardColumn.tsx +0 -165
  182. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +0 -46
  183. package/src/components/EntityCollectionView/BoardSortableList.tsx +0 -177
  184. package/src/components/EntityCollectionView/board_types.ts +0 -113
  185. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +0 -141
  186. package/src/hooks/navigation/useNavigationRegistry.ts +0 -164
  187. package/src/hooks/navigation/useNavigationURLs.ts +0 -56
@@ -3,7 +3,7 @@ import type { EntityCollection } from "@rebasepro/types";
3
3
  import type { FormContext } from "../types/fields";
4
4
  import type { EntityAction, EntityActionClickProps, SideEntityController } from "@rebasepro/types";
5
5
  import React, { useMemo } from "react";
6
- import { Entity, RebaseContext } from "@rebasepro/types";
6
+ import { Entity, getCollectionDataPath, RebaseContext } from "@rebasepro/types";
7
7
  import type { EntityFormActionsProps } from "../types/components/EntityFormActionsProps";
8
8
  import { copyEntityAction, deleteEntityAction } from "../components";
9
9
  import { mergeEntityActions } from "../util/entity_actions";
@@ -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,10 +164,11 @@ 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,
170
- path: collection.slug,
171
+ path: getCollectionDataPath(collection),
171
172
  collection: collection,
172
173
  context,
173
174
  sideEntityController,
@@ -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")}
@@ -286,14 +292,14 @@ function buildSideActions<M extends Record<string, unknown>>({
286
292
  const props = {
287
293
  view: "form",
288
294
  entity,
289
- path: collection.slug,
295
+ path: getCollectionDataPath(collection),
290
296
  collection: collection,
291
297
  context,
292
298
  sideEntityController,
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
- return unFilteredNavigationEntries.filter(e => e.type === "view" || e.type === "admin");
79
+ return unFilteredNavigationEntries.filter(e => (e.type === "view" && studioViewSlugs.has(e.slug)) || 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));
@@ -10,19 +10,19 @@ 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, 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,
21
- plugins,
22
22
  collectionEditor,
23
23
  navigationGroupMappings });
24
24
  return () => dispatch.unregisterCMS();
25
- }, [dispatch, collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings]);
25
+ }, [dispatch, collections, views, homePage, entityViews, entityActions, collectionEditor, navigationGroupMappings]);
26
26
 
27
27
  return null;
28
28
  }
@@ -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,32 @@ 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
+
198
+ const parentCustomizationController = useCustomizationController();
199
+
139
200
  const navigationStateController = useBuildNavigationStateController({
140
- plugins: registry.cmsConfig?.plugins ?? EMPTY_PLUGINS,
201
+ plugins: parentCustomizationController.plugins ?? EMPTY_PLUGINS,
141
202
  collections: collectionsBuilder,
142
- views: devViews,
203
+ views: mergedViews,
143
204
  navigationGroupMappings: registry.cmsConfig?.navigationGroupMappings,
144
205
  authController: context.authController!,
145
- data: context.data,
206
+ data: routedData,
146
207
  collectionRegistryController,
147
208
  urlController,
148
209
  adminMode: adminModeController?.mode
@@ -154,7 +215,6 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
154
215
  // automatically merged. We re-provide an enriched controller here so that
155
216
  // downstream consumers (EntityEditView, side panels, etc.) can resolve
156
217
  // string-keyed entity views like "blog_preview".
157
- const parentCustomizationController = useCustomizationController();
158
218
  const enrichedCustomizationController = useMemo(() => {
159
219
  const cmsEntityViews = (registry.cmsConfig?.entityViews ?? []) as EntityCustomView[];
160
220
  const cmsEntityActions = (registry.cmsConfig?.entityActions ?? []) as EntityAction[];
@@ -175,7 +235,11 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
175
235
  }, [parentCustomizationController, registry.cmsConfig?.entityViews, registry.cmsConfig?.entityActions]);
176
236
 
177
237
  // ── Inner content with all context providers ──────────────────────
238
+ // Re-provide RebaseDataContext with the routed data so that every CMS
239
+ // consumer (list/entity views, references, board, import/export, and
240
+ // `context.data`) is routed to the correct driver by collection path.
178
241
  const navigationContent = (
242
+ <RebaseDataContext.Provider value={routedData}>
179
243
  <CustomizationControllerContext.Provider value={enrichedCustomizationController}>
180
244
  <StudioBridgeRegistryProvider>
181
245
  <CollectionRegistryContext.Provider value={collectionRegistryController}>
@@ -194,6 +258,7 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
194
258
  </CollectionRegistryContext.Provider>
195
259
  </StudioBridgeRegistryProvider>
196
260
  </CustomizationControllerContext.Provider>
261
+ </RebaseDataContext.Provider>
197
262
  );
198
263
 
199
264
  // ── Wrap with ConfigControllerProvider when collection editor is enabled ──
@@ -24,7 +24,7 @@ import {
24
24
  import * as React from "react";
25
25
  import { useCallback, useEffect, useRef, useState } from "react";
26
26
  import { Command as CommandPrimitive } from "cmdk";
27
- import { Entity, EntityRelation, FilterValues, Relation } from "@rebasepro/types";
27
+ import { Entity, EntityRelation, FilterValues, Relation, getCollectionDataPath } from "@rebasepro/types";
28
28
  import { EntityPreviewData } from "./EntityPreview";
29
29
  import { useData, useRelationSelector } from "@rebasepro/core";
30
30
  import { useSideEntityController } from "../hooks/useSideEntityController";
@@ -115,7 +115,7 @@ export const RelationSelector = React.forwardRef<
115
115
  loadMore,
116
116
  entityToRelationItem
117
117
  } = useRelationSelector({
118
- path: collection.slug,
118
+ path: getCollectionDataPath(collection),
119
119
  collection,
120
120
  fixedFilter,
121
121
  pageSize
@@ -1,8 +1,8 @@
1
- import { createSelectionStore } from "./SelectionStore";
1
+ import { createSelectionStore, AdminSelectedCell } 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";
@@ -192,7 +192,15 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
192
192
 
193
193
  const select = useCallback((cell?: SelectedCellProps<M>) => {
194
194
  setLocalSelectedCell(cell);
195
- selectionStore.select(cell);
195
+ if (cell) {
196
+ selectionStore.select({
197
+ ...cell,
198
+ columnKey: cell.propertyKey,
199
+ rowId: cell.entityId
200
+ } as AdminSelectedCell);
201
+ } else {
202
+ selectionStore.select(undefined);
203
+ }
196
204
  }, [selectionStore]);
197
205
 
198
206
  const unselect = useCallback(() => {
@@ -244,7 +252,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
244
252
  <div className="h-full w-full flex flex-col bg-white dark:bg-surface-900"
245
253
  ref={ref}>
246
254
 
247
- <VirtualTable
255
+ <TableView
248
256
  data={data as unknown as Record<string, unknown>[]}
249
257
  columns={columns}
250
258
  cellRenderer={(props) => cellRenderer(props as unknown as CellRendererParams<Entity<M>>)}
@@ -255,7 +263,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
255
263
  onColumnResize={onColumnResize}
256
264
  rowHeight={getRowHeight(size)}
257
265
  loading={dataLoading}
258
- filter={filterValues}
266
+ filter={filterValues as any}
259
267
  onFilterUpdate={setFilterValues ? onFilterUpdate : undefined}
260
268
  sortBy={sortBy}
261
269
  onSortByUpdate={setSortBy as ((sortBy?: [string, "asc" | "desc"]) => void)}
@@ -1,64 +1,34 @@
1
1
  import { useSyncExternalStore, useCallback, useRef } from "react";
2
2
  import { SelectedCellProps } from "@rebasepro/types";
3
+ import {
4
+ createVirtualTableSelectionStore,
5
+ VirtualTableSelectionStore,
6
+ SelectedCell
7
+ } from "@rebasepro/ui";
8
+
9
+ export interface AdminSelectedCell extends SelectedCell {
10
+ entityPath: string;
11
+ entityId: string | number;
12
+ propertyKey: string;
13
+ cellRect: DOMRect;
14
+ width: number;
15
+ height: number;
16
+ }
3
17
 
4
- /**
5
- * A ref-based selection store that avoids React context re-renders.
6
- *
7
- * The problem with putting `selectedCell` in a React context value is that
8
- * ANY cell selection change triggers a context value change, which forces
9
- * ALL consumers (every PropertyTableCell in the table) to re-render.
10
- * This causes the DOM to be replaced between mousedown and click events,
11
- * breaking `alwaysInteractive` cells (like relation selectors) where the
12
- * user needs to click a button on the very first interaction.
13
- *
14
- * This store uses `useSyncExternalStore` so only cells whose `selected`
15
- * derivation actually changed will re-render.
16
- */
17
18
  export function createSelectionStore() {
18
- let selectedCell: SelectedCellProps | undefined = undefined;
19
- const listeners = new Set<() => void>();
20
-
21
- function getSnapshot(): SelectedCellProps | undefined {
22
- return selectedCell;
23
- }
24
-
25
- function subscribe(listener: () => void): () => void {
26
- listeners.add(listener);
27
- return () => listeners.delete(listener);
28
- }
29
-
30
- function select(cell: SelectedCellProps | undefined) {
31
- selectedCell = cell;
32
- listeners.forEach(l => l());
33
- }
34
-
35
- return { getSnapshot,
36
- subscribe,
37
- select };
19
+ return createVirtualTableSelectionStore<AdminSelectedCell>();
38
20
  }
39
21
 
40
- export type SelectionStore = ReturnType<typeof createSelectionStore>;
22
+ export type SelectionStore = VirtualTableSelectionStore<AdminSelectedCell>;
41
23
 
42
- /**
43
- * Hook that subscribes a cell to the selection store and returns
44
- * whether THIS cell is selected. Only re-renders when the cell's
45
- * `selected` boolean actually changes (not on every store update).
46
- */
47
24
  export function useCellSelected(
48
25
  store: { getSnapshot: () => SelectedCellProps | undefined; subscribe: (listener: () => void) => () => void },
49
26
  propertyKey: string,
50
27
  entityPath: string,
51
28
  entityId: string | number
52
29
  ): boolean {
53
- // Memoize a selector that derives a boolean from the store snapshot.
54
- // useSyncExternalStore calls this on every store notification, but
55
- // React only re-renders when the returned value !== the previous one.
56
- const selectorRef = useRef({ propertyKey,
57
- entityPath,
58
- entityId });
59
- selectorRef.current = { propertyKey,
60
- entityPath,
61
- entityId };
30
+ const selectorRef = useRef({ propertyKey, entityPath, entityId });
31
+ selectorRef.current = { propertyKey, entityPath, entityId };
62
32
 
63
33
  const getSnapshot = useCallback(() => {
64
34
  const cell = store.getSnapshot();
@@ -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
  }}
@@ -1,6 +1,12 @@
1
1
  import { CopyIcon, iconSize, KeyRoundIcon, PencilIcon, Trash2Icon } from "@rebasepro/ui";
2
2
  import type { EntityAction, User, UserCreationResult } from "@rebasepro/types";
3
- import { ConfirmationDialog, useSnackbarController, useTranslation, useRebaseClient, useAuthController } from "@rebasepro/core";
3
+ import {
4
+ ConfirmationDialog,
5
+ useAuthController,
6
+ useRebaseClient,
7
+ useSnackbarController,
8
+ useTranslation
9
+ } from "@rebasepro/core";
4
10
  import { DeleteEntityDialog } from "../DeleteEntityDialog";
5
11
  import { addRecentId } from "../EntityCollectionView/utils";
6
12
  import { navigateToEntity } from "../../util/navigation_utils";
@@ -8,7 +14,6 @@ import { resolveDefaultSelectedView } from "@rebasepro/common";
8
14
  import { CreationResultDialog } from "../admin/CreationResultDialog";
9
15
  import React, { useState } from "react";
10
16
 
11
-
12
17
  export const editEntityAction: EntityAction = {
13
18
  icon: <PencilIcon size={iconSize.smallest}/>,
14
19
  key: "edit",
@@ -36,7 +41,7 @@ export const editEntityAction: EntityAction = {
36
41
 
37
42
  highlightEntity?.(entity);
38
43
 
39
- context.analyticsController?.onAnalyticsEvent?.("entity_click", {
44
+ context?.analyticsController?.onAnalyticsEvent?.("entity_click", {
40
45
  path: entity.path,
41
46
  entityId: entity.id
42
47
  });
@@ -55,6 +60,9 @@ export const editEntityAction: EntityAction = {
55
60
  entityId: entity.id
56
61
  }
57
62
  );
63
+ if (!context?.urlController) {
64
+ throw new Error("INTERNAL: editEntityAction: urlController is undefined");
65
+ }
58
66
  navigateToEntity({
59
67
  openEntityMode,
60
68
  collection,
@@ -62,7 +70,7 @@ export const editEntityAction: EntityAction = {
62
70
  path: newFullIdPath,
63
71
  sideEntityController,
64
72
  onClose: () => unhighlightEntity?.(entity),
65
- navigation: context.urlController!,
73
+ navigation: context.urlController,
66
74
  selectedTab: defaultSelectedView
67
75
  });
68
76
 
@@ -92,12 +100,15 @@ export const copyEntityAction: EntityAction = {
92
100
  throw new Error("INTERNAL: copyEntityAction: sideEntityController is undefined");
93
101
  }
94
102
  highlightEntity?.(entity);
95
- context.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
103
+ context?.analyticsController?.onAnalyticsEvent?.("copy_entity_click", {
96
104
  path: entity.path,
97
105
  entityId: entity.id
98
106
  });
99
107
 
100
108
  const usedPath = path ?? collection?.slug ?? entity.path;
109
+ if (!context?.urlController) {
110
+ throw new Error("INTERNAL: copyEntityAction: urlController is undefined");
111
+ }
101
112
  navigateToEntity({
102
113
  openEntityMode,
103
114
  collection,
@@ -106,7 +117,7 @@ export const copyEntityAction: EntityAction = {
106
117
  copy: true,
107
118
  sideEntityController,
108
119
  onClose: () => unhighlightEntity?.(entity),
109
- navigation: context.urlController!
120
+ navigation: context.urlController
110
121
  });
111
122
 
112
123
  return Promise.resolve(undefined);
@@ -131,7 +142,7 @@ export const deleteEntityAction: EntityAction = {
131
142
  if (!entity) {
132
143
  throw new Error("INTERNAL: deleteEntityAction: Entity is undefined");
133
144
  }
134
- if (!context.dialogsController) {
145
+ if (!context?.dialogsController) {
135
146
  throw new Error("INTERNAL: deleteEntityAction: context.dialogsController is undefined");
136
147
  }
137
148
  const { closeDialog } = context.dialogsController.open({
@@ -146,14 +157,14 @@ export const deleteEntityAction: EntityAction = {
146
157
  callbacks={collection.callbacks}
147
158
  open={open}
148
159
  onEntityDelete={() => {
149
- context.analyticsController?.onAnalyticsEvent?.("single_entity_deleted", {
160
+ context?.analyticsController?.onAnalyticsEvent?.("single_entity_deleted", {
150
161
  path
151
162
  });
152
163
  selectionController?.setSelectedEntities(selectionController.selectedEntities.filter(e => e.id !== entity.id));
153
164
  onCollectionChange?.();
154
165
  // In full-screen mode, navigateBack would go to the deleted entity's
155
166
  // detail URL, which no longer exists. Navigate to the parent collection instead.
156
- if (openEntityMode === "full_screen" && context.urlController) {
167
+ if (openEntityMode === "full_screen" && context?.urlController) {
157
168
  const collectionUrl = context.urlController.buildUrlCollectionPath(path);
158
169
  context.urlController.navigate(collectionUrl, { replace: true });
159
170
  } else {
@@ -258,7 +269,7 @@ export const resetPasswordAction: EntityAction = {
258
269
  if (!entity) {
259
270
  throw new Error("INTERNAL: resetPasswordAction: Entity is undefined");
260
271
  }
261
- if (!context.dialogsController) {
272
+ if (!context?.dialogsController) {
262
273
  throw new Error("INTERNAL: resetPasswordAction: context.dialogsController is undefined");
263
274
  }
264
275
 
@@ -272,7 +283,7 @@ export const resetPasswordAction: EntityAction = {
272
283
  roles: entity.values?.roles as string[] || []
273
284
  };
274
285
 
275
- const { closeDialog } = context.dialogsController.open({
286
+ const { closeDialog } = context!.dialogsController.open({
276
287
  key: "reset_password_dialog_" + entity.id,
277
288
  Component: ({ open }) => (
278
289
  <ResetPasswordActionDialog
@@ -9,7 +9,6 @@ import { KeyValueFieldBinding } from "../form/field_bindings/KeyValueFieldBindin
9
9
  import { MapFieldBinding } from "../form/field_bindings/MapFieldBinding";
10
10
  import { MarkdownEditorFieldBinding } from "../form/field_bindings/MarkdownEditorFieldBinding";
11
11
  import { MultiSelectFieldBinding } from "../form/field_bindings/MultiSelectFieldBinding";
12
- import { ReferenceAsStringFieldBinding } from "../form/field_bindings/ReferenceAsStringFieldBinding";
13
12
  import { ReferenceFieldBinding } from "../form/field_bindings/ReferenceFieldBinding";
14
13
  import { RepeatFieldBinding } from "../form/field_bindings/RepeatFieldBinding";
15
14
  import { SelectFieldBinding } from "../form/field_bindings/SelectFieldBinding";
@@ -220,17 +219,6 @@ Field: TextFieldBinding }
220
219
  ui: { Field: StorageUploadFieldBinding }
221
220
  }
222
221
  },
223
- reference_as_string: {
224
- key: "reference_as_string",
225
- name: "Reference (as string)",
226
- description: "The value refers to a different collection (it is saved as a string)",
227
- Icon: LinkIcon,
228
- color: "#154fb3",
229
- property: {
230
- type: "string",
231
- ui: { Field: ReferenceAsStringFieldBinding }
232
- }
233
- },
234
222
  reference: {
235
223
  key: "reference",
236
224
  name: "Reference",
@@ -393,8 +381,6 @@ export function getDefaultFieldId(property: Property) {
393
381
  return "select";
394
382
  } else if (property.userSelect) {
395
383
  return "user_select";
396
- } else if ((property as StringProperty).reference) {
397
- return "reference_as_string";
398
384
  } else {
399
385
  return "text_field";
400
386
  }