@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
@@ -0,0 +1,288 @@
1
+ import type { AdminCollection } from "@rebasepro/admin-types";
2
+ import type { EntityStatus } from "@rebasepro/types";
3
+ import React, { useState } from "react";
4
+ import { useTranslation } from "@rebasepro/app";
5
+ import {
6
+ Button,
7
+ CheckIcon,
8
+ cls,
9
+ CodeIcon,
10
+ CopyIcon,
11
+ defaultBorderMixin,
12
+ HistoryIcon,
13
+ IconButton,
14
+ iconSize,
15
+ LoadingButton,
16
+ ArrowLeftIcon,
17
+ Menu,
18
+ MenuItem,
19
+ MoreVerticalIcon,
20
+ Separator,
21
+ Tooltip,
22
+ Typography
23
+ } from "@rebasepro/ui";
24
+
25
+ export interface EntityIdentityBarProps {
26
+ /** Plural collection name, shown as the breadcrumb ahead of the title. */
27
+ collection: AdminCollection;
28
+ /** Resolved record title, already guarded against showing a field default. */
29
+ title: string;
30
+ entityId?: string | number;
31
+ status: EntityStatus;
32
+
33
+ dirty: boolean;
34
+ saving: boolean;
35
+ /** Undefined hides the Save button — read-only views, or a custom form. */
36
+ onSave?: () => void;
37
+ onDiscard?: () => void;
38
+ saveDisabled?: boolean;
39
+ hasErrors?: boolean;
40
+
41
+ /**
42
+ * Save and dismiss, for the side panel and the dialog where the dominant
43
+ * action is "done with this record" rather than "keep editing".
44
+ */
45
+ onSaveAndClose?: () => void;
46
+
47
+ onBack?: () => void;
48
+ onInspect?: () => void;
49
+ /** Opens the inspector straight on its history tab. */
50
+ onViewHistory?: () => void;
51
+
52
+ /**
53
+ * Actions performed *on* the record — copy, delete, whatever the collection
54
+ * adds — as menu items under the overflow button. They live here rather
55
+ * than in a footer, which is what let the footer go away entirely.
56
+ */
57
+ recordActions?: React.ReactNode;
58
+
59
+ /** Plugin-contributed actions, rendered inline before Save. */
60
+ pluginActions?: React.ReactNode;
61
+
62
+ /** Close / full-screen buttons supplied by the panel that owns this view. */
63
+ trailing?: React.ReactNode;
64
+
65
+ /**
66
+ * Controls the owning panel puts at the bar's leading edge, ahead of the
67
+ * breadcrumb — where the side panel's close button lives.
68
+ */
69
+ leading?: React.ReactNode;
70
+ }
71
+
72
+ /**
73
+ * The persistent header of an entity view: who this record is, and what you can
74
+ * do to it.
75
+ *
76
+ * The title and the id used to live inside the scrolling form, above 72px of
77
+ * empty space — 219px of chrome that carried nothing and scrolled away the
78
+ * moment you touched the wheel, leaving no indication of which record you were
79
+ * editing. They live here instead, and the left half of a bar that was
80
+ * previously empty now does the work.
81
+ */
82
+ export function EntityIdentityBar({
83
+ collection,
84
+ title,
85
+ entityId,
86
+ status,
87
+ dirty,
88
+ saving,
89
+ onSave,
90
+ onDiscard,
91
+ saveDisabled,
92
+ hasErrors,
93
+ onSaveAndClose,
94
+ onBack,
95
+ onInspect,
96
+ onViewHistory,
97
+ recordActions,
98
+ pluginActions,
99
+ trailing,
100
+ leading
101
+ }: EntityIdentityBarProps) {
102
+
103
+ const { t } = useTranslation();
104
+
105
+ const saveLabel = status === "existing"
106
+ ? t("save")
107
+ : status === "copy" ? t("create_copy") : t("create");
108
+ const closeLabel = status === "existing"
109
+ ? t("save_and_close")
110
+ : status === "copy" ? t("create_copy_and_close") : t("create_and_close");
111
+ const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory);
112
+
113
+ return (
114
+ <div className={cls(
115
+ "h-[52px] shrink-0 flex items-center gap-2 pl-1.5 pr-2 border-b",
116
+ "bg-surface-50 dark:bg-surface-900",
117
+ defaultBorderMixin
118
+ )}>
119
+ {leading}
120
+
121
+ {onBack && (
122
+ <Tooltip title={t("back")}>
123
+ <IconButton size={"small"} onClick={onBack} aria-label={t("back")}>
124
+ <ArrowLeftIcon size={iconSize.smallest}/>
125
+ </IconButton>
126
+ </Tooltip>
127
+ )}
128
+
129
+ <Typography variant={"caption"}
130
+ className={"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden sm:block"}>
131
+ {collection.name}&nbsp;/
132
+ </Typography>
133
+
134
+ <span className={"font-headers font-semibold text-[15px] tracking-tight truncate min-w-0"}
135
+ title={title}>
136
+ {title}
137
+ </span>
138
+
139
+ {entityId !== undefined && <IdChip value={String(entityId)}/>}
140
+
141
+ <div className={"flex-1"}/>
142
+
143
+ {/* Only where there is something to save. The read-only detail
144
+ view has no Save button, and reporting "Saved" there implies an
145
+ editing session the user never started. */}
146
+ {onSave && <SaveState dirty={dirty} saving={saving} status={status} t={t}/>}
147
+
148
+ {pluginActions}
149
+
150
+ {onDiscard && dirty && !saving && (
151
+ <Button variant={"text"} size={"small"} onClick={onDiscard}>
152
+ {status === "existing" ? t("discard") : t("clear")}
153
+ </Button>
154
+ )}
155
+
156
+ {onSave && (
157
+ <Tooltip title={hasErrors
158
+ ? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving")
159
+ : undefined}>
160
+ <LoadingButton
161
+ // Where there is something to close, closing is the
162
+ // dominant intent and takes the filled treatment.
163
+ variant={onSaveAndClose ? "text" : "filled"}
164
+ color={"primary"}
165
+ size={"small"}
166
+ loading={saving && !onSaveAndClose}
167
+ disabled={saveDisabled}
168
+ onClick={onSave}>
169
+ {saveLabel}
170
+ </LoadingButton>
171
+ </Tooltip>
172
+ )}
173
+
174
+ {onSaveAndClose && (
175
+ <LoadingButton variant={"filled"}
176
+ color={"primary"}
177
+ size={"small"}
178
+ loading={saving}
179
+ disabled={saveDisabled}
180
+ onClick={onSaveAndClose}>
181
+ {closeLabel}
182
+ </LoadingButton>
183
+ )}
184
+
185
+ {hasMenu && (
186
+ <Menu align={"end"}
187
+ trigger={
188
+ <IconButton size={"small"} aria-label={t("more_actions") ?? "More actions"}>
189
+ <MoreVerticalIcon size={iconSize.smallest}/>
190
+ </IconButton>
191
+ }>
192
+ {recordActions}
193
+ {recordActions && (onInspect || onViewHistory) && <Separator orientation={"horizontal"}/>}
194
+ {onViewHistory && (
195
+ <MenuItem onClick={onViewHistory}>
196
+ <HistoryIcon size={iconSize.smallest}/>
197
+ {t("record_history") ?? "History"}
198
+ </MenuItem>
199
+ )}
200
+ {onInspect && (
201
+ <MenuItem onClick={onInspect}>
202
+ <CodeIcon size={iconSize.smallest}/>
203
+ {t("inspect_record") ?? "Inspect record"}
204
+ </MenuItem>
205
+ )}
206
+ </Menu>
207
+ )}
208
+
209
+ {trailing}
210
+ </div>
211
+ );
212
+ }
213
+
214
+ /**
215
+ * The words the unlabelled floating circle never said.
216
+ *
217
+ * That indicator was a ✓ / pencil / spinner chip stuck to the top-right of the
218
+ * scroll area in an `h-0 overflow-visible` container, so it drifted across
219
+ * field values as you scrolled — and it duplicated what the Save button was
220
+ * already able to tell you.
221
+ */
222
+ function SaveState({
223
+ dirty,
224
+ saving,
225
+ status,
226
+ t
227
+ }: {
228
+ dirty: boolean;
229
+ saving: boolean;
230
+ status: EntityStatus;
231
+ t: (key: string) => string;
232
+ }) {
233
+ if (saving) {
234
+ return <StateText>{t("saving")}</StateText>;
235
+ }
236
+ if (dirty) {
237
+ return <StateText>{t("unsaved_changes_title")}</StateText>;
238
+ }
239
+ if (status === "existing") {
240
+ return <StateText>{t("entity_saved") ?? "Saved"}</StateText>;
241
+ }
242
+ return null;
243
+ }
244
+
245
+ function StateText({ children }: { children: React.ReactNode }) {
246
+ return (
247
+ <span className={"text-xs text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden md:inline"}>
248
+ {children}
249
+ </span>
250
+ );
251
+ }
252
+
253
+ /**
254
+ * The id, as a chip you can copy rather than a full-width alert holding a UUID.
255
+ */
256
+ function IdChip({ value }: { value: string }) {
257
+
258
+ const { t } = useTranslation();
259
+ const [copied, setCopied] = useState(false);
260
+
261
+ const truncated = value.length > 14
262
+ ? `${value.slice(0, 8)}…${value.slice(-4)}`
263
+ : value;
264
+
265
+ const copy = () => {
266
+ navigator.clipboard?.writeText(value).then(() => {
267
+ setCopied(true);
268
+ setTimeout(() => setCopied(false), 1600);
269
+ }, () => undefined);
270
+ };
271
+
272
+ return (
273
+ <Tooltip title={copied ? t("copied") : value}>
274
+ <button type={"button"}
275
+ onClick={copy}
276
+ aria-label={`${t("copy_id")} ${value}`}
277
+ className={cls(
278
+ "hidden md:inline-flex items-center gap-1.5 shrink-0 px-2 py-0.5 rounded-md",
279
+ "font-mono text-[11px] text-text-secondary dark:text-text-secondary-dark",
280
+ "bg-surface-accent-200/50 dark:bg-white/[0.055]",
281
+ "hover:bg-surface-accent-200/75 dark:hover:bg-white/[0.09] transition-colors"
282
+ )}>
283
+ {truncated}
284
+ {copied ? <CheckIcon size={iconSize.smallest}/> : <CopyIcon size={iconSize.smallest}/>}
285
+ </button>
286
+ </Tooltip>
287
+ );
288
+ }
@@ -0,0 +1,137 @@
1
+ import type { AdminCollection } from "@rebasepro/admin-types";
2
+ import type { Entity } from "@rebasepro/types";
3
+ import type { FormContext } from "../types/fields";
4
+ import React, { lazy, Suspense, useEffect } from "react";
5
+ import {
6
+ CircularProgressCenter,
7
+ cls,
8
+ defaultBorderMixin,
9
+ ErrorBoundary,
10
+ IconButton,
11
+ iconSize,
12
+ Tab,
13
+ Tabs,
14
+ XIcon
15
+ } from "@rebasepro/ui";
16
+ import { useTranslation } from "@rebasepro/app";
17
+ import { JsonPreviewBinding } from "./JsonPreviewBinding";
18
+
19
+ const EntityHistoryView = lazy(() => import("./history").then(m => ({ default: m.EntityHistoryView })));
20
+
21
+ export type InspectorTab = "json" | "history";
22
+
23
+ export interface EntityInspectorProps {
24
+ /** The open tab, or null when the panel is closed. */
25
+ tab: InspectorTab | null;
26
+ onTabChange: (tab: InspectorTab) => void;
27
+ onClose: () => void;
28
+ collection: AdminCollection;
29
+ entity?: Entity<Record<string, unknown>>;
30
+ formContext?: FormContext<Record<string, unknown>>;
31
+ values?: Record<string, unknown>;
32
+ /** History is only offered when the collection records it. */
33
+ includeHistory: boolean;
34
+ /**
35
+ * Bumped once per save by the view that owns the form. The panel stays open
36
+ * across a save — the bar's Save button sits above it — so without this the
37
+ * history list would still be the one fetched when the panel opened.
38
+ */
39
+ refreshToken?: number;
40
+ }
41
+
42
+ /**
43
+ * Raw values and revision history, as an inspector rather than as tabs.
44
+ *
45
+ * These were the first two entries in the entity tab strip — two icon-only tabs
46
+ * with no label and no tooltip, sitting *before* the record you opened the page
47
+ * to edit. They are developer tools, not places the record lives, so they belong
48
+ * behind one affordance in the bar; a content editor should never meet `<>`.
49
+ *
50
+ * The panel sits below the identity bar and beside the record deliberately: you
51
+ * keep seeing which record you are inspecting, and keep being able to edit it.
52
+ */
53
+ export function EntityInspector({
54
+ tab,
55
+ onTabChange,
56
+ onClose,
57
+ collection,
58
+ entity,
59
+ formContext,
60
+ values,
61
+ includeHistory,
62
+ refreshToken
63
+ }: EntityInspectorProps) {
64
+
65
+ const { t } = useTranslation();
66
+ const open = tab !== null;
67
+
68
+ useEffect(() => {
69
+ if (!open) return;
70
+ const onKeyDown = (e: KeyboardEvent) => {
71
+ if (e.key === "Escape") {
72
+ e.stopPropagation();
73
+ onClose();
74
+ }
75
+ };
76
+ window.addEventListener("keydown", onKeyDown);
77
+ return () => window.removeEventListener("keydown", onKeyDown);
78
+ }, [open, onClose]);
79
+
80
+ if (!open) return null;
81
+
82
+ return (
83
+ // Docked beside the record rather than over it. As an overlay with a
84
+ // scrim this was modal in effect: the first click into the form went to
85
+ // the scrim and closed the panel, so a revision list could never be
86
+ // open while you edited the record it belongs to — which is the only
87
+ // time watching it is worth anything.
88
+ <div className={cls(
89
+ "relative flex flex-col shrink-0 h-full w-full max-w-md border-l",
90
+ "bg-white dark:bg-surface-800",
91
+ defaultBorderMixin
92
+ )}
93
+ role={"complementary"}
94
+ aria-label={t("inspect_record") ?? "Inspect record"}>
95
+
96
+ <div className={cls("h-11 shrink-0 flex items-center gap-2 pl-4 pr-1.5 border-b", defaultBorderMixin)}>
97
+ <span className={"text-sm font-semibold"}>{t("inspect") ?? "Inspect"}</span>
98
+
99
+ <div className={"flex-1"}/>
100
+
101
+ {includeHistory
102
+ ? <Tabs value={tab ?? "json"}
103
+ className={"!w-fit"}
104
+ onValueChange={(v) => onTabChange(v as InspectorTab)}>
105
+ <Tab value={"json"} className={"text-xs"}>{t("json")}</Tab>
106
+ <Tab value={"history"} className={"text-xs"}>
107
+ {t("record_history") ?? "History"}
108
+ </Tab>
109
+ </Tabs>
110
+ : null}
111
+
112
+ <IconButton size={"small"} onClick={onClose}
113
+ aria-label={t("close_inspector") ?? "Close inspector"}>
114
+ <XIcon size={iconSize.smallest}/>
115
+ </IconButton>
116
+ </div>
117
+
118
+ <div className={"flex-1 min-h-0 overflow-auto"}>
119
+ <ErrorBoundary>
120
+ {tab === "json" && (
121
+ <JsonPreviewBinding values={values ?? entity?.values ?? {}}/>
122
+ )}
123
+ {tab === "history" && includeHistory && formContext && (
124
+ <Suspense fallback={<CircularProgressCenter/>}>
125
+ <EntityHistoryView
126
+ collection={collection}
127
+ entity={entity}
128
+ formContext={formContext}
129
+ refreshToken={refreshToken}
130
+ modifiedValues={values ?? entity?.values}/>
131
+ </Suspense>
132
+ )}
133
+ </ErrorBoundary>
134
+ </div>
135
+ </div>
136
+ );
137
+ }
@@ -29,6 +29,7 @@ import { getEntityPreviewKeys, getEntityTitlePropertyKeyForEntity } from "../uti
29
29
  import { getValueInPath } from "@rebasepro/utils";
30
30
  import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
31
31
  import { useSidePanel } from "../hooks/useSidePanel";
32
+ import { NestedEntityPreviewBoundary } from "./EntityPreviewNesting";
32
33
  import type { AdminCollection } from "@rebasepro/admin-types";
33
34
 
34
35
  export type EntityPreviewBindingProps = {
@@ -114,7 +115,10 @@ export function EntityPreviewBindingData({
114
115
  : undefined;
115
116
 
116
117
  return (
117
- <>
118
+ // Everything below renders *inside* this preview: a reference or
119
+ // relation among the preview properties collapses to one line instead
120
+ // of nesting another bordered card with its own id and action button.
121
+ <NestedEntityPreviewBoundary>
118
122
  <div className={cls("flex shrink-0", {
119
123
  "w-6 h-6 mx-1 my-0.5": size === "small" || size === "smallest",
120
124
  "w-8 h-8 ml-1 mr-2 m-2 self-start": size === "medium",
@@ -214,7 +218,7 @@ export function EntityPreviewBindingData({
214
218
  </div>}
215
219
 
216
220
  {actions && <div className="flex-shrink-0">{actions}</div>}
217
- </>
221
+ </NestedEntityPreviewBoundary>
218
222
  );
219
223
  }
220
224
 
@@ -0,0 +1,50 @@
1
+ import * as React from "react";
2
+
3
+ /**
4
+ * How deep the current entity preview is nested inside another one.
5
+ *
6
+ * A reference or relation rendered on its own (a form field, a table cell) is
7
+ * at depth 0 and gets the full card treatment: image slot, id, title, preview
8
+ * properties and a link to the side panel. A reference rendered *inside*
9
+ * another entity preview is at depth 1+, where repeating that card produces a
10
+ * box-inside-a-box with its own borders, id line and action button. Nested
11
+ * previews collapse to a compact chip instead.
12
+ */
13
+ const EntityPreviewDepthContext = React.createContext<number>(0);
14
+
15
+ /**
16
+ * Current entity preview nesting depth. 0 means "not inside another preview".
17
+ */
18
+ export function useEntityPreviewDepth(): number {
19
+ return React.useContext(EntityPreviewDepthContext);
20
+ }
21
+
22
+ /**
23
+ * True when the preview being rendered lives inside another entity preview,
24
+ * and should therefore render its compact variant.
25
+ */
26
+ export function useIsNestedEntityPreview(): boolean {
27
+ return useEntityPreviewDepth() > 0;
28
+ }
29
+
30
+ /**
31
+ * Marks its children as being rendered inside an entity preview, so any
32
+ * reference or relation below collapses to a chip.
33
+ */
34
+ export function NestedEntityPreviewBoundary({ children }: { children: React.ReactNode }) {
35
+ const depth = useEntityPreviewDepth();
36
+ return <EntityPreviewDepthContext.Provider value={depth + 1}>
37
+ {children}
38
+ </EntityPreviewDepthContext.Provider>;
39
+ }
40
+
41
+ /**
42
+ * Resets the nesting depth back to 0. Used by surfaces that open a fresh
43
+ * rendering context for an entity — a side panel, a dialog — from within a
44
+ * preview, where the children are no longer visually nested.
45
+ */
46
+ export function RootEntityPreviewBoundary({ children }: { children: React.ReactNode }) {
47
+ return <EntityPreviewDepthContext.Provider value={0}>
48
+ {children}
49
+ </EntityPreviewDepthContext.Provider>;
50
+ }
@@ -3,10 +3,8 @@ import type { Properties } from "@rebasepro/types";
3
3
  import type { CustomizationController, AdminCollection } from "@rebasepro/admin-types";
4
4
  import React from "react";
5
5
  import { Entity } from "@rebasepro/types";
6
- import { cls, defaultBorderMixin, ExternalLinkIcon, IconButton, Typography } from "@rebasepro/ui";
7
- ;
6
+ import { ExternalLinkIcon, IconButton, Tooltip } from "@rebasepro/ui";
8
7
  import { useCustomizationController } from "@rebasepro/app";
9
- import { useAuthController } from "@rebasepro/app";
10
8
  import { PropertyCollectionView } from "./PropertyCollectionView";
11
9
 
12
10
  /**
@@ -19,6 +17,15 @@ export interface EntityViewBindingProps<M extends Record<string, unknown>> {
19
17
  className?: string;
20
18
  }
21
19
 
20
+ /**
21
+ * The read-only rendering of a record's values.
22
+ *
23
+ * This used to prepend a synthetic `Id` row of its own. Between that row, the
24
+ * collection's own id property, and the `path/id` chip above it, the same UUID
25
+ * appeared three times — and in the ~340px split pane each copy wrapped over
26
+ * four lines, so most of the first screen was one id repeated. The id is now a
27
+ * copyable chip in the identity bar, once.
28
+ */
22
29
  export function EntityViewBinding<M extends Record<string, unknown>>(
23
30
  {
24
31
  entity,
@@ -29,40 +36,36 @@ export function EntityViewBinding<M extends Record<string, unknown>>(
29
36
 
30
37
  const customizationController: CustomizationController = useCustomizationController();
31
38
 
32
- const properties: Properties = collection.properties;
39
+ // A single surrogate id is the chip in the identity bar, so listing it again
40
+ // as the first row here is the duplication the form already removed.
41
+ //
42
+ // A composite key is different: `entity.id` is then a synthesised address
43
+ // (`a:::b`), and the real key columns are data — on a junction row they say
44
+ // which two records this joins. Those stay in the table.
45
+ const properties: Properties = React.useMemo(() => {
46
+ const entries = Object.entries(collection.properties ?? {});
47
+ const idKeys = entries.filter(([, p]) => p && "isId" in p && p.isId);
48
+ if (idKeys.length !== 1) return collection.properties;
49
+ return Object.fromEntries(entries.filter(([key]) => key !== idKeys[0][0])) as Properties;
50
+ }, [collection.properties]);
51
+ const externalLink = customizationController?.entityLinkBuilder?.({ entity });
33
52
 
34
53
  return (
35
- <div className={"w-full " + className}>
36
- <div className={"w-full mb-4 p-4"}>
54
+ <div className={"w-full " + (className ?? "")}>
37
55
 
38
- <div className={`grid grid-cols-12 gap-x-4 py-4 items-start border-b ${defaultBorderMixin}`}>
39
- <div className="col-span-4 pr-2">
40
- <Typography variant="caption"
41
- color={"secondary"}
42
- component={"span"}
43
- className="break-words">
44
- Id
45
- </Typography>
46
- </div>
47
- <div className="col-span-8">
48
- <div
49
- className="flex-grow text-surface-900 dark:text-white flex items-center">
50
- <span className="flex-grow mr-2">{entity.id}</span>
51
- {customizationController?.entityLinkBuilder &&
52
- <a href={customizationController.entityLinkBuilder({ entity })}
53
- rel="noopener noreferrer"
54
- target="_blank">
55
- <IconButton>
56
- <ExternalLinkIcon/>
57
- </IconButton>
58
- </a>}
59
- </div>
60
- </div>
56
+ {externalLink && (
57
+ <div className={"flex justify-end mb-2"}>
58
+ <Tooltip title={"Open in the live site"}>
59
+ <a href={externalLink} rel={"noopener noreferrer"} target={"_blank"}>
60
+ <IconButton size={"small"}>
61
+ <ExternalLinkIcon/>
62
+ </IconButton>
63
+ </a>
64
+ </Tooltip>
61
65
  </div>
66
+ )}
62
67
 
63
- <PropertyCollectionView data={entity.values} properties={properties} size={"medium"}/>
64
-
65
- </div>
68
+ <PropertyCollectionView data={entity.values} properties={properties} size={"medium"}/>
66
69
  </div>
67
70
  );
68
71
  }
@@ -1,5 +1,5 @@
1
1
  import type { NavigationEntry } from "@rebasepro/admin-types";
2
- import { useNavigate } from "react-router-dom";
2
+ import { useNavigate } from "react-router";
3
3
  ;
4
4
  import { Chip, Collapse, StarIcon } from "@rebasepro/ui";
5
5
  import { useUserConfigurationPersistence } from "@rebasepro/app";
@@ -1,6 +1,6 @@
1
1
 
2
2
  import type { NavigationEntry, AdminCollection } from "@rebasepro/admin-types";
3
- import { useNavigate } from "react-router-dom";
3
+ import { useNavigate } from "react-router";
4
4
  ;
5
5
  import { IconButton, StarIcon, Tooltip } from "@rebasepro/ui";
6
6
  import { NavigationCard } from "./NavigationCard";
@@ -1,6 +1,6 @@
1
1
  import { cardClickableMixin, cardMixin, cls, Typography } from "@rebasepro/ui";
2
2
 
3
- import { Link } from "react-router-dom";
3
+ import { Link } from "react-router";
4
4
 
5
5
  export type SmallNavigationCardProps = {
6
6
  name: string,