@rebasepro/admin 0.13.0 → 0.13.1-canary.g3660bd5
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-MODRemil.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-MODRemil.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-Bt-VM0zQ.js} +2 -2
- package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-Bt-VM0zQ.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-D54P9o8O.js} +4 -4
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-D54P9o8O.js.map} +1 -1
- 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 +37 -0
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +5 -3
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +29 -4
- package/dist/components/DetailViewBinding.d.ts +6 -0
- package/dist/components/DrawerNavigationGroup.d.ts +5 -4
- package/dist/components/DrawerNavigationItem.d.ts +4 -2
- package/dist/components/EditViewBinding.d.ts +7 -1
- package/dist/components/EntityDisplayHeader.d.ts +53 -0
- package/dist/components/EntityIdentityBar.d.ts +20 -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/app/Scaffold.d.ts +9 -3
- package/dist/components/index.d.ts +2 -0
- package/dist/{export-J79rMoNj.js → export-Bl1V7lKO.js} +2 -2
- package/dist/{export-J79rMoNj.js.map → export-Bl1V7lKO.js.map} +1 -1
- package/dist/form/components/FieldBlock.d.ts +47 -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/{history-D7L_JlLO.js → history-DN_dzFAF.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-DN_dzFAF.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-BJXrHdFv.js} +3 -3
- package/dist/{import-BvYl--U4.js.map → import-BJXrHdFv.js.map} +1 -1
- package/dist/index.js +149 -51
- 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-EXfeGzbH.js} +8142 -7353
- package/dist/util-EXfeGzbH.js.map +1 -0
- package/package.json +9 -9
- package/src/components/AdditionalFieldValue.tsx +68 -0
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +5 -6
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +9 -27
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +11 -24
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +10 -33
- package/src/components/CollectionViewBinding/SlotValue.tsx +60 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +7 -5
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +14 -1
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +115 -48
- package/src/components/DefaultDrawer.tsx +19 -23
- package/src/components/DetailViewBinding.tsx +74 -54
- package/src/components/DrawerNavigationGroup.tsx +4 -7
- package/src/components/DrawerNavigationItem.tsx +23 -4
- package/src/components/EditViewBinding.tsx +54 -33
- package/src/components/EntityDisplayHeader.tsx +170 -0
- package/src/components/EntityIdentityBar.tsx +49 -5
- package/src/components/EntityPreviewBinding.tsx +54 -17
- package/src/components/EntityViewBinding.tsx +341 -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/app/Scaffold.tsx +112 -10
- package/src/components/index.ts +3 -0
- package/src/form/EntityForm.tsx +28 -33
- package/src/form/components/FieldBlock.tsx +65 -8
- 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/ReadOnlyFieldBinding.tsx +7 -1
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
- package/src/hooks/useEntityDisplay.tsx +262 -0
- package/src/hooks/useEntityDisplayTitle.tsx +27 -38
- package/src/preview/PropertyPreview.tsx +18 -2
- 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/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/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -45,6 +45,7 @@ import type { OnUpdateParams } from "../types/components/EntityFormProps";
|
|
|
45
45
|
import { EditFormActions } from "./EditFormActions";
|
|
46
46
|
import { EntityIdentityBar } from "./EntityIdentityBar";
|
|
47
47
|
import { SplitListCloseButton } from "./CollectionViewBinding/SplitListCloseButton";
|
|
48
|
+
import { SplitListShowButton } from "./CollectionViewBinding/SplitListShowButton";
|
|
48
49
|
import { useRecordActions } from "../hooks/useRecordActions";
|
|
49
50
|
import { useSideDialogContext } from "./SideDialogs";
|
|
50
51
|
import { useAdminContext } from "../hooks/useAdminContext";
|
|
@@ -96,6 +97,12 @@ export interface EditViewBindingProps<M extends Record<string, unknown> = Record
|
|
|
96
97
|
* of the breadcrumb — the side panel's close button.
|
|
97
98
|
*/
|
|
98
99
|
barActionsStart?: React.ReactNode;
|
|
100
|
+
/**
|
|
101
|
+
* Full screen only: bring the list back beside this record. Set when the
|
|
102
|
+
* record was opened out of a split collection, so folding the list away is
|
|
103
|
+
* reversible; it takes the place of the back arrow when present.
|
|
104
|
+
*/
|
|
105
|
+
onShowList?: () => void;
|
|
99
106
|
formProps?: Partial<EntityFormBindingProps<M>>,
|
|
100
107
|
/**
|
|
101
108
|
* Pre-populate the form with these values when creating a new entity.
|
|
@@ -192,6 +199,7 @@ export function EditViewBindingInner<M extends Record<string, unknown>>({
|
|
|
192
199
|
layout = "side_panel",
|
|
193
200
|
barActions,
|
|
194
201
|
barActionsStart,
|
|
202
|
+
onShowList,
|
|
195
203
|
status,
|
|
196
204
|
setStatus,
|
|
197
205
|
formProps,
|
|
@@ -405,8 +413,8 @@ parentEntityIds,
|
|
|
405
413
|
|
|
406
414
|
const displayTitle = useEntityDisplayTitle({
|
|
407
415
|
collection,
|
|
416
|
+
entity: usedEntity,
|
|
408
417
|
values: (formContext?.values ?? usedEntity?.values) as Record<string, unknown> | undefined,
|
|
409
|
-
entityId,
|
|
410
418
|
status
|
|
411
419
|
});
|
|
412
420
|
|
|
@@ -524,35 +532,39 @@ parentEntityIds,
|
|
|
524
532
|
const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
|
|
525
533
|
const formViewIncludeActions = formViewConfig?.includeActions !== false;
|
|
526
534
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
535
|
+
// Without edit permission the record is rendered read-only — the same
|
|
536
|
+
// sections, spans and rail as the form, so losing the permission changes the
|
|
537
|
+
// controls and nothing else. The `h4` that used to head this repeated the
|
|
538
|
+
// collection name the identity bar above it already carries.
|
|
539
|
+
const entityReadOnlyView = !canEdit && entity ? (
|
|
540
|
+
FormViewBuilder && readOnlyFormContext
|
|
541
|
+
? <div className={cls(
|
|
542
|
+
"flex-1 flex flex-row w-full overflow-y-auto justify-center",
|
|
543
|
+
(canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : ""
|
|
544
|
+
)}>
|
|
545
|
+
<div className={"w-full max-w-3xl 2xl:max-w-4xl flex flex-col pt-6 pb-16 px-5 sm:px-8"}>
|
|
546
|
+
<ErrorBoundary>
|
|
547
|
+
<Suspense fallback={<CircularProgressCenter />}>
|
|
548
|
+
<FormViewBuilder
|
|
549
|
+
collection={collection}
|
|
550
|
+
parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
|
|
551
|
+
entity={usedEntity}
|
|
552
|
+
modifiedValues={usedEntity?.values}
|
|
553
|
+
formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
|
|
554
|
+
/>
|
|
555
|
+
</Suspense>
|
|
556
|
+
</ErrorBoundary>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
: <EntityViewBinding
|
|
560
|
+
className={cls((canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}
|
|
561
|
+
entity={entity}
|
|
562
|
+
path={path}
|
|
563
|
+
collection={collection}
|
|
564
|
+
asPage
|
|
565
|
+
openEntityMode={layout}
|
|
566
|
+
formContext={readOnlyFormContext}/>
|
|
567
|
+
) : null;
|
|
556
568
|
|
|
557
569
|
const entityView = FormViewBuilder ? (
|
|
558
570
|
// formView.Builder replaces the default form
|
|
@@ -687,6 +699,9 @@ parentEntityIds,
|
|
|
687
699
|
|
|
688
700
|
<EntityIdentityBar
|
|
689
701
|
collection={collection as AdminCollection}
|
|
702
|
+
collectionUrl={layout === "full_screen" || layout === "split"
|
|
703
|
+
? withViewMode(urlController.buildUrlCollectionPath(path))
|
|
704
|
+
: undefined}
|
|
690
705
|
title={displayTitle}
|
|
691
706
|
entityId={status === "existing" ? entityId : undefined}
|
|
692
707
|
status={status}
|
|
@@ -696,8 +711,10 @@ parentEntityIds,
|
|
|
696
711
|
saveDisabled={!canEdit || saveDisabled}
|
|
697
712
|
// Full screen replaces the collection entirely, leaving browser Back
|
|
698
713
|
// as the only route to it. The detail view has carried this arrow
|
|
699
|
-
// for that reason; the edit view is reached the same way.
|
|
700
|
-
|
|
714
|
+
// for that reason; the edit view is reached the same way. Where the
|
|
715
|
+
// list can be unfolded back beside the record, that chevron reaches
|
|
716
|
+
// the same collection and the arrow beside it is noise.
|
|
717
|
+
onBack={layout === "full_screen" && !onShowList
|
|
701
718
|
? () => navigate(withViewMode(urlController.buildUrlCollectionPath(path)))
|
|
702
719
|
: undefined}
|
|
703
720
|
onSave={canEdit && formContext ? () => {
|
|
@@ -711,14 +728,18 @@ parentEntityIds,
|
|
|
711
728
|
onDiscard={canEdit && formContext ? () => formContext.formex.resetForm() : undefined}
|
|
712
729
|
onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
|
|
713
730
|
onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
|
|
731
|
+
externalLink={usedEntity
|
|
732
|
+
? customizationController?.entityLinkBuilder?.({ entity: usedEntity })
|
|
733
|
+
: undefined}
|
|
714
734
|
recordActions={recordActionItems}
|
|
715
735
|
pluginActions={formPluginActions}
|
|
716
736
|
leading={<>
|
|
717
737
|
{barActionsStart}
|
|
718
738
|
{/* Split view: closing the list is opening this record
|
|
719
739
|
full screen, and the control for it belongs at this
|
|
720
|
-
bar's leading edge. */}
|
|
740
|
+
bar's leading edge. Full screen carries its mirror. */}
|
|
721
741
|
{layout === "split" && <SplitListCloseButton/>}
|
|
742
|
+
{layout === "full_screen" && onShowList && <SplitListShowButton onClick={onShowList}/>}
|
|
722
743
|
</>}
|
|
723
744
|
trailing={<>
|
|
724
745
|
{pluginActionsTop}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
2
|
+
import type { Entity, Property } from "@rebasepro/types";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Chip, cls, Typography } from "@rebasepro/ui";
|
|
5
|
+
|
|
6
|
+
import { useEntityDisplayValues } from "../hooks/useEntityDisplay";
|
|
7
|
+
import { PropertyPreview } from "../preview";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The roles whose property the read-only view takes out of its grid.
|
|
11
|
+
*
|
|
12
|
+
* A role rendered here and left in the grid prints the value twice, so the
|
|
13
|
+
* header's own rendering and this list have to agree — but the list is *not*
|
|
14
|
+
* simply "every role the header draws". Two are deliberately absent:
|
|
15
|
+
*
|
|
16
|
+
* `title`, because the heading above a record and a labelled field are not the
|
|
17
|
+
* same thing. `Order #` is a column with a name, a description and a format that
|
|
18
|
+
* someone reads the record to find; `ORD-2026-0043` sitting in the identity bar
|
|
19
|
+
* beside the breadcrumb is a label for the page. Removing the field because the
|
|
20
|
+
* page is named after it takes a value out of the record on the grounds that its
|
|
21
|
+
* text appears elsewhere, which is not a reason — a value in the database is
|
|
22
|
+
* shown.
|
|
23
|
+
*
|
|
24
|
+
* `date`, because the rail's record block already carries the timestamps, and a
|
|
25
|
+
* collection whose `date` role points at a business date — an order date, a due
|
|
26
|
+
* date — wants that field where it declared it.
|
|
27
|
+
*/
|
|
28
|
+
export const HEADER_DISPLAY_ROLES = ["subtitle", "image", "status", "tags"] as const;
|
|
29
|
+
|
|
30
|
+
export interface EntityDisplayHeaderProps<M extends Record<string, unknown>> {
|
|
31
|
+
entity: Entity<M>;
|
|
32
|
+
collection: AdminCollection<M>;
|
|
33
|
+
/**
|
|
34
|
+
* Render the title. Off when an identity bar above already shows it — which
|
|
35
|
+
* is every full-page use — and on inside a dialog that has no bar.
|
|
36
|
+
*/
|
|
37
|
+
showTitle?: boolean;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What the record *is*, above the fields that say what it holds.
|
|
43
|
+
*
|
|
44
|
+
* A record's picture, name, state and labels are the things you opened the page
|
|
45
|
+
* to see; on the plain grid they were fields like any other, so an order's
|
|
46
|
+
* status sat in the second cell of the first row at the same size and weight as
|
|
47
|
+
* its shipping cost, and its tags were a wrapped list of grey text.
|
|
48
|
+
*
|
|
49
|
+
* Every role here comes from `admin.display` and nothing is derived except the
|
|
50
|
+
* title, which was already derived for the identity bar. A collection that
|
|
51
|
+
* declares no display block therefore gets no header at all rather than a
|
|
52
|
+
* heuristic guess at which of its enums is a "status" — the panel does not know
|
|
53
|
+
* what these records are, and a wrong guess here is louder than no guess.
|
|
54
|
+
*
|
|
55
|
+
* A role filled by a *property* renders through {@link PropertyPreview}, so a
|
|
56
|
+
* status keeps its enum's colour and an image stays a storage thumbnail. A role
|
|
57
|
+
* filled by a resolver has no property behind it and renders as what it
|
|
58
|
+
* returned.
|
|
59
|
+
*/
|
|
60
|
+
export function EntityDisplayHeader<M extends Record<string, unknown>>({
|
|
61
|
+
entity,
|
|
62
|
+
collection,
|
|
63
|
+
showTitle = false,
|
|
64
|
+
className
|
|
65
|
+
}: EntityDisplayHeaderProps<M>) {
|
|
66
|
+
|
|
67
|
+
const display = useEntityDisplayValues<M>({
|
|
68
|
+
collection,
|
|
69
|
+
entity
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const title = showTitle ? asText(display.title.value) : undefined;
|
|
73
|
+
const subtitle = asText(display.subtitle.value);
|
|
74
|
+
const tags = asTags(display.tags.value);
|
|
75
|
+
|
|
76
|
+
const hasStatus = display.status.value !== undefined;
|
|
77
|
+
const hasImage = display.image.value !== undefined;
|
|
78
|
+
|
|
79
|
+
if (!title && !subtitle && !hasStatus && !hasImage && !tags.length) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={cls("flex items-start gap-4 min-w-0", className)}>
|
|
83
|
+
|
|
84
|
+
{hasImage && (
|
|
85
|
+
<div className={"shrink-0 w-16 h-16 rounded-lg overflow-hidden flex items-center justify-center bg-surface-100 dark:bg-surface-800"}>
|
|
86
|
+
<RoleValue role={display.image} propertyFallback={
|
|
87
|
+
// A resolver's image is documented as a URL; a storage
|
|
88
|
+
// path only resolves when a property is behind it, which
|
|
89
|
+
// is what carries the storage source.
|
|
90
|
+
typeof display.image.value === "string"
|
|
91
|
+
? <img src={display.image.value}
|
|
92
|
+
alt={""}
|
|
93
|
+
className={"w-full h-full object-cover"}/>
|
|
94
|
+
: null
|
|
95
|
+
}/>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
<div className={"flex flex-col gap-1.5 min-w-0 flex-1"}>
|
|
100
|
+
|
|
101
|
+
{title && (
|
|
102
|
+
<Typography variant={"h5"} className={"min-w-0 truncate m-0"}>
|
|
103
|
+
{title}
|
|
104
|
+
</Typography>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{subtitle && (
|
|
108
|
+
<Typography variant={"body2"}
|
|
109
|
+
color={"secondary"}
|
|
110
|
+
className={"min-w-0 m-0"}>
|
|
111
|
+
{subtitle}
|
|
112
|
+
</Typography>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{(hasStatus || tags.length > 0) && (
|
|
116
|
+
<div className={"flex flex-wrap items-center gap-1.5"}>
|
|
117
|
+
{hasStatus && (
|
|
118
|
+
<RoleValue role={display.status} propertyFallback={
|
|
119
|
+
<Chip size={"small"}>{asText(display.status.value)}</Chip>
|
|
120
|
+
}/>
|
|
121
|
+
)}
|
|
122
|
+
{tags.map(tag => (
|
|
123
|
+
<Chip key={tag} size={"small"} outlined>{tag}</Chip>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* One resolved role, rendered richly when a property is behind it.
|
|
134
|
+
*
|
|
135
|
+
* `source` is set only when the value came from a property of this collection
|
|
136
|
+
* (see `useEntityDisplay`), and it carries what a rich rendering needs. Its
|
|
137
|
+
* absence is exactly the "this was computed" signal, and a computed value is a
|
|
138
|
+
* bare string with no property to describe it — hence the fallback.
|
|
139
|
+
*/
|
|
140
|
+
function RoleValue({
|
|
141
|
+
role,
|
|
142
|
+
propertyFallback
|
|
143
|
+
}: {
|
|
144
|
+
role: { value: unknown, source?: { key: string, property: Property | undefined, value: unknown } };
|
|
145
|
+
propertyFallback: React.ReactNode;
|
|
146
|
+
}) {
|
|
147
|
+
const source = role.source;
|
|
148
|
+
if (!source?.property) return <>{propertyFallback}</>;
|
|
149
|
+
return <PropertyPreview propertyKey={source.key}
|
|
150
|
+
value={source.value}
|
|
151
|
+
property={source.property}
|
|
152
|
+
hideLabel
|
|
153
|
+
size={"medium"}/>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A role's value as a line of text, or nothing when it holds nothing to read. */
|
|
157
|
+
function asText(value: unknown): string | undefined {
|
|
158
|
+
if (value === undefined || value === null) return undefined;
|
|
159
|
+
const text = String(value).trim();
|
|
160
|
+
return text.length ? text : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The `tags` role, which takes a single string as shorthand for one tag. */
|
|
164
|
+
function asTags(value: unknown): string[] {
|
|
165
|
+
if (value === undefined || value === null) return [];
|
|
166
|
+
const list = Array.isArray(value) ? value : [value];
|
|
167
|
+
return list
|
|
168
|
+
.map(entry => asText(entry))
|
|
169
|
+
.filter((entry): entry is string => Boolean(entry));
|
|
170
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
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,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
CodeIcon,
|
|
10
11
|
CopyIcon,
|
|
11
12
|
defaultBorderMixin,
|
|
13
|
+
ExternalLinkIcon,
|
|
12
14
|
HistoryIcon,
|
|
13
15
|
IconButton,
|
|
14
16
|
iconSize,
|
|
@@ -25,6 +27,15 @@ import {
|
|
|
25
27
|
export interface EntityIdentityBarProps {
|
|
26
28
|
/** Plural collection name, shown as the breadcrumb ahead of the title. */
|
|
27
29
|
collection: AdminCollection;
|
|
30
|
+
/**
|
|
31
|
+
* Where the breadcrumb points. Without it the collection name is inert text
|
|
32
|
+
* that still reads as a breadcrumb — worse than no breadcrumb at all, and
|
|
33
|
+
* the only way back once a layout drops its back arrow.
|
|
34
|
+
*
|
|
35
|
+
* Left unset by the overlays (side panel, dialog), where the collection is
|
|
36
|
+
* already underneath and navigating would dismiss the record instead.
|
|
37
|
+
*/
|
|
38
|
+
collectionUrl?: string;
|
|
28
39
|
/** Resolved record title, already guarded against showing a field default. */
|
|
29
40
|
title: string;
|
|
30
41
|
entityId?: string | number;
|
|
@@ -49,6 +60,17 @@ export interface EntityIdentityBarProps {
|
|
|
49
60
|
/** Opens the inspector straight on its history tab. */
|
|
50
61
|
onViewHistory?: () => void;
|
|
51
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Where this record appears on the live site, from `entityLinkBuilder`.
|
|
65
|
+
*
|
|
66
|
+
* Here rather than above the record, where it used to be a single unlabelled
|
|
67
|
+
* icon alone on its own right-aligned row — the only thing between the bar
|
|
68
|
+
* and the first field, and the only control in the view that did not say
|
|
69
|
+
* what it was. It is a thing you do *to* a record, so it belongs with the
|
|
70
|
+
* others.
|
|
71
|
+
*/
|
|
72
|
+
externalLink?: string;
|
|
73
|
+
|
|
52
74
|
/**
|
|
53
75
|
* Actions performed *on* the record — copy, delete, whatever the collection
|
|
54
76
|
* adds — as menu items under the overflow button. They live here rather
|
|
@@ -81,6 +103,7 @@ export interface EntityIdentityBarProps {
|
|
|
81
103
|
*/
|
|
82
104
|
export function EntityIdentityBar({
|
|
83
105
|
collection,
|
|
106
|
+
collectionUrl,
|
|
84
107
|
title,
|
|
85
108
|
entityId,
|
|
86
109
|
status,
|
|
@@ -94,6 +117,7 @@ export function EntityIdentityBar({
|
|
|
94
117
|
onBack,
|
|
95
118
|
onInspect,
|
|
96
119
|
onViewHistory,
|
|
120
|
+
externalLink,
|
|
97
121
|
recordActions,
|
|
98
122
|
pluginActions,
|
|
99
123
|
trailing,
|
|
@@ -108,7 +132,8 @@ export function EntityIdentityBar({
|
|
|
108
132
|
const closeLabel = status === "existing"
|
|
109
133
|
? t("save_and_close")
|
|
110
134
|
: status === "copy" ? t("create_copy_and_close") : t("create_and_close");
|
|
111
|
-
const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory)
|
|
135
|
+
const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory)
|
|
136
|
+
|| Boolean(externalLink);
|
|
112
137
|
|
|
113
138
|
return (
|
|
114
139
|
<div className={cls(
|
|
@@ -126,10 +151,21 @@ export function EntityIdentityBar({
|
|
|
126
151
|
</Tooltip>
|
|
127
152
|
)}
|
|
128
153
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
{collectionUrl
|
|
155
|
+
? <Link to={collectionUrl}
|
|
156
|
+
className={"visited:text-inherit dark:visited:text-inherit hidden sm:block"}>
|
|
157
|
+
<Typography variant={"caption"}
|
|
158
|
+
className={cls(
|
|
159
|
+
"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap",
|
|
160
|
+
"hover:text-text-primary dark:hover:text-text-primary-dark transition-colors"
|
|
161
|
+
)}>
|
|
162
|
+
{collection.name} /
|
|
163
|
+
</Typography>
|
|
164
|
+
</Link>
|
|
165
|
+
: <Typography variant={"caption"}
|
|
166
|
+
className={"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden sm:block"}>
|
|
167
|
+
{collection.name} /
|
|
168
|
+
</Typography>}
|
|
133
169
|
|
|
134
170
|
<span className={"font-headers font-semibold text-[15px] tracking-tight truncate min-w-0"}
|
|
135
171
|
title={title}>
|
|
@@ -189,6 +225,14 @@ export function EntityIdentityBar({
|
|
|
189
225
|
<MoreVerticalIcon size={iconSize.smallest}/>
|
|
190
226
|
</IconButton>
|
|
191
227
|
}>
|
|
228
|
+
{externalLink && (
|
|
229
|
+
<MenuItem onClick={() => window.open(externalLink, "_blank", "noopener,noreferrer")}>
|
|
230
|
+
<ExternalLinkIcon size={iconSize.smallest}/>
|
|
231
|
+
{t("open_in_live_site") ?? "Open in the live site"}
|
|
232
|
+
</MenuItem>
|
|
233
|
+
)}
|
|
234
|
+
{externalLink && (recordActions || onInspect || onViewHistory) &&
|
|
235
|
+
<Separator orientation={"horizontal"}/>}
|
|
192
236
|
{recordActions}
|
|
193
237
|
{recordActions && (onInspect || onViewHistory) && <Separator orientation={"horizontal"}/>}
|
|
194
238
|
{onViewHistory && (
|
|
@@ -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
|
|
@@ -364,7 +401,7 @@ export const EntityPreviewContainer = React.forwardRef<HTMLDivElement, EntityPre
|
|
|
364
401
|
"bg-white dark:bg-surface-900",
|
|
365
402
|
size === "small" ? "min-h-[32px]" : "min-h-[44px]",
|
|
366
403
|
fullwidth ? "w-full" : "",
|
|
367
|
-
"items-center",
|
|
404
|
+
"items-center overflow-hidden",
|
|
368
405
|
hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
|
|
369
406
|
size === "small" ? "p-1" : "px-2 py-1",
|
|
370
407
|
"flex border rounded-lg",
|