@rebasepro/admin 0.0.1-canary.f81da60 → 0.1.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 (93) hide show
  1. package/dist/{CollectionEditorDialog-D509-IMx.js → CollectionEditorDialog-MbvXGzEq.js} +18 -18
  2. package/dist/CollectionEditorDialog-MbvXGzEq.js.map +1 -0
  3. package/dist/{CollectionsStudioView-B549BDpU.js → CollectionsStudioView-D9X6aiAr.js} +4 -4
  4. package/dist/{CollectionsStudioView-B549BDpU.js.map → CollectionsStudioView-D9X6aiAr.js.map} +1 -1
  5. package/dist/{ContentHomePage--Bl1FXk7.js → ContentHomePage-CfVB1eUo.js} +26 -26
  6. package/dist/ContentHomePage-CfVB1eUo.js.map +1 -0
  7. package/dist/{ExportCollectionAction-CttNAdM1.js → ExportCollectionAction-CUwJg4F9.js} +2 -2
  8. package/dist/{ExportCollectionAction-CttNAdM1.js.map → ExportCollectionAction-CUwJg4F9.js.map} +1 -1
  9. package/dist/{ImportCollectionAction-BB33kxAN.js → ImportCollectionAction-DGa_SF_8.js} +2 -2
  10. package/dist/{ImportCollectionAction-BB33kxAN.js.map → ImportCollectionAction-DGa_SF_8.js.map} +1 -1
  11. package/dist/{PropertyEditView-UtDO8g0A.js → PropertyEditView-C4nlYmAc.js} +79 -101
  12. package/dist/PropertyEditView-C4nlYmAc.js.map +1 -0
  13. package/dist/{RolesView-B0E7L0hE.js → RolesView-CNWxnR8e.js} +2 -2
  14. package/dist/{RolesView-B0E7L0hE.js.map → RolesView-CNWxnR8e.js.map} +1 -1
  15. package/dist/{UsersView-BM2_7VPV.js → UsersView-YiTIcXkA.js} +6 -28
  16. package/dist/UsersView-YiTIcXkA.js.map +1 -0
  17. package/dist/collection_editor/ConfigControllerProvider.d.ts +0 -4
  18. package/dist/collection_editor/ui/collection_editor/CollectionDetailsForm.d.ts +1 -3
  19. package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +0 -2
  20. package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +1 -2
  21. package/dist/collection_editor_ui.js +3 -3
  22. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -1
  23. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -1
  24. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +2 -1
  25. package/dist/components/EntityEditView.d.ts +6 -0
  26. package/dist/components/RebaseCMS.d.ts +1 -1
  27. package/dist/{index-CNDetux9.js → index-CtzpHzMQ.js} +2 -2
  28. package/dist/{index-CNDetux9.js.map → index-CtzpHzMQ.js.map} +1 -1
  29. package/dist/{index-C9YDsMC9.js → index-DKlrVD1m.js} +3 -3
  30. package/dist/index-DKlrVD1m.js.map +1 -0
  31. package/dist/{index-DO7lMeNB.js → index-kHJXfLNI.js} +3 -3
  32. package/dist/index-kHJXfLNI.js.map +1 -0
  33. package/dist/index.js +18 -14
  34. package/dist/index.js.map +1 -1
  35. package/dist/util/navigation_utils.d.ts +10 -1
  36. package/dist/{util-DK1O3uM0.js → util-CwLmSpGp.js} +708 -598
  37. package/dist/util-CwLmSpGp.js.map +1 -0
  38. package/package.json +8 -8
  39. package/src/collection_editor/ConfigControllerProvider.tsx +1 -10
  40. package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +3 -47
  41. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +2 -10
  42. package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +1 -3
  43. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -3
  44. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +0 -1
  45. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
  46. package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
  47. package/src/collection_editor/ui/collection_editor/properties/ReferencePropertyField.tsx +15 -49
  48. package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +2 -3
  49. package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +1 -1
  50. package/src/collection_editor/ui/collection_editor/templates/products_template.ts +2 -2
  51. package/src/components/DefaultAppBar.tsx +2 -2
  52. package/src/components/DefaultDrawer.tsx +25 -17
  53. package/src/components/DrawerNavigationGroup.tsx +4 -4
  54. package/src/components/DrawerNavigationItem.tsx +6 -6
  55. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +8 -2
  56. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +1 -1
  57. package/src/components/EntityCollectionTable/table_bindings.tsx +37 -27
  58. package/src/components/EntityCollectionView/EntityCard.tsx +2 -2
  59. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +4 -3
  60. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +5 -4
  61. package/src/components/EntityCollectionView/EntityCollectionView.tsx +50 -7
  62. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +17 -8
  63. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -4
  64. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +33 -5
  65. package/src/components/EntityEditView.tsx +80 -81
  66. package/src/components/EntitySidePanel.tsx +11 -7
  67. package/src/components/HomePage/ContentHomePage.tsx +24 -15
  68. package/src/components/HomePage/NavigationCard.tsx +4 -4
  69. package/src/components/HomePage/NavigationGroup.tsx +2 -2
  70. package/src/components/RebaseAuthGate.tsx +2 -0
  71. package/src/components/RebaseCMS.tsx +4 -3
  72. package/src/components/RebaseNavigation.tsx +7 -4
  73. package/src/components/RelationSelector.tsx +30 -2
  74. package/src/components/SelectableTable/SelectableTable.tsx +1 -1
  75. package/src/components/UserSelector.tsx +1 -1
  76. package/src/components/admin/UsersView.tsx +2 -17
  77. package/src/components/app/Scaffold.tsx +3 -3
  78. package/src/components/field_configs.tsx +3 -3
  79. package/src/form/PropertyFieldBinding.tsx +10 -6
  80. package/src/hooks/navigation/useResolvedViews.tsx +1 -3
  81. package/src/hooks/navigation/useTopLevelNavigation.ts +1 -1
  82. package/src/hooks/navigation/utils.ts +1 -1
  83. package/src/preview/PropertyPreview.tsx +17 -13
  84. package/src/routes/RebaseRoute.tsx +27 -2
  85. package/src/util/navigation_utils.ts +16 -2
  86. package/src/util/previews.ts +14 -5
  87. package/dist/CollectionEditorDialog-D509-IMx.js.map +0 -1
  88. package/dist/ContentHomePage--Bl1FXk7.js.map +0 -1
  89. package/dist/PropertyEditView-UtDO8g0A.js.map +0 -1
  90. package/dist/UsersView-BM2_7VPV.js.map +0 -1
  91. package/dist/index-C9YDsMC9.js.map +0 -1
  92. package/dist/index-DO7lMeNB.js.map +0 -1
  93. package/dist/util-DK1O3uM0.js.map +0 -1
@@ -1,9 +1,9 @@
1
- import type { EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
1
+ import type { ComponentRef, EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
2
2
  import type { FormContext } from "../types/fields";
3
3
  import type { PluginFormActionProps } from "@rebasepro/types";
4
- import React, { lazy, Suspense, useEffect, useMemo, useState } from "react";
4
+ import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { Entity, EntityStatus } from "@rebasepro/types";
6
- import { PluginProviderStack } from "@rebasepro/core";
6
+ import { PluginProviderStack, resolveComponentRef } from "@rebasepro/core";
7
7
 
8
8
  import { EntityCollectionView, EntityView } from "../components";
9
9
  import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
@@ -16,7 +16,6 @@ import {
16
16
  resolveDefaultSelectedView
17
17
  } from "@rebasepro/common";
18
18
  import { resolvedSelectedEntityView } from "../util/resolutions";
19
- import { getEntityTitlePropertyKey } from "../util/previews";
20
19
  import { CenteredView, CircularProgress, cls, defaultBorderMixin, IconButton, Tab, Tabs, Tooltip, Typography, Skeleton } from "@rebasepro/ui";
21
20
  import {
22
21
  useCustomizationController,
@@ -73,6 +72,12 @@ export interface EntityEditViewProps<M extends Record<string, unknown> = Record<
73
72
  layout?: "side_panel" | "full_screen" | "split";
74
73
  barActions?: (params: BarActionsParams) => any;
75
74
  formProps?: Partial<EntityFormProps<M>>,
75
+ /**
76
+ * Pre-populate the form with these values when creating a new entity.
77
+ * Only applied when the form is in "new" mode (no entityId).
78
+ * Sourced from EntitySidePanelProps (side panel) or location.state (full screen).
79
+ */
80
+ defaultValues?: Partial<M>;
76
81
  }
77
82
 
78
83
  /**
@@ -99,7 +104,7 @@ export function EntityEditView<M extends Record<string, unknown>>({
99
104
 
100
105
  const initialDirtyValues = entityId
101
106
  ? getEntityFromMemoryCache(props.path + "/" + entityId)
102
- : getEntityFromMemoryCache(props.path + "#new");
107
+ : (props.defaultValues ?? getEntityFromMemoryCache(props.path + "#new"));
103
108
 
104
109
  const { canEdit: canEditHook } = usePermissions();
105
110
 
@@ -183,7 +188,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
183
188
  const customizationController = useCustomizationController();
184
189
  const plugins = customizationController.plugins;
185
190
 
186
- const formActionTopProps: PluginFormActionProps = {
191
+ const formActionTopProps: PluginFormActionProps = useMemo(() => ({
187
192
  entityId,
188
193
  parentCollectionSlugs, parentEntityIds,
189
194
  path: path,
@@ -193,7 +198,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
193
198
  formContext: formContext as FormContext<Record<string, unknown>> | undefined,
194
199
  openEntityMode: layout,
195
200
  disabled: false
196
- };
201
+ }), [entityId, parentCollectionSlugs, parentEntityIds, path, status, collection, context, formContext, layout]);
197
202
  const pluginActionsTop = useSlot("form.actions.top", formActionTopProps);
198
203
 
199
204
  const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
@@ -230,15 +235,51 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
230
235
 
231
236
  const mainViewVisible = selectedTab === MAIN_TAB_VALUE || Boolean(selectedSecondaryForm);
232
237
 
233
- const customViewsView: any[] | undefined = customViews && resolvedEntityViews
234
- .filter(e => !e.includeActions)
238
+ // Track which custom view tabs have been visited so we keep them mounted
239
+ // (preserving their state) but don't eagerly mount tabs never visited.
240
+ const mountedTabsRef = useRef<Set<string>>(new Set());
241
+ if (selectedTab) {
242
+ mountedTabsRef.current.add(selectedTab);
243
+ }
244
+
245
+ // Memoize the read-only fallback form context to avoid recreating it every render
246
+ const readOnlyFormContext = useMemo<FormContext<M> | undefined>(() => {
247
+ if (formContext) return undefined; // not needed when real formContext exists
248
+ if (!entityId) return undefined;
249
+ const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
250
+ return {
251
+ entityId,
252
+ disabled: false,
253
+ openEntityMode: layout,
254
+ status: status,
255
+ values: usedEntity?.values ?? ({} as M),
256
+ setFieldValue: (key: string, value: any) => {
257
+ throw new Error("You can't update values in read only mode");
258
+ },
259
+ save: () => {
260
+ throw new Error("You can't save in read only mode");
261
+ },
262
+ collection,
263
+ path: path,
264
+ entity: usedEntity,
265
+ savingError: undefined,
266
+ formex: formexStub
267
+ };
268
+ }, [formContext, entityId, layout, status, usedEntity, collection, path]);
269
+
270
+ const nonActionCustomViews = useMemo(() =>
271
+ resolvedEntityViews.filter(e => !e.includeActions),
272
+ [resolvedEntityViews]
273
+ );
274
+
275
+ const customViewsView: any[] | undefined = customViews && nonActionCustomViews
235
276
  .map((customView) => {
236
277
 
237
278
  if (!customView)
238
279
  return null;
239
- const Builder = customView.Builder;
280
+ const Builder = resolveComponentRef<EntityCustomViewParams>(customView.Builder);
240
281
  if (!Builder) {
241
- console.error("INTERNAL: customView.Builder is not defined");
282
+ console.error("INTERNAL: customView.Builder is not defined or could not be resolved");
242
283
  return null;
243
284
  }
244
285
 
@@ -246,48 +287,41 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
246
287
  return null;
247
288
  }
248
289
 
249
- const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
250
- const usedFormContext: FormContext<M> = formContext ?? {
251
- entityId,
252
- disabled: false,
253
- openEntityMode: layout,
254
- status: status,
255
- values: usedEntity?.values ?? ({} as M),
256
- setFieldValue: (key: string, value: any) => {
257
- throw new Error("You can't update values in read only mode");
258
- },
259
- save: () => {
260
- throw new Error("You can't save in read only mode");
261
- },
262
- collection,
263
- path: path,
264
- entity: usedEntity,
265
- savingError: undefined,
266
- formex: formexStub
267
- };
290
+ // Only mount tabs that have been visited at least once
291
+ const isActive = selectedTab === customView.key;
292
+ const hasBeenMounted = mountedTabsRef.current.has(customView.key);
293
+ if (!isActive && !hasBeenMounted) {
294
+ return null;
295
+ }
296
+
297
+ const usedFormContext: FormContext<M> = formContext ?? readOnlyFormContext!;
268
298
 
269
299
  return <div
270
300
  className={cls(defaultBorderMixin,
271
301
  "relative flex-1 w-full h-full overflow-auto",
272
- { "hidden": selectedTab !== customView.key }
302
+ { "hidden": !isActive }
273
303
  )}
274
304
  key={`custom_view_${customView.key}`}
275
305
  role="tabpanel">
276
306
  <ErrorBoundary>
277
- {usedFormContext && <Builder
278
- collection={collection}
279
- parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
280
- entity={usedEntity}
281
- modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
282
- formContext={usedFormContext as unknown as FormContext<Record<string, unknown>>}
283
- />}
307
+ <Suspense fallback={<CircularProgressCenter />}>
308
+ {usedFormContext && <Builder
309
+ collection={collection}
310
+ parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
311
+ entity={usedEntity}
312
+ modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
313
+ formContext={usedFormContext as unknown as FormContext<Record<string, unknown>>}
314
+ />}
315
+ </Suspense>
284
316
  </ErrorBoundary>
285
317
  </div>;
286
318
  }).filter(Boolean);
287
319
 
288
320
  const globalLoading = (dataLoading && !usedEntity) || (canEdit === undefined && (status === "existing" || status === "copy"));
289
321
 
290
- const jsonView = <div
322
+ // Only mount JSON view when its tab is selected (or was previously selected)
323
+ const jsonTabMounted = mountedTabsRef.current.has(JSON_TAB_VALUE);
324
+ const jsonView = (selectedTab === JSON_TAB_VALUE || jsonTabMounted) ? <div
291
325
  className={cls("relative flex-1 h-full overflow-auto w-full",
292
326
  { "hidden": selectedTab !== JSON_TAB_VALUE })}
293
327
  key={"json_view"}
@@ -296,11 +330,11 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
296
330
  <EntityJsonPreview
297
331
  values={formContext?.values ?? entity?.values ?? {}} />
298
332
  </ErrorBoundary>
299
- </div>;
333
+ </div> : null;
300
334
 
301
- const historyView = includeHistoryView ? <div
302
- className={cls("relative flex-1 h-full overflow-auto w-full",
303
- { "hidden": selectedTab !== HISTORY_TAB_VALUE })}
335
+ // Only mount history view when its tab is actually selected
336
+ const historyView = includeHistoryView && selectedTab === HISTORY_TAB_VALUE ? <div
337
+ className={"relative flex-1 h-full overflow-auto w-full"}
304
338
  key={"history_view"}
305
339
  role="tabpanel">
306
340
  <ErrorBoundary>
@@ -349,7 +383,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
349
383
  );
350
384
  }).filter(Boolean);
351
385
 
352
- const onSideTabClick = (value: string) => {
386
+ const onSideTabClick = useCallback((value: string) => {
353
387
  setSelectedTab(value);
354
388
  if (status === "existing") {
355
389
  onTabChange?.({
@@ -359,7 +393,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
359
393
  collection
360
394
  });
361
395
  }
362
- };
396
+ }, [status, onTabChange, path, entityId, collection]);
363
397
 
364
398
  const entityReadOnlyView = !canEdit && entity ? <div
365
399
  className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", (canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}>
@@ -411,7 +445,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
411
445
  onSaved?.(res);
412
446
  formProps?.onSaved?.(res);
413
447
  }}
414
- Builder={selectedSecondaryForm?.Builder as React.ComponentType<EntityCustomViewParams<M>> | undefined}
448
+ Builder={resolveComponentRef(selectedSecondaryForm?.Builder as ComponentRef<EntityCustomViewParams<M>> | undefined) as React.ComponentType<EntityCustomViewParams<M>> | undefined}
415
449
  />;
416
450
 
417
451
  const subcollectionTabs = subcollections && subcollections.map((subcollection) =>
@@ -458,33 +492,6 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
458
492
  </Tooltip>
459
493
  ) : null;
460
494
 
461
- // Compute contextual title for subcollection tabs, e.g. "Orders of James"
462
- const subcollectionContextTitle = useMemo(() => {
463
- if (selectedTab === MAIN_TAB_VALUE || selectedTab === JSON_TAB_VALUE || selectedTab === HISTORY_TAB_VALUE) {
464
- return null;
465
- }
466
- // Check if the selected tab is a subcollection
467
- const matchedSubcollection = subcollections.find(sc => sc.slug === selectedTab);
468
- if (!matchedSubcollection) {
469
- return null;
470
- }
471
- // Check if the selected tab is a custom view (not a subcollection)
472
- const isCustomView = resolvedEntityViews.some(v => v.key === selectedTab);
473
- if (isCustomView) {
474
- return null;
475
- }
476
- // Resolve the parent entity's title
477
- const titleKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
478
- const entityValues = usedEntity?.values;
479
- if (!titleKey || !entityValues) {
480
- return matchedSubcollection.name;
481
- }
482
- const titleValue = entityValues[titleKey as keyof M];
483
- if (!titleValue || typeof titleValue !== "string") {
484
- return matchedSubcollection.name;
485
- }
486
- return `${matchedSubcollection.name} of ${titleValue}`;
487
- }, [selectedTab, subcollections, resolvedEntityViews, collection, customizationController.propertyConfigs, usedEntity?.values]);
488
495
 
489
496
  let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
490
497
 
@@ -500,14 +507,6 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
500
507
  status
501
508
  })}
502
509
 
503
- {subcollectionContextTitle && (
504
- <Typography
505
- variant="label"
506
- className="truncate min-w-0 shrink ml-2 text-surface-600 dark:text-surface-400"
507
- >
508
- {subcollectionContextTitle}
509
- </Typography>
510
- )}
511
510
 
512
511
  {pluginActionsTop}
513
512
 
@@ -9,6 +9,7 @@ import { EntityEditView } from "./EntityEditView";
9
9
  import { useSideDialogContext } from "./SideDialogs";
10
10
  import { IconButton } from "@rebasepro/ui";
11
11
  import { useLocation, useNavigate } from "react-router-dom";
12
+ import { removeInitialAndTrailingSlashes } from "@rebasepro/common";
12
13
  import { saveEntityToMemoryCache } from "@rebasepro/core";
13
14
  import { useCollectionRegistryController, useSideEntityController } from "../index";
14
15
  import { useUrlController } from "../index";
@@ -147,13 +148,16 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
147
148
  selectedTab,
148
149
  collection
149
150
  }) => {
150
- sideEntityController.replace({
151
- path: path,
152
- entityId,
153
- selectedTab,
154
- updateUrl: true,
155
- collection
156
- });
151
+ // Only update the URL to reflect the new tab — don't call
152
+ // sideEntityController.replace() which would recreate the
153
+ // entire EntitySidePanel component, causing a full
154
+ // unmount/remount and expensive re-render of the form.
155
+ if (entityId) {
156
+ const collectionPath = removeInitialAndTrailingSlashes(path);
157
+ const tabSuffix = selectedTab ? "/" + selectedTab : "";
158
+ const fullUrl = urlController.buildUrlCollectionPath(`${collectionPath}/${entityId}${tabSuffix}#side`);
159
+ navigate(fullUrl, { replace: true, state: location.state });
160
+ }
157
161
  }}
158
162
  formProps={formProps}
159
163
  />
@@ -19,9 +19,10 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
19
19
  import { restrictToVerticalAxis, restrictToWindowEdges } from "@dnd-kit/modifiers";
20
20
  import { RenameGroupDialog } from "./RenameGroupDialog";
21
21
  import { toArray } from "@rebasepro/utils";
22
- import { useCollapsedGroups, useCustomizationController, useTranslation, useSlot, useAdminModeController } from "@rebasepro/core";
22
+ import { useCollapsedGroups, buildCollapsedDefaults, useCustomizationController, useTranslation, useSlot, useAdminModeController, useRebaseRegistry } from "@rebasepro/core";
23
23
  import { useRestoreScroll } from "@rebasepro/core";
24
- import { STUDIO_NAVIGATION_GROUPS } from "@rebasepro/core";
24
+
25
+ import { BootstrapAdminBanner } from "@rebasepro/core";
25
26
  import { useBreadcrumbsController, useCMSContext } from "../../index";
26
27
 
27
28
  export const DEFAULT_GROUP_NAME = "Views";
@@ -45,7 +46,8 @@ export function ContentHomePage({
45
46
  const { navigationStateController } = context;
46
47
  const customizationController = useCustomizationController();
47
48
  const adminModeController = useAdminModeController();
48
- const isStudioMode = adminModeController.mode === "studio";
49
+ const registry = useRebaseRegistry();
50
+
49
51
  const { resolvedSlots } = customizationController;
50
52
  const breadcrumbs = useBreadcrumbsController();
51
53
  const { t } = useTranslation();
@@ -61,19 +63,19 @@ export function ContentHomePage({
61
63
  onNavigationEntriesUpdate = () => {}
62
64
  } = navigationStateController.topLevelNavigation || {};
63
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.
64
68
  const rawNavigationEntries = useMemo(() => {
65
- return unFilteredNavigationEntries.filter(e => {
66
- const isStudioGroup = e.group ? STUDIO_NAVIGATION_GROUPS.includes(e.group) : false;
67
- return isStudioMode ? isStudioGroup : !isStudioGroup;
68
- });
69
- }, [unFilteredNavigationEntries, isStudioMode]);
69
+ if (adminModeController.mode === "studio") {
70
+ return unFilteredNavigationEntries.filter(e => e.type === "view" || e.type === "admin");
71
+ }
72
+ return unFilteredNavigationEntries.filter(e => e.type !== "view");
73
+ }, [unFilteredNavigationEntries, adminModeController.mode]);
70
74
 
71
75
  const groupOrderFromNavController = useMemo(() => {
72
- return unFilteredGroupOrder.filter(g => {
73
- const isStudioGroup = STUDIO_NAVIGATION_GROUPS.includes(g);
74
- return isStudioMode ? isStudioGroup : !isStudioGroup;
75
- });
76
- }, [unFilteredGroupOrder, isStudioMode]);
76
+ const entryGroups = new Set(rawNavigationEntries.map(e => e.group).filter(Boolean));
77
+ return unFilteredGroupOrder.filter(g => entryGroups.has(g));
78
+ }, [unFilteredGroupOrder, rawNavigationEntries]);
77
79
 
78
80
  const fuse = useRef<Fuse<NavigationEntry> | null>(null);
79
81
  const [filteredUrls, setFilteredUrls] = useState<string[] | null>(null);
@@ -244,7 +246,11 @@ export function ContentHomePage({
244
246
  ...(adminGroupData ? [adminGroupData.name] : [])
245
247
  ], [items, adminGroupData]);
246
248
 
247
- const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupNames, "home");
249
+ const collapsedDefaults = useMemo(
250
+ () => buildCollapsedDefaults(registry.cmsConfig?.navigationGroupMappings, "home"),
251
+ [registry.cmsConfig?.navigationGroupMappings]
252
+ );
253
+ const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupNames, "home", collapsedDefaults);
248
254
 
249
255
  const {
250
256
  sensors,
@@ -312,8 +318,11 @@ export function ContentHomePage({
312
318
  Render
313
319
  ─────────────────────────────────────────────────────────────── */
314
320
  return (
315
- <div ref={containerRef} className="py-2 overflow-auto h-full w-full bg-surface-100 dark:bg-surface-800">
321
+ <div ref={containerRef} className="py-2 overflow-auto h-full w-full bg-surface-50 dark:bg-surface-800">
316
322
  <Container maxWidth="6xl">
323
+ <div className="mb-4">
324
+ <BootstrapAdminBanner />
325
+ </div>
317
326
  {/* search & actions */}
318
327
  <div
319
328
  className="w-full sticky py-4 transition-all duration-400 ease-in-out top-0 z-10 flex flex-row gap-4"
@@ -26,10 +26,10 @@ export const NavigationCard = React.memo(function NavigationCard({
26
26
  return (
27
27
  <Card
28
28
  className={cls(
29
- "group h-full p-5 cursor-pointer transition-all duration-200 ease-in-out",
30
- "border-surface-200/40 dark:border-surface-700/40",
31
- "hover:-translate-y-0.5 hover:shadow-md hover:shadow-primary/5",
32
- "hover:border-primary/30 dark:hover:border-primary/20",
29
+ "group h-full p-4 cursor-pointer transition-all duration-150 ease-in-out",
30
+ "border-surface-200 dark:border-surface-700/40",
31
+ "hover:shadow-md hover:shadow-black/[0.04]",
32
+ "hover:border-surface-300 dark:hover:border-primary/20",
33
33
  shrink && "w-full max-w-full min-h-0 scale-75"
34
34
  )}
35
35
  onClick={() => {
@@ -40,8 +40,8 @@ export function NavigationGroup({
40
40
  component={"h2"}
41
41
  color="secondary"
42
42
  className={cls(
43
- "p-4 py-2 rounded",
44
- "font-medium text-sm text-surface-600 dark:text-surface-400"
43
+ "px-4 py-1 rounded",
44
+ "font-semibold text-[11px] uppercase tracking-wider text-surface-400 dark:text-surface-400"
45
45
  )}
46
46
  >
47
47
  {currentGroupName}
@@ -27,6 +27,8 @@ export function RebaseAuthGate({ children }: { children: React.ReactNode }) {
27
27
  const registry = useRebaseRegistry();
28
28
  const authController = useAuthController();
29
29
 
30
+ console.log("[AuthGate] initialLoading:", authController?.initialLoading, "user:", authController?.user?.email ?? null);
31
+
30
32
  if (authController?.initialLoading) {
31
33
  return <CircularProgressCenter size={"large"}/>;
32
34
  }
@@ -10,7 +10,7 @@ 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 }: RebaseCMSConfig) {
13
+ export function RebaseCMS({ collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings }: RebaseCMSConfig) {
14
14
  const dispatch = useRebaseRegistryDispatch();
15
15
 
16
16
  useLayoutEffect(() => {
@@ -19,9 +19,10 @@ homePage,
19
19
  entityViews,
20
20
  entityActions,
21
21
  plugins,
22
- collectionEditor });
22
+ collectionEditor,
23
+ navigationGroupMappings });
23
24
  return () => dispatch.unregisterCMS();
24
- }, [dispatch, collections, homePage, entityViews, entityActions, plugins, collectionEditor]);
25
+ }, [dispatch, collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings]);
25
26
 
26
27
  return null;
27
28
  }
@@ -69,11 +69,14 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
69
69
  const userConfigPersistence = useBuildLocalConfigurationPersistence();
70
70
 
71
71
  // ── Collection Editor resolution ──────────────────────────────────
72
+ // The collection editor is ALWAYS enabled when Studio is registered.
73
+ // The `collectionEditor` CMS config is for fine-tuning (readOnly, auth, etc.),
74
+ // not for opting-in. When omitted, the editor defaults to enabled
75
+ // (read-only in production).
72
76
  const collectionEditorConfig = registry.cmsConfig?.collectionEditor;
73
- const collectionEditorEnabled = Boolean(collectionEditorConfig);
77
+ const collectionEditorEnabled = Boolean(collectionEditorConfig) || Boolean(registry.studioConfig);
74
78
  const collectionEditorOptions: CollectionEditorOptions | undefined = useMemo(() => {
75
- if (!collectionEditorConfig) return undefined;
76
- if (collectionEditorConfig === true) return {};
79
+ if (collectionEditorConfig === true || !collectionEditorConfig) return {};
77
80
  return collectionEditorConfig;
78
81
  }, [collectionEditorConfig]);
79
82
 
@@ -117,7 +120,6 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
117
120
  return {
118
121
  slug: "schema",
119
122
  name: "Edit collections",
120
- group: "Database",
121
123
  icon: "LayoutList",
122
124
  nestedRoutes: true,
123
125
  view: (
@@ -138,6 +140,7 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
138
140
  plugins: registry.cmsConfig?.plugins ?? EMPTY_PLUGINS,
139
141
  collections: collectionsBuilder,
140
142
  views: devViews,
143
+ navigationGroupMappings: registry.cmsConfig?.navigationGroupMappings,
141
144
  authController: context.authController!,
142
145
  data: context.data,
143
146
  collectionRegistryController,
@@ -83,6 +83,10 @@ export const RelationSelector = React.forwardRef<
83
83
  // Track IDs that were set via local user interaction (onItemClick / handleClear / handleRemoveItem).
84
84
  // When an incoming value change matches these IDs exactly, we skip async re-resolution.
85
85
  const localSelectionIdsRef = useRef<string | null>(null);
86
+ // Snapshot of selected IDs captured when the popover opens.
87
+ // Used to sort the dropdown list so selected items appear at the top.
88
+ // Stays stable for the entire popover session so items don't jump around.
89
+ const pinnedIdsRef = useRef<Set<string> | null>(null);
86
90
 
87
91
  const {
88
92
  items: availableItems,
@@ -323,6 +327,7 @@ relation } as RelationItem;
323
327
  newSelected = [item];
324
328
  setIsPopoverOpen(false);
325
329
  isPopoverOpenRef.current = false;
330
+ pinnedIdsRef.current = null;
326
331
  }
327
332
  setSelectedItems(newSelected);
328
333
  // Mark this fingerprint so the resolution effect skips async work
@@ -348,11 +353,13 @@ relation } as RelationItem;
348
353
  if (disabled) return;
349
354
  // We control open manually; only allow opening attempts from Radix (e.g. trigger press)
350
355
  if (next) {
356
+ // Capture current selection so we can pin those items to the top of the list
357
+ pinnedIdsRef.current = new Set(selectedItems.map(i => String(i.id)));
351
358
  setIsPopoverOpen(true);
352
359
  isPopoverOpenRef.current = true;
353
360
  }
354
361
  // Ignore close attempts here; outside click/Escape handled manually; single select closes explicitly on selection.
355
- }, [disabled]);
362
+ }, [disabled, selectedItems]);
356
363
 
357
364
  // Outside click + Escape handling (simple and reliable)
358
365
  useEffect(() => {
@@ -367,12 +374,14 @@ relation } as RelationItem;
367
374
  // Outside
368
375
  setIsPopoverOpen(false);
369
376
  isPopoverOpenRef.current = false;
377
+ pinnedIdsRef.current = null;
370
378
  }
371
379
 
372
380
  function handleKey(ev: KeyboardEvent) {
373
381
  if (ev.key === "Escape") {
374
382
  setIsPopoverOpen(false);
375
383
  isPopoverOpenRef.current = false;
384
+ pinnedIdsRef.current = null;
376
385
  }
377
386
  }
378
387
 
@@ -389,6 +398,7 @@ relation } as RelationItem;
389
398
  const closePopover = useCallback(() => {
390
399
  setIsPopoverOpen(false);
391
400
  isPopoverOpenRef.current = false;
401
+ pinnedIdsRef.current = null;
392
402
  }, []);
393
403
 
394
404
  const resolvedPlaceholder = placeholder || emptyPlaceholder || <EmptyValue className={"ml-2"}/>;
@@ -412,6 +422,11 @@ relation } as RelationItem;
412
422
  setIsPopoverOpen(o => {
413
423
  const next = !o;
414
424
  isPopoverOpenRef.current = next;
425
+ if (next) {
426
+ pinnedIdsRef.current = new Set(selectedItems.map(i => String(i.id)));
427
+ } else {
428
+ pinnedIdsRef.current = null;
429
+ }
415
430
  return next;
416
431
  });
417
432
  }}
@@ -589,7 +604,20 @@ relation } as RelationItem;
589
604
  </CommandPrimitive.Empty>
590
605
  )}
591
606
  <CommandPrimitive.Group>
592
- {availableItems.map((item) => {
607
+ {(() => {
608
+ // Sort items so that initially-selected (pinned) items appear first.
609
+ // We use the snapshot taken when the popover opened so items don't
610
+ // jump around as the user checks/unchecks.
611
+ const pinned = pinnedIdsRef.current;
612
+ const sortedItems = pinned && pinned.size > 0
613
+ ? [...availableItems].sort((a, b) => {
614
+ const aP = pinned.has(String(a.id)) ? 0 : 1;
615
+ const bP = pinned.has(String(b.id)) ? 0 : 1;
616
+ return aP - bP;
617
+ })
618
+ : availableItems;
619
+ return sortedItems;
620
+ })().map((item) => {
593
621
  const isSelected = selectedItems.some(v => String(v.id) === String(item.id));
594
622
  return (
595
623
  <CommandPrimitive.Item
@@ -264,7 +264,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
264
264
  checkFilterCombination={checkFilterCombination}
265
265
  createFilterField={filterable ? createFilterField : undefined}
266
266
  rowClassName={useCallback((entity: Entity<M>) => {
267
- return highlightedRow?.(entity) ? "bg-surface-50/75 bg-surface-100/75 dark:!bg-surface-800/60" : "";
267
+ return highlightedRow?.(entity) ? "bg-surface-accent-50 dark:!bg-surface-accent-950" : "";
268
268
  }, [highlightedRow])}
269
269
  className="grow"
270
270
  emptyComponent={emptyComponent}
@@ -42,7 +42,7 @@ export const UserSelector = React.forwardRef<
42
42
  clearable = true,
43
43
  className,
44
44
  pageSize,
45
- searchPlaceholder = "SearchIcon users...",
45
+ searchPlaceholder = "Search users...",
46
46
  noResultsText = "No users found.",
47
47
  loadingText = "Loading..."
48
48
  },
@@ -7,7 +7,7 @@ import { Button, Container, Dialog, DialogActions, DialogContent, DialogTitle, I
7
7
  import { MailIcon, KeyRoundIcon, PlusIcon, Trash2Icon, CopyIcon, CheckCircleIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
8
8
  import { RoleChip } from "./RoleChip";
9
9
  import { UserManagementDelegate, Role, UserCreationResult } from "@rebasepro/types";
10
- import { ConfirmationDialog } from "@rebasepro/core";
10
+ import { ConfirmationDialog, BootstrapAdminBanner } from "@rebasepro/core";
11
11
 
12
12
  const PAGE_SIZE = 25;
13
13
 
@@ -249,22 +249,7 @@ message: error instanceof Error ? error.message : t("error_resetting_password")
249
249
 
250
250
  return (
251
251
  <Container className="w-full flex flex-col py-4 gap-4" maxWidth={"6xl"}>
252
- {/* Bootstrap warning when no admins */}
253
- {!delegateLoading && !hasAdmin && !usersError && loggedInUser && bootstrapAdmin && (
254
- <div className="bg-yellow-100 dark:bg-yellow-900 border border-yellow-400 dark:border-yellow-700 rounded p-4 flex items-center justify-between">
255
- <div>
256
- <Typography variant="label" className="text-yellow-800 dark:text-yellow-200">
257
- {t("no_users_or_roles_defined")}
258
- </Typography>
259
- </div>
260
- <Button
261
- onClick={handleBootstrap}
262
- disabled={bootstrapping}
263
- >
264
- {bootstrapping ? <CircularProgress size="small"/> : t("add_logged_user_as_admin")}
265
- </Button>
266
- </div>
267
- )}
252
+ <BootstrapAdminBanner className="mb-4" />
268
253
 
269
254
  <div className="flex items-center mt-12 mb-4 gap-4">
270
255
  <Typography gutterBottom variant="h4" className="grow mb-0" component="h4">