@rebasepro/admin 0.12.0 → 0.12.1-canary.g009ed95

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 (172) hide show
  1. package/README.md +1 -1
  2. package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-B-iVE9y3.js} +75 -62
  3. package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-B-iVE9y3.js.map} +1 -1
  4. package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-DoLEH_bQ.js} +36 -16
  5. package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-DoLEH_bQ.js.map} +1 -1
  6. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-C3669Ezm.js} +5 -5
  7. package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-C3669Ezm.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/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
  14. package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
  15. package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
  16. package/dist/components/DetailViewBinding.d.ts +5 -0
  17. package/dist/components/EditFormActions.d.ts +12 -1
  18. package/dist/components/EditViewBinding.d.ts +6 -1
  19. package/dist/components/EntityIdentityBar.d.ts +53 -0
  20. package/dist/components/EntityInspector.d.ts +29 -0
  21. package/dist/components/EntityPreviewNesting.d.ts +25 -0
  22. package/dist/components/EntityViewBinding.d.ts +9 -0
  23. package/dist/components/InlineEntityPreview.d.ts +47 -0
  24. package/dist/components/index.d.ts +2 -0
  25. package/dist/editor.js +18 -10
  26. package/dist/editor.js.map +1 -1
  27. package/dist/{export-BzIpUdSK.js → export-B8DIn-u8.js} +2 -2
  28. package/dist/{export-BzIpUdSK.js.map → export-B8DIn-u8.js.map} +1 -1
  29. package/dist/form/EntityForm.d.ts +1 -1
  30. package/dist/form/EntityFormActions.d.ts +11 -1
  31. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  32. package/dist/form/components/FieldBlock.d.ts +30 -0
  33. package/dist/form/components/FormRail.d.ts +18 -0
  34. package/dist/form/components/FormSections.d.ts +16 -0
  35. package/dist/form/components/index.d.ts +3 -2
  36. package/dist/form/components/useRailVisible.d.ts +24 -0
  37. package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
  38. package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
  39. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
  40. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  41. package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
  42. package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
  43. package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
  44. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
  45. package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
  46. package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
  47. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
  48. package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
  49. package/dist/{history-g9688IaK.js → history-ClwDEtqN.js} +32 -31
  50. package/dist/history-ClwDEtqN.js.map +1 -0
  51. package/dist/hooks/navigation/utils.d.ts +16 -0
  52. package/dist/hooks/useBuildSidePanel.d.ts +14 -0
  53. package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
  54. package/dist/hooks/useRecordActions.d.ts +17 -0
  55. package/dist/{import-BjUG-cIY.js → import-Cwzf4RW5.js} +2 -2
  56. package/dist/{import-BjUG-cIY.js.map → import-Cwzf4RW5.js.map} +1 -1
  57. package/dist/index.js +106 -35
  58. package/dist/index.js.map +1 -1
  59. package/dist/markdown-CklalUUk.js.map +1 -1
  60. package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
  61. package/dist/preview/index.d.ts +1 -0
  62. package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
  63. package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
  64. package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
  65. package/dist/types/components/EntityFormProps.d.ts +0 -5
  66. package/dist/types/fields.d.ts +24 -0
  67. package/dist/{util-B5fJm1FA.js → util-DSh3HRzK.js} +1974 -1130
  68. package/dist/util-DSh3HRzK.js.map +1 -0
  69. package/package.json +32 -34
  70. package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
  71. package/src/collection_editor/serializable_types.ts +4 -3
  72. package/src/collection_editor/serializable_utils.ts +51 -17
  73. package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
  74. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
  75. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
  76. package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
  77. package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
  78. package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
  79. package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
  80. package/src/collection_editor/validateCollectionJson.ts +3 -3
  81. package/src/components/AdminModeSyncer.tsx +1 -1
  82. package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
  83. package/src/components/CollectionTableBinding/column_utils.tsx +26 -2
  84. package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
  85. package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
  86. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
  87. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +8 -2
  88. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +22 -11
  89. package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
  90. package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
  91. package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
  92. package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
  93. package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
  94. package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
  95. package/src/components/DefaultAppBar.tsx +1 -1
  96. package/src/components/DefaultDrawer.tsx +1 -1
  97. package/src/components/DetailViewBinding.tsx +100 -143
  98. package/src/components/DrawerNavigationItem.tsx +1 -1
  99. package/src/components/EditFormActions.tsx +54 -129
  100. package/src/components/EditViewBinding.tsx +182 -93
  101. package/src/components/EntityIdentityBar.tsx +288 -0
  102. package/src/components/EntityInspector.tsx +132 -0
  103. package/src/components/EntityPreviewBinding.tsx +6 -2
  104. package/src/components/EntityPreviewNesting.tsx +50 -0
  105. package/src/components/EntityViewBinding.tsx +35 -32
  106. package/src/components/HomePage/FavouritesView.tsx +1 -1
  107. package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
  108. package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
  109. package/src/components/InlineEntityPreview.tsx +149 -0
  110. package/src/components/RebaseLayout.tsx +1 -1
  111. package/src/components/RebaseNavigation.tsx +45 -5
  112. package/src/components/RebaseRouteDefs.tsx +1 -1
  113. package/src/components/SelectableTable/SelectableTable.tsx +2 -2
  114. package/src/components/SidePanelBinding.tsx +19 -13
  115. package/src/components/UserSelector.tsx +12 -6
  116. package/src/components/common/default_entity_actions.tsx +4 -3
  117. package/src/components/history/EntityHistoryEntry.tsx +28 -7
  118. package/src/components/history/EntityHistoryView.tsx +20 -20
  119. package/src/components/history/LastEditedByIndicator.tsx +4 -3
  120. package/src/components/index.ts +2 -0
  121. package/src/data_import/utils/get_import_inference_type.ts +6 -0
  122. package/src/form/EntityForm.tsx +267 -179
  123. package/src/form/EntityFormActions.tsx +50 -99
  124. package/src/form/EntityFormBinding.tsx +4 -2
  125. package/src/form/PropertyFieldBinding.tsx +4 -0
  126. package/src/form/components/FieldBlock.tsx +119 -0
  127. package/src/form/components/FormRail.tsx +125 -0
  128. package/src/form/components/FormSections.tsx +118 -0
  129. package/src/form/components/index.tsx +3 -2
  130. package/src/form/components/useRailVisible.tsx +46 -0
  131. package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
  132. package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
  133. package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
  134. package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
  135. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
  136. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
  137. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
  138. package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
  139. package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
  140. package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
  141. package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
  142. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
  143. package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
  144. package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
  145. package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
  146. package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
  147. package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
  148. package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
  149. package/src/hooks/navigation/utils.ts +20 -0
  150. package/src/hooks/useBuildSideDialogsController.tsx +2 -2
  151. package/src/hooks/useBuildSidePanel.tsx +22 -61
  152. package/src/hooks/useEntityDisplayTitle.tsx +60 -0
  153. package/src/hooks/useHistory.ts +6 -5
  154. package/src/hooks/useRecordActions.tsx +69 -0
  155. package/src/hooks/useResolvedUsers.ts +10 -5
  156. package/src/preview/PropertyPreview.tsx +48 -22
  157. package/src/preview/components/InlineEntityListPreview.tsx +42 -0
  158. package/src/preview/components/ReferencePreview.tsx +34 -14
  159. package/src/preview/components/RelationPreview.tsx +34 -14
  160. package/src/preview/index.ts +1 -0
  161. package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
  162. package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
  163. package/src/routes/RebaseRoute.tsx +2 -2
  164. package/src/types/components/EntityFormActionsProps.tsx +0 -1
  165. package/src/types/components/EntityFormProps.tsx +0 -6
  166. package/src/types/fields.tsx +27 -0
  167. package/dist/form/components/FormEntry.d.ts +0 -6
  168. package/dist/form/components/FormLayout.d.ts +0 -5
  169. package/dist/history-g9688IaK.js.map +0 -1
  170. package/dist/util-B5fJm1FA.js.map +0 -1
  171. package/src/form/components/FormEntry.tsx +0 -22
  172. package/src/form/components/FormLayout.tsx +0 -16
@@ -24,11 +24,21 @@ import { getIcon } from "@rebasepro/app";
24
24
  import { useAdminContext } from "../hooks/useAdminContext";
25
25
  import { useChildViewSource } from "../hooks/useChildViewSource";
26
26
 
27
+ /**
28
+ * The entity form's footer.
29
+ *
30
+ * It used to render in two shapes: this footer, and a `w-80 2xl:w-96` sticky
31
+ * side rail carrying the same Save and Discard above several hundred pixels of
32
+ * nothing. The rail is gone, and Save/Discard have moved to the identity bar
33
+ * where they stay visible while the form scrolls — so with
34
+ * `showDefaultActions={false}` this renders only what the bar cannot host:
35
+ * record actions (copy, delete, custom), plugin actions, the saving error, and
36
+ * the side-panel-only "save and close".
37
+ */
27
38
  export function EditFormActions({
28
39
  collection,
29
40
  path,
30
41
  entity,
31
- layout,
32
42
  savingError,
33
43
  formex,
34
44
  disabled,
@@ -69,10 +79,11 @@ export function EditFormActions({
69
79
  return actions;
70
80
  }, [canCreate, canDelete, collection, path, customizationController.entityActions?.length, entity, collection.disableDefaultActions, isLinkedChildView]);
71
81
 
72
- const formActions = showDefaultActions ? entityActions.filter(a => a.includeInForm === undefined || a.includeInForm) : [];
82
+ // Record actions stay in the footer whether or not the bar carries Save;
83
+ // they are things you do *to* the record, not ways to leave the form.
84
+ const formActions = entityActions.filter(a => a.includeInForm === undefined || a.includeInForm);
73
85
 
74
- // HMR reload trigger
75
- const bottomActions = buildBottomActions({
86
+ return buildBottomActions({
76
87
  savingError,
77
88
  entity,
78
89
  formActions,
@@ -88,36 +99,8 @@ export function EditFormActions({
88
99
  formContext,
89
100
  formex,
90
101
  t,
91
- className: layout === "responsive" ? "@6xl:hidden" : undefined
102
+ showDefaultActions
92
103
  });
93
-
94
- const sideActions = buildSideActions({
95
- savingError,
96
- entity,
97
- formActions,
98
- collection,
99
- context,
100
- sidePanelController,
101
- sideDialogContext,
102
- disabled,
103
- status,
104
- pluginActions,
105
- openEntityMode,
106
- navigateBack,
107
- formContext,
108
- formex,
109
- t,
110
- className: layout === "responsive" ? "hidden @6xl:flex" : undefined
111
- });
112
-
113
- if (layout === "responsive") {
114
- return <>
115
- {bottomActions}
116
- {sideActions}
117
- </>;
118
- }
119
-
120
- return layout === "bottom" ? bottomActions : sideActions;
121
104
  }
122
105
 
123
106
  type ActionsViewProps<M extends Record<string, unknown>> = {
@@ -136,6 +119,7 @@ type ActionsViewProps<M extends Record<string, unknown>> = {
136
119
  formContext: FormContext,
137
120
  formex: FormexController<Record<string, unknown>>;
138
121
  t: (key: string, vars?: Record<string, string>) => string;
122
+ showDefaultActions: boolean;
139
123
  className?: string;
140
124
  };
141
125
 
@@ -155,14 +139,28 @@ function buildBottomActions<M extends Record<string, unknown>>({
155
139
  formContext,
156
140
  formex,
157
141
  t,
142
+ showDefaultActions,
158
143
  className
159
144
  }: ActionsViewProps<M>) {
160
145
 
161
146
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
147
+ // "Save and close" only means something where there is something to close.
162
148
  const canClose = openEntityMode === "side_panel" || openEntityMode === "dialog";
149
+ const showBackToDetail = collection.defaultEntityAction === "view" && status === "existing";
150
+
151
+ // When the container carries the actions — the entity view's identity bar
152
+ // does — this renders nothing at all. A full-width bar holding a copy icon
153
+ // and a delete icon is the same waste as the side rail it replaced: the
154
+ // record actions moved into the bar's overflow menu, and Save, Discard and
155
+ // "save and close" are buttons up there too.
156
+ const hasContent = showDefaultActions || Boolean(savingError);
157
+
158
+ if (!hasContent) return null;
159
+
163
160
  return <DialogActions
164
161
  className={className}
165
- position={"absolute"}>
162
+ // In normal flow at the foot of the form column, not overlaying it.
163
+ position={"sticky"}>
166
164
  {savingError &&
167
165
  <div className="text-right">
168
166
  <Typography color={"error"}>{savingError.message}</Typography>
@@ -202,13 +200,8 @@ function buildBottomActions<M extends Record<string, unknown>>({
202
200
  <ErrorTooltip title={"This form has errors"}>
203
201
  <AlertCircleIcon className="ml-4 text-red-500" size={iconSize.smallest}/>
204
202
  </ErrorTooltip> : null}
205
- <Button variant="text"
206
- color="primary"
207
- disabled={disabled || formex.isSubmitting}
208
- type="reset">
209
- {status === "existing" ? t("discard") : t("clear")}
210
- </Button>
211
- {collection.defaultEntityAction === "view" && status === "existing" && (
203
+
204
+ {showBackToDetail && (
212
205
  <Button variant="text"
213
206
  color="primary"
214
207
  disabled={disabled}
@@ -216,19 +209,29 @@ function buildBottomActions<M extends Record<string, unknown>>({
216
209
  {t("back_to_detail") ?? "Back to details"}
217
210
  </Button>
218
211
  )}
219
- <Tooltip title={hasErrors ? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving") : undefined}>
220
- <Button variant={canClose ? "text" : "filled"}
212
+
213
+ {showDefaultActions && <>
214
+ <Button variant="text"
221
215
  color="primary"
222
- type="submit"
223
216
  disabled={disabled || formex.isSubmitting}
224
- onClick={() => {
225
- sideDialogContext.setPendingClose(false);
226
- }}>
227
- {status === "existing" && t("save")}
228
- {status === "copy" && t("create_copy")}
229
- {status === "new" && t("create")}
217
+ type="reset">
218
+ {status === "existing" ? t("discard") : t("clear")}
230
219
  </Button>
231
- </Tooltip>
220
+ <Tooltip title={hasErrors ? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving") : undefined}>
221
+ <Button variant={canClose ? "text" : "filled"}
222
+ color="primary"
223
+ type="submit"
224
+ disabled={disabled || formex.isSubmitting}
225
+ onClick={() => {
226
+ sideDialogContext.setPendingClose(false);
227
+ }}>
228
+ {status === "existing" && t("save")}
229
+ {status === "copy" && t("create_copy")}
230
+ {status === "new" && t("create")}
231
+ </Button>
232
+ </Tooltip>
233
+ </>}
234
+
232
235
  {canClose && <LoadingButton variant="filled"
233
236
  color="primary"
234
237
  type="submit"
@@ -244,84 +247,6 @@ function buildBottomActions<M extends Record<string, unknown>>({
244
247
  </DialogActions>;
245
248
  }
246
249
 
247
- function buildSideActions<M extends Record<string, unknown>>({
248
- savingError,
249
- entity,
250
- formActions,
251
- collection,
252
- context,
253
- sidePanelController,
254
- disabled,
255
- status,
256
- sideDialogContext,
257
- pluginActions,
258
- openEntityMode,
259
- navigateBack,
260
- formContext,
261
- formex,
262
- t,
263
- className
264
- }: ActionsViewProps<M>) {
265
-
266
- const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
267
- return <div
268
- className={cls("overflow-auto h-full flex flex-col gap-2 w-80 2xl:w-96 px-4 py-16 sticky top-0 border-l", defaultBorderMixin, className)}>
269
- <Tooltip title={hasErrors ? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving") : undefined}>
270
- <LoadingButton fullWidth={true}
271
- variant="filled"
272
- color="primary"
273
- type="submit"
274
- loading={formex.isSubmitting}
275
- startIcon={hasErrors ? <AlertCircleIcon/> : undefined}
276
- disabled={disabled || formex.isSubmitting}
277
- onClick={() => {
278
- sideDialogContext.setPendingClose?.(false);
279
- }}>
280
- {status === "existing" && t("save")}
281
- {status === "copy" && t("create_copy")}
282
- {status === "new" && t("create")}
283
- </LoadingButton>
284
- </Tooltip>
285
-
286
- <Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
287
- {status === "existing" ? t("discard") : t("clear")}
288
- </Button>
289
- {collection.defaultEntityAction === "view" && status === "existing" && (
290
- <Button fullWidth={true} variant="text" disabled={disabled} onClick={navigateBack}>
291
- {t("back_to_detail") ?? "Back to details"}
292
- </Button>
293
- )}
294
-
295
- {pluginActions}
296
-
297
- {formActions.length > 0 && <div className="flex flex-row flex-wrap mt-2">
298
- {formActions.map((action, index) => {
299
- const props = {
300
- view: "form",
301
- entity,
302
- path: getCollectionDataPath(collection),
303
- collection: collection,
304
- context,
305
- sidePanelController,
306
- openEntityMode,
307
- navigateBack,
308
- formContext
309
- } satisfies EntityActionClickProps<Record<string, unknown>>;
310
- const isEnabled = !action.isEnabled || action.isEnabled(props);
311
- return (
312
- <EntityActionButton key={action.key ?? action.name ?? index} action={action} enabled={isEnabled} props={props}/>
313
- );
314
- })}
315
- </div>}
316
-
317
- {savingError &&
318
- <div className="text-right">
319
- <Typography color={"error"}>{savingError.message}</Typography>
320
- </div>
321
- }
322
- </div>;
323
- }
324
-
325
250
  function EntityActionButton({
326
251
  action,
327
252
  enabled,
@@ -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,72 @@ 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
+
425
+ /* ---- record actions, in the bar's overflow menu ---------------------- */
426
+
427
+ const sideDialogContext = useSideDialogContext();
428
+ const canCloseAfterSave = layout === "side_panel" || layout === "dialog";
429
+
430
+ const recordActions = useRecordActions({
431
+ collection,
432
+ path,
433
+ entity: usedEntity
434
+ });
435
+
436
+ const recordActionItems = usedEntity && recordActions.length
437
+ ? recordActions.map((action, index) => {
438
+ const clickProps = {
439
+ view: "form" as const,
440
+ entity: usedEntity,
441
+ path,
442
+ collection,
443
+ context,
444
+ sidePanelController: adminContext.sidePanelController,
445
+ openEntityMode: layout,
446
+ navigateBack: navigateBack ?? (() => undefined),
447
+ formContext
448
+ };
449
+ const enabled = !action.isEnabled || action.isEnabled(clickProps as never);
450
+ return (
451
+ <MenuItem key={action.key ?? action.name ?? index}
452
+ disabled={!enabled}
453
+ onClick={() => action.onClick(clickProps as never)}>
454
+ {getIcon(action.icon, undefined, undefined, "smallest")}
455
+ {action.name}
456
+ </MenuItem>
457
+ );
458
+ })
459
+ : null;
460
+
461
+ // Plugin form actions (Autofill and friends) render inline in the bar; the
462
+ // form no longer has a footer to put them in.
463
+ const formPluginActions = useSlot("form.actions", formActionTopProps);
464
+
465
+ // A URL still naming the old `json`/`history` tab opens the inspector on
466
+ // that pane instead of 404-ing into the record with nothing shown.
467
+ useEffect(() => {
468
+ if (legacyInspectorTab) setInspectorTab(legacyInspectorTab);
469
+ }, [legacyInspectorTab]);
413
470
 
414
471
  const subCollectionsViews = childViews && childViews.map(({ collection: subcollection }) => {
415
472
  const subcollectionId = subcollection.slug;
@@ -520,7 +577,10 @@ parentEntityIds,
520
577
  entity={entity}
521
578
  initialDirtyValues={initialDirtyValues}
522
579
  openEntityMode={layout}
523
- forceActionsAtTheBottom={actionsAtTheBottom}
580
+ // Save and Discard live in the identity bar now, where they stay
581
+ // visible while the form scrolls. The form's own footer would be a
582
+ // second copy of them.
583
+ showDefaultActions={false}
524
584
  initialStatus={status}
525
585
  className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
526
586
  EntityFormActionsComponent={ResolvedFormActions as React.FC<typeof ResolvedFormActions extends React.ComponentType<infer P> ? P : never>}
@@ -601,9 +661,9 @@ parentEntityIds,
601
661
  );
602
662
  });
603
663
 
604
- const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog";
605
-
606
- const fullScreenButton = !barActions && (layout === "side_panel" || layout === "split" || layout === "dialog") && entityId ? (
664
+ const fullScreenButton = !barActions && // Not in split: the list panel beside it carries the close control,
665
+ // and an expand button there competes with it.
666
+ (layout === "side_panel" || layout === "dialog") && entityId ? (
607
667
  <Tooltip title={"Open full screen"}>
608
668
  <IconButton
609
669
  size="small"
@@ -620,59 +680,82 @@ parentEntityIds,
620
680
 
621
681
  let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
622
682
 
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)}>
683
+ <EntityIdentityBar
684
+ collection={collection as AdminCollection}
685
+ title={displayTitle}
686
+ entityId={status === "existing" ? entityId : undefined}
687
+ status={status}
688
+ dirty={Boolean(formContext?.formex?.dirty)}
689
+ saving={Boolean(formContext?.isSaving)}
690
+ hasErrors={hasFormErrors}
691
+ saveDisabled={!canEdit || saveDisabled}
692
+ // Full screen replaces the collection entirely, leaving browser Back
693
+ // as the only route to it. The detail view has carried this arrow
694
+ // for that reason; the edit view is reached the same way.
695
+ onBack={layout === "full_screen"
696
+ ? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path)))
697
+ : undefined}
698
+ onSave={canEdit && formContext ? () => {
699
+ sideDialogContext.setPendingClose?.(false);
700
+ formContext.submit();
701
+ } : undefined}
702
+ onSaveAndClose={canEdit && formContext && canCloseAfterSave ? () => {
703
+ sideDialogContext.setPendingClose?.(true);
704
+ formContext.submit();
705
+ } : undefined}
706
+ onDiscard={canEdit && formContext ? () => formContext.formex.resetForm() : undefined}
707
+ onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
708
+ onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
709
+ recordActions={recordActionItems}
710
+ pluginActions={formPluginActions}
711
+ leading={<>
712
+ {barActionsStart}
713
+ {/* Split view: closing the list is opening this record
714
+ full screen, and the control for it belongs at this
715
+ bar's leading edge. */}
716
+ {layout === "split" && <SplitListCloseButton/>}
717
+ </>}
718
+ trailing={<>
719
+ {pluginActionsTop}
720
+ {fullScreenButton}
721
+ {barActions?.({
722
+ path,
723
+ entityId,
724
+ values: formContext?.values ?? usedEntity?.values ?? {},
725
+ status
726
+ })}
727
+ </>}
728
+ />
625
729
 
626
- {fullScreenButton}
730
+ {/* Destinations only: the record is the page, so its tab leads rather
731
+ than sitting third behind two unlabelled developer tools. The row
732
+ disappears entirely when there is nowhere else to go. */}
733
+ {hasAdditionalViews && <div className={cls(
734
+ "h-10 shrink-0 flex items-stretch border-b px-2 min-w-0",
735
+ defaultBorderMixin
736
+ )}>
737
+ <Tabs
738
+ className={"!w-full"}
739
+ innerClassName={"h-full"}
740
+ variant={"boxy"}
741
+ value={activeTab}
742
+ onValueChange={(value) => {
743
+ onSideTabClick(value);
744
+ }}>
627
745
 
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>
746
+ <Tab value={MAIN_TAB_VALUE}>
747
+ <span className="flex items-center gap-1.5">
748
+ {getIcon(collection.icon, undefined, undefined, "smallest")}
749
+ {collection.singularName ?? collection.name}
750
+ </span>
751
+ </Tab>
668
752
 
669
- {customViewTabsStart}
753
+ {customViewTabsStart}
670
754
 
671
- {customViewTabsEnd}
755
+ {customViewTabsEnd}
672
756
 
673
- {subcollectionTabs}
674
- </Tabs>
675
- </div>}
757
+ {subcollectionTabs}
758
+ </Tabs>
676
759
  </div>}
677
760
 
678
761
  {globalLoading
@@ -682,14 +765,20 @@ parentEntityIds,
682
765
  {entityView}
683
766
  </>}
684
767
 
685
- {jsonView}
686
-
687
- {historyView}
688
-
689
768
  {customViewsView}
690
769
 
691
770
  {subCollectionsViews}
692
771
 
772
+ <EntityInspector
773
+ tab={inspectorTab}
774
+ onTabChange={setInspectorTab}
775
+ onClose={() => setInspectorTab(null)}
776
+ collection={collection as AdminCollection}
777
+ entity={usedEntity as Entity<Record<string, unknown>> | undefined}
778
+ formContext={formContext as FormContext<Record<string, unknown>> | undefined}
779
+ values={formContext?.values ?? usedEntity?.values}
780
+ includeHistory={includeHistoryView}/>
781
+
693
782
  </div>;
694
783
 
695
784
  if (plugins && plugins.length > 0) {