@rebasepro/admin 0.12.1-canary.gf5f1d39 → 0.13.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 (178) hide show
  1. package/README.md +1 -1
  2. package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
  3. package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
  4. package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
  5. package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
  7. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
  8. package/dist/collection_editor/serializable_types.d.ts +4 -1
  9. package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
  10. package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
  11. package/dist/collection_editor_ui.js +5 -5
  12. package/dist/collection_editor_ui.js.map +1 -1
  13. package/dist/components/CollectionTableBinding/column_utils.d.ts +17 -0
  14. package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
  15. package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
  16. package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
  17. package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
  18. package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
  19. package/dist/components/DetailViewBinding.d.ts +5 -0
  20. package/dist/components/EditFormActions.d.ts +12 -1
  21. package/dist/components/EditViewBinding.d.ts +6 -1
  22. package/dist/components/EntityIdentityBar.d.ts +53 -0
  23. package/dist/components/EntityInspector.d.ts +35 -0
  24. package/dist/components/EntityPreviewNesting.d.ts +25 -0
  25. package/dist/components/EntityViewBinding.d.ts +9 -0
  26. package/dist/components/InlineEntityPreview.d.ts +47 -0
  27. package/dist/components/history/EntityHistoryView.d.ts +4 -1
  28. package/dist/components/index.d.ts +2 -0
  29. package/dist/editor.js +18 -10
  30. package/dist/editor.js.map +1 -1
  31. package/dist/{export-BzIpUdSK.js → export-J79rMoNj.js} +2 -2
  32. package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.js.map} +1 -1
  33. package/dist/form/EntityForm.d.ts +1 -1
  34. package/dist/form/EntityFormActions.d.ts +11 -1
  35. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  36. package/dist/form/components/FieldBlock.d.ts +30 -0
  37. package/dist/form/components/FormRail.d.ts +18 -0
  38. package/dist/form/components/FormSections.d.ts +16 -0
  39. package/dist/form/components/index.d.ts +3 -2
  40. package/dist/form/components/useRailVisible.d.ts +24 -0
  41. package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
  42. package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
  43. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
  44. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  45. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  46. package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
  47. package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
  48. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  49. package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
  50. package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
  51. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
  52. package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
  53. package/dist/{history-g9688IaK.js → history-D7L_JlLO.js} +35 -33
  54. package/dist/history-D7L_JlLO.js.map +1 -0
  55. package/dist/hooks/navigation/utils.d.ts +16 -0
  56. package/dist/hooks/useBuildSidePanel.d.ts +14 -0
  57. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
  58. package/dist/hooks/useHistory.d.ts +7 -0
  59. package/dist/hooks/useRecordActions.d.ts +17 -0
  60. package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
  61. package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.js.map} +1 -1
  62. package/dist/index.js +106 -35
  63. package/dist/index.js.map +1 -1
  64. package/dist/markdown-CklalUUk.js.map +1 -1
  65. package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
  66. package/dist/preview/index.d.ts +1 -0
  67. package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
  68. package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
  69. package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
  70. package/dist/types/components/EntityFormProps.d.ts +0 -5
  71. package/dist/types/fields.d.ts +24 -0
  72. package/dist/{util-B5fJm1FA.js → util-GiwPgxnL.js} +2106 -1117
  73. package/dist/util-GiwPgxnL.js.map +1 -0
  74. package/package.json +32 -34
  75. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  76. package/src/collection_editor/serializable_types.ts +4 -3
  77. package/src/collection_editor/serializable_utils.ts +51 -17
  78. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
  79. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
  80. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
  81. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
  82. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
  83. package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
  84. package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
  85. package/src/collection_editor/validateCollectionJson.ts +3 -3
  86. package/src/components/AdminModeSyncer.tsx +1 -1
  87. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
  88. package/src/components/CollectionTableBinding/column_utils.tsx +59 -2
  89. package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
  90. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
  91. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
  92. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +9 -2
  93. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
  94. package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
  95. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
  96. package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
  97. package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
  98. package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
  99. package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
  100. package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
  101. package/src/components/DefaultAppBar.tsx +1 -1
  102. package/src/components/DefaultDrawer.tsx +1 -1
  103. package/src/components/DetailViewBinding.tsx +112 -148
  104. package/src/components/DrawerNavigationItem.tsx +1 -1
  105. package/src/components/EditFormActions.tsx +54 -129
  106. package/src/components/EditViewBinding.tsx +202 -99
  107. package/src/components/EntityIdentityBar.tsx +288 -0
  108. package/src/components/EntityInspector.tsx +137 -0
  109. package/src/components/EntityPreviewBinding.tsx +6 -2
  110. package/src/components/EntityPreviewNesting.tsx +50 -0
  111. package/src/components/EntityViewBinding.tsx +35 -32
  112. package/src/components/HomePage/FavouritesView.tsx +1 -1
  113. package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
  114. package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
  115. package/src/components/InlineEntityPreview.tsx +149 -0
  116. package/src/components/RebaseLayout.tsx +1 -1
  117. package/src/components/RebaseNavigation.tsx +45 -5
  118. package/src/components/RebaseRouteDefs.tsx +1 -1
  119. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  120. package/src/components/SidePanelBinding.tsx +19 -13
  121. package/src/components/UserSelector.tsx +12 -6
  122. package/src/components/common/default_entity_actions.tsx +4 -3
  123. package/src/components/history/EntityHistoryEntry.tsx +28 -7
  124. package/src/components/history/EntityHistoryView.tsx +29 -24
  125. package/src/components/history/LastEditedByIndicator.tsx +4 -3
  126. package/src/components/index.ts +2 -0
  127. package/src/data_import/utils/get_import_inference_type.ts +6 -0
  128. package/src/form/EntityForm.tsx +267 -179
  129. package/src/form/EntityFormActions.tsx +50 -99
  130. package/src/form/EntityFormBinding.tsx +4 -2
  131. package/src/form/PropertyFieldBinding.tsx +4 -0
  132. package/src/form/components/FieldBlock.tsx +119 -0
  133. package/src/form/components/FormRail.tsx +125 -0
  134. package/src/form/components/FormSections.tsx +118 -0
  135. package/src/form/components/index.tsx +3 -2
  136. package/src/form/components/useRailVisible.tsx +46 -0
  137. package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
  138. package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
  139. package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
  140. package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
  141. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
  142. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
  143. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
  144. package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
  145. package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
  146. package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
  147. package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
  148. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
  149. package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
  150. package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
  151. package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
  152. package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
  153. package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
  154. package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
  155. package/src/hooks/navigation/utils.ts +20 -0
  156. package/src/hooks/useBuildSideDialogsController.tsx +2 -2
  157. package/src/hooks/useBuildSidePanel.tsx +22 -61
  158. package/src/hooks/useEntityDisplayTitle.tsx +60 -0
  159. package/src/hooks/useHistory.ts +20 -10
  160. package/src/hooks/useRecordActions.tsx +69 -0
  161. package/src/hooks/useResolvedUsers.ts +10 -5
  162. package/src/preview/PropertyPreview.tsx +48 -22
  163. package/src/preview/components/InlineEntityListPreview.tsx +42 -0
  164. package/src/preview/components/ReferencePreview.tsx +34 -14
  165. package/src/preview/components/RelationPreview.tsx +34 -14
  166. package/src/preview/index.ts +1 -0
  167. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
  168. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
  169. package/src/routes/RebaseRoute.tsx +2 -2
  170. package/src/types/components/EntityFormActionsProps.tsx +0 -1
  171. package/src/types/components/EntityFormProps.tsx +0 -6
  172. package/src/types/fields.tsx +27 -0
  173. package/dist/form/components/FormEntry.d.ts +0 -6
  174. package/dist/form/components/FormLayout.d.ts +0 -5
  175. package/dist/history-g9688IaK.js.map +0 -1
  176. package/dist/util-B5fJm1FA.js.map +0 -1
  177. package/src/form/components/FormEntry.tsx +0 -22
  178. package/src/form/components/FormLayout.tsx +0 -16
@@ -18,6 +18,7 @@ import {
18
18
  HistoryIcon,
19
19
  IconButton,
20
20
  Maximize2Icon,
21
+ MenuItem,
21
22
  Skeleton,
22
23
  Tab,
23
24
  Tabs,
@@ -42,13 +43,18 @@ import { EntityFormBinding } from "../form";
42
43
  import type { EntityFormBindingProps } from "../form";
43
44
  import type { OnUpdateParams } from "../types/components/EntityFormProps";
44
45
  import { EditFormActions } from "./EditFormActions";
45
- import { JsonPreviewBinding } from "../components/JsonPreviewBinding";
46
- // Lazy-load history view only loaded when user clicks the HistoryIcon tab
47
- const EntityHistoryView = lazy(() => import("../components/history").then(m => ({ default: m.EntityHistoryView })));
46
+ import { EntityIdentityBar } from "./EntityIdentityBar";
47
+ import { SplitListCloseButton } from "./CollectionViewBinding/SplitListCloseButton";
48
+ import { useRecordActions } from "../hooks/useRecordActions";
49
+ import { useSideDialogContext } from "./SideDialogs";
50
+ import { useAdminContext } from "../hooks/useAdminContext";
51
+ import { EntityInspector, InspectorTab } from "./EntityInspector";
52
+ import { useEntityDisplayTitle } from "../hooks/useEntityDisplayTitle";
48
53
  import { createFormexStub, getEntityFromCache } from "@rebasepro/app";
49
54
  import { usePermissions } from "@rebasepro/app";
50
55
  import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
51
- import { useNavigate } from "react-router-dom";
56
+ import { withViewMode } from "../util/view_mode";
57
+ import { useNavigate } from "react-router";
52
58
 
53
59
  import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/view_constants";
54
60
  export { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE };
@@ -85,6 +91,11 @@ export interface EditViewBindingProps<M extends Record<string, unknown> = Record
85
91
  navigateBack?: () => void;
86
92
  layout?: "side_panel" | "full_screen" | "split" | "dialog";
87
93
  barActions?: (params: BarActionsParams) => React.ReactNode;
94
+ /**
95
+ * Controls the owning panel puts at the identity bar's leading edge, ahead
96
+ * of the breadcrumb — the side panel's close button.
97
+ */
98
+ barActionsStart?: React.ReactNode;
88
99
  formProps?: Partial<EntityFormBindingProps<M>>,
89
100
  /**
90
101
  * Pre-populate the form with these values when creating a new entity.
@@ -180,6 +191,7 @@ export function EditViewBindingInner<M extends Record<string, unknown>>({
180
191
  dataLoading,
181
192
  layout = "side_panel",
182
193
  barActions,
194
+ barActionsStart,
183
195
  status,
184
196
  setStatus,
185
197
  formProps,
@@ -198,6 +210,7 @@ export function EditViewBindingInner<M extends Record<string, unknown>>({
198
210
  const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
199
211
 
200
212
  const context = useRebaseContext();
213
+ const adminContext = useAdminContext();
201
214
  const urlController = useUrlController();
202
215
  const navigate = useNavigate();
203
216
 
@@ -265,22 +278,30 @@ parentEntityIds,
265
278
  const customViewsCount = customViews?.length ?? 0;
266
279
  const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
267
280
  const includeHistoryView = Boolean(collection.history);
268
- const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView || includeHistoryView;
281
+ // The inspector holds the developer tools; the tab strip holds destinations.
282
+ // A strip with only the record in it has nothing to choose between, so it
283
+ // does not render at all.
284
+ const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0;
269
285
 
270
286
  const {
271
287
  resolvedEntityViews
272
288
  } = resolvedSelectedEntityView(customViews, customizationController, undefined, canEdit);
273
289
 
290
+ // JSON and history are no longer tab values, but a bookmarked or in-flight
291
+ // URL may still name one. They resolve to the record, and open the
292
+ // inspector on that pane, so an old link keeps working.
293
+ const legacyInspectorTab = selectedTab === JSON_TAB_VALUE
294
+ ? "json"
295
+ : selectedTab === HISTORY_TAB_VALUE ? "history" : undefined;
296
+
274
297
  const validTabValues = useMemo(() => {
275
298
  const set = new Set<string>([
276
299
  MAIN_TAB_VALUE,
277
- ...(includeJsonView ? [JSON_TAB_VALUE] : []),
278
- ...(includeHistoryView ? [HISTORY_TAB_VALUE] : []),
279
300
  ...resolvedEntityViews.map(v => v.key),
280
301
  ...subcollections.map(s => s.slug)
281
302
  ]);
282
303
  return set;
283
- }, [includeJsonView, includeHistoryView, resolvedEntityViews, subcollections]);
304
+ }, [resolvedEntityViews, subcollections]);
284
305
 
285
306
  const activeTab = validTabValues.has(selectedTab) ? selectedTab : MAIN_TAB_VALUE;
286
307
 
@@ -289,7 +310,6 @@ parentEntityIds,
289
310
  selectedSecondaryForm
290
311
  } = resolvedSelectedEntityView(customViews, customizationController, activeTab, canEdit);
291
312
 
292
- const actionsAtTheBottom = layout === "side_panel" || layout === "dialog" || selectedEntityView?.includeActions === "bottom";
293
313
 
294
314
  const mainViewVisible = activeTab === MAIN_TAB_VALUE || Boolean(selectedSecondaryForm);
295
315
 
@@ -381,35 +401,76 @@ parentEntityIds,
381
401
 
382
402
  const globalLoading = (dataLoading && !usedEntity) || (canEdit === undefined && (status === "existing" || status === "copy"));
383
403
 
384
- // Only mount JSON view when its tab is selected (or was previously selected)
385
- const jsonTabMounted = mountedTabsRef.current.has(JSON_TAB_VALUE);
386
- const jsonView = (activeTab === JSON_TAB_VALUE || jsonTabMounted) ? <div
387
- className={cls("relative flex-1 h-full overflow-auto w-full",
388
- { "hidden": activeTab !== JSON_TAB_VALUE })}
389
- key={"json_view"}
390
- role="tabpanel">
391
- <ErrorBoundary>
392
- <JsonPreviewBinding
393
- values={formContext?.values ?? entity?.values ?? {}} />
394
- </ErrorBoundary>
395
- </div> : null;
404
+ /* ---- identity bar ---------------------------------------------------- */
396
405
 
397
- // Only mount history view when its tab is actually selected
398
- const historyView = includeHistoryView && activeTab === HISTORY_TAB_VALUE ? <div
399
- className={"relative flex-1 h-full overflow-auto w-full"}
400
- key={"history_view"}
401
- role="tabpanel">
402
- <ErrorBoundary>
403
- <Suspense fallback={<CircularProgressCenter />}>
404
- <EntityHistoryView
405
- collection={collection}
406
- entity={usedEntity}
407
- formContext={formContext as FormContext<Record<string, unknown>>}
408
- modifiedValues={formContext?.values ?? usedEntity?.values}
409
- />
410
- </Suspense>
411
- </ErrorBoundary>
412
- </div> : null;
406
+ const displayTitle = useEntityDisplayTitle({
407
+ collection,
408
+ values: (formContext?.values ?? usedEntity?.values) as Record<string, unknown> | undefined,
409
+ entityId,
410
+ status
411
+ });
412
+
413
+ const formex = formContext?.formex;
414
+ const hasFormErrors = Boolean(formex && Object.keys(formex.errors).length > 0 && formex.submitCount > 0);
415
+ // Same rule the footer used: an untouched existing record has nothing to save.
416
+ const saveDisabled = Boolean(
417
+ !formContext
418
+ || formContext.disabled
419
+ || formex?.isSubmitting
420
+ || (status === "existing" && !formex?.dirty)
421
+ );
422
+
423
+ const [inspectorTab, setInspectorTab] = useState<InspectorTab | null>(null);
424
+ // A save adds a revision, and the inspector can be open while it happens:
425
+ // Save lives in the identity bar, above the panel. Counting saves gives the
426
+ // history list something to react to.
427
+ const [savedCount, setSavedCount] = useState(0);
428
+
429
+ /* ---- record actions, in the bar's overflow menu ---------------------- */
430
+
431
+ const sideDialogContext = useSideDialogContext();
432
+ const canCloseAfterSave = layout === "side_panel" || layout === "dialog";
433
+
434
+ const recordActions = useRecordActions({
435
+ collection,
436
+ path,
437
+ entity: usedEntity
438
+ });
439
+
440
+ const recordActionItems = usedEntity && recordActions.length
441
+ ? recordActions.map((action, index) => {
442
+ const clickProps = {
443
+ view: "form" as const,
444
+ entity: usedEntity,
445
+ path,
446
+ collection,
447
+ context,
448
+ sidePanelController: adminContext.sidePanelController,
449
+ openEntityMode: layout,
450
+ navigateBack: navigateBack ?? (() => undefined),
451
+ formContext
452
+ };
453
+ const enabled = !action.isEnabled || action.isEnabled(clickProps as never);
454
+ return (
455
+ <MenuItem key={action.key ?? action.name ?? index}
456
+ disabled={!enabled}
457
+ onClick={() => action.onClick(clickProps as never)}>
458
+ {getIcon(action.icon, undefined, undefined, "smallest")}
459
+ {action.name}
460
+ </MenuItem>
461
+ );
462
+ })
463
+ : null;
464
+
465
+ // Plugin form actions (Autofill and friends) render inline in the bar; the
466
+ // form no longer has a footer to put them in.
467
+ const formPluginActions = useSlot("form.actions", formActionTopProps);
468
+
469
+ // A URL still naming the old `json`/`history` tab opens the inspector on
470
+ // that pane instead of 404-ing into the record with nothing shown.
471
+ useEffect(() => {
472
+ if (legacyInspectorTab) setInspectorTab(legacyInspectorTab);
473
+ }, [legacyInspectorTab]);
413
474
 
414
475
  const subCollectionsViews = childViews && childViews.map(({ collection: subcollection }) => {
415
476
  const subcollectionId = subcollection.slug;
@@ -520,7 +581,10 @@ parentEntityIds,
520
581
  entity={entity}
521
582
  initialDirtyValues={initialDirtyValues}
522
583
  openEntityMode={layout}
523
- forceActionsAtTheBottom={actionsAtTheBottom}
584
+ // Save and Discard live in the identity bar now, where they stay
585
+ // visible while the form scrolls. The form's own footer would be a
586
+ // second copy of them.
587
+ showDefaultActions={false}
524
588
  initialStatus={status}
525
589
  className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
526
590
  EntityFormActionsComponent={ResolvedFormActions as React.FC<typeof ResolvedFormActions extends React.ComponentType<infer P> ? P : never>}
@@ -540,6 +604,7 @@ parentEntityIds,
540
604
  formProps?.onFormContextReady?.(formContext);
541
605
  }}
542
606
  onSaved={(params) => {
607
+ setSavedCount(count => count + 1);
543
608
  const res = {
544
609
  ...params,
545
610
  selectedTab: MAIN_TAB_VALUE === activeTab ? undefined : activeTab
@@ -601,9 +666,9 @@ parentEntityIds,
601
666
  );
602
667
  });
603
668
 
604
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog";
605
-
606
- const fullScreenButton = !barActions && (layout === "side_panel" || layout === "split" || layout === "dialog") && entityId ? (
669
+ const fullScreenButton = !barActions && // Not in split: the list panel beside it carries the close control,
670
+ // and an expand button there competes with it.
671
+ (layout === "side_panel" || layout === "dialog") && entityId ? (
607
672
  <Tooltip title={"Open full screen"}>
608
673
  <IconButton
609
674
  size="small"
@@ -620,75 +685,113 @@ parentEntityIds,
620
685
 
621
686
  let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
622
687
 
623
- {shouldShowTopBar && <div
624
- 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)}>
688
+ <EntityIdentityBar
689
+ collection={collection as AdminCollection}
690
+ title={displayTitle}
691
+ entityId={status === "existing" ? entityId : undefined}
692
+ status={status}
693
+ dirty={Boolean(formContext?.formex?.dirty)}
694
+ saving={Boolean(formContext?.isSaving)}
695
+ hasErrors={hasFormErrors}
696
+ saveDisabled={!canEdit || saveDisabled}
697
+ // Full screen replaces the collection entirely, leaving browser Back
698
+ // as the only route to it. The detail view has carried this arrow
699
+ // for that reason; the edit view is reached the same way.
700
+ onBack={layout === "full_screen"
701
+ ? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path)))
702
+ : undefined}
703
+ onSave={canEdit && formContext ? () => {
704
+ sideDialogContext.setPendingClose?.(false);
705
+ formContext.submit();
706
+ } : undefined}
707
+ onSaveAndClose={canEdit && formContext && canCloseAfterSave ? () => {
708
+ sideDialogContext.setPendingClose?.(true);
709
+ formContext.submit();
710
+ } : undefined}
711
+ onDiscard={canEdit && formContext ? () => formContext.formex.resetForm() : undefined}
712
+ onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
713
+ onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
714
+ recordActions={recordActionItems}
715
+ pluginActions={formPluginActions}
716
+ leading={<>
717
+ {barActionsStart}
718
+ {/* Split view: closing the list is opening this record
719
+ full screen, and the control for it belongs at this
720
+ bar's leading edge. */}
721
+ {layout === "split" && <SplitListCloseButton/>}
722
+ </>}
723
+ trailing={<>
724
+ {pluginActionsTop}
725
+ {fullScreenButton}
726
+ {barActions?.({
727
+ path,
728
+ entityId,
729
+ values: formContext?.values ?? usedEntity?.values ?? {},
730
+ status
731
+ })}
732
+ </>}
733
+ />
625
734
 
626
- {fullScreenButton}
735
+ {/* Destinations only: the record is the page, so its tab leads rather
736
+ than sitting third behind two unlabelled developer tools. The row
737
+ disappears entirely when there is nowhere else to go. */}
738
+ {hasAdditionalViews && <div className={cls(
739
+ "h-10 shrink-0 flex items-stretch border-b px-2 min-w-0",
740
+ defaultBorderMixin
741
+ )}>
742
+ <Tabs
743
+ className={"!w-full"}
744
+ innerClassName={"h-full"}
745
+ variant={"boxy"}
746
+ value={activeTab}
747
+ onValueChange={(value) => {
748
+ onSideTabClick(value);
749
+ }}>
627
750
 
628
- {barActions?.({
629
- path,
630
- entityId,
631
- values: formContext?.values ?? usedEntity?.values ?? {},
632
- status
633
- })}
634
-
635
- {pluginActionsTop}
636
-
637
- {hasAdditionalViews && <div className={"flex-1 flex justify-end min-w-0 shrink-0"}>
638
- <Tabs
639
- className={"!w-fit max-w-full"}
640
- value={activeTab}
641
- onValueChange={(value) => {
642
- onSideTabClick(value);
643
- }}>
644
-
645
- {includeJsonView && <Tab
646
- disabled={!hasAdditionalViews}
647
- value={JSON_TAB_VALUE}
648
- className={"text-sm"}>
649
- <CodeIcon size={iconSize.smallest} />
650
- </Tab>}
651
-
652
- {includeHistoryView && <Tab
653
- disabled={!hasAdditionalViews}
654
- value={HISTORY_TAB_VALUE}
655
- className={"text-sm"}>
656
- <HistoryIcon size={iconSize.smallest} />
657
- </Tab>}
658
-
659
- <Tab
660
- disabled={!hasAdditionalViews}
661
- value={MAIN_TAB_VALUE}
662
- className={"text-sm min-w-[90px]"}>
663
- <span className="flex items-center gap-1.5">
664
- {getIcon(collection.icon, undefined, undefined, "smallest")}
665
- {collection.singularName ?? collection.name}
666
- </span>
667
- </Tab>
751
+ <Tab value={MAIN_TAB_VALUE}>
752
+ <span className="flex items-center gap-1.5">
753
+ {getIcon(collection.icon, undefined, undefined, "smallest")}
754
+ {collection.singularName ?? collection.name}
755
+ </span>
756
+ </Tab>
668
757
 
669
- {customViewTabsStart}
758
+ {customViewTabsStart}
670
759
 
671
- {customViewTabsEnd}
760
+ {customViewTabsEnd}
672
761
 
673
- {subcollectionTabs}
674
- </Tabs>
675
- </div>}
762
+ {subcollectionTabs}
763
+ </Tabs>
676
764
  </div>}
677
765
 
678
- {globalLoading
679
- ? <EntityFormSkeleton collection={collection} />
680
- : <>
681
- {entityReadOnlyView}
682
- {entityView}
683
- </>}
766
+ {/* The record and the inspector share the space below the bar rather
767
+ than stacking, so the form stays reachable while the panel is open.
768
+ `min-w-0` keeps a wide form from pushing the panel off the edge. */}
769
+ <div className={"flex-1 min-h-0 flex"}>
684
770
 
685
- {jsonView}
771
+ <div className={"flex-1 min-w-0 flex flex-col"}>
772
+ {globalLoading
773
+ ? <EntityFormSkeleton collection={collection} />
774
+ : <>
775
+ {entityReadOnlyView}
776
+ {entityView}
777
+ </>}
686
778
 
687
- {historyView}
779
+ {customViewsView}
688
780
 
689
- {customViewsView}
781
+ {subCollectionsViews}
782
+ </div>
690
783
 
691
- {subCollectionsViews}
784
+ <EntityInspector
785
+ tab={inspectorTab}
786
+ onTabChange={setInspectorTab}
787
+ onClose={() => setInspectorTab(null)}
788
+ collection={collection as AdminCollection}
789
+ entity={usedEntity as Entity<Record<string, unknown>> | undefined}
790
+ formContext={formContext as FormContext<Record<string, unknown>> | undefined}
791
+ values={formContext?.values ?? usedEntity?.values}
792
+ refreshToken={savedCount}
793
+ includeHistory={includeHistoryView}/>
794
+ </div>
692
795
 
693
796
  </div>;
694
797