@rebasepro/admin 0.13.0 → 0.13.1-canary.gcd6689e
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-CZZZpYOz.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-CZZZpYOz.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CeoHSmUg.js} +2 -2
- package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-CeoHSmUg.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-CMKFWM2J.js} +4 -4
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-CMKFWM2J.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/AdditionalFieldValue.d.ts +25 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +29 -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/EditViewBinding.d.ts +7 -1
- package/dist/components/EntityIdentityBar.d.ts +10 -1
- package/dist/components/EntityViewBinding.d.ts +41 -6
- package/dist/components/app/Scaffold.d.ts +4 -3
- package/dist/{export-J79rMoNj.js → export-l19WevKo.js} +2 -2
- package/dist/{export-J79rMoNj.js.map → export-l19WevKo.js.map} +1 -1
- package/dist/form/components/FieldBlock.d.ts +16 -0
- package/dist/{history-D7L_JlLO.js → history-JL1ZmSKD.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-JL1ZmSKD.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-DNrKrFQV.js} +3 -3
- package/dist/{import-BvYl--U4.js.map → import-DNrKrFQV.js.map} +1 -1
- package/dist/index.js +79 -15
- package/dist/index.js.map +1 -1
- package/dist/preview/components/BooleanPreview.d.ts +3 -1
- package/dist/routes/RebaseRoute.d.ts +1 -1
- package/dist/types/components/PropertyPreviewProps.d.ts +7 -0
- package/dist/{util-GiwPgxnL.js → util-pQ3YwBve.js} +1022 -641
- package/dist/util-pQ3YwBve.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/EntityCardBinding.tsx +10 -33
- package/src/components/CollectionViewBinding/SlotValue.tsx +52 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +7 -5
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +115 -48
- package/src/components/DetailViewBinding.tsx +71 -54
- package/src/components/EditViewBinding.tsx +51 -33
- package/src/components/EntityIdentityBar.tsx +26 -4
- package/src/components/EntityPreviewBinding.tsx +24 -12
- package/src/components/EntityViewBinding.tsx +237 -39
- package/src/components/InlineEntityPreview.tsx +10 -10
- package/src/components/app/Scaffold.tsx +48 -7
- package/src/form/EntityForm.tsx +6 -24
- package/src/form/components/FieldBlock.tsx +24 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
- package/src/hooks/useEntityDisplay.tsx +258 -0
- package/src/hooks/useEntityDisplayTitle.tsx +27 -38
- package/src/preview/PropertyPreview.tsx +1 -1
- package/src/preview/components/BooleanPreview.tsx +5 -2
- package/src/routes/RebaseRoute.tsx +62 -4
- package/src/types/components/PropertyPreviewProps.tsx +8 -0
- package/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -3,12 +3,14 @@ import type { Property, RelationProperty } from "@rebasepro/types";
|
|
|
3
3
|
import type { Entity, EntityRelation } from "@rebasepro/types";
|
|
4
4
|
import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
5
5
|
import { getEntityImagePreviewPropertyKey } from "@rebasepro/app";
|
|
6
|
+
import { getDisplayPropertyKey } from "@rebasepro/app";
|
|
6
7
|
import { getLeadingRelationTitleKey, getTitlePropertyCandidates, looksLikeIdentifierValue } from "@rebasepro/app";
|
|
7
8
|
import { getEntityFromCache } from "@rebasepro/app";
|
|
8
9
|
import { getEntityPreviewKeys } from "../../util/previews";
|
|
9
10
|
import { getValueInPath } from "@rebasepro/utils";
|
|
10
11
|
import type { AuthController } from "@rebasepro/admin-types";
|
|
11
12
|
import { ChipColorScheme, CHIP_COLORS, CHIP_SEED_KEYS } from "@rebasepro/ui";
|
|
13
|
+
import { useEntityDisplay } from "../../hooks/useEntityDisplay";
|
|
12
14
|
|
|
13
15
|
/** Whether an authored relation yields many rows. Derived from its kind. */
|
|
14
16
|
function relationCardinality(relation: { kind?: string; cardinality?: string } | undefined): "one" | "many" | undefined {
|
|
@@ -23,12 +25,18 @@ function relationCardinality(relation: { kind?: string; cardinality?: string } |
|
|
|
23
25
|
// ── Slot types ────────────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* A resolved "slot"
|
|
27
|
-
*
|
|
28
|
+
* A resolved "slot": what fills one display role for one record.
|
|
29
|
+
*
|
|
30
|
+
* `property` and `propertyKey` are set when the value was read off the record,
|
|
31
|
+
* and absent when a `display` resolver computed it — a computed title has no
|
|
32
|
+
* column behind it, so there is nothing to render it *as*. Renderers must go
|
|
33
|
+
* through {@link SlotValue}, which makes that choice once: a property renders
|
|
34
|
+
* with its own preview (an enum keeps its coloured chip, a date its format), a
|
|
35
|
+
* computed value renders as text.
|
|
28
36
|
*/
|
|
29
37
|
export interface PreviewSlot {
|
|
30
|
-
property
|
|
31
|
-
propertyKey
|
|
38
|
+
property?: Property;
|
|
39
|
+
propertyKey?: string;
|
|
32
40
|
value: unknown;
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -129,10 +137,19 @@ export function resolveCollectionSlotKeys(
|
|
|
129
137
|
? [leadingRelationKey, ...rankedCandidates.filter(key => key !== leadingRelationKey)]
|
|
130
138
|
: rankedCandidates;
|
|
131
139
|
const titleKey = titleKeyCandidates[0];
|
|
140
|
+
// `getEntityImagePreviewPropertyKey` prefers `display.image` itself, so
|
|
141
|
+
// every caller of it — this, the reference card, the board — agrees.
|
|
132
142
|
const imageKey = getEntityImagePreviewPropertyKey(collection);
|
|
133
143
|
|
|
134
|
-
// Status: first string-enum that isn't the title
|
|
135
|
-
|
|
144
|
+
// Status: declared, else the first string-enum that isn't the title.
|
|
145
|
+
//
|
|
146
|
+
// Everything below this line is inference — "the first enum", "a key that
|
|
147
|
+
// looks like a timestamp", "the longest remaining string". It is a good
|
|
148
|
+
// guess and it is only a guess, so a collection that states the role wins
|
|
149
|
+
// outright and the ladder is skipped. That is the whole point of
|
|
150
|
+
// `admin.display`: the heuristics stay for the collections that never say
|
|
151
|
+
// anything, and stop overruling the ones that do.
|
|
152
|
+
let statusKey: string | undefined = getDisplayPropertyKey(collection, "status");
|
|
136
153
|
|
|
137
154
|
// 1. Explicitly defined in previewProperties
|
|
138
155
|
if (!statusKey && collection.previewProperties) {
|
|
@@ -169,13 +186,15 @@ export function resolveCollectionSlotKeys(
|
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
|
|
172
|
-
// Date:
|
|
173
|
-
let dateKey: string | undefined;
|
|
189
|
+
// Date: declared, else a well-known timestamp field, else any date
|
|
190
|
+
let dateKey: string | undefined = getDisplayPropertyKey(collection, "date");
|
|
174
191
|
const dateCandidates = ["updated_at", "updatedAt", "modified_at", "modifiedAt", "created_at", "createdAt"];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
192
|
+
if (!dateKey) {
|
|
193
|
+
for (const candidate of dateCandidates) {
|
|
194
|
+
if (collection.properties[candidate]) {
|
|
195
|
+
dateKey = candidate;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
179
198
|
}
|
|
180
199
|
}
|
|
181
200
|
if (!dateKey) {
|
|
@@ -222,7 +241,8 @@ export function resolveCollectionSlotKeys(
|
|
|
222
241
|
const scoreB = propB?.type === "string" ? (propB.admin?.multiline ? 2 : 1) : 0;
|
|
223
242
|
return scoreB - scoreA;
|
|
224
243
|
});
|
|
225
|
-
const subtitleKey =
|
|
244
|
+
const subtitleKey = getDisplayPropertyKey(collection, "subtitle")
|
|
245
|
+
?? (sortedPreviewKeys.length > 0 ? sortedPreviewKeys[0] : undefined);
|
|
226
246
|
|
|
227
247
|
return { titleKey,
|
|
228
248
|
titleKeyCandidates,
|
|
@@ -506,43 +526,26 @@ function resolveRelationDisplayName(
|
|
|
506
526
|
}
|
|
507
527
|
|
|
508
528
|
// Helper: extract display name from entity values using the target collection
|
|
529
|
+
//
|
|
530
|
+
// This used to carry a ranking of its own — priority keys `name/title/label/
|
|
531
|
+
// displayName`, then the first visible non-id string — which is a *seventh*
|
|
532
|
+
// answer to "what is this record called", and one that disagreed with the
|
|
533
|
+
// shared one: it read `titleProperty` with a flat lookup (so a dotted path
|
|
534
|
+
// was ignored), never saw `display.title`, and ranked by key name where the
|
|
535
|
+
// shared ranking excludes identifiers structurally. A chip and the record it
|
|
536
|
+
// points at could therefore be labelled differently on the same screen.
|
|
537
|
+
//
|
|
538
|
+
// What is left here is the part that is genuinely local: a relation carries
|
|
539
|
+
// *eagerly loaded* values, not an entity, so a resolver cannot run against
|
|
540
|
+
// it — a computed title falls back to the ranked property, which is the same
|
|
541
|
+
// thing the list row shows while its own resolver is in flight.
|
|
509
542
|
const extractDisplayName = (values: Record<string, unknown>): string | undefined => {
|
|
510
543
|
if (targetCollection) {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// Helper to check if a property is hidden/internal
|
|
517
|
-
const isHiddenProp = (p: Property): boolean => {
|
|
518
|
-
if (p.admin?.hideFromCollection) return true;
|
|
519
|
-
if (typeof p.admin?.disabled === "object" && p.admin.disabled.hidden) return true;
|
|
520
|
-
return false;
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Helper to check if a property is a visible, non-id string
|
|
524
|
-
const isDisplayCandidate = (p: Property): boolean => {
|
|
525
|
-
return p.type === "string" && !p.admin?.multiline && !p.admin?.markdown && !p.storage
|
|
526
|
-
&& !("isId" in p && p.isId) && !isHiddenProp(p);
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
// Prioritize common title-like fields: name, title, label, displayName
|
|
530
|
-
const priorityKeys = ["name", "title", "label", "displayName"];
|
|
531
|
-
for (const pk of priorityKeys) {
|
|
532
|
-
const p = targetCollection.properties[pk] as Property | undefined;
|
|
533
|
-
if (p && isDisplayCandidate(p) && values[pk] !== undefined && values[pk] !== null) {
|
|
534
|
-
return String(values[pk]);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Fallback: find first visible, non-id string property in target
|
|
539
|
-
for (const [k, p] of Object.entries(targetCollection.properties)) {
|
|
540
|
-
const tp = p as Property;
|
|
541
|
-
if (isDisplayCandidate(tp)) {
|
|
542
|
-
if (values[k] !== undefined && values[k] !== null) {
|
|
543
|
-
return String(values[k]);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
544
|
+
for (const key of getTitlePropertyCandidates(targetCollection)) {
|
|
545
|
+
const value = getValueInPath(values, key);
|
|
546
|
+
if (value === undefined || value === null || value === "") continue;
|
|
547
|
+
if (typeof value === "string" && looksLikeIdentifierValue(value)) continue;
|
|
548
|
+
return String(value);
|
|
546
549
|
}
|
|
547
550
|
}
|
|
548
551
|
// Generic fallback: walk entity values for any short string.
|
|
@@ -582,3 +585,67 @@ function resolveRelationDisplayName(
|
|
|
582
585
|
// 3. Final fallback: show the ID
|
|
583
586
|
return id !== undefined ? String(id) : undefined;
|
|
584
587
|
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* The slots for one record, with `admin.display` resolvers applied.
|
|
591
|
+
*
|
|
592
|
+
* {@link resolveEntitySlots} answers the question from values already in hand —
|
|
593
|
+
* which is everything a slot filled by a property needs, and nothing a slot
|
|
594
|
+
* filled by a *resolver* can use. A computed title may have to be fetched, so it
|
|
595
|
+
* arrives after the first render; this hook is where the two meet.
|
|
596
|
+
*
|
|
597
|
+
* Until a resolver lands, the derived slot shows. That is deliberate: a list of
|
|
598
|
+
* fifty rows must not blank its titles for a beat while fifty promises settle,
|
|
599
|
+
* and the derived answer — the best-ranked property carrying a readable value —
|
|
600
|
+
* is a reasonable thing to be looking at meanwhile. When the resolved value
|
|
601
|
+
* arrives it replaces the slot outright, property and all, because there is no
|
|
602
|
+
* property behind a computed value (see {@link PreviewSlot}).
|
|
603
|
+
*
|
|
604
|
+
* Every role is hooked unconditionally, so the hook order is fixed regardless of
|
|
605
|
+
* what a given collection declares.
|
|
606
|
+
*/
|
|
607
|
+
export function useEntitySlots(
|
|
608
|
+
entity: Entity<Record<string, unknown>>,
|
|
609
|
+
collection: AdminCollection<Record<string, unknown>>,
|
|
610
|
+
slotKeys: CollectionSlotKeys
|
|
611
|
+
): EntityPreviewSlots {
|
|
612
|
+
|
|
613
|
+
const derived = useMemo(
|
|
614
|
+
() => resolveEntitySlots(entity, collection, slotKeys),
|
|
615
|
+
[entity, collection, slotKeys]
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
const params = { collection,
|
|
619
|
+
entity };
|
|
620
|
+
const title = useEntityDisplay<string>("title", params);
|
|
621
|
+
const subtitle = useEntityDisplay<string>("subtitle", params);
|
|
622
|
+
const image = useEntityDisplay<string>("image", params);
|
|
623
|
+
const status = useEntityDisplay<string>("status", params);
|
|
624
|
+
const date = useEntityDisplay<Date | string | number>("date", params);
|
|
625
|
+
|
|
626
|
+
return useMemo(() => {
|
|
627
|
+
// `source` is set only when the value came from a property, so its
|
|
628
|
+
// absence is exactly the "this was computed" signal — no second flag to
|
|
629
|
+
// keep in step with the first.
|
|
630
|
+
const computed = (
|
|
631
|
+
resolved: { value: unknown, source?: unknown }
|
|
632
|
+
): PreviewSlot | undefined =>
|
|
633
|
+
resolved.value !== undefined && resolved.source === undefined
|
|
634
|
+
? { value: resolved.value }
|
|
635
|
+
: undefined;
|
|
636
|
+
|
|
637
|
+
const computedDate = computed(date);
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
...derived,
|
|
641
|
+
title: computed(title) ?? derived.title,
|
|
642
|
+
subtitle: computed(subtitle) ?? derived.subtitle,
|
|
643
|
+
image: computed(image) ?? derived.image,
|
|
644
|
+
status: computed(status) ?? derived.status,
|
|
645
|
+
date: computedDate
|
|
646
|
+
? { ...computedDate,
|
|
647
|
+
formatted: formatDateValue(computedDate.value) }
|
|
648
|
+
: derived.date
|
|
649
|
+
};
|
|
650
|
+
}, [derived, title, subtitle, image, status, date]);
|
|
651
|
+
}
|
|
@@ -10,11 +10,11 @@ import { CollectionViewBinding } from "./CollectionViewBinding/CollectionViewBin
|
|
|
10
10
|
import { EntityViewBinding } from "./EntityViewBinding";
|
|
11
11
|
import { EntityIdentityBar } from "./EntityIdentityBar";
|
|
12
12
|
import { SplitListCloseButton } from "./CollectionViewBinding/SplitListCloseButton";
|
|
13
|
+
import { SplitListShowButton } from "./CollectionViewBinding/SplitListShowButton";
|
|
13
14
|
import { EntityInspector, InspectorTab } from "./EntityInspector";
|
|
14
15
|
import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
|
|
15
16
|
import {
|
|
16
17
|
Alert,
|
|
17
|
-
ArrowLeftIcon,
|
|
18
18
|
Button,
|
|
19
19
|
CenteredView,
|
|
20
20
|
cls,
|
|
@@ -34,6 +34,7 @@ import { ErrorBoundary } from "@rebasepro/ui";
|
|
|
34
34
|
import { ErrorView, createFormexStub, usePermissions, useTranslation, getIcon } from "@rebasepro/app";
|
|
35
35
|
|
|
36
36
|
import { removeInitialAndTrailingSlashes, resolveDefaultSelectedView } from "@rebasepro/app";
|
|
37
|
+
import { withViewMode } from "../util/view_mode";
|
|
37
38
|
import { resolvedSelectedEntityView } from "../util/resolutions";
|
|
38
39
|
import {
|
|
39
40
|
useCustomizationController,
|
|
@@ -45,9 +46,7 @@ import {
|
|
|
45
46
|
import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
|
|
46
47
|
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
47
48
|
import { useNavigate } from "react-router";
|
|
48
|
-
import {
|
|
49
|
-
import { getEntityTitlePropertyKeyForEntity, isUserSelectProperty, resolveTitleToString } from "../util/previews";
|
|
50
|
-
import { getUserLabel, useResolvedUser } from "../hooks/useResolvedUsers";
|
|
49
|
+
import { useEntityDisplayTitle } from "../hooks/useEntityDisplayTitle";
|
|
51
50
|
|
|
52
51
|
|
|
53
52
|
import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/view_constants";
|
|
@@ -82,6 +81,12 @@ export interface DetailViewBindingProps<M extends Record<string, unknown> = Reco
|
|
|
82
81
|
* of the breadcrumb — the side panel's close button.
|
|
83
82
|
*/
|
|
84
83
|
barActionsStart?: React.ReactNode;
|
|
84
|
+
/**
|
|
85
|
+
* Full screen only: bring the list back beside this record. Set when the
|
|
86
|
+
* record was opened out of a split collection, so folding the list away is
|
|
87
|
+
* reversible; it takes the place of the back arrow when present.
|
|
88
|
+
*/
|
|
89
|
+
onShowList?: () => void;
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
export function DetailViewBinding<M extends Record<string, unknown>>({
|
|
@@ -141,7 +146,8 @@ function DetailViewBindingInner<M extends Record<string, unknown>>({
|
|
|
141
146
|
dataLoading,
|
|
142
147
|
layout = "full_screen",
|
|
143
148
|
barActions,
|
|
144
|
-
barActionsStart
|
|
149
|
+
barActionsStart,
|
|
150
|
+
onShowList
|
|
145
151
|
}: DetailViewBindingProps<M> & {
|
|
146
152
|
entity?: Entity<M>,
|
|
147
153
|
dataLoading: boolean,
|
|
@@ -283,18 +289,13 @@ entityId }
|
|
|
283
289
|
const formViewConfig = (collection as AdminCollection<M> & { formView?: FormViewConfig<M> }).formView;
|
|
284
290
|
const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
|
|
285
291
|
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const title = titleUser
|
|
294
|
-
? getUserLabel(titleUser)
|
|
295
|
-
: (rawTitle !== undefined && rawTitle !== null
|
|
296
|
-
? resolveTitleToString(rawTitle)
|
|
297
|
-
: (collection.singularName ?? collection.name));
|
|
292
|
+
// The heading, resolved the one way — this used to be a copy of
|
|
293
|
+
// `useEntityDisplayTitle`'s body, and the two had already drifted.
|
|
294
|
+
const title = useEntityDisplayTitle({
|
|
295
|
+
collection,
|
|
296
|
+
entity: usedEntity,
|
|
297
|
+
status: "existing"
|
|
298
|
+
});
|
|
298
299
|
|
|
299
300
|
// Non-action custom views
|
|
300
301
|
const nonActionCustomViews = useMemo(() =>
|
|
@@ -380,20 +381,31 @@ entityId }
|
|
|
380
381
|
}, [onTabChange, path, entityId, collection]);
|
|
381
382
|
|
|
382
383
|
const propertyDetailView = () => {
|
|
383
|
-
// formView.Builder replaces the default property display
|
|
384
|
+
// formView.Builder replaces the default property display. It gets the
|
|
385
|
+
// centred column the record used to be given here; the default display
|
|
386
|
+
// now lays itself out, rail included.
|
|
384
387
|
if (FormViewBuilder && usedEntity) {
|
|
385
|
-
return <
|
|
386
|
-
<
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
388
|
+
return <div className={"flex-1 min-w-0 overflow-y-auto flex justify-center items-start"}>
|
|
389
|
+
<div className={cls(
|
|
390
|
+
"w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
|
|
391
|
+
layout === "dialog"
|
|
392
|
+
? "pt-5 pb-12 px-6 sm:px-8"
|
|
393
|
+
: "pt-6 pb-16 px-5 sm:px-8"
|
|
394
|
+
)}>
|
|
395
|
+
<ErrorBoundary>
|
|
396
|
+
<Suspense fallback={<CircularProgressCenter />}>
|
|
397
|
+
<FormViewBuilder
|
|
398
|
+
collection={collection}
|
|
399
|
+
parentCollectionSlugs={parentCollectionSlugs}
|
|
400
|
+
parentEntityIds={parentEntityIds}
|
|
401
|
+
entity={usedEntity}
|
|
402
|
+
modifiedValues={usedEntity?.values}
|
|
403
|
+
formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
|
|
404
|
+
/>
|
|
405
|
+
</Suspense>
|
|
406
|
+
</ErrorBoundary>
|
|
407
|
+
</div>
|
|
408
|
+
</div>;
|
|
397
409
|
}
|
|
398
410
|
|
|
399
411
|
return (
|
|
@@ -402,6 +414,9 @@ entityId }
|
|
|
402
414
|
entity={usedEntity}
|
|
403
415
|
collection={collection}
|
|
404
416
|
path={path}
|
|
417
|
+
asPage
|
|
418
|
+
openEntityMode={layout}
|
|
419
|
+
formContext={readOnlyFormContext}
|
|
405
420
|
/>}
|
|
406
421
|
</>
|
|
407
422
|
);
|
|
@@ -458,24 +473,27 @@ entityId }
|
|
|
458
473
|
);
|
|
459
474
|
});
|
|
460
475
|
|
|
476
|
+
// The breadcrumb's destination, and the back arrow's when it renders. Both
|
|
477
|
+
// keep the view mode: a collection reached from one of its own records must
|
|
478
|
+
// come back as the user left it.
|
|
479
|
+
const collectionUrl = withViewMode(urlController.buildUrlCollectionPath(path));
|
|
480
|
+
|
|
461
481
|
// Full screen is the one layout with no way out of its own: a side panel and a
|
|
462
482
|
// dialog can be dismissed, and a split keeps the list beside it, but opening an
|
|
463
483
|
// entity full screen replaces the collection entirely and left browser Back as
|
|
464
484
|
// the only route back to it — which is not an affordance the page shows, and is
|
|
465
485
|
// wrong anyway once the user has moved between tabs inside the entity.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews || layout === "side_panel" || layout === "dialog" || Boolean(backToCollectionButton);
|
|
486
|
+
//
|
|
487
|
+
// Unless the list can be unfolded back beside the record: that chevron
|
|
488
|
+
// leads to the same collection while keeping the record open, so it stands
|
|
489
|
+
// alone rather than beside an arrow going almost the same place.
|
|
490
|
+
//
|
|
491
|
+
// It goes to the bar's leading edge — where a back control reads as back,
|
|
492
|
+
// and where the edit view has always put the same arrow — rather than to
|
|
493
|
+
// the trailing edge among the record's own actions.
|
|
494
|
+
const backToCollection = layout === "full_screen" && !onShowList
|
|
495
|
+
? () => navigate(collectionUrl)
|
|
496
|
+
: undefined;
|
|
479
497
|
|
|
480
498
|
const fullScreenButton = !barActions && // Not in split: the list panel beside it carries the close control,
|
|
481
499
|
// and an expand button there competes with it.
|
|
@@ -508,43 +526,42 @@ entityId }
|
|
|
508
526
|
// Main content view. The title, the `path/id` chip and the empty band under
|
|
509
527
|
// them have moved into the identity bar, and the `w-80 2xl:w-96` rail that
|
|
510
528
|
// held a single Edit button is gone — the bar holds it now.
|
|
529
|
+
//
|
|
530
|
+
// The scroll now lives on the record's own column rather than on this row,
|
|
531
|
+
// so the metadata rail beside it stays put while the record scrolls — the
|
|
532
|
+
// same arrangement the form uses.
|
|
511
533
|
const mainView = <div
|
|
512
534
|
className={cls(
|
|
513
|
-
"flex-1 flex flex-row w-full
|
|
535
|
+
"flex-1 flex flex-row w-full min-h-0",
|
|
514
536
|
!mainViewVisible ? "hidden" : ""
|
|
515
537
|
)}>
|
|
516
|
-
|
|
517
|
-
"w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
|
|
518
|
-
layout === "dialog"
|
|
519
|
-
? "pt-5 pb-12 px-6 sm:px-8"
|
|
520
|
-
: "pt-6 pb-16 px-5 sm:px-8"
|
|
521
|
-
)}>
|
|
522
|
-
{propertyDetailView()}
|
|
523
|
-
</div>
|
|
538
|
+
{propertyDetailView()}
|
|
524
539
|
</div>;
|
|
525
540
|
|
|
526
541
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
|
|
527
542
|
|
|
528
543
|
<EntityIdentityBar
|
|
529
544
|
collection={collection as AdminCollection}
|
|
545
|
+
collectionUrl={layout === "full_screen" || layout === "split" ? collectionUrl : undefined}
|
|
530
546
|
title={title}
|
|
531
547
|
entityId={entityId}
|
|
532
548
|
status={"existing"}
|
|
533
549
|
dirty={false}
|
|
534
550
|
saving={false}
|
|
551
|
+
onBack={backToCollection}
|
|
535
552
|
onInspect={includeJsonView ? () => setInspectorTab("json") : undefined}
|
|
536
553
|
onViewHistory={includeHistoryView ? () => setInspectorTab("history") : undefined}
|
|
537
554
|
leading={<>
|
|
538
555
|
{barActionsStart}
|
|
539
556
|
{/* Split view: closing the list is opening this record
|
|
540
557
|
full screen, and the control for it belongs at this
|
|
541
|
-
bar's leading edge. */}
|
|
558
|
+
bar's leading edge. Full screen carries its mirror. */}
|
|
542
559
|
{layout === "split" && <SplitListCloseButton/>}
|
|
560
|
+
{layout === "full_screen" && onShowList && <SplitListShowButton onClick={onShowList}/>}
|
|
543
561
|
</>}
|
|
544
562
|
trailing={<>
|
|
545
563
|
{editButton}
|
|
546
564
|
{pluginActionsTop}
|
|
547
|
-
{backToCollectionButton}
|
|
548
565
|
{fullScreenButton}
|
|
549
566
|
{barActions?.({
|
|
550
567
|
path,
|
|
@@ -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 ? () => {
|
|
@@ -717,8 +734,9 @@ parentEntityIds,
|
|
|
717
734
|
{barActionsStart}
|
|
718
735
|
{/* Split view: closing the list is opening this record
|
|
719
736
|
full screen, and the control for it belongs at this
|
|
720
|
-
bar's leading edge. */}
|
|
737
|
+
bar's leading edge. Full screen carries its mirror. */}
|
|
721
738
|
{layout === "split" && <SplitListCloseButton/>}
|
|
739
|
+
{layout === "full_screen" && onShowList && <SplitListShowButton onClick={onShowList}/>}
|
|
722
740
|
</>}
|
|
723
741
|
trailing={<>
|
|
724
742
|
{pluginActionsTop}
|
|
@@ -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,
|
|
@@ -25,6 +26,15 @@ import {
|
|
|
25
26
|
export interface EntityIdentityBarProps {
|
|
26
27
|
/** Plural collection name, shown as the breadcrumb ahead of the title. */
|
|
27
28
|
collection: AdminCollection;
|
|
29
|
+
/**
|
|
30
|
+
* Where the breadcrumb points. Without it the collection name is inert text
|
|
31
|
+
* that still reads as a breadcrumb — worse than no breadcrumb at all, and
|
|
32
|
+
* the only way back once a layout drops its back arrow.
|
|
33
|
+
*
|
|
34
|
+
* Left unset by the overlays (side panel, dialog), where the collection is
|
|
35
|
+
* already underneath and navigating would dismiss the record instead.
|
|
36
|
+
*/
|
|
37
|
+
collectionUrl?: string;
|
|
28
38
|
/** Resolved record title, already guarded against showing a field default. */
|
|
29
39
|
title: string;
|
|
30
40
|
entityId?: string | number;
|
|
@@ -81,6 +91,7 @@ export interface EntityIdentityBarProps {
|
|
|
81
91
|
*/
|
|
82
92
|
export function EntityIdentityBar({
|
|
83
93
|
collection,
|
|
94
|
+
collectionUrl,
|
|
84
95
|
title,
|
|
85
96
|
entityId,
|
|
86
97
|
status,
|
|
@@ -126,10 +137,21 @@ export function EntityIdentityBar({
|
|
|
126
137
|
</Tooltip>
|
|
127
138
|
)}
|
|
128
139
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
{collectionUrl
|
|
141
|
+
? <Link to={collectionUrl}
|
|
142
|
+
className={"visited:text-inherit dark:visited:text-inherit hidden sm:block"}>
|
|
143
|
+
<Typography variant={"caption"}
|
|
144
|
+
className={cls(
|
|
145
|
+
"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap",
|
|
146
|
+
"hover:text-text-primary dark:hover:text-text-primary-dark transition-colors"
|
|
147
|
+
)}>
|
|
148
|
+
{collection.name} /
|
|
149
|
+
</Typography>
|
|
150
|
+
</Link>
|
|
151
|
+
: <Typography variant={"caption"}
|
|
152
|
+
className={"text-text-disabled dark:text-text-disabled-dark whitespace-nowrap hidden sm:block"}>
|
|
153
|
+
{collection.name} /
|
|
154
|
+
</Typography>}
|
|
133
155
|
|
|
134
156
|
<span className={"font-headers font-semibold text-[15px] tracking-tight truncate min-w-0"}
|
|
135
157
|
title={title}>
|