@rebasepro/admin 0.13.0 → 0.13.1-canary.g18cfeb7
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.
- package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-CUFXevVg.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-CUFXevVg.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-DDUC7giL.js} +2 -2
- package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-DDUC7giL.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-vzS848Gf.js} +9 -13
- package/dist/RouterCollectionsStudioView-vzS848Gf.js.map +1 -0
- package/dist/collection_editor/serializable_types.d.ts +1 -0
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/AdditionalFieldValue.d.ts +25 -0
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
- package/dist/components/DetailViewBinding.d.ts +15 -0
- package/dist/components/DrawerNavigationGroup.d.ts +9 -6
- package/dist/components/DrawerNavigationItem.d.ts +16 -6
- package/dist/components/EditViewBinding.d.ts +30 -1
- package/dist/components/EntityDisplayHeader.d.ts +53 -0
- package/dist/components/EntityIdentityBar.d.ts +43 -1
- package/dist/components/EntityViewBinding.d.ts +60 -6
- package/dist/components/RebaseLayout.d.ts +1 -1
- package/dist/components/RebaseShell.d.ts +1 -1
- package/dist/components/RelationSelector.d.ts +8 -1
- package/dist/components/UserSelector.d.ts +8 -1
- package/dist/components/app/Scaffold.d.ts +9 -3
- package/dist/components/index.d.ts +2 -0
- package/dist/{export-J79rMoNj.js → export-5bl2EoSs.js} +2 -2
- package/dist/{export-J79rMoNj.js.map → export-5bl2EoSs.js.map} +1 -1
- package/dist/form/components/FieldBlock.d.ts +58 -4
- package/dist/form/components/FormRail.d.ts +13 -0
- package/dist/form/components/FormSections.d.ts +15 -1
- package/dist/form/components/useRailVisible.d.ts +5 -4
- package/dist/form/form_utils.d.ts +38 -1
- package/dist/{history-D7L_JlLO.js → history-BuKKAhoY.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-BuKKAhoY.js.map} +1 -1
- package/dist/hooks/useEntityDisplay.d.ts +102 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
- package/dist/{import-BvYl--U4.js → import-BhdWs34H.js} +3 -3
- package/dist/{import-BvYl--U4.js.map → import-BhdWs34H.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +152 -57
- package/dist/index.js.map +1 -1
- package/dist/preview/compact_text.d.ts +23 -0
- package/dist/preview/components/BooleanPreview.d.ts +3 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
- package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
- package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
- package/dist/routes/RebaseRoute.d.ts +1 -1
- package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
- package/dist/util/number_format.d.ts +14 -0
- package/dist/util/previews.d.ts +9 -2
- package/dist/{util-GiwPgxnL.js → util-DNG7iK0w.js} +3756 -2300
- package/dist/util-DNG7iK0w.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/components/AdditionalFieldValue.tsx +68 -0
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
- package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +723 -348
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +38 -8
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +23 -36
- package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +33 -2
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +19 -23
- package/src/components/DetailViewBinding.tsx +93 -56
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +44 -16
- package/src/components/EditViewBinding.tsx +215 -42
- package/src/components/EntityDisplayHeader.tsx +170 -0
- package/src/components/EntityIdentityBar.tsx +163 -23
- package/src/components/EntityInspector.tsx +24 -6
- package/src/components/EntityPreviewBinding.tsx +58 -18
- package/src/components/EntityViewBinding.tsx +342 -41
- package/src/components/InlineEntityPreview.tsx +10 -10
- package/src/components/RebaseLayout.tsx +2 -2
- package/src/components/RebaseShell.tsx +2 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
- package/src/components/RelationSelector.tsx +15 -4
- package/src/components/SideDialogs.tsx +15 -3
- package/src/components/SidePanelBinding.tsx +122 -66
- package/src/components/UserSelector.tsx +15 -4
- package/src/components/app/Scaffold.tsx +112 -10
- package/src/components/history/LastEditedByIndicator.tsx +7 -12
- package/src/components/index.ts +3 -0
- package/src/data_import/utils/data.ts +9 -1
- package/src/form/EntityForm.tsx +64 -52
- package/src/form/EntityFormBinding.tsx +24 -11
- package/src/form/PropertyFieldBinding.tsx +2 -2
- package/src/form/components/FieldBlock.tsx +80 -10
- package/src/form/components/FormRail.tsx +19 -2
- package/src/form/components/FormSections.tsx +32 -5
- package/src/form/components/StorageUploadProgress.tsx +9 -3
- package/src/form/components/useRailVisible.tsx +5 -4
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
- package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
- package/src/form/form_utils.ts +78 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
- package/src/hooks/useEntityDisplay.tsx +262 -0
- package/src/hooks/useEntityDisplayTitle.tsx +27 -38
- package/src/index.ts +9 -1
- package/src/preview/PropertyPreview.tsx +25 -5
- package/src/preview/compact_text.ts +68 -0
- package/src/preview/components/BooleanPreview.tsx +5 -2
- package/src/preview/components/StorageThumbnail.tsx +5 -2
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
- package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
- package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
- package/src/routes/RebaseRoute.tsx +62 -4
- package/src/types/components/PropertyPreviewProps.tsx +21 -0
- package/src/util/number_format.ts +46 -0
- package/src/util/previews.ts +8 -55
- package/src/util/property_utils.tsx +6 -1
- package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
- package/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
2
2
|
import type { EntityStatus } from "@rebasepro/types";
|
|
3
3
|
import React, { useState } from "react";
|
|
4
|
+
import { Link } from "react-router";
|
|
4
5
|
import { useTranslation } from "@rebasepro/app";
|
|
5
6
|
import {
|
|
6
7
|
Button,
|
|
7
8
|
CheckIcon,
|
|
9
|
+
ChevronDownIcon,
|
|
8
10
|
cls,
|
|
9
11
|
CodeIcon,
|
|
10
12
|
CopyIcon,
|
|
11
13
|
defaultBorderMixin,
|
|
14
|
+
ExternalLinkIcon,
|
|
12
15
|
HistoryIcon,
|
|
13
16
|
IconButton,
|
|
14
17
|
iconSize,
|
|
@@ -19,12 +22,22 @@ import {
|
|
|
19
22
|
MoreVerticalIcon,
|
|
20
23
|
Separator,
|
|
21
24
|
Tooltip,
|
|
22
|
-
Typography
|
|
25
|
+
Typography,
|
|
26
|
+
XIcon
|
|
23
27
|
} from "@rebasepro/ui";
|
|
24
28
|
|
|
25
29
|
export interface EntityIdentityBarProps {
|
|
26
30
|
/** Plural collection name, shown as the breadcrumb ahead of the title. */
|
|
27
31
|
collection: AdminCollection;
|
|
32
|
+
/**
|
|
33
|
+
* Where the breadcrumb points. Without it the collection name is inert text
|
|
34
|
+
* that still reads as a breadcrumb — worse than no breadcrumb at all, and
|
|
35
|
+
* the only way back once a layout drops its back arrow.
|
|
36
|
+
*
|
|
37
|
+
* Left unset by the overlays (side panel, dialog), where the collection is
|
|
38
|
+
* already underneath and navigating would dismiss the record instead.
|
|
39
|
+
*/
|
|
40
|
+
collectionUrl?: string;
|
|
28
41
|
/** Resolved record title, already guarded against showing a field default. */
|
|
29
42
|
title: string;
|
|
30
43
|
entityId?: string | number;
|
|
@@ -44,11 +57,47 @@ export interface EntityIdentityBarProps {
|
|
|
44
57
|
*/
|
|
45
58
|
onSaveAndClose?: () => void;
|
|
46
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Where {@link onSaveAndClose} is offered.
|
|
62
|
+
*
|
|
63
|
+
* `"button"` — its own filled button, and Save demotes to text. For the
|
|
64
|
+
* overlays, where you opened the record to do one thing to it and closing is
|
|
65
|
+
* the dominant intent.
|
|
66
|
+
*
|
|
67
|
+
* `"menu"` — a `▾` welded to the Save button. For the split, where the list
|
|
68
|
+
* is right there and `j`/`k` walk from record to record: there, saving and
|
|
69
|
+
* *staying* is the common case, so it keeps the filled button and dismissing
|
|
70
|
+
* is one step further in. The gesture still exists, so muscle memory carried
|
|
71
|
+
* over from the side panel finds it.
|
|
72
|
+
*/
|
|
73
|
+
saveAndClosePlacement?: "button" | "menu";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dismiss this record, from the ✕ at the bar's trailing edge.
|
|
77
|
+
*
|
|
78
|
+
* Fenced off from Save by a separator: it is chrome, not the last item in
|
|
79
|
+
* the action row. The panel that supplies it decides what dismissing means —
|
|
80
|
+
* in the split it is a navigation, which the layout's unsaved-changes
|
|
81
|
+
* blocker already guards, so the ✕ cannot drop an edit without asking.
|
|
82
|
+
*/
|
|
83
|
+
onClose?: () => void;
|
|
84
|
+
|
|
47
85
|
onBack?: () => void;
|
|
48
86
|
onInspect?: () => void;
|
|
49
87
|
/** Opens the inspector straight on its history tab. */
|
|
50
88
|
onViewHistory?: () => void;
|
|
51
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Where this record appears on the live site, from `entityLinkBuilder`.
|
|
92
|
+
*
|
|
93
|
+
* Here rather than above the record, where it used to be a single unlabelled
|
|
94
|
+
* icon alone on its own right-aligned row — the only thing between the bar
|
|
95
|
+
* and the first field, and the only control in the view that did not say
|
|
96
|
+
* what it was. It is a thing you do *to* a record, so it belongs with the
|
|
97
|
+
* others.
|
|
98
|
+
*/
|
|
99
|
+
externalLink?: string;
|
|
100
|
+
|
|
52
101
|
/**
|
|
53
102
|
* Actions performed *on* the record — copy, delete, whatever the collection
|
|
54
103
|
* adds — as menu items under the overflow button. They live here rather
|
|
@@ -81,6 +130,7 @@ export interface EntityIdentityBarProps {
|
|
|
81
130
|
*/
|
|
82
131
|
export function EntityIdentityBar({
|
|
83
132
|
collection,
|
|
133
|
+
collectionUrl,
|
|
84
134
|
title,
|
|
85
135
|
entityId,
|
|
86
136
|
status,
|
|
@@ -91,9 +141,12 @@ export function EntityIdentityBar({
|
|
|
91
141
|
saveDisabled,
|
|
92
142
|
hasErrors,
|
|
93
143
|
onSaveAndClose,
|
|
144
|
+
saveAndClosePlacement = "button",
|
|
145
|
+
onClose,
|
|
94
146
|
onBack,
|
|
95
147
|
onInspect,
|
|
96
148
|
onViewHistory,
|
|
149
|
+
externalLink,
|
|
97
150
|
recordActions,
|
|
98
151
|
pluginActions,
|
|
99
152
|
trailing,
|
|
@@ -108,7 +161,17 @@ export function EntityIdentityBar({
|
|
|
108
161
|
const closeLabel = status === "existing"
|
|
109
162
|
? t("save_and_close")
|
|
110
163
|
: status === "copy" ? t("create_copy_and_close") : t("create_and_close");
|
|
111
|
-
const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory)
|
|
164
|
+
const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory)
|
|
165
|
+
|| Boolean(externalLink);
|
|
166
|
+
|
|
167
|
+
// The two shapes "save and close" takes, so the Save button next to it knows
|
|
168
|
+
// whether it is still the primary action.
|
|
169
|
+
const saveAndCloseButton = Boolean(onSaveAndClose) && saveAndClosePlacement === "button";
|
|
170
|
+
const saveAndCloseMenu = Boolean(onSaveAndClose) && saveAndClosePlacement === "menu";
|
|
171
|
+
|
|
172
|
+
const saveTooltip = hasErrors
|
|
173
|
+
? (t("fix_errors_before_saving") ?? "Fix highlighted errors before saving")
|
|
174
|
+
: undefined;
|
|
112
175
|
|
|
113
176
|
return (
|
|
114
177
|
<div className={cls(
|
|
@@ -126,10 +189,21 @@ export function EntityIdentityBar({
|
|
|
126
189
|
</Tooltip>
|
|
127
190
|
)}
|
|
128
191
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
192
|
+
{collectionUrl
|
|
193
|
+
? <Link to={collectionUrl}
|
|
194
|
+
className={"visited:text-inherit dark:visited:text-inherit hidden sm:block"}>
|
|
195
|
+
<Typography variant={"caption"}
|
|
196
|
+
className={cls(
|
|
197
|
+
"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap",
|
|
198
|
+
"hover:text-text-primary dark:hover:text-text-primary-dark transition-colors"
|
|
199
|
+
)}>
|
|
200
|
+
{collection.name} /
|
|
201
|
+
</Typography>
|
|
202
|
+
</Link>
|
|
203
|
+
: <Typography variant={"caption"}
|
|
204
|
+
className={"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden sm:block"}>
|
|
205
|
+
{collection.name} /
|
|
206
|
+
</Typography>}
|
|
133
207
|
|
|
134
208
|
<span className={"font-headers font-semibold text-[15px] tracking-tight truncate min-w-0"}
|
|
135
209
|
title={title}>
|
|
@@ -154,24 +228,66 @@ export function EntityIdentityBar({
|
|
|
154
228
|
)}
|
|
155
229
|
|
|
156
230
|
{onSave && (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
231
|
+
// `rounded-lg overflow-hidden`: the two halves of the split
|
|
232
|
+
// button are square inside and the group carries the radius, so
|
|
233
|
+
// there is one control with a seam rather than two buttons that
|
|
234
|
+
// happen to touch. Their borders match their own fill, so the
|
|
235
|
+
// seam has to be drawn — see the `▾` half for where and in what.
|
|
236
|
+
<div className={"flex items-stretch rounded-lg overflow-hidden"}>
|
|
237
|
+
<Tooltip title={saveTooltip}>
|
|
238
|
+
<LoadingButton
|
|
239
|
+
// Where a separate close button carries it, closing
|
|
240
|
+
// is the dominant intent and takes the filled
|
|
241
|
+
// treatment. Under the `▾` the record is staying
|
|
242
|
+
// open, so Save keeps it.
|
|
243
|
+
variant={saveAndCloseButton ? "text" : "filled"}
|
|
244
|
+
color={"primary"}
|
|
245
|
+
size={"small"}
|
|
246
|
+
loading={saving && !saveAndCloseButton}
|
|
247
|
+
disabled={saveDisabled}
|
|
248
|
+
onClick={onSave}
|
|
249
|
+
className={saveAndCloseMenu ? "rounded-none" : undefined}>
|
|
250
|
+
{saveLabel}
|
|
251
|
+
</LoadingButton>
|
|
252
|
+
</Tooltip>
|
|
253
|
+
|
|
254
|
+
{saveAndCloseMenu && <>
|
|
255
|
+
<Menu align={"end"}
|
|
256
|
+
trigger={
|
|
257
|
+
<Button variant={"filled"}
|
|
258
|
+
color={"primary"}
|
|
259
|
+
size={"small"}
|
|
260
|
+
disabled={saveDisabled}
|
|
261
|
+
aria-label={closeLabel}
|
|
262
|
+
// The seam is this half's own left border,
|
|
263
|
+
// tinted from `currentColor` — the ink the
|
|
264
|
+
// label is drawn in — so it holds on any
|
|
265
|
+
// button colour and dims with the control
|
|
266
|
+
// rather than needing a case per state. It
|
|
267
|
+
// was a `bg-white/25` span between the
|
|
268
|
+
// halves: a bright hairline scratched over a
|
|
269
|
+
// saturated blue, and, being a sibling of
|
|
270
|
+
// the buttons rather than part of one, it
|
|
271
|
+
// stayed at full strength while `disabled`
|
|
272
|
+
// dropped both halves to `opacity-40` — a
|
|
273
|
+
// rule brighter than the button it bisected.
|
|
274
|
+
// Inline because it has to beat the
|
|
275
|
+
// variant's `border-<color>` on one edge.
|
|
276
|
+
style={{ borderLeftColor: "color-mix(in oklab, currentColor 15%, transparent)" }}
|
|
277
|
+
className={"rounded-none px-1.5"}>
|
|
278
|
+
<ChevronDownIcon size={iconSize.smallest}/>
|
|
279
|
+
</Button>
|
|
280
|
+
}>
|
|
281
|
+
<MenuItem onClick={onSaveAndClose}>
|
|
282
|
+
<CheckIcon size={iconSize.smallest}/>
|
|
283
|
+
{closeLabel}
|
|
284
|
+
</MenuItem>
|
|
285
|
+
</Menu>
|
|
286
|
+
</>}
|
|
287
|
+
</div>
|
|
172
288
|
)}
|
|
173
289
|
|
|
174
|
-
{
|
|
290
|
+
{saveAndCloseButton && (
|
|
175
291
|
<LoadingButton variant={"filled"}
|
|
176
292
|
color={"primary"}
|
|
177
293
|
size={"small"}
|
|
@@ -189,6 +305,14 @@ export function EntityIdentityBar({
|
|
|
189
305
|
<MoreVerticalIcon size={iconSize.smallest}/>
|
|
190
306
|
</IconButton>
|
|
191
307
|
}>
|
|
308
|
+
{externalLink && (
|
|
309
|
+
<MenuItem onClick={() => window.open(externalLink, "_blank", "noopener,noreferrer")}>
|
|
310
|
+
<ExternalLinkIcon size={iconSize.smallest}/>
|
|
311
|
+
{t("open_in_live_site") ?? "Open in the live site"}
|
|
312
|
+
</MenuItem>
|
|
313
|
+
)}
|
|
314
|
+
{externalLink && (recordActions || onInspect || onViewHistory) &&
|
|
315
|
+
<Separator orientation={"horizontal"}/>}
|
|
192
316
|
{recordActions}
|
|
193
317
|
{recordActions && (onInspect || onViewHistory) && <Separator orientation={"horizontal"}/>}
|
|
194
318
|
{onViewHistory && (
|
|
@@ -207,6 +331,18 @@ export function EntityIdentityBar({
|
|
|
207
331
|
)}
|
|
208
332
|
|
|
209
333
|
{trailing}
|
|
334
|
+
|
|
335
|
+
{/* Last, and behind a rule. A ✕ that sits flush against Save reads
|
|
336
|
+
as the next item in the action row, and the two adjacent controls
|
|
337
|
+
are then "commit this edit" and "abandon it". */}
|
|
338
|
+
{onClose && <>
|
|
339
|
+
<Separator orientation={"vertical"} className={"h-5 mx-1 shrink-0"}/>
|
|
340
|
+
<Tooltip title={`${t("close")} · Esc`}>
|
|
341
|
+
<IconButton size={"small"} onClick={onClose} aria-label={t("close")}>
|
|
342
|
+
<XIcon size={iconSize.smallest}/>
|
|
343
|
+
</IconButton>
|
|
344
|
+
</Tooltip>
|
|
345
|
+
</>}
|
|
210
346
|
</div>
|
|
211
347
|
);
|
|
212
348
|
}
|
|
@@ -275,7 +411,11 @@ function IdChip({ value }: { value: string }) {
|
|
|
275
411
|
onClick={copy}
|
|
276
412
|
aria-label={`${t("copy_id")} ${value}`}
|
|
277
413
|
className={cls(
|
|
278
|
-
|
|
414
|
+
// `whitespace-nowrap`: the middle of the id is elided with a
|
|
415
|
+
// `…`, and a line break is allowed after one. In the dialog,
|
|
416
|
+
// where the bar is narrow and every button is on it, the
|
|
417
|
+
// chip broke across two lines inside a 52px row.
|
|
418
|
+
"hidden md:inline-flex items-center gap-1.5 shrink-0 whitespace-nowrap px-2 py-0.5 rounded-md",
|
|
279
419
|
"font-mono text-[11px] text-text-secondary dark:text-text-secondary-dark",
|
|
280
420
|
"bg-surface-accent-200/50 dark:bg-white/[0.055]",
|
|
281
421
|
"hover:bg-surface-accent-200/75 dark:hover:bg-white/[0.09] transition-colors"
|
|
@@ -65,16 +65,34 @@ export function EntityInspector({
|
|
|
65
65
|
const { t } = useTranslation();
|
|
66
66
|
const open = tab !== null;
|
|
67
67
|
|
|
68
|
+
// Escape closes the inspector, and only the inspector.
|
|
69
|
+
//
|
|
70
|
+
// The split view holds its own Escape listener for as long as a record is
|
|
71
|
+
// selected, and closing the record panel is a navigation — so an Escape
|
|
72
|
+
// meant for this panel was taking the whole record with it. On `window` in
|
|
73
|
+
// the bubble phase, as this listener used to be, `stopPropagation` cannot
|
|
74
|
+
// prevent that: it governs an event's travel *between* elements and says
|
|
75
|
+
// nothing about the other listeners on the element it is called from. Both
|
|
76
|
+
// ran, and the split's ran first, because it registered first — this one
|
|
77
|
+
// only exists while the inspector is open, which is always later.
|
|
78
|
+
//
|
|
79
|
+
// Capture on `document` is the idiom that holds regardless of mount order:
|
|
80
|
+
// it runs on the way down, before anything bubbles back to `window`, and
|
|
81
|
+
// there `stopPropagation` is enough. It is what the relation and user
|
|
82
|
+
// selectors already use to own the key while their popovers are open.
|
|
83
|
+
// Pinned in escape_key_ownership.test.tsx; see docs/bug-classes.md.
|
|
68
84
|
useEffect(() => {
|
|
69
85
|
if (!open) return;
|
|
70
86
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
71
|
-
if (e.key
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
if (e.key !== "Escape") return;
|
|
88
|
+
// A Radix overlay opened from inside the inspector is above it and
|
|
89
|
+
// owns the key first — the same rule the split view applies.
|
|
90
|
+
if (document.querySelector('[role="dialog"][data-state="open"]')) return;
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
onClose();
|
|
75
93
|
};
|
|
76
|
-
|
|
77
|
-
return () =>
|
|
94
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
95
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
78
96
|
}, [open, onClose]);
|
|
79
97
|
|
|
80
98
|
if (!open) return null;
|
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
import { useAnalyticsController } from "@rebasepro/app";
|
|
26
26
|
import { IconForView } from "@rebasepro/app";
|
|
27
27
|
import { getPropertyInPath } from "../util/property_utils";
|
|
28
|
-
import {
|
|
28
|
+
import { resolveCollectionSlotKeys } from "./CollectionViewBinding/usePreviewSlots";
|
|
29
|
+
import { useEntityTitle } from "../hooks/useEntityDisplay";
|
|
29
30
|
import { getValueInPath } from "@rebasepro/utils";
|
|
30
31
|
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
31
32
|
import { useSidePanel } from "../hooks/useSidePanel";
|
|
@@ -87,10 +88,33 @@ export function EntityPreviewBindingData({
|
|
|
87
88
|
|
|
88
89
|
const collection = collectionProp ?? collectionRegistryController.getCollection(entity.path);
|
|
89
90
|
|
|
91
|
+
// Above the `!collection` return, because hooks are. `useEntityTitle`
|
|
92
|
+
// tolerates a missing collection for exactly this reason.
|
|
93
|
+
const title = useEntityTitle({
|
|
94
|
+
collection,
|
|
95
|
+
entity
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// What the card says under the title.
|
|
99
|
+
//
|
|
100
|
+
// A stated list wins outright — `previewProperties` is the one place a
|
|
101
|
+
// developer says "show these", and nothing here is allowed to second-guess
|
|
102
|
+
// it. Failing that, the card fills the same slots as every other surface
|
|
103
|
+
// that renders one record in a row: a supporting line and a status. It used
|
|
104
|
+
// to take the first few properties in declaration order instead, which is
|
|
105
|
+
// how an author card ended up rendering an entire Markdown biography.
|
|
106
|
+
const statedKeys = previewKeys ?? (collection?.previewProperties as string[] | undefined);
|
|
90
107
|
const listProperties = useMemo(() => {
|
|
91
108
|
if (!collection) return [];
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
if (statedKeys && statedKeys.length > 0) {
|
|
110
|
+
return statedKeys.filter(key => getPropertyInPath(collection.properties, key));
|
|
111
|
+
}
|
|
112
|
+
const slotKeys = resolveCollectionSlotKeys(
|
|
113
|
+
collection as AdminCollection<Record<string, unknown>>,
|
|
114
|
+
authController,
|
|
115
|
+
customizationController.propertyConfigs);
|
|
116
|
+
return [slotKeys.subtitleKey, slotKeys.statusKey].filter(Boolean) as string[];
|
|
117
|
+
}, [statedKeys, collection, authController, customizationController.propertyConfigs]);
|
|
94
118
|
|
|
95
119
|
if (!collection) {
|
|
96
120
|
return (
|
|
@@ -100,7 +124,7 @@ export function EntityPreviewBindingData({
|
|
|
100
124
|
);
|
|
101
125
|
}
|
|
102
126
|
|
|
103
|
-
const titleProperty = includeTitle ?
|
|
127
|
+
const titleProperty = includeTitle ? title.source?.key : undefined;
|
|
104
128
|
const imagePropertyKey = includeImage ? getEntityImagePreviewPropertyKey(collection) : undefined;
|
|
105
129
|
const imageProperty = imagePropertyKey ? collection.properties[imagePropertyKey] : undefined;
|
|
106
130
|
const ofProp = imageProperty && "of" in imageProperty ? imageProperty.of : undefined;
|
|
@@ -119,7 +143,10 @@ export function EntityPreviewBindingData({
|
|
|
119
143
|
// relation among the preview properties collapses to one line instead
|
|
120
144
|
// of nesting another bordered card with its own id and action button.
|
|
121
145
|
<NestedEntityPreviewBoundary>
|
|
122
|
-
|
|
146
|
+
{/* `overflow-hidden`: the image slot is a fixed box, and a thumbnail
|
|
147
|
+
that fails to load renders a "file not found" message sized for a
|
|
148
|
+
full preview — which spilled across the title beside it. */}
|
|
149
|
+
<div className={cls("flex shrink-0 overflow-hidden", {
|
|
123
150
|
"w-6 h-6 mx-1 my-0.5": size === "small" || size === "smallest",
|
|
124
151
|
"w-8 h-8 ml-1 mr-2 m-2 self-start": size === "medium",
|
|
125
152
|
"w-10 h-10 ml-2 mr-2 m-2 self-start": size === "large"
|
|
@@ -148,20 +175,24 @@ export function EntityPreviewBindingData({
|
|
|
148
175
|
</div>
|
|
149
176
|
: <Skeleton/>)}
|
|
150
177
|
|
|
151
|
-
{
|
|
178
|
+
{/* A title that came from a property keeps that property's own
|
|
179
|
+
rendering — an enum stays its coloured chip, a relation stays
|
|
180
|
+
a link. A computed one is text, because that is all a
|
|
181
|
+
resolver returns. */}
|
|
182
|
+
{includeTitle && (titleProperty || title.value) && (
|
|
152
183
|
<div
|
|
153
184
|
className={"truncate my-0.5 text-sm font-medium text-text-primary dark:text-text-primary-dark"}>
|
|
154
|
-
{
|
|
155
|
-
|
|
185
|
+
{!entity && titleProperty
|
|
186
|
+
? <SkeletonPropertyComponent
|
|
187
|
+
property={getPropertyInPath(collection.properties, titleProperty) as Property}
|
|
188
|
+
size={"medium"}/>
|
|
189
|
+
: titleProperty
|
|
156
190
|
? <PropertyPreview
|
|
157
|
-
propertyKey={titleProperty
|
|
158
|
-
value={
|
|
159
|
-
property={collection.properties
|
|
160
|
-
size={"medium"}/>
|
|
161
|
-
: <SkeletonPropertyComponent
|
|
162
|
-
property={collection.properties[titleProperty as string] as Property}
|
|
191
|
+
propertyKey={titleProperty}
|
|
192
|
+
value={title.source?.value as never}
|
|
193
|
+
property={getPropertyInPath(collection.properties, titleProperty) as Property}
|
|
163
194
|
size={"medium"}/>
|
|
164
|
-
|
|
195
|
+
: title.value}
|
|
165
196
|
</div>
|
|
166
197
|
)}
|
|
167
198
|
|
|
@@ -171,8 +202,14 @@ export function EntityPreviewBindingData({
|
|
|
171
202
|
|
|
172
203
|
const valueInPath = getValueInPath(entity.values, key);
|
|
173
204
|
return (
|
|
205
|
+
// `truncate` clamps a line of text; it does nothing to a
|
|
206
|
+
// preview that renders blocks. Custom `admin.Preview`
|
|
207
|
+
// components can render anything at all, so the height
|
|
208
|
+
// is capped here as well — a card that loses the tail of
|
|
209
|
+
// an unusual value beats a card the size of a document.
|
|
174
210
|
<div key={"ref_prev_" + key}
|
|
175
|
-
className={cls("truncate
|
|
211
|
+
className={cls("truncate min-w-0 max-h-8 overflow-hidden",
|
|
212
|
+
restProperties.length > 1 ? "my-0.5" : "my-0")}>
|
|
176
213
|
{
|
|
177
214
|
entity
|
|
178
215
|
? <PropertyPreview
|
|
@@ -362,9 +399,12 @@ export const EntityPreviewContainer = React.forwardRef<HTMLDivElement, EntityPre
|
|
|
362
399
|
style={style}
|
|
363
400
|
className={cls(
|
|
364
401
|
"bg-white dark:bg-surface-900",
|
|
365
|
-
|
|
402
|
+
// On the shared 28/32/40/48 control scale. `44px` was between two
|
|
403
|
+
// steps of it, so a reference in a form was 4px short of the field
|
|
404
|
+
// next to it and their boxes never quite agreed.
|
|
405
|
+
size === "small" ? "min-h-[32px]" : "min-h-[48px]",
|
|
366
406
|
fullwidth ? "w-full" : "",
|
|
367
|
-
"items-center",
|
|
407
|
+
"items-center overflow-hidden",
|
|
368
408
|
hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
|
|
369
409
|
size === "small" ? "p-1" : "px-2 py-1",
|
|
370
410
|
"flex border rounded-lg",
|