@rebasepro/admin 0.11.1-canary.gfd39654 → 0.12.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 (124) hide show
  1. package/README.md +1 -1
  2. package/dist/{CollectionEditorDialog-o7F_zbGY.js → CollectionEditorDialog-Dpc75wIZ.js} +56 -260
  3. package/dist/CollectionEditorDialog-Dpc75wIZ.js.map +1 -0
  4. package/dist/{PropertyEditView-DgB2b22q.js → PropertyEditView-DH3XZC2x.js} +2 -2
  5. package/dist/{PropertyEditView-DgB2b22q.js.map → PropertyEditView-DH3XZC2x.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-DaLRgPkj.js → RouterCollectionsStudioView-BZLq4MGp.js} +4 -4
  7. package/dist/{RouterCollectionsStudioView-DaLRgPkj.js.map → RouterCollectionsStudioView-BZLq4MGp.js.map} +1 -1
  8. package/dist/collection_editor/CollectionEditorDialogsContext.d.ts +1 -1
  9. package/dist/collection_editor/_cms_internals.d.ts +1 -1
  10. package/dist/collection_editor/extensibility_types.d.ts +1 -1
  11. package/dist/collection_editor/serializable_types.d.ts +40 -12
  12. package/dist/collection_editor/serializable_utils.d.ts +1 -1
  13. package/dist/collection_editor/ui/collection_editor/CollectionRLSTab.d.ts +1 -11
  14. package/dist/collection_editor_ui.js +4 -4
  15. package/dist/components/CollectionEditorDialogs.d.ts +2 -2
  16. package/dist/components/CollectionTableBinding/column_utils.d.ts +7 -1
  17. package/dist/components/CollectionViewBinding/CollectionListViewBinding.d.ts +2 -2
  18. package/dist/components/DefaultDrawer.d.ts +3 -3
  19. package/dist/components/DrawerNavigationGroup.d.ts +8 -1
  20. package/dist/components/DrawerNavigationItem.d.ts +7 -1
  21. package/dist/components/EditViewBinding.d.ts +1 -1
  22. package/dist/components/RebaseAdmin.d.ts +1 -1
  23. package/dist/components/RebaseAuthGate.d.ts +1 -1
  24. package/dist/components/RebaseLayout.d.ts +5 -1
  25. package/dist/components/RebaseNavigation.d.ts +2 -2
  26. package/dist/components/RebaseRouteDefs.d.ts +1 -1
  27. package/dist/components/RebaseShell.d.ts +6 -1
  28. package/dist/components/RelationSelector.d.ts +14 -0
  29. package/dist/components/SelectableTable/filters/RelationFilterField.d.ts +27 -0
  30. package/dist/components/SelectableTable/filters/null_filter.d.ts +46 -0
  31. package/dist/components/SidePanelProvider.d.ts +2 -2
  32. package/dist/components/app/Scaffold.d.ts +11 -0
  33. package/dist/components/field_configs.d.ts +1 -1
  34. package/dist/{export-BvqhjdEb.js → export-BzIpUdSK.js} +3 -3
  35. package/dist/export-BzIpUdSK.js.map +1 -0
  36. package/dist/form/EntityForm.d.ts +2 -2
  37. package/dist/form/EntityFormBinding.d.ts +5 -5
  38. package/dist/{history-Dz20hH38.js → history-g9688IaK.js} +2 -2
  39. package/dist/{history-Dz20hH38.js.map → history-g9688IaK.js.map} +1 -1
  40. package/dist/hooks/index.d.ts +1 -1
  41. package/dist/hooks/useAdminContext.d.ts +29 -0
  42. package/dist/hooks/useHistory.d.ts +7 -11
  43. package/dist/{import-DINSB-Vu.js → import-BjUG-cIY.js} +2 -2
  44. package/dist/{import-DINSB-Vu.js.map → import-BjUG-cIY.js.map} +1 -1
  45. package/dist/index.js +61 -38
  46. package/dist/index.js.map +1 -1
  47. package/dist/types/components/EntityFormProps.d.ts +1 -1
  48. package/dist/types/fields.d.ts +1 -1
  49. package/dist/{util-BhhfHY1M.js → util-B5fJm1FA.js} +296 -169
  50. package/dist/util-B5fJm1FA.js.map +1 -0
  51. package/package.json +9 -9
  52. package/src/collection_editor/CollectionEditorDialogsContext.tsx +1 -1
  53. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  54. package/src/collection_editor/_cms_internals.ts +1 -1
  55. package/src/collection_editor/extensibility_types.ts +1 -1
  56. package/src/collection_editor/index.ts +2 -2
  57. package/src/collection_editor/serializable_types.ts +44 -13
  58. package/src/collection_editor/serializable_utils.ts +68 -59
  59. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +1 -1
  60. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +66 -41
  61. package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +1 -1
  62. package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +1 -1
  63. package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +2 -2
  64. package/src/components/CollectionEditorDialogs.tsx +2 -2
  65. package/src/components/CollectionTableBinding/CollectionRowActions.tsx +5 -4
  66. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +8 -2
  67. package/src/components/CollectionTableBinding/column_utils.tsx +19 -5
  68. package/src/components/CollectionTableBinding/table_bindings.tsx +10 -10
  69. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +8 -7
  70. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +2 -2
  71. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +2 -2
  72. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +2 -2
  73. package/src/components/DefaultDrawer.tsx +19 -8
  74. package/src/components/DeleteEntityDialog.tsx +2 -2
  75. package/src/components/DetailViewBinding.tsx +21 -1
  76. package/src/components/DrawerNavigationGroup.tsx +47 -4
  77. package/src/components/DrawerNavigationItem.tsx +19 -5
  78. package/src/components/EditFormActions.tsx +4 -3
  79. package/src/components/EditViewBinding.tsx +1 -1
  80. package/src/components/HomePage/ContentHomePage.tsx +4 -4
  81. package/src/components/RebaseAdmin.tsx +3 -3
  82. package/src/components/RebaseAuthGate.tsx +1 -1
  83. package/src/components/RebaseLayout.tsx +8 -2
  84. package/src/components/RebaseNavigation.tsx +9 -9
  85. package/src/components/RebaseRouteDefs.tsx +2 -2
  86. package/src/components/RebaseShell.tsx +10 -1
  87. package/src/components/RelationSelector.tsx +18 -3
  88. package/src/components/SelectableTable/filters/FilterFieldBinding.tsx +1 -1
  89. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +36 -9
  90. package/src/components/SelectableTable/filters/RelationFilterField.tsx +93 -17
  91. package/src/components/SelectableTable/filters/null_filter.ts +63 -0
  92. package/src/components/SidePanelProvider.tsx +2 -2
  93. package/src/components/app/Scaffold.tsx +16 -1
  94. package/src/data_export/export/ExportCollectionAction.tsx +2 -2
  95. package/src/form/EntityForm.tsx +2 -2
  96. package/src/form/EntityFormActions.tsx +2 -1
  97. package/src/form/EntityFormBinding.tsx +7 -7
  98. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +3 -3
  99. package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +16 -5
  100. package/src/form/field_bindings/ReferenceFieldBinding.tsx +3 -3
  101. package/src/form/field_bindings/RelationFieldBinding.tsx +17 -6
  102. package/src/form/field_bindings/RepeatFieldBinding.tsx +2 -2
  103. package/src/hooks/index.ts +1 -1
  104. package/src/hooks/navigation/useNavigationResolution.ts +10 -4
  105. package/src/hooks/{useCMSContext.tsx → useAdminContext.tsx} +10 -10
  106. package/src/hooks/useHistory.ts +7 -11
  107. package/src/hooks/useResolvedNavigationFrom.tsx +2 -2
  108. package/src/index.ts +4 -4
  109. package/src/preview/PropertyPreview.tsx +6 -6
  110. package/src/preview/components/RelationPreview.tsx +1 -1
  111. package/src/preview/property_previews/ArrayOfMapsPreview.tsx +1 -1
  112. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +2 -2
  113. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +2 -2
  114. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +2 -2
  115. package/src/types/components/EntityFormProps.tsx +2 -2
  116. package/src/types/fields.tsx +1 -1
  117. package/dist/CollectionEditorDialog-o7F_zbGY.js.map +0 -1
  118. package/dist/collection_editor/pgColumnToProperty.d.ts +0 -7
  119. package/dist/editor/extensions/Image/index.d.ts +0 -6
  120. package/dist/export-BvqhjdEb.js.map +0 -1
  121. package/dist/hooks/useCMSContext.d.ts +0 -29
  122. package/dist/util-BhhfHY1M.js.map +0 -1
  123. package/src/collection_editor/pgColumnToProperty.ts +0 -291
  124. package/src/editor/extensions/Image/index.ts +0 -133
@@ -16,12 +16,12 @@ const PropertyFormDialog = lazy(() =>
16
16
 
17
17
  /**
18
18
  * Renders the CollectionEditorDialog and PropertyFormDialog inside
19
- * the RebaseShell tree where CMS-internal contexts
19
+ * the RebaseShell tree where admin-internal contexts
20
20
  * (CollectionRegistryContext, NavigationStateContext, UrlContext)
21
21
  * are available.
22
22
  *
23
23
  * Both dialog components are loaded dynamically via React.lazy so that
24
- * production CMS bundles that never open the collection editor don't pay
24
+ * production admin bundles that never open the collection editor don't pay
25
25
  * the download/parse cost up-front.
26
26
  *
27
27
  * The dialog state (open/close, props) is managed by ConfigControllerProvider
@@ -16,9 +16,10 @@ import {
16
16
  Tooltip
17
17
  } from "@rebasepro/ui";
18
18
  import { useTranslation } from "@rebasepro/app";
19
+ import { getIcon } from "@rebasepro/app";
19
20
  import { getEntityFromCache } from "@rebasepro/app";
20
21
  import { getLocalChangesBackup } from "@rebasepro/app";
21
- import { useCMSContext } from "../../hooks/useCMSContext";
22
+ import { useAdminContext } from "../../hooks/useAdminContext";
22
23
 
23
24
  /**
24
25
  *
@@ -79,7 +80,7 @@ export const CollectionRowActions = function CollectionRowActions({
79
80
  isDraggable?: boolean;
80
81
  }) {
81
82
 
82
- const context = useCMSContext();
83
+ const context = useAdminContext();
83
84
  const sidePanelCtrl = context.sidePanelController;
84
85
  const { t } = useTranslation();
85
86
 
@@ -140,7 +141,7 @@ export const CollectionRowActions = function CollectionRowActions({
140
141
  });
141
142
  }}
142
143
  size={iconSize}>
143
- {action.icon}
144
+ {getIcon(action.icon, undefined, undefined, "smallest")}
144
145
  </IconButton>;
145
146
  if (isEditAction && hasDraft) {
146
147
  iconButton = (
@@ -183,7 +184,7 @@ export const CollectionRowActions = function CollectionRowActions({
183
184
  openEntityMode: openEntityMode ?? collection?.openEntityMode
184
185
  });
185
186
  }}>
186
- {action.icon}
187
+ {getIcon(action.icon, undefined, undefined, "smallest")}
187
188
  {action.name}
188
189
  </MenuItem>
189
190
  ))}
@@ -4,7 +4,7 @@ import { Entity, User, Property } from "@rebasepro/types";
4
4
  import { CollectionSize, RebaseContext } from "@rebasepro/admin-types";
5
5
  import { PropertyTableCell } from "./PropertyTableCell";
6
6
  import { ErrorBoundary } from "@rebasepro/ui";
7
- import { useRebaseContext, useLargeLayout } from "@rebasepro/app";
7
+ import { useRebaseContext, useLargeLayout, useCollectionScope } from "@rebasepro/app";
8
8
  import { CellRendererParams, VirtualTableColumn } from "@rebasepro/ui";
9
9
  import { CollectionRowActions } from "./CollectionRowActions";
10
10
  import { CollectionTableToolbar } from "./internal/CollectionTableToolbar";
@@ -248,11 +248,17 @@ export const CollectionTableBinding = function CollectionTableBinding<M extends
248
248
 
249
249
  }, [size]);
250
250
 
251
+ // Same source the filter fields read their engine from, so the header's
252
+ // filter control and the field it opens cannot disagree about what the
253
+ // collection's driver can answer.
254
+ const engine = useCollectionScope()?.engine;
255
+
251
256
  const collectionColumns: VirtualTableColumn[] = useMemo(() => {
252
257
  const columnsResult: VirtualTableColumn[] = propertiesToColumns({
253
258
  properties,
254
259
  sortable,
255
260
  fixedFilter,
261
+ engine,
256
262
  AdditionalHeaderWidget
257
263
  });
258
264
 
@@ -273,7 +279,7 @@ export const CollectionTableBinding = function CollectionTableBinding<M extends
273
279
  }))
274
280
  : [];
275
281
  return [...columnsResult, ...additionalTableColumns];
276
- }, [properties, sortable, fixedFilter, AdditionalHeaderWidget, additionalFields]);
282
+ }, [properties, sortable, fixedFilter, engine, AdditionalHeaderWidget, additionalFields]);
277
283
 
278
284
  const idColumn: VirtualTableColumn = useMemo(() => ({
279
285
  key: "id_ewcfedcswdf3",
@@ -4,7 +4,7 @@ import { getTableCellAlignment, getTablePropertyColumnWidth } from "./internal/c
4
4
  import { FilterValues } from "@rebasepro/types";
5
5
  import { VirtualTableColumn } from "@rebasepro/ui";
6
6
  import { getIconForProperty, getResolvedPropertyInPath } from "../../util/property_utils";
7
- import { getColumnKeysForProperty } from "@rebasepro/app";
7
+ import { getColumnKeysForProperty, isFilterableRelation } from "@rebasepro/app";
8
8
 
9
9
  export function buildIdColumn(largeLayout?: boolean): VirtualTableColumn {
10
10
  return {
@@ -22,6 +22,12 @@ export interface PropertiesToColumnsParams<M extends Record<string, unknown>> {
22
22
  properties: Properties;
23
23
  sortable?: boolean;
24
24
  fixedFilter?: FilterValues<keyof M extends string ? keyof M : never>;
25
+ /**
26
+ * The engine backing the collection (`collection.engine`). Decides which
27
+ * relation kinds can be filtered — see `isFilterableRelation`. Omitted
28
+ * falls back to the default engine's capabilities.
29
+ */
30
+ engine?: string;
25
31
  AdditionalHeaderWidget?: React.ComponentType<{
26
32
  property: Property,
27
33
  propertyKey: string,
@@ -29,7 +35,7 @@ export interface PropertiesToColumnsParams<M extends Record<string, unknown>> {
29
35
  }>;
30
36
  }
31
37
 
32
- export function propertiesToColumns<M extends Record<string, unknown>>({ properties, sortable, fixedFilter, AdditionalHeaderWidget }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
38
+ export function propertiesToColumns<M extends Record<string, unknown>>({ properties, sortable, fixedFilter, engine, AdditionalHeaderWidget }: PropertiesToColumnsParams<M>): VirtualTableColumn[] {
33
39
  const disabledFilter = Boolean(fixedFilter);
34
40
  return Object.entries<Property>(properties)
35
41
  .flatMap(([key, property]) => getColumnKeysForProperty(property, key))
@@ -40,7 +46,7 @@ export function propertiesToColumns<M extends Record<string, unknown>>({ propert
40
46
  const property = getResolvedPropertyInPath(properties, key) as Property;
41
47
  if (!property)
42
48
  throw Error("Internal error: no property found in path " + key);
43
- const filterable = filterableProperty(property);
49
+ const filterable = filterableProperty(property, false, engine);
44
50
  return {
45
51
  key: key as string,
46
52
  align: getTableCellAlignment(property),
@@ -61,13 +67,21 @@ export function propertiesToColumns<M extends Record<string, unknown>>({ propert
61
67
  });
62
68
  }
63
69
 
64
- function filterableProperty(property: Property, partOfArray = false): boolean {
70
+ function filterableProperty(property: Property, partOfArray = false, engine?: string): boolean {
71
+ // A relation this engine's driver cannot compile into a `WHERE` has
72
+ // nothing to filter on, so the header's filter control would open onto a
73
+ // field that renders nothing (`FilterFieldBinding` returns null on an
74
+ // empty operator list) — and, before the driver started failing closed,
75
+ // sending one silently returned every row. Same authority as the operator
76
+ // resolution, and engine-aware for the same reason: this table renders
77
+ // over Postgres, Mongo, Firestore and anything a developer registers.
78
+ if (!isFilterableRelation(property, engine)) return false;
65
79
  if (partOfArray) {
66
80
  return ["string", "number", "date", "reference", "relation"].includes(property.type);
67
81
  }
68
82
  if (property.type === "array") {
69
83
  if (property.of && !Array.isArray(property.of))
70
- return filterableProperty(property.of, true);
84
+ return filterableProperty(property.of, true, engine);
71
85
  else
72
86
  return false;
73
87
  }
@@ -222,10 +222,10 @@ export function getTableBindingForProperty(
222
222
  path={(property as ReferenceProperty).path!}
223
223
  multiselect={false}
224
224
  previewProperties={(property as ReferenceProperty).admin?.previewProperties}
225
- includeId={(property as ReferenceProperty).includeId}
226
- includeEntityLink={(property as ReferenceProperty).includeEntityLink}
225
+ includeId={(property as ReferenceProperty).admin?.includeId}
226
+ includeEntityLink={(property as ReferenceProperty).admin?.includeEntityLink}
227
227
  title={property.name ?? propertyKey}
228
- fixedFilter={(property as ReferenceProperty).fixedFilter}
228
+ fixedFilter={(property as ReferenceProperty).admin?.fixedFilter}
229
229
  />
230
230
  );
231
231
  },
@@ -290,9 +290,9 @@ export function getTableBindingForProperty(
290
290
  path={refOfProp.path!}
291
291
  previewProperties={refOfProp.admin?.previewProperties}
292
292
  title={arrayProperty.name}
293
- fixedFilter={refOfProp.fixedFilter}
294
- includeId={refOfProp.includeId}
295
- includeEntityLink={refOfProp.includeEntityLink}
293
+ fixedFilter={refOfProp.admin?.fixedFilter}
294
+ includeId={refOfProp.admin?.includeId}
295
+ includeEntityLink={refOfProp.admin?.includeEntityLink}
296
296
  />
297
297
  ),
298
298
  allowScroll: false
@@ -318,10 +318,10 @@ function RelationDialogBindingComponent({ propertyKey, internalValue, updateValu
318
318
  multiselect={false}
319
319
  relation={relProp.relation!}
320
320
  previewProperties={relProp.admin?.previewProperties}
321
- includeId={relProp.includeId}
322
- includeEntityLink={relProp.includeEntityLink}
321
+ includeId={relProp.admin?.includeId}
322
+ includeEntityLink={relProp.admin?.includeEntityLink}
323
323
  title={relProp.name ?? propertyKey}
324
- fixedFilter={relProp.fixedFilter}
324
+ fixedFilter={relProp.admin?.fixedFilter}
325
325
  />
326
326
  );
327
327
  }
@@ -337,7 +337,7 @@ function RelationSelectorBindingComponent({ propertyKey, internalValue, updateVa
337
337
  disabled={disabled}
338
338
  size={"small"}
339
339
  relation={relProp.relation!}
340
- fixedFilter={relProp.fixedFilter}
340
+ fixedFilter={relProp.admin?.fixedFilter}
341
341
  />
342
342
  );
343
343
  }
@@ -22,9 +22,10 @@ import {
22
22
  import { useAnalyticsController } from "@rebasepro/app";
23
23
  import { getEntityPreviewKeys } from "../../util/previews";
24
24
  import { IconForView } from "@rebasepro/app";
25
+ import { getIcon } from "@rebasepro/app";
25
26
  import { getValueInPath } from "@rebasepro/utils";
26
27
  import { useCollectionSlotKeys, resolveEntitySlots, type CollectionSlotKeys } from "./usePreviewSlots";
27
- import { useCMSContext } from "../../hooks/useCMSContext";
28
+ import { useAdminContext } from "../../hooks/useAdminContext";
28
29
  import { resolveEntityAction } from "../../util/resolutions";
29
30
 
30
31
  export type CollectionListViewBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
@@ -265,8 +266,8 @@ year: diffDays > 365 ? "numeric" : undefined });
265
266
  }
266
267
 
267
268
  /**
268
- * Classic CMS list view for displaying entities.
269
- * Designed to be the most familiar, stereotypical CMS content management view:
269
+ * Classic admin list view for displaying entities.
270
+ * Designed to be the most familiar, stereotypical admin content management view:
270
271
  * - Clean rows with checkbox, icon/avatar, title, metadata, and actions
271
272
  * - Column-sortable headers
272
273
  * - Infinite scroll
@@ -289,7 +290,7 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
289
290
  const authController = useAuthController();
290
291
  const customizationController = useCustomizationController();
291
292
  const analyticsController = useAnalyticsController();
292
- const context = useCMSContext();
293
+ const context = useAdminContext();
293
294
 
294
295
  const containerRef = useRef<HTMLDivElement>(null);
295
296
  const [containerWidth, setContainerWidth] = useState(1200);
@@ -622,7 +623,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
622
623
  isLast: boolean;
623
624
  isActive?: boolean;
624
625
  listViewActions?: EntityAction[];
625
- context?: ReturnType<typeof useCMSContext>;
626
+ context?: ReturnType<typeof useAdminContext>;
626
627
  path?: string;
627
628
  selectionController?: SelectionController<M>;
628
629
  openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
@@ -864,7 +865,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
864
865
  openEntityMode: openEntityMode ?? collection?.openEntityMode ?? "full_screen"
865
866
  });
866
867
  }}>
867
- {action.icon}
868
+ {getIcon(action.icon, undefined, undefined, "smallest")}
868
869
  </IconButton>
869
870
  </Tooltip>
870
871
  ))}
@@ -888,7 +889,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
888
889
  isLast: boolean;
889
890
  isActive?: boolean;
890
891
  listViewActions?: EntityAction[];
891
- context?: ReturnType<typeof useCMSContext>;
892
+ context?: ReturnType<typeof useAdminContext>;
892
893
  path?: string;
893
894
  selectionController?: SelectionController<M>;
894
895
  openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
@@ -13,7 +13,7 @@ const ImportCollectionAction = lazy(() => import("../../data_import/import").the
13
13
  const ExportCollectionAction = lazy(() => import("../../data_export/export").then(m => ({ default: m.ExportCollectionAction })));
14
14
  import { EditorCollectionAction } from "../../collection_editor/ui/EditorCollectionAction";
15
15
  import { useCollectionEditorController } from "../../collection_editor/useCollectionEditorController";
16
- import { useCMSContext } from "../../hooks/useCMSContext";
16
+ import { useAdminContext } from "../../hooks/useAdminContext";
17
17
 
18
18
  export type CollectionViewActionsProps<M extends Record<string, unknown>> = {
19
19
  collection: AdminCollection<M>;
@@ -52,7 +52,7 @@ export function CollectionViewActions<M extends Record<string, unknown>>({
52
52
  children,
53
53
  openNewDocument
54
54
  }: CollectionViewActionsProps<M>) {
55
- const context = useCMSContext();
55
+ const context = useAdminContext();
56
56
 
57
57
  const { canCreate, canDelete } = usePermissions();
58
58
 
@@ -75,7 +75,7 @@ import { addRecentId, getRecentIds } from "./utils";
75
75
  import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM } from "../../util/view_mode";
76
76
  import { mergeDeep } from "@rebasepro/utils";
77
77
  import { useBreadcrumbsController } from "../../hooks/useBreadcrumbsController";
78
- import { useCMSContext } from "../../hooks/useCMSContext";
78
+ import { useAdminContext } from "../../hooks/useAdminContext";
79
79
  import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
80
80
  import { useSidePanel } from "../../hooks/useSidePanel";
81
81
  import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
@@ -167,7 +167,7 @@ const CollectionViewBindingInner = React.memo(
167
167
  ) {
168
168
 
169
169
  const { t } = useTranslation();
170
- const context = useCMSContext();
170
+ const context = useAdminContext();
171
171
  const collectionRegistry = useCollectionRegistryController();
172
172
  const urlController = useUrlController();
173
173
  const breadcrumbs = useBreadcrumbsController();
@@ -11,7 +11,7 @@ import { FilterPresetsButton } from "./FilterPresetsButton";
11
11
  import { toArray } from "@rebasepro/utils";
12
12
  import { useNavigate } from "react-router-dom";
13
13
  import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
14
- import { useCMSContext } from "../../hooks/useCMSContext";
14
+ import { useAdminContext } from "../../hooks/useAdminContext";
15
15
  import { withViewMode } from "../../util/view_mode";
16
16
 
17
17
  export type CollectionViewStartActionsProps<M extends Record<string, unknown>> = {
@@ -43,7 +43,7 @@ export function CollectionViewStartActions<M extends Record<string, unknown>>({
43
43
  openNewDocument
44
44
  }: CollectionViewStartActionsProps<M>) {
45
45
 
46
- const context = useCMSContext();
46
+ const context = useAdminContext();
47
47
  const largeLayout = useLargeLayout();
48
48
  const { t } = useTranslation();
49
49
 
@@ -33,9 +33,9 @@ import { LanguageToggle, RebaseLogo } from "@rebasepro/app";
33
33
  import { useApp } from "./app/useApp";
34
34
 
35
35
  /**
36
- * Default drawer used in the CMS.
36
+ * Default drawer used in the admin.
37
37
  *
38
- * When no `children` are provided, renders the full CMS navigation
38
+ * When no `children` are provided, renders the full admin navigation
39
39
  * (collection groups, mode switch, plugin slots).
40
40
  *
41
41
  * When `children` **are** provided, renders the shared drawer shell
@@ -60,7 +60,7 @@ export function DefaultDrawer({
60
60
  logoDestination?: string;
61
61
  /**
62
62
  * Custom navigation content for the drawer.
63
- * When provided, replaces the default CMS navigation (collection groups,
63
+ * When provided, replaces the default admin navigation (collection groups,
64
64
  * mode switch, slots). The shared shell (logo, scroll area, footer
65
65
  * actions, collapse toggle) is still rendered around it.
66
66
  */
@@ -115,7 +115,7 @@ export function DefaultDrawer({
115
115
  ? "linear-gradient(to bottom, transparent 0, black 20px, black calc(100% - 20px), transparent 100%)"
116
116
  : "linear-gradient(to bottom, black 0, black calc(100% - 20px), transparent 100%)"
117
117
  }}>
118
- {children ?? <CMSNavigationContent />}
118
+ {children ?? <AdminNavigationContent />}
119
119
  </div>
120
120
 
121
121
  {footerActions !== null && (
@@ -138,11 +138,11 @@ export function DefaultDrawer({
138
138
  }
139
139
 
140
140
  /**
141
- * Default CMS navigation content — rendered inside DefaultDrawer when no
141
+ * Default admin navigation content — rendered inside DefaultDrawer when no
142
142
  * custom `children` are provided. Contains the mode switch, navigation
143
143
  * groups, and header / footer plugin slots.
144
144
  */
145
- function CMSNavigationContent() {
145
+ function AdminNavigationContent() {
146
146
 
147
147
  const {
148
148
  drawerHovered,
@@ -166,7 +166,7 @@ function CMSNavigationContent() {
166
166
  const allNavigationEntries = navigationState.topLevelNavigation?.navigationEntries ?? [];
167
167
 
168
168
  // Build a set of Studio-only dev view slugs so we can distinguish them from
169
- // CMS custom views added via <RebaseAdmin views={...}> or plugins.
169
+ // admin custom views added via <RebaseAdmin views={...}> or plugins.
170
170
  const studioViewSlugs = useMemo(() => {
171
171
  const slugs = new Set<string>();
172
172
  (registry.studioConfig?.devViews ?? []).forEach(v => slugs.add(v.slug));
@@ -176,7 +176,7 @@ function CMSNavigationContent() {
176
176
  }, [registry.studioConfig, registry.cmsConfig?.collectionEditor]);
177
177
 
178
178
  // Studio mode shows Studio dev views + admin entries (Users/Roles).
179
- // Content mode shows collections, CMS custom views, and admin entries — but not Studio dev views.
179
+ // Content mode shows collections, admin custom views, and admin entries — but not Studio dev views.
180
180
  const filteredEntries = adminModeController.mode === "studio"
181
181
  ? allNavigationEntries.filter(e => (e.type === "view" && studioViewSlugs.has(e.slug)) || e.type === "admin")
182
182
  : allNavigationEntries.filter(e => e.type !== "view" || !studioViewSlugs.has(e.slug));
@@ -189,6 +189,16 @@ function CMSNavigationContent() {
189
189
  ...[...entryGroups].filter(g => !orderedGroups.includes(g))
190
190
  ];
191
191
 
192
+ // Group header icons, declared on `navigationGroupMappings`. Keyed by name
193
+ // because that is what a navigation entry's `group` holds.
194
+ const groupIcons = useMemo(() => {
195
+ const icons = new Map<string, string>();
196
+ (registry.cmsConfig?.navigationGroupMappings ?? []).forEach(mapping => {
197
+ if (mapping.icon) icons.set(mapping.name, mapping.icon);
198
+ });
199
+ return icons;
200
+ }, [registry.cmsConfig?.navigationGroupMappings]);
201
+
192
202
  // Collapsible groups state - using "drawer" namespace for independent state from home page
193
203
  const collapsedDefaults = useMemo(
194
204
  () => buildCollapsedDefaults(registry.cmsConfig?.navigationGroupMappings, "drawer"),
@@ -244,6 +254,7 @@ context });
244
254
  tooltipsOpen={tooltipsOpen}
245
255
  adminMenuOpen={adminMenuOpen}
246
256
  onItemClick={onItemClick}
257
+ icon={groupIcons.get(group)}
247
258
  />
248
259
  );
249
260
  })}
@@ -10,7 +10,7 @@ import {
10
10
  useSnackbarController,
11
11
  useTranslation
12
12
  } from "@rebasepro/app";
13
- import { useCMSContext } from "../hooks";
13
+ import { useAdminContext } from "../hooks";
14
14
  import { EntityViewBinding } from "./EntityViewBinding";
15
15
  import type { AdminCollection } from "@rebasepro/admin-types";
16
16
 
@@ -54,7 +54,7 @@ export function DeleteEntityDialog<M extends Record<string, unknown>>({
54
54
  const { t } = useTranslation();
55
55
  const [loading, setLoading] = useState(false);
56
56
 
57
- const context = useCMSContext();
57
+ const context = useAdminContext();
58
58
  const entityOrEntities = Array.isArray(entityOrEntitiesToDelete) && entityOrEntitiesToDelete.length === 1
59
59
  ? entityOrEntitiesToDelete[0]
60
60
  : entityOrEntitiesToDelete;
@@ -11,6 +11,7 @@ import { EntityViewBinding } from "./EntityViewBinding";
11
11
  import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
12
12
  import {
13
13
  Alert,
14
+ ArrowLeftIcon,
14
15
  Button,
15
16
  CenteredView,
16
17
  cls,
@@ -470,7 +471,24 @@ entityId }
470
471
  );
471
472
  });
472
473
 
473
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog";
474
+ // Full screen is the one layout with no way out of its own: a side panel and a
475
+ // dialog can be dismissed, and a split keeps the list beside it, but opening an
476
+ // entity full screen replaces the collection entirely and left browser Back as
477
+ // the only route back to it — which is not an affordance the page shows, and is
478
+ // wrong anyway once the user has moved between tabs inside the entity.
479
+ const backToCollectionButton = layout === "full_screen" ? (
480
+ <Tooltip title={t("back")}>
481
+ <IconButton
482
+ size="small"
483
+ aria-label={t("back")}
484
+ onClick={() => navigate(urlController.buildUrlCollectionPath(path))}
485
+ >
486
+ <ArrowLeftIcon size={iconSize.smallest} />
487
+ </IconButton>
488
+ </Tooltip>
489
+ ) : null;
490
+
491
+ const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
474
492
 
475
493
  const fullScreenButton = !barActions && (layout === "side_panel" || layout === "split" || layout === "dialog") && entityId ? (
476
494
  <Tooltip title={"Open full screen"}>
@@ -562,6 +580,8 @@ entityId }
562
580
  {shouldShowTopBar && <div
563
581
  className={cls("h-[52px] items-center flex overflow-hidden w-full border-b pl-2 pr-2 flex bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
564
582
 
583
+ {backToCollectionButton}
584
+
565
585
  {fullScreenButton}
566
586
 
567
587
  {barActions?.({
@@ -2,7 +2,7 @@ import type { NavigationEntry } from "@rebasepro/admin-types";
2
2
  import React from "react";
3
3
  import { ChevronDownIcon, cls, iconSize, Typography } from "@rebasepro/ui";
4
4
 
5
- import { IconForView } from "@rebasepro/app";
5
+ import { IconForView, getIcon } from "@rebasepro/app";
6
6
  import { DrawerNavigationItem } from "./DrawerNavigationItem";
7
7
  import { useTranslation, useComponentOverride } from "@rebasepro/app";
8
8
 
@@ -47,6 +47,13 @@ export interface DrawerNavigationGroupProps {
47
47
  * Hide the group header (title and expandable panel)
48
48
  */
49
49
  hideHeader?: boolean;
50
+ /**
51
+ * Lucide icon name for the group header, from `NavigationGroupMapping.icon`.
52
+ *
53
+ * Supplying one moves the visual anchor from the rows to the group: the header
54
+ * gets the icon, and the entries below trade theirs for an indent.
55
+ */
56
+ icon?: string;
50
57
  }
51
58
 
52
59
  /**
@@ -63,10 +70,23 @@ export function DrawerNavigationGroup({
63
70
  adminMenuOpen,
64
71
  headerActions,
65
72
  onItemClick,
66
- hideHeader
73
+ hideHeader,
74
+ icon
67
75
  }: DrawerNavigationGroupProps) {
68
76
  const { t } = useTranslation();
69
77
  const ResolvedDrawerNavigationItem = useComponentOverride("Shell.DrawerNavigationItem", DrawerNavigationItem);
78
+
79
+ // Opt-in, per group: declaring `icon` on a `NavigationGroupMapping` switches
80
+ // this group to the categorised treatment — icon on the header, entries
81
+ // indented beneath it without their own. A group that declares no icon renders
82
+ // exactly as it did before, so an existing project sees no change at all.
83
+ //
84
+ // Two cases stay on the flat rendering regardless: with no header there is
85
+ // nothing to indent under, and collapsed to a rail the entry icons are the only
86
+ // affordance left to click.
87
+ const groupIcon = !hideHeader && drawerOpen ? getIcon(icon, undefined, undefined, "small") : undefined;
88
+ const categorised = Boolean(groupIcon);
89
+ const indentEntries = categorised;
70
90
  return (
71
91
  <div
72
92
  className={"my-2 mx-2 flex flex-col"}
@@ -83,14 +103,36 @@ export function DrawerNavigationGroup({
83
103
  <ChevronDownIcon
84
104
  size={iconSize.small}
85
105
  className={cls(
86
- "text-surface-400 dark:text-surface-400 transition-transform duration-200 mr-1",
106
+ categorised
107
+ ? "text-surface-500 dark:text-surface-400"
108
+ : "text-surface-400 dark:text-surface-400",
109
+ "transition-transform duration-200 mr-1",
87
110
  collapsed ? "-rotate-90" : "rotate-0"
88
111
  )}
89
112
  />
113
+ {groupIcon && (
114
+ <span className="shrink-0 mr-2 flex items-center text-surface-600 dark:text-surface-300 [&>svg]:size-4">
115
+ {groupIcon}
116
+ </span>
117
+ )}
90
118
  <Typography
91
119
  variant={"caption"}
92
120
  color={"secondary"}
93
- className="font-semibold text-[11px] uppercase tracking-wider flex-grow line-clamp-1 text-surface-400 dark:text-surface-400"
121
+ // Two treatments, and which one applies is decided by the group
122
+ // itself. Without an icon this is byte-for-byte the original: the
123
+ // drawer is a flat list of same-weight rows, and a louder header
124
+ // would just compete with them.
125
+ //
126
+ // With one, the group becomes the thing you scan — its rows have
127
+ // given up their icons to indent beneath it — so the label steps up
128
+ // to match. At 11px in surface-400 it sat *below* the contrast of
129
+ // the rows it labels, which is backwards once it is the anchor.
130
+ className={cls(
131
+ "font-semibold uppercase flex-grow line-clamp-1",
132
+ categorised
133
+ ? "text-[12px] tracking-wide text-surface-600 dark:text-surface-300"
134
+ : "text-[11px] tracking-wider text-surface-400 dark:text-surface-400"
135
+ )}
94
136
  >
95
137
  {(group || t("views_group"))}
96
138
  </Typography>
@@ -122,6 +164,7 @@ export function DrawerNavigationGroup({
122
164
  onClick={() => onItemClick?.(entry)}
123
165
  url={entry.url}
124
166
  name={entry.name}
167
+ indented={indentEntries}
125
168
  />
126
169
  ))}
127
170
  </div>
@@ -10,7 +10,8 @@ export function DrawerNavigationItem({
10
10
  adminMenuOpen,
11
11
  tooltipsOpen,
12
12
  url,
13
- onClick
13
+ onClick,
14
+ indented = false
14
15
  }: {
15
16
  icon: React.ReactElement,
16
17
  name: string,
@@ -19,12 +20,25 @@ export function DrawerNavigationItem({
19
20
  adminMenuOpen?: boolean,
20
21
  url: string,
21
22
  onClick?: () => void,
23
+ /**
24
+ * Render as a child of an icon-bearing group: no icon of its own, indented to
25
+ * sit under the group's label. Set by {@link DrawerNavigationGroup} only when
26
+ * the group actually has an icon and the drawer is expanded.
27
+ */
28
+ indented?: boolean,
22
29
  }) {
23
30
 
24
- const iconWrap = <div
25
- className={"shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-text-secondary-dark [&>svg]:size-4 group-hover/nav:text-primary transition-colors duration-150"}>
26
- {icon}
27
- </div>;
31
+ // Indented rows give up the icon rather than keeping it *and* indenting: a
32
+ // per-row icon repeated down a group is what flattens the hierarchy, since
33
+ // every row then reads at the weight of the category above it. The indent is
34
+ // the same 44px the icon occupied, so labels stay on the original grid and the
35
+ // rail width does not change.
36
+ const iconWrap = indented
37
+ ? <div className={"shrink-0 w-[44px] h-[30px]"} aria-hidden={true}/>
38
+ : <div
39
+ className={"shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-text-secondary-dark [&>svg]:size-4 group-hover/nav:text-primary transition-colors duration-150"}>
40
+ {icon}
41
+ </div>;
28
42
 
29
43
  const listItem = <div>
30
44
  <NavLink
@@ -20,7 +20,8 @@ import { SideDialogController, useSideDialogContext } from "./SideDialogs";
20
20
  import { FormexController } from "@rebasepro/forms";
21
21
  import { ErrorTooltip } from "@rebasepro/app";
22
22
  import { usePermissions } from "@rebasepro/app";
23
- import { useCMSContext } from "../hooks/useCMSContext";
23
+ import { getIcon } from "@rebasepro/app";
24
+ import { useAdminContext } from "../hooks/useAdminContext";
24
25
  import { useChildViewSource } from "../hooks/useChildViewSource";
25
26
 
26
27
  export function EditFormActions({
@@ -40,7 +41,7 @@ export function EditFormActions({
40
41
  }: EntityFormActionsProps) {
41
42
 
42
43
  const { canCreate, canDelete } = usePermissions();
43
- const context = useCMSContext();
44
+ const context = useAdminContext();
44
45
  const sidePanelController = context.sidePanelController;
45
46
  const sideDialogContext = useSideDialogContext();
46
47
  const customizationController = useCustomizationController();
@@ -371,7 +372,7 @@ function EntityActionButton({
371
372
  });
372
373
  }
373
374
  }}>
374
- {loading ? <CircularProgress size={"smallest"}/> : action.icon}
375
+ {loading ? <CircularProgress size={"smallest"}/> : getIcon(action.icon, undefined, undefined, "smallest")}
375
376
  </IconButton>
376
377
  </Tooltip>;
377
378
  }
@@ -70,7 +70,7 @@ export type OnTabChangeParams<M extends Record<string, unknown>> = {
70
70
 
71
71
  export interface EditViewBindingProps<M extends Record<string, unknown> = Record<string, unknown>> {
72
72
  /**
73
- * The CMS path of the entity, e.g. "users" or "products".
73
+ * The admin path of the entity, e.g. "users" or "products".
74
74
  */
75
75
  path: string;
76
76
  collection: AdminCollection<M>;