@rebasepro/admin 0.13.0 → 0.13.1-canary.g249daa1

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 (154) hide show
  1. package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-DgfCYDmF.js} +3 -3
  2. package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-DgfCYDmF.js.map} +1 -1
  3. package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CCrruunW.js} +2 -2
  4. package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-CCrruunW.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-C_EwDSi1.js} +9 -13
  6. package/dist/RouterCollectionsStudioView-C_EwDSi1.js.map +1 -0
  7. package/dist/collection_editor/serializable_types.d.ts +1 -0
  8. package/dist/collection_editor_ui.js +4 -4
  9. package/dist/components/AdditionalFieldValue.d.ts +25 -0
  10. package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
  11. package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
  12. package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
  13. package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +92 -0
  14. package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
  15. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
  16. package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
  17. package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
  18. package/dist/components/DetailViewBinding.d.ts +15 -0
  19. package/dist/components/DrawerNavigationGroup.d.ts +9 -6
  20. package/dist/components/DrawerNavigationItem.d.ts +16 -6
  21. package/dist/components/EditViewBinding.d.ts +30 -1
  22. package/dist/components/EntityDisplayHeader.d.ts +53 -0
  23. package/dist/components/EntityIdentityBar.d.ts +43 -1
  24. package/dist/components/EntityViewBinding.d.ts +60 -6
  25. package/dist/components/RebaseLayout.d.ts +1 -1
  26. package/dist/components/RebaseShell.d.ts +1 -1
  27. package/dist/components/RelationSelector.d.ts +8 -1
  28. package/dist/components/UserSelector.d.ts +8 -1
  29. package/dist/components/app/Scaffold.d.ts +9 -3
  30. package/dist/components/index.d.ts +2 -0
  31. package/dist/{export-J79rMoNj.js → export-CV-qQfxK.js} +2 -2
  32. package/dist/{export-J79rMoNj.js.map → export-CV-qQfxK.js.map} +1 -1
  33. package/dist/form/components/FieldBlock.d.ts +58 -4
  34. package/dist/form/components/FormRail.d.ts +13 -0
  35. package/dist/form/components/FormSections.d.ts +15 -1
  36. package/dist/form/components/useRailVisible.d.ts +5 -4
  37. package/dist/form/form_utils.d.ts +38 -1
  38. package/dist/{history-D7L_JlLO.js → history-By7QT3V0.js} +4 -4
  39. package/dist/{history-D7L_JlLO.js.map → history-By7QT3V0.js.map} +1 -1
  40. package/dist/hooks/useEntityDisplay.d.ts +102 -0
  41. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
  42. package/dist/{import-BvYl--U4.js → import-CYbvYa_x.js} +3 -3
  43. package/dist/{import-BvYl--U4.js.map → import-CYbvYa_x.js.map} +1 -1
  44. package/dist/index.d.ts +2 -2
  45. package/dist/index.js +152 -57
  46. package/dist/index.js.map +1 -1
  47. package/dist/preview/compact_text.d.ts +23 -0
  48. package/dist/preview/components/BooleanPreview.d.ts +3 -1
  49. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  50. package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
  51. package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
  53. package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
  54. package/dist/routes/RebaseRoute.d.ts +1 -1
  55. package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
  56. package/dist/util/number_format.d.ts +14 -0
  57. package/dist/util/previews.d.ts +9 -2
  58. package/dist/util/view_constants.d.ts +24 -0
  59. package/dist/util/view_mode.d.ts +18 -0
  60. package/dist/{util-GiwPgxnL.js → util-B8hurkxt.js} +3569 -1692
  61. package/dist/util-B8hurkxt.js.map +1 -0
  62. package/package.json +9 -9
  63. package/src/collection_editor/serializable_types.ts +1 -0
  64. package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
  65. package/src/components/AdditionalFieldValue.tsx +68 -0
  66. package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
  67. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
  68. package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
  69. package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
  70. package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
  71. package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
  72. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +822 -354
  73. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
  74. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +51 -12
  75. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +51 -39
  76. package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
  77. package/src/components/CollectionViewBinding/SearchHighlight.tsx +285 -0
  78. package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
  79. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
  80. package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
  81. package/src/components/CollectionViewBinding/SplitListView.tsx +47 -7
  82. package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
  83. package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
  84. package/src/components/CollectionViewBinding/utils.ts +7 -4
  85. package/src/components/DefaultDrawer.tsx +19 -23
  86. package/src/components/DetailViewBinding.tsx +109 -62
  87. package/src/components/DrawerNavigationGroup.tsx +16 -39
  88. package/src/components/DrawerNavigationItem.tsx +44 -16
  89. package/src/components/EditViewBinding.tsx +233 -50
  90. package/src/components/EntityDisplayHeader.tsx +170 -0
  91. package/src/components/EntityIdentityBar.tsx +163 -23
  92. package/src/components/EntityInspector.tsx +24 -6
  93. package/src/components/EntityPreviewBinding.tsx +58 -18
  94. package/src/components/EntityViewBinding.tsx +344 -41
  95. package/src/components/InlineEntityPreview.tsx +10 -10
  96. package/src/components/RebaseLayout.tsx +2 -2
  97. package/src/components/RebaseShell.tsx +2 -2
  98. package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
  99. package/src/components/RelationSelector.tsx +23 -9
  100. package/src/components/SideDialogs.tsx +15 -3
  101. package/src/components/SidePanelBinding.tsx +122 -66
  102. package/src/components/UserSelector.tsx +20 -8
  103. package/src/components/app/Scaffold.tsx +112 -10
  104. package/src/components/history/LastEditedByIndicator.tsx +7 -12
  105. package/src/components/index.ts +3 -0
  106. package/src/data_import/utils/data.ts +9 -1
  107. package/src/form/EntityForm.tsx +74 -53
  108. package/src/form/EntityFormBinding.tsx +24 -11
  109. package/src/form/PropertyFieldBinding.tsx +2 -2
  110. package/src/form/components/FieldBlock.tsx +80 -10
  111. package/src/form/components/FormRail.tsx +19 -2
  112. package/src/form/components/FormSections.tsx +32 -5
  113. package/src/form/components/StorageUploadProgress.tsx +9 -3
  114. package/src/form/components/useRailVisible.tsx +5 -4
  115. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
  116. package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
  117. package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
  118. package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
  119. package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
  120. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
  121. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
  122. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +21 -2
  123. package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
  124. package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
  125. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  126. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
  127. package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
  128. package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
  129. package/src/form/form_utils.ts +78 -0
  130. package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
  131. package/src/hooks/useEntityDisplay.tsx +262 -0
  132. package/src/hooks/useEntityDisplayTitle.tsx +27 -38
  133. package/src/index.ts +9 -1
  134. package/src/preview/PropertyPreview.tsx +25 -5
  135. package/src/preview/compact_text.ts +68 -0
  136. package/src/preview/components/BooleanPreview.tsx +5 -2
  137. package/src/preview/components/StorageThumbnail.tsx +5 -2
  138. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
  139. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
  140. package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
  141. package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
  142. package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
  143. package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
  144. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
  145. package/src/routes/RebaseRoute.tsx +62 -4
  146. package/src/types/components/PropertyPreviewProps.tsx +21 -0
  147. package/src/util/navigation_utils.ts +4 -2
  148. package/src/util/number_format.ts +46 -0
  149. package/src/util/previews.ts +8 -55
  150. package/src/util/property_utils.tsx +6 -1
  151. package/src/util/view_constants.ts +27 -0
  152. package/src/util/view_mode.ts +36 -0
  153. package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
  154. package/dist/util-GiwPgxnL.js.map +0 -1
@@ -10,11 +10,11 @@ import { CollectionViewBinding } from "./CollectionViewBinding/CollectionViewBin
10
10
  import { EntityViewBinding } from "./EntityViewBinding";
11
11
  import { EntityIdentityBar } from "./EntityIdentityBar";
12
12
  import { SplitListCloseButton } from "./CollectionViewBinding/SplitListCloseButton";
13
+ import { SplitListShowButton } from "./CollectionViewBinding/SplitListShowButton";
13
14
  import { EntityInspector, InspectorTab } from "./EntityInspector";
14
15
  import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
15
16
  import {
16
17
  Alert,
17
- ArrowLeftIcon,
18
18
  Button,
19
19
  CenteredView,
20
20
  cls,
@@ -34,6 +34,7 @@ import { ErrorBoundary } from "@rebasepro/ui";
34
34
  import { ErrorView, createFormexStub, usePermissions, useTranslation, getIcon } from "@rebasepro/app";
35
35
 
36
36
  import { removeInitialAndTrailingSlashes, resolveDefaultSelectedView } from "@rebasepro/app";
37
+ import { withViewMode } from "../util/view_mode";
37
38
  import { resolvedSelectedEntityView } from "../util/resolutions";
38
39
  import {
39
40
  useCustomizationController,
@@ -45,18 +46,25 @@ import {
45
46
  import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
46
47
  import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
47
48
  import { useNavigate } from "react-router";
48
- import { getValueInPath } from "@rebasepro/utils";
49
- import { getEntityTitlePropertyKeyForEntity, isUserSelectProperty, resolveTitleToString } from "../util/previews";
50
- import { getUserLabel, useResolvedUser } from "../hooks/useResolvedUsers";
49
+ import { useEntityDisplayTitle } from "../hooks/useEntityDisplayTitle";
51
50
 
52
51
 
53
- import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/view_constants";
52
+ import {
53
+ MAIN_TAB_VALUE,
54
+ JSON_TAB_VALUE,
55
+ HISTORY_TAB_VALUE,
56
+ ENTITY_VIEW_TAB_BAR_CLASS,
57
+ ENTITY_VIEW_TAB_CLASS,
58
+ ENTITY_VIEW_TAB_LABEL_CLASS
59
+ } from "../util/view_constants";
54
60
 
55
61
  export type BarActionsParams = {
56
62
  values: object,
57
63
  status: EntityStatus,
58
64
  path: string,
59
65
  entityId?: string | number;
66
+ /** Always false here — this view reads a record, it does not edit one. */
67
+ dirty: boolean;
60
68
  };
61
69
 
62
70
  export type OnTabChangeParams<M extends Record<string, unknown>> = {
@@ -75,6 +83,13 @@ export interface DetailViewBindingProps<M extends Record<string, unknown> = Reco
75
83
  parentEntityIds: string[];
76
84
  onTabChange?: (props: OnTabChangeParams<M>) => void;
77
85
  onEditClick?: () => void;
86
+ /**
87
+ * Dismiss this record — the ✕ at the identity bar's trailing edge.
88
+ *
89
+ * Set by the layouts that own the record's place on screen but are not side
90
+ * dialogs: the split, where dismissing is navigating back to the collection.
91
+ */
92
+ onCloseRequest?: () => void;
78
93
  layout?: "side_panel" | "full_screen" | "split" | "dialog";
79
94
  barActions?: (params: BarActionsParams) => React.ReactNode;
80
95
  /**
@@ -82,6 +97,12 @@ export interface DetailViewBindingProps<M extends Record<string, unknown> = Reco
82
97
  * of the breadcrumb — the side panel's close button.
83
98
  */
84
99
  barActionsStart?: React.ReactNode;
100
+ /**
101
+ * Full screen only: bring the list back beside this record. Set when the
102
+ * record was opened out of a split collection, so folding the list away is
103
+ * reversible; it takes the place of the back arrow when present.
104
+ */
105
+ onShowList?: () => void;
85
106
  }
86
107
 
87
108
  export function DetailViewBinding<M extends Record<string, unknown>>({
@@ -137,11 +158,13 @@ function DetailViewBindingInner<M extends Record<string, unknown>>({
137
158
  parentEntityIds,
138
159
  onTabChange,
139
160
  onEditClick,
161
+ onCloseRequest,
140
162
  entity,
141
163
  dataLoading,
142
164
  layout = "full_screen",
143
165
  barActions,
144
- barActionsStart
166
+ barActionsStart,
167
+ onShowList
145
168
  }: DetailViewBindingProps<M> & {
146
169
  entity?: Entity<M>,
147
170
  dataLoading: boolean,
@@ -189,7 +212,12 @@ entityId }
189
212
  }
190
213
  }, [selectedTabProp, defaultSelectedView]);
191
214
 
192
- const childViews = getAdminEntityChildViews(collection).filter(v => !v.collection.hideFromNavigation);
215
+ // `hideFromEntityViews`, not `hideFromNavigation`: the latter governs the
216
+ // drawer. Filtering tabs on it defeated the introspection generator, which
217
+ // sets `hideFromNavigation` on owned-child tables precisely *because* "every
218
+ // inbound foreign key renders as a tab on the parent" — so the rows it
219
+ // deliberately kept out of the drawer became reachable from nowhere.
220
+ const childViews = getAdminEntityChildViews(collection).filter(v => !v.collection.hideFromEntityViews);
193
221
  const subcollections = childViews.map(v => v.collection);
194
222
  const subcollectionsCount = subcollections?.length ?? 0;
195
223
  const customViews = collection.entityViews ?? [];
@@ -283,18 +311,13 @@ entityId }
283
311
  const formViewConfig = (collection as AdminCollection<M> & { formView?: FormViewConfig<M> }).formView;
284
312
  const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
285
313
 
286
- // Title resolution
287
- const titlePropertyKey = getEntityTitlePropertyKeyForEntity(collection, usedEntity?.values, usedEntity?.id);
288
- const rawTitle = usedEntity?.values && titlePropertyKey ? getValueInPath(usedEntity.values, titlePropertyKey) : undefined;
289
- // A user picker stores an id: resolve it to the person, like a relation.
290
- const titleUser = useResolvedUser(isUserSelectProperty(collection, titlePropertyKey) && typeof rawTitle === "string"
291
- ? rawTitle
292
- : undefined);
293
- const title = titleUser
294
- ? getUserLabel(titleUser)
295
- : (rawTitle !== undefined && rawTitle !== null
296
- ? resolveTitleToString(rawTitle)
297
- : (collection.singularName ?? collection.name));
314
+ // The heading, resolved the one way — this used to be a copy of
315
+ // `useEntityDisplayTitle`'s body, and the two had already drifted.
316
+ const title = useEntityDisplayTitle({
317
+ collection,
318
+ entity: usedEntity,
319
+ status: "existing"
320
+ });
298
321
 
299
322
  // Non-action custom views
300
323
  const nonActionCustomViews = useMemo(() =>
@@ -380,20 +403,32 @@ entityId }
380
403
  }, [onTabChange, path, entityId, collection]);
381
404
 
382
405
  const propertyDetailView = () => {
383
- // formView.Builder replaces the default property display
406
+ // formView.Builder replaces the default property display. It gets the
407
+ // centred column the record used to be given here; the default display
408
+ // now lays itself out, rail included.
384
409
  if (FormViewBuilder && usedEntity) {
385
- return <ErrorBoundary>
386
- <Suspense fallback={<CircularProgressCenter />}>
387
- <FormViewBuilder
388
- collection={collection}
389
- parentCollectionSlugs={parentCollectionSlugs}
390
- parentEntityIds={parentEntityIds}
391
- entity={usedEntity}
392
- modifiedValues={usedEntity?.values}
393
- formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
394
- />
395
- </Suspense>
396
- </ErrorBoundary>;
410
+ return <div className={"@container/form flex-1 min-w-0 overflow-y-auto flex justify-center items-start"}>
411
+ <div className={cls(
412
+ // Same widths as the default form (see EntityForm).
413
+ "w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col",
414
+ layout === "dialog"
415
+ ? "pt-5 pb-12 px-6 sm:px-8"
416
+ : "pt-6 pb-16 px-5 sm:px-8"
417
+ )}>
418
+ <ErrorBoundary>
419
+ <Suspense fallback={<CircularProgressCenter />}>
420
+ <FormViewBuilder
421
+ collection={collection}
422
+ parentCollectionSlugs={parentCollectionSlugs}
423
+ parentEntityIds={parentEntityIds}
424
+ entity={usedEntity}
425
+ modifiedValues={usedEntity?.values}
426
+ formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
427
+ />
428
+ </Suspense>
429
+ </ErrorBoundary>
430
+ </div>
431
+ </div>;
397
432
  }
398
433
 
399
434
  return (
@@ -402,6 +437,9 @@ entityId }
402
437
  entity={usedEntity}
403
438
  collection={collection}
404
439
  path={path}
440
+ asPage
441
+ openEntityMode={layout}
442
+ formContext={readOnlyFormContext}
405
443
  />}
406
444
  </>
407
445
  );
@@ -412,7 +450,7 @@ entityId }
412
450
  const icon = getIcon(subcollection.icon, undefined, undefined, "smallest");
413
451
  return (
414
452
  <Tab
415
- className="text-sm min-w-[90px]"
453
+ className={cls(ENTITY_VIEW_TAB_CLASS, ENTITY_VIEW_TAB_LABEL_CLASS)}
416
454
  value={subcollection.slug}
417
455
  key={`entity_detail_collection_tab_${subcollection.name}`}>
418
456
  <span className="flex items-center gap-1.5">
@@ -428,7 +466,7 @@ entityId }
428
466
  const icon = getIcon(view.icon, undefined, undefined, "smallest");
429
467
  return (
430
468
  <Tab
431
- className={!view.tabComponent ? "text-sm min-w-[90px]" : undefined}
469
+ className={cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && ENTITY_VIEW_TAB_LABEL_CLASS)}
432
470
  value={view.key}
433
471
  key={`entity_detail_collection_tab_${view.name}`}>
434
472
  {view.tabComponent ?? (
@@ -445,7 +483,7 @@ entityId }
445
483
  const icon = getIcon(view.icon, undefined, undefined, "smallest");
446
484
  return (
447
485
  <Tab
448
- className={!view.tabComponent ? "text-sm min-w-[90px]" : undefined}
486
+ className={cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && ENTITY_VIEW_TAB_LABEL_CLASS)}
449
487
  value={view.key}
450
488
  key={`entity_detail_collection_tab_${view.name}`}>
451
489
  {view.tabComponent ?? (
@@ -458,24 +496,27 @@ entityId }
458
496
  );
459
497
  });
460
498
 
499
+ // The breadcrumb's destination, and the back arrow's when it renders. Both
500
+ // keep the view mode: a collection reached from one of its own records must
501
+ // come back as the user left it.
502
+ const collectionUrl = withViewMode(urlController.buildUrlCollectionPath(path));
503
+
461
504
  // Full screen is the one layout with no way out of its own: a side panel and a
462
505
  // dialog can be dismissed, and a split keeps the list beside it, but opening an
463
506
  // entity full screen replaces the collection entirely and left browser Back as
464
507
  // the only route back to it — which is not an affordance the page shows, and is
465
508
  // wrong anyway once the user has moved between tabs inside the entity.
466
- const backToCollectionButton = layout === "full_screen" ? (
467
- <Tooltip title={t("back")}>
468
- <IconButton
469
- size="small"
470
- aria-label={t("back")}
471
- onClick={() => navigate(urlController.buildUrlCollectionPath(path))}
472
- >
473
- <ArrowLeftIcon size={iconSize.smallest} />
474
- </IconButton>
475
- </Tooltip>
476
- ) : null;
477
-
478
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
509
+ //
510
+ // Unless the list can be unfolded back beside the record: that chevron
511
+ // leads to the same collection while keeping the record open, so it stands
512
+ // alone rather than beside an arrow going almost the same place.
513
+ //
514
+ // It goes to the bar's leading edge — where a back control reads as back,
515
+ // and where the edit view has always put the same arrow — rather than to
516
+ // the trailing edge among the record's own actions.
517
+ const backToCollection = layout === "full_screen" && !onShowList
518
+ ? () => navigate(collectionUrl)
519
+ : undefined;
479
520
 
480
521
  const fullScreenButton = !barActions && // Not in split: the list panel beside it carries the close control,
481
522
  // and an expand button there competes with it.
@@ -508,59 +549,64 @@ entityId }
508
549
  // Main content view. The title, the `path/id` chip and the empty band under
509
550
  // them have moved into the identity bar, and the `w-80 2xl:w-96` rail that
510
551
  // held a single Edit button is gone — the bar holds it now.
552
+ //
553
+ // The scroll now lives on the record's own column rather than on this row,
554
+ // so the metadata rail beside it stays put while the record scrolls — the
555
+ // same arrangement the form uses.
511
556
  const mainView = <div
512
557
  className={cls(
513
- "flex-1 flex flex-row w-full overflow-y-auto justify-center",
558
+ "flex-1 flex flex-row w-full min-h-0",
514
559
  !mainViewVisible ? "hidden" : ""
515
560
  )}>
516
- <div className={cls(
517
- "w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
518
- layout === "dialog"
519
- ? "pt-5 pb-12 px-6 sm:px-8"
520
- : "pt-6 pb-16 px-5 sm:px-8"
521
- )}>
522
- {propertyDetailView()}
523
- </div>
561
+ {propertyDetailView()}
524
562
  </div>;
525
563
 
526
564
  let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
527
565
 
528
566
  <EntityIdentityBar
529
567
  collection={collection as AdminCollection}
568
+ collectionUrl={layout === "full_screen" || layout === "split" ? collectionUrl : undefined}
530
569
  title={title}
531
570
  entityId={entityId}
532
571
  status={"existing"}
533
572
  dirty={false}
534
573
  saving={false}
574
+ onBack={backToCollection}
575
+ onClose={onCloseRequest}
535
576
  onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
536
577
  onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
578
+ externalLink={usedEntity
579
+ ? customizationController?.entityLinkBuilder?.({ entity: usedEntity })
580
+ : undefined}
537
581
  leading={<>
538
582
  {barActionsStart}
539
583
  {/* Split view: closing the list is opening this record
540
584
  full screen, and the control for it belongs at this
541
- bar's leading edge. */}
585
+ bar's leading edge. Full screen carries its mirror. */}
542
586
  {layout === "split" && <SplitListCloseButton/>}
587
+ {layout === "full_screen" && onShowList && <SplitListShowButton onClick={onShowList}/>}
543
588
  </>}
544
589
  trailing={<>
545
590
  {editButton}
546
591
  {pluginActionsTop}
547
- {backToCollectionButton}
548
592
  {fullScreenButton}
549
593
  {barActions?.({
550
594
  path,
551
595
  entityId,
552
596
  values: usedEntity?.values ?? {},
553
- status: "existing"
597
+ status: "existing",
598
+ dirty: false
554
599
  })}
555
600
  </>}
556
601
  />
557
602
 
558
603
  {hasAdditionalViews && <div className={cls(
559
604
  "h-10 shrink-0 flex items-stretch border-b px-2 min-w-0",
605
+ ENTITY_VIEW_TAB_BAR_CLASS,
560
606
  defaultBorderMixin
561
607
  )}>
562
608
  <Tabs
563
- className={"!w-full"}
609
+ className={"!w-full items-stretch"}
564
610
  innerClassName={"h-full"}
565
611
  variant={"boxy"}
566
612
  value={activeTab}
@@ -568,7 +614,8 @@ entityId }
568
614
  onSideTabClick(value);
569
615
  }}>
570
616
 
571
- <Tab value={MAIN_TAB_VALUE}>
617
+ <Tab value={MAIN_TAB_VALUE}
618
+ className={cls(ENTITY_VIEW_TAB_CLASS, ENTITY_VIEW_TAB_LABEL_CLASS)}>
572
619
  <span className="flex items-center gap-1.5">
573
620
  {getIcon(collection.icon, undefined, undefined, "smallest")}
574
621
  {collection.singularName ?? collection.name}
@@ -28,11 +28,12 @@ export interface DrawerNavigationGroupProps {
28
28
  */
29
29
  drawerOpen: boolean;
30
30
  /**
31
- * Whether tooltips should be shown (drawer closed + hovered)
31
+ * @deprecated No longer read. Entry tooltips are uncontrolled — they follow the
32
+ * pointer and focus, and are suppressed wherever the label is already visible.
32
33
  */
33
- tooltipsOpen: boolean;
34
+ tooltipsOpen?: boolean;
34
35
  /**
35
- * Whether admin menu is open (used to control tooltip visibility)
36
+ * @deprecated No longer read. See {@link tooltipsOpen}.
36
37
  */
37
38
  adminMenuOpen?: boolean;
38
39
  /**
@@ -50,8 +51,10 @@ export interface DrawerNavigationGroupProps {
50
51
  /**
51
52
  * Lucide icon name for the group header, from `NavigationGroupMapping.icon`.
52
53
  *
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.
54
+ * It decorates the header and nothing else: the entries below are untouched and
55
+ * keep their own icons. An app that wants the categorised look — rows giving up
56
+ * their icons to indent under the group — builds it in its own drawer, by
57
+ * overriding `Shell.DrawerNavigationItem` and passing `indented`.
55
58
  */
56
59
  icon?: string;
57
60
  }
@@ -66,8 +69,6 @@ export function DrawerNavigationGroup({
66
69
  collapsed,
67
70
  onToggleCollapsed,
68
71
  drawerOpen,
69
- tooltipsOpen,
70
- adminMenuOpen,
71
72
  headerActions,
72
73
  onItemClick,
73
74
  hideHeader,
@@ -76,17 +77,13 @@ export function DrawerNavigationGroup({
76
77
  const { t } = useTranslation();
77
78
  const ResolvedDrawerNavigationItem = useComponentOverride("Shell.DrawerNavigationItem", DrawerNavigationItem);
78
79
 
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.
80
+ // The icon decorates the header, and stops there. It used to also strip the
81
+ // entries of theirs and indent them, which made a per-app styling choice into
82
+ // framework behaviour: every project that labelled a group with an icon lost the
83
+ // icons on its rows, with nothing to turn it off. An app that wants that look
84
+ // now opts into it in its own drawer — see `indented` on
85
+ // {@link DrawerNavigationItem}.
87
86
  const groupIcon = !hideHeader && drawerOpen ? getIcon(icon, undefined, undefined, "small") : undefined;
88
- const categorised = Boolean(groupIcon);
89
- const indentEntries = categorised;
90
87
  return (
91
88
  <div
92
89
  className={"my-2 mx-2 flex flex-col"}
@@ -103,10 +100,7 @@ export function DrawerNavigationGroup({
103
100
  <ChevronDownIcon
104
101
  size={iconSize.small}
105
102
  className={cls(
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",
103
+ "text-surface-400 dark:text-surface-400 transition-transform duration-200 mr-1",
110
104
  collapsed ? "-rotate-90" : "rotate-0"
111
105
  )}
112
106
  />
@@ -118,21 +112,7 @@ export function DrawerNavigationGroup({
118
112
  <Typography
119
113
  variant={"caption"}
120
114
  color={"secondary"}
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
- )}
115
+ className="font-semibold text-[11px] uppercase tracking-wider flex-grow line-clamp-1 text-surface-400 dark:text-surface-400"
136
116
  >
137
117
  {(group || t("views_group"))}
138
118
  </Typography>
@@ -158,13 +138,10 @@ export function DrawerNavigationGroup({
158
138
  <ResolvedDrawerNavigationItem
159
139
  key={entry.id}
160
140
  icon={<IconForView collectionOrView={entry.collection ?? entry.view} size={"small"}/>}
161
- tooltipsOpen={!collapsed && tooltipsOpen}
162
- adminMenuOpen={adminMenuOpen}
163
141
  drawerOpen={drawerOpen}
164
142
  onClick={() => onItemClick?.(entry)}
165
143
  url={entry.url}
166
144
  name={entry.name}
167
- indented={indentEntries}
168
145
  />
169
146
  ))}
170
147
  </div>
@@ -3,30 +3,39 @@ import React from "react";
3
3
  import { NavLink } from "react-router";
4
4
  import { cls, Tooltip } from "@rebasepro/ui";
5
5
 
6
- export function DrawerNavigationItem({
7
- name,
8
- icon,
9
- drawerOpen,
10
- adminMenuOpen,
11
- tooltipsOpen,
12
- url,
13
- onClick,
14
- indented = false
15
- }: {
6
+ /**
7
+ * Props of a drawer navigation row, named so an app overriding
8
+ * `Shell.DrawerNavigationItem` can type its wrapper against them.
9
+ */
10
+ export type DrawerNavigationItemProps = {
16
11
  icon: React.ReactElement,
17
12
  name: string,
18
- tooltipsOpen: boolean,
13
+ /** @deprecated No longer read: the tooltip is uncontrolled. */
14
+ tooltipsOpen?: boolean,
19
15
  drawerOpen: boolean,
16
+ /** @deprecated No longer read: the tooltip is uncontrolled. */
20
17
  adminMenuOpen?: boolean,
21
18
  url: string,
22
19
  onClick?: () => void,
23
20
  /**
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.
21
+ * Drop the row's icon and indent the label into the space it occupied, so the
22
+ * row reads as a child of the group header above it.
23
+ *
24
+ * Nothing in the framework sets this: the stock drawer always passes `false`,
25
+ * and an app that wants the categorised look opts into it by overriding
26
+ * `Shell.DrawerNavigationItem` and deciding for itself which rows get it.
27
27
  */
28
28
  indented?: boolean,
29
- }) {
29
+ };
30
+
31
+ export function DrawerNavigationItem({
32
+ name,
33
+ icon,
34
+ drawerOpen,
35
+ url,
36
+ onClick,
37
+ indented = false
38
+ }: DrawerNavigationItemProps) {
30
39
 
31
40
  // Indented rows give up the icon rather than keeping it *and* indenting: a
32
41
  // per-row icon repeated down a group is what flattens the hierarchy, since
@@ -40,6 +49,15 @@ export function DrawerNavigationItem({
40
49
  {icon}
41
50
  </div>;
42
51
 
52
+ const [wantsTooltip, setWantsTooltip] = React.useState(false);
53
+
54
+ // Radix will not report a close it never noticed: while the tooltip is masked
55
+ // the pointer can leave the row without `onOpenChange(false)` ever firing. So
56
+ // the stale `true` is dropped on the way *into* the masked state — otherwise
57
+ // it surfaces the moment the mask lifts, as a tooltip hanging over a rail the
58
+ // pointer left long ago.
59
+ if (drawerOpen && wantsTooltip) setWantsTooltip(false);
60
+
43
61
  const listItem = <div>
44
62
  <NavLink
45
63
  onClick={onClick}
@@ -70,8 +88,18 @@ export function DrawerNavigationItem({
70
88
  </NavLink>
71
89
  </div>;
72
90
 
91
+ // Per row, and always controlled. The old shared flag could only say "every
92
+ // tooltip" or "none", and the state it needed was unreachable, so no entry
93
+ // tooltip ever opened — leaving a bare rail of unlabelled icons. Radix decides
94
+ // when this one wants to be open, from the pointer and from focus; the drawer
95
+ // only masks it once the label says the same thing.
96
+ //
97
+ // Masked rather than withheld: dropping `open`, or the title, would move the
98
+ // tooltip between controlled and uncontrolled as the drawer opens, which
99
+ // strands the old state — a tooltip left up over a pointer nowhere near it.
73
100
  return <Tooltip
74
- open={drawerOpen || adminMenuOpen ? false : tooltipsOpen}
101
+ open={!drawerOpen && wantsTooltip}
102
+ onOpenChange={setWantsTooltip}
75
103
  side="right"
76
104
  title={name}>
77
105
  {listItem}