@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.
- package/README.md +1 -1
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js → CollectionEditorDialog-CjXkWZ64.js} +75 -62
- package/dist/{CollectionEditorDialog-Dpc75wIZ.js.map → CollectionEditorDialog-CjXkWZ64.js.map} +1 -1
- package/dist/{PropertyEditView-DH3XZC2x.js → PropertyEditView-W8I3YV89.js} +36 -16
- package/dist/{PropertyEditView-DH3XZC2x.js.map → PropertyEditView-W8I3YV89.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js → RouterCollectionsStudioView-BsLTNoqX.js} +5 -5
- package/dist/{RouterCollectionsStudioView-BZLq4MGp.js.map → RouterCollectionsStudioView-BsLTNoqX.js.map} +1 -1
- package/dist/collection_editor/serializable_types.d.ts +4 -1
- package/dist/collection_editor/ui/collection_editor/CollectionRelationsTab.d.ts +1 -0
- package/dist/collection_editor/useLocalCollectionsConfigController.d.ts +8 -0
- package/dist/collection_editor_ui.js +5 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/components/CollectionTableBinding/column_utils.d.ts +17 -0
- package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +7 -2
- package/dist/components/CollectionViewBinding/SortButton.d.ts +24 -0
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +10 -0
- package/dist/components/CollectionViewBinding/SplitViewContext.d.ts +23 -0
- package/dist/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.d.ts +26 -0
- package/dist/components/DetailViewBinding.d.ts +5 -0
- package/dist/components/EditFormActions.d.ts +12 -1
- package/dist/components/EditViewBinding.d.ts +6 -1
- package/dist/components/EntityIdentityBar.d.ts +53 -0
- package/dist/components/EntityInspector.d.ts +35 -0
- package/dist/components/EntityPreviewNesting.d.ts +25 -0
- package/dist/components/EntityViewBinding.d.ts +9 -0
- package/dist/components/InlineEntityPreview.d.ts +47 -0
- package/dist/components/history/EntityHistoryView.d.ts +4 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/editor.js +18 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-BzIpUdSK.js → export-J79rMoNj.js} +2 -2
- package/dist/{export-BzIpUdSK.js.map → export-J79rMoNj.js.map} +1 -1
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/EntityFormActions.d.ts +11 -1
- package/dist/form/PropertyFieldBinding.d.ts +1 -1
- package/dist/form/components/FieldBlock.d.ts +30 -0
- package/dist/form/components/FormRail.d.ts +18 -0
- package/dist/form/components/FormSections.d.ts +16 -0
- package/dist/form/components/index.d.ts +3 -2
- package/dist/form/components/useRailVisible.d.ts +24 -0
- package/dist/form/field_bindings/BinaryFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/DateTimeFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MultiSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/ReadOnlyFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/SwitchFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/TextFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/VectorFieldBinding.d.ts +1 -1
- package/dist/{history-g9688IaK.js → history-D7L_JlLO.js} +35 -33
- package/dist/history-D7L_JlLO.js.map +1 -0
- package/dist/hooks/navigation/utils.d.ts +16 -0
- package/dist/hooks/useBuildSidePanel.d.ts +14 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -0
- package/dist/hooks/useHistory.d.ts +7 -0
- package/dist/hooks/useRecordActions.d.ts +17 -0
- package/dist/{import-BjUG-cIY.js → import-BvYl--U4.js} +2 -2
- package/dist/{import-BjUG-cIY.js.map → import-BvYl--U4.js.map} +1 -1
- package/dist/index.js +106 -35
- package/dist/index.js.map +1 -1
- package/dist/markdown-CklalUUk.js.map +1 -1
- package/dist/preview/components/InlineEntityListPreview.d.ts +15 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/ArrayOfReferencesPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOfRelationsPreview.d.ts +1 -1
- package/dist/types/components/EntityFormActionsProps.d.ts +0 -1
- package/dist/types/components/EntityFormProps.d.ts +0 -5
- package/dist/types/fields.d.ts +24 -0
- package/dist/{util-B5fJm1FA.js → util-GiwPgxnL.js} +2106 -1117
- package/dist/util-GiwPgxnL.js.map +1 -0
- package/package.json +32 -34
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -1
- package/src/collection_editor/serializable_types.ts +4 -3
- package/src/collection_editor/serializable_utils.ts +51 -17
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +65 -22
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +38 -30
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +16 -0
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +127 -15
- package/src/collection_editor/validateCollectionJson.ts +3 -3
- package/src/components/AdminModeSyncer.tsx +1 -1
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +10 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +59 -2
- package/src/components/CollectionTableBinding/table_bindings.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +23 -16
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +9 -2
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +47 -12
- package/src/components/CollectionViewBinding/SortButton.tsx +189 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +33 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +18 -3
- package/src/components/CollectionViewBinding/SplitViewContext.tsx +34 -0
- package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +48 -9
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +80 -25
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +18 -6
- package/src/components/DefaultAppBar.tsx +1 -1
- package/src/components/DefaultDrawer.tsx +1 -1
- package/src/components/DetailViewBinding.tsx +112 -148
- package/src/components/DrawerNavigationItem.tsx +1 -1
- package/src/components/EditFormActions.tsx +54 -129
- package/src/components/EditViewBinding.tsx +202 -99
- package/src/components/EntityIdentityBar.tsx +288 -0
- package/src/components/EntityInspector.tsx +137 -0
- package/src/components/EntityPreviewBinding.tsx +6 -2
- package/src/components/EntityPreviewNesting.tsx +50 -0
- package/src/components/EntityViewBinding.tsx +35 -32
- package/src/components/HomePage/FavouritesView.tsx +1 -1
- package/src/components/HomePage/NavigationCardBinding.tsx +1 -1
- package/src/components/HomePage/SmallNavigationCard.tsx +1 -1
- package/src/components/InlineEntityPreview.tsx +149 -0
- package/src/components/RebaseLayout.tsx +1 -1
- package/src/components/RebaseNavigation.tsx +45 -5
- package/src/components/RebaseRouteDefs.tsx +1 -1
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/SidePanelBinding.tsx +19 -13
- package/src/components/UserSelector.tsx +12 -6
- package/src/components/common/default_entity_actions.tsx +4 -3
- package/src/components/history/EntityHistoryEntry.tsx +28 -7
- package/src/components/history/EntityHistoryView.tsx +29 -24
- package/src/components/history/LastEditedByIndicator.tsx +4 -3
- package/src/components/index.ts +2 -0
- package/src/data_import/utils/get_import_inference_type.ts +6 -0
- package/src/form/EntityForm.tsx +267 -179
- package/src/form/EntityFormActions.tsx +50 -99
- package/src/form/EntityFormBinding.tsx +4 -2
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/components/FieldBlock.tsx +119 -0
- package/src/form/components/FormRail.tsx +125 -0
- package/src/form/components/FormSections.tsx +118 -0
- package/src/form/components/index.tsx +3 -2
- package/src/form/components/useRailVisible.tsx +46 -0
- package/src/form/field_bindings/BinaryFieldBinding.tsx +3 -2
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -2
- package/src/form/field_bindings/GeopointFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MapFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +2 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +2 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +6 -4
- package/src/form/field_bindings/RepeatFieldBinding.tsx +4 -1
- package/src/form/field_bindings/SelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +31 -8
- package/src/form/field_bindings/SwitchFieldBinding.tsx +7 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -16
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +3 -2
- package/src/form/field_bindings/VectorFieldBinding.tsx +2 -1
- package/src/hooks/navigation/useBuildUrlController.tsx +1 -1
- package/src/hooks/navigation/useTopLevelNavigation.ts +18 -7
- package/src/hooks/navigation/utils.ts +20 -0
- package/src/hooks/useBuildSideDialogsController.tsx +2 -2
- package/src/hooks/useBuildSidePanel.tsx +22 -61
- package/src/hooks/useEntityDisplayTitle.tsx +60 -0
- package/src/hooks/useHistory.ts +20 -10
- package/src/hooks/useRecordActions.tsx +69 -0
- package/src/hooks/useResolvedUsers.ts +10 -5
- package/src/preview/PropertyPreview.tsx +48 -22
- package/src/preview/components/InlineEntityListPreview.tsx +42 -0
- package/src/preview/components/ReferencePreview.tsx +34 -14
- package/src/preview/components/RelationPreview.tsx +34 -14
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +24 -1
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +25 -1
- package/src/routes/RebaseRoute.tsx +2 -2
- package/src/types/components/EntityFormActionsProps.tsx +0 -1
- package/src/types/components/EntityFormProps.tsx +0 -6
- package/src/types/fields.tsx +27 -0
- package/dist/form/components/FormEntry.d.ts +0 -6
- package/dist/form/components/FormLayout.d.ts +0 -5
- package/dist/history-g9688IaK.js.map +0 -1
- package/dist/util-B5fJm1FA.js.map +0 -1
- package/src/form/components/FormEntry.tsx +0 -22
- 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} /
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</
|
|
46
|
-
</
|
|
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
|
-
|
|
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
|
|
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
|
|
3
|
+
import { useNavigate } from "react-router";
|
|
4
4
|
;
|
|
5
5
|
import { IconButton, StarIcon, Tooltip } from "@rebasepro/ui";
|
|
6
6
|
import { NavigationCard } from "./NavigationCard";
|