@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
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type { EntityViewBindingProps } from "./EntityViewBinding";
|
|
2
2
|
export { EntityViewBinding } from "./EntityViewBinding";
|
|
3
3
|
|
|
4
|
+
export type { EntityDisplayHeaderProps } from "./EntityDisplayHeader";
|
|
5
|
+
export { EntityDisplayHeader, HEADER_DISPLAY_ROLES } from "./EntityDisplayHeader";
|
|
6
|
+
|
|
4
7
|
export * from "./DetailViewBinding";
|
|
5
8
|
|
|
6
9
|
export type { SelectionProps } from "./ReferenceTable/SelectionTableBinding";
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -13,11 +13,12 @@ import { isReadOnly } from "@rebasepro/app";
|
|
|
13
13
|
|
|
14
14
|
import { getFormFieldKeys, resolveFormLayout } from "@rebasepro/app";
|
|
15
15
|
import type { ResolvedFormField } from "@rebasepro/app";
|
|
16
|
-
import { Alert, Button, cls, Dialog, DialogActions, DialogContent, DialogTitle,
|
|
16
|
+
import { Alert, Button, cls, defaultBorderMixin, Dialog, DialogActions, DialogContent, DialogTitle, paperMixin, Typography } from "@rebasepro/ui";
|
|
17
17
|
import { Formex, FormexController, useCreateFormex } from "@rebasepro/forms";
|
|
18
18
|
|
|
19
|
-
import { FieldBlock, isSelfLabellingProperty } from "./components/FieldBlock";
|
|
20
|
-
import {
|
|
19
|
+
import { FieldBlock, isSelfLabellingProperty, LABEL_ICON_SIZE, spanClass } from "./components/FieldBlock";
|
|
20
|
+
import { AdditionalFieldValue } from "../components/AdditionalFieldValue";
|
|
21
|
+
import { FormRail, RecordMeta } from "./components/FormRail";
|
|
21
22
|
import { FormSections } from "./components/FormSections";
|
|
22
23
|
import { useRailVisible } from "./components/useRailVisible";
|
|
23
24
|
import { LabelWithIconAndTooltip } from "./components/LabelWithIconAndTooltip";
|
|
@@ -440,10 +441,10 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
440
441
|
const child = AdditionalFieldBuilder
|
|
441
442
|
? <AdditionalFieldBuilder entity={entity} context={additionalFieldContext}/>
|
|
442
443
|
: <div className={"w-full"}>
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
<AdditionalFieldValue
|
|
445
|
+
field={additionalField}
|
|
446
|
+
entity={entity}
|
|
447
|
+
context={additionalFieldContext}/>
|
|
447
448
|
</div>;
|
|
448
449
|
|
|
449
450
|
return (
|
|
@@ -451,7 +452,7 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
451
452
|
className={spanClass(field.span)}>
|
|
452
453
|
<LabelWithIconAndTooltip
|
|
453
454
|
propertyKey={field.key}
|
|
454
|
-
icon={<AlignLeftIcon size={
|
|
455
|
+
icon={<AlignLeftIcon size={LABEL_ICON_SIZE}/>}
|
|
455
456
|
title={additionalField.name}
|
|
456
457
|
className={"text-text-secondary dark:text-text-secondary-dark"}/>
|
|
457
458
|
<div className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-auto no-scrollbar")}>
|
|
@@ -537,12 +538,25 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
537
538
|
}
|
|
538
539
|
];
|
|
539
540
|
|
|
540
|
-
return
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
541
|
+
return <>
|
|
542
|
+
<FormSections
|
|
543
|
+
sections={sections}
|
|
544
|
+
collapsed={collapsedSections}
|
|
545
|
+
onToggle={toggleSection}
|
|
546
|
+
errorKeys={errorKeys}
|
|
547
|
+
renderField={renderField}/>
|
|
548
|
+
|
|
549
|
+
{/* Folded back with the rail's fields, and for the same reason. The
|
|
550
|
+
rail renders only where it has been measured to fit, so on the
|
|
551
|
+
split pane, the side panel and the dialog this block was dropped
|
|
552
|
+
outright and the record's id was nowhere on screen but the
|
|
553
|
+
truncated chip in the bar. */}
|
|
554
|
+
{!showRail && layout.showRecordMeta && status === "existing" && entity && (
|
|
555
|
+
<div className={cls("mt-8 pt-5 border-t max-w-sm", defaultBorderMixin)}>
|
|
556
|
+
<RecordMeta entity={entity as Entity<Record<string, unknown>>}/>
|
|
557
|
+
</div>
|
|
558
|
+
)}
|
|
559
|
+
</>;
|
|
546
560
|
};
|
|
547
561
|
|
|
548
562
|
const formRef = useRef<HTMLDivElement>(null);
|
|
@@ -706,25 +720,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
706
720
|
);
|
|
707
721
|
}
|
|
708
722
|
|
|
709
|
-
/**
|
|
710
|
-
* Grid placement for a span.
|
|
711
|
-
*
|
|
712
|
-
* Written out as literal class strings rather than composed, because Tailwind
|
|
713
|
-
* scans source text: a template literal would produce classes that exist at
|
|
714
|
-
* runtime and were never generated into the stylesheet.
|
|
715
|
-
*
|
|
716
|
-
* Below the column breakpoint every field takes the single available column, so
|
|
717
|
-
* the side panel, the dialog and mobile all get a plain stack.
|
|
718
|
-
*/
|
|
719
|
-
function spanClass(span: 1 | 2 | 3 | 4): string {
|
|
720
|
-
switch (span) {
|
|
721
|
-
case 1: return "@2xl/col:col-span-1";
|
|
722
|
-
case 2: return "@2xl/col:col-span-2";
|
|
723
|
-
case 3: return "@2xl/col:col-span-3";
|
|
724
|
-
case 4: return "@2xl/col:col-span-4";
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
723
|
function useOnAutoSave<M extends Record<string, unknown>>(autoSave: undefined | boolean, formex: FormexController<M>, lastSavedValues: React.MutableRefObject<EntityValues<M> | undefined>, save: (values: EntityValues<M>) => Promise<Entity<M> | void>) {
|
|
729
724
|
useEffect(() => {
|
|
730
725
|
if (!autoSave) return;
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import type { Property } from "@rebasepro/types";
|
|
2
|
+
import type { PropertySpan } from "@rebasepro/admin-types";
|
|
2
3
|
import React from "react";
|
|
3
4
|
import { cls, ErrorBoundary, Typography } from "@rebasepro/ui";
|
|
4
5
|
import { getIconForProperty } from "../../util/property_utils";
|
|
5
6
|
import { PropertyIdCopyTooltip } from "../../components/PropertyIdCopyTooltip";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Grid placement for a span.
|
|
10
|
+
*
|
|
11
|
+
* Written out as literal class strings rather than composed, because Tailwind
|
|
12
|
+
* scans source text: a template literal would produce classes that exist at
|
|
13
|
+
* runtime and were never generated into the stylesheet.
|
|
14
|
+
*
|
|
15
|
+
* Below the column breakpoint every field takes the single available column, so
|
|
16
|
+
* the side panel, the dialog and mobile all get a plain stack.
|
|
17
|
+
*
|
|
18
|
+
* Lives here rather than in the form, because the read-only rendering of a
|
|
19
|
+
* record places its fields on the same grid — a record that changes shape when
|
|
20
|
+
* you press Edit is the bug this whole layout exists to avoid.
|
|
21
|
+
*/
|
|
22
|
+
export function spanClass(span: PropertySpan): string {
|
|
23
|
+
switch (span) {
|
|
24
|
+
case 1: return "@2xl/col:col-span-1";
|
|
25
|
+
case 2: return "@2xl/col:col-span-2";
|
|
26
|
+
case 3: return "@2xl/col:col-span-3";
|
|
27
|
+
case 4: return "@2xl/col:col-span-4";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
/**
|
|
8
32
|
* Does this property's editor supply its own heading?
|
|
9
33
|
*
|
|
@@ -46,28 +70,58 @@ export function isSelfLabellingProperty(property: Property | undefined): boolean
|
|
|
46
70
|
}
|
|
47
71
|
}
|
|
48
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The type icon in a field label, in px.
|
|
75
|
+
*
|
|
76
|
+
* Matched to the label's 13px rather than taken from `iconSize.smallest` (16),
|
|
77
|
+
* which is sized for buttons. Exported so the surfaces that draw a label
|
|
78
|
+
* themselves — the additional-field cards, the rail — can stay in step.
|
|
79
|
+
*/
|
|
80
|
+
export const LABEL_ICON_SIZE = 14;
|
|
81
|
+
|
|
49
82
|
export interface FieldBlockProps {
|
|
50
83
|
propertyKey: string;
|
|
51
84
|
property?: Property;
|
|
52
85
|
/** Rendered by the form rather than by the field, unless self-labelling. */
|
|
53
86
|
showLabel: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Which of the two renderings this block frames.
|
|
89
|
+
*
|
|
90
|
+
* The **label is identical in both** — same size, same weight, same colour,
|
|
91
|
+
* same type icon, same required marker. A field is the same field whether or
|
|
92
|
+
* not you are editing it, and a record whose labels restyle themselves under
|
|
93
|
+
* Edit reads as two different screens.
|
|
94
|
+
*
|
|
95
|
+
* What `"read"` drops is the *description*, which is not part of the label:
|
|
96
|
+
* it is input help. "Sum of all line items before tax and shipping" under a
|
|
97
|
+
* number that is visibly a sum tells a reader nothing, and a line of it
|
|
98
|
+
* under every field is what a record has instead of room.
|
|
99
|
+
*/
|
|
100
|
+
mode?: "edit" | "read";
|
|
54
101
|
children: React.ReactNode;
|
|
55
102
|
}
|
|
56
103
|
|
|
57
104
|
/**
|
|
58
105
|
* One field's worth of the form grid: label, control, description.
|
|
59
106
|
*
|
|
60
|
-
* The
|
|
61
|
-
*
|
|
62
|
-
*
|
|
107
|
+
* The same block frames the read-only rendering of a record, so a field sits in
|
|
108
|
+
* the same place, at the same width, under the same label whether or not you are
|
|
109
|
+
* editing it — see {@link EntityViewBinding}.
|
|
110
|
+
*
|
|
111
|
+
* The label is one size down from the control it names (13px against the body's
|
|
112
|
+
* 14) with its icon matched to it at 14px. Labels are scaffolding: you read them
|
|
113
|
+
* to find the value, then read the value. Setting them at body size made a
|
|
114
|
+
* column of records scan as a column of field names with data attached.
|
|
63
115
|
*/
|
|
64
116
|
export function FieldBlock({
|
|
65
117
|
propertyKey,
|
|
66
118
|
property,
|
|
67
119
|
showLabel,
|
|
120
|
+
mode = "edit",
|
|
68
121
|
children
|
|
69
122
|
}: FieldBlockProps) {
|
|
70
123
|
|
|
124
|
+
const reading = mode === "read";
|
|
71
125
|
const description = property?.description?.trim();
|
|
72
126
|
const required = Boolean(property?.validation?.required);
|
|
73
127
|
|
|
@@ -82,15 +136,18 @@ export function FieldBlock({
|
|
|
82
136
|
{showLabel && (
|
|
83
137
|
<PropertyIdCopyTooltip propertyKey={propertyKey}>
|
|
84
138
|
<div className={cls(
|
|
85
|
-
"flex items-center gap-1.5
|
|
86
|
-
"text-text-secondary dark:text-text-secondary-dark"
|
|
139
|
+
"flex items-center gap-1.5 font-medium leading-tight mb-1.5",
|
|
140
|
+
"text-[13px] text-text-secondary dark:text-text-secondary-dark"
|
|
87
141
|
)}>
|
|
88
142
|
{/* Small and quiet: enough to tell a number from a
|
|
89
143
|
relation at a glance, not enough to compete with the
|
|
90
|
-
field name.
|
|
144
|
+
field name. Matched to the label's 13px rather than
|
|
145
|
+
left at the 16px `smallest` token, which put the
|
|
146
|
+
heaviest mark on the row on the least important
|
|
147
|
+
thing in it. */}
|
|
91
148
|
{property && (
|
|
92
149
|
<span className={"shrink-0 text-text-disabled dark:text-text-disabled-dark"}>
|
|
93
|
-
{getIconForProperty(property,
|
|
150
|
+
{getIconForProperty(property, LABEL_ICON_SIZE)}
|
|
94
151
|
</span>
|
|
95
152
|
)}
|
|
96
153
|
<span className={"truncate"}>{property?.name ?? propertyKey}</span>
|
|
@@ -107,7 +164,7 @@ export function FieldBlock({
|
|
|
107
164
|
|
|
108
165
|
{/* Under the control, where it reads as a note about the thing you
|
|
109
166
|
just filled in rather than a subtitle of the next label. */}
|
|
110
|
-
{showLabel && description && (
|
|
167
|
+
{showLabel && description && !reading && (
|
|
111
168
|
<Typography variant={"caption"}
|
|
112
169
|
color={"disabled"}
|
|
113
170
|
className={"mt-1.5 ml-0.5 leading-snug"}>
|
|
@@ -32,7 +32,14 @@ export function FormRail({
|
|
|
32
32
|
// No responsive hiding here. The caller decides whether the rail is
|
|
33
33
|
// shown, by measuring; a CSS `hidden` would leave the fields the
|
|
34
34
|
// resolver moved in here unreachable rather than folding them back.
|
|
35
|
-
|
|
35
|
+
//
|
|
36
|
+
// `w-76` (304px), between the old 288 and a full `w-80`. The rail
|
|
37
|
+
// holds real controls — a status select, a date picker — plus a
|
|
38
|
+
// record block whose id is a 36-character UUID, so 288 left the
|
|
39
|
+
// select tighter than the identical select in the column. 320 took
|
|
40
|
+
// more than it gave back: past about 300px the extra goes to the
|
|
41
|
+
// gap beside a chip, not to anything that needed it.
|
|
42
|
+
"flex flex-col gap-6 shrink-0 w-76 border-l overflow-y-auto",
|
|
36
43
|
"px-5 py-6 bg-surface-50 dark:bg-surface-900",
|
|
37
44
|
defaultBorderMixin
|
|
38
45
|
)}>
|
|
@@ -47,7 +54,17 @@ export function FormRail({
|
|
|
47
54
|
);
|
|
48
55
|
}
|
|
49
56
|
|
|
50
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The record's id and audit timestamps.
|
|
59
|
+
*
|
|
60
|
+
* Exported because the rail is not the only place it renders. The rail is shown
|
|
61
|
+
* only when it has been *measured* to fit, and every layout narrower than that —
|
|
62
|
+
* the split pane, the side panel, the dialog — folds its contents back into the
|
|
63
|
+
* column. The fields folded back but this block did not, so on three of the four
|
|
64
|
+
* layouts a record simply had no id on screen: the only copy of it was the
|
|
65
|
+
* truncated `e166a394…b422` chip in the identity bar.
|
|
66
|
+
*/
|
|
67
|
+
export function RecordMeta({ entity }: { entity: Entity<Record<string, unknown>> }) {
|
|
51
68
|
|
|
52
69
|
const createdAt = pickDate(entity.values, ["created_at", "createdAt", "created_on"]);
|
|
53
70
|
const updatedAt = pickDate(entity.values, ["updated_at", "updatedAt", "modified_at"]);
|
|
@@ -14,6 +14,20 @@ export interface FormSectionsProps {
|
|
|
14
14
|
*/
|
|
15
15
|
errorKeys: ReadonlySet<string>;
|
|
16
16
|
renderField: (field: ResolvedFormField) => React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Which rendering this is. `"read"` opens the gaps up rather than closing
|
|
19
|
+
* them: a control is a bordered 40px box that separates itself from the next
|
|
20
|
+
* one, and a value is bare text that does not. The form can also afford
|
|
21
|
+
* tighter rows because a description usually sits between them; with that
|
|
22
|
+
* gone, the same numbers ran a record's values together into one block.
|
|
23
|
+
*/
|
|
24
|
+
mode?: "edit" | "read";
|
|
25
|
+
/**
|
|
26
|
+
* Renders one field as a summary row, for a section that asked for
|
|
27
|
+
* `readVariant: "summary"`. Only the read view supplies it; without it such
|
|
28
|
+
* a section falls back to the grid rather than to nothing.
|
|
29
|
+
*/
|
|
30
|
+
renderSummaryField?: (field: ResolvedFormField, index: number, total: number) => React.ReactNode;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
export function FormSections({
|
|
@@ -21,15 +35,22 @@ export function FormSections({
|
|
|
21
35
|
collapsed,
|
|
22
36
|
onToggle,
|
|
23
37
|
errorKeys,
|
|
24
|
-
renderField
|
|
38
|
+
renderField,
|
|
39
|
+
mode = "edit",
|
|
40
|
+
renderSummaryField
|
|
25
41
|
}: FormSectionsProps) {
|
|
26
42
|
|
|
43
|
+
const reading = mode === "read";
|
|
44
|
+
|
|
27
45
|
return (
|
|
28
|
-
<div className={"flex flex-col gap-8"}>
|
|
46
|
+
<div className={cls("flex flex-col", reading ? "gap-10" : "gap-8")}>
|
|
29
47
|
{sections.map((section) => {
|
|
30
48
|
|
|
31
49
|
const hasError = section.fields.some(f => errorKeys.has(f.key));
|
|
32
50
|
const isCollapsed = section.collapsible && collapsed.has(section.key) && !hasError;
|
|
51
|
+
const summary = reading
|
|
52
|
+
&& section.readVariant === "summary"
|
|
53
|
+
&& Boolean(renderSummaryField);
|
|
33
54
|
|
|
34
55
|
return (
|
|
35
56
|
<section key={section.key} className={"min-w-0"}>
|
|
@@ -43,15 +64,21 @@ export function FormSections({
|
|
|
43
64
|
onToggle={() => onToggle(section.key)}/>
|
|
44
65
|
)}
|
|
45
66
|
|
|
46
|
-
{!isCollapsed && (
|
|
67
|
+
{!isCollapsed && (summary
|
|
68
|
+
? <div id={`form_section_${section.key}`}
|
|
69
|
+
className={"flex flex-col min-w-0 @2xl:max-w-md @2xl:ml-auto"}>
|
|
70
|
+
{section.fields.map((field, index) =>
|
|
71
|
+
renderSummaryField!(field, index, section.fields.length))}
|
|
72
|
+
</div>
|
|
47
73
|
// A container query, not a media query: the form is
|
|
48
74
|
// rendered at four very different widths (full
|
|
49
75
|
// screen, side panel, split pane, dialog) inside the
|
|
50
76
|
// same viewport, so spans have to answer to the
|
|
51
77
|
// column they are in, not to the window.
|
|
52
|
-
<div id={`form_section_${section.key}`}
|
|
78
|
+
: <div id={`form_section_${section.key}`}
|
|
53
79
|
className={cls(
|
|
54
|
-
"grid
|
|
80
|
+
"grid min-w-0",
|
|
81
|
+
reading ? "gap-x-8 gap-y-7" : "gap-x-4 gap-y-5",
|
|
55
82
|
"grid-cols-1 @2xl:grid-cols-4"
|
|
56
83
|
)}>
|
|
57
84
|
{section.fields.map(renderField)}
|
|
@@ -87,9 +87,15 @@ export function StorageUploadProgress({
|
|
|
87
87
|
}, [entry.file, entry.fileName, upload]);
|
|
88
88
|
|
|
89
89
|
if (simple) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
// `imageSize` is a pixel count, so it has to be sized inline: `w-${n}`
|
|
91
|
+
// is neither a real Tailwind scale value nor a class the JIT can see.
|
|
92
|
+
return <div style={{
|
|
93
|
+
width: imageSize,
|
|
94
|
+
height: imageSize
|
|
95
|
+
}}>
|
|
96
|
+
|
|
97
|
+
{loading && <Skeleton width={imageSize}
|
|
98
|
+
height={imageSize}/>}
|
|
93
99
|
|
|
94
100
|
</div>
|
|
95
101
|
}
|
|
@@ -3,10 +3,11 @@ import React, { useLayoutEffect, useState } from "react";
|
|
|
3
3
|
/**
|
|
4
4
|
* Below this the form is a single column and the rail is not shown.
|
|
5
5
|
*
|
|
6
|
-
* Roughly the
|
|
7
|
-
* four-up row
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Roughly the 320px rail plus a content column still wide enough to hold a
|
|
7
|
+
* four-up row — at the threshold that leaves ~680px, which is four columns of
|
|
8
|
+
* ~145px once the gutters are taken out. Above it the rail is worth having even
|
|
9
|
+
* in the split view; below it — a narrow split pane, the side panel, the dialog,
|
|
10
|
+
* a phone — its fields fold back into the column.
|
|
10
11
|
*/
|
|
11
12
|
export const RAIL_MIN_WIDTH = 1000;
|
|
12
13
|
|
|
@@ -53,7 +53,13 @@ export function ReadOnlyFieldBinding({
|
|
|
53
53
|
"w-full overflow-x-scroll no-scrollbar flex items-center",
|
|
54
54
|
skipCardWrapper
|
|
55
55
|
? ""
|
|
56
|
-
|
|
56
|
+
// `min-h-12` is `TextField`'s own `large` height, which is
|
|
57
|
+
// what every editable field in the form is. At 40px a
|
|
58
|
+
// read-only field sat 8px short of the input beside it,
|
|
59
|
+
// so a row holding one of each had its two boxes'
|
|
60
|
+
// baselines and bottom edges disagree — the field looked
|
|
61
|
+
// misaligned rather than uneditable.
|
|
62
|
+
: "rounded-lg border border-surface-200 dark:border-surface-700 px-4 md:px-6 min-h-12 opacity-80"
|
|
57
63
|
)}>
|
|
58
64
|
|
|
59
65
|
<ErrorBoundary>
|
|
@@ -41,11 +41,35 @@ export function useBuildCollectionRegistryController(props: {
|
|
|
41
41
|
if (!collectionPath) return undefined;
|
|
42
42
|
|
|
43
43
|
let collection: AdminCollection | undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
|
|
45
|
+
// An exact slug match first, because a slug is allowed to contain
|
|
46
|
+
// slashes and several drivers need it to: a Firestore collection
|
|
47
|
+
// partitioned by locale is declared as `content/de-DE/podcasts`, and
|
|
48
|
+
// that string is its *name*, not a path to walk.
|
|
49
|
+
//
|
|
50
|
+
// Without this, such a slug went straight to `resolvePathToCollections`,
|
|
51
|
+
// which reads any `a/b/c` as collection/entity/subcollection, looked for
|
|
52
|
+
// a root collection called `content`, found none and threw — and the
|
|
53
|
+
// catch below turned that into `undefined`. `RebaseRoute` renders
|
|
54
|
+
// `null` for an unresolved collection, so the whole view came out blank
|
|
55
|
+
// with nothing in the console above `debug`. Thirty-five collections in
|
|
56
|
+
// one app were unreachable that way.
|
|
57
|
+
// `collectionPath`, not `cleanedPath`: an even segment count means the
|
|
58
|
+
// path ends at a record, and the id has already been trimmed off above.
|
|
59
|
+
// Matching the raw path instead would only ever hit for a collection
|
|
60
|
+
// route, leaving `content/de-DE/podcasts/abc123` back on the walker.
|
|
61
|
+
collection = registry.get(collectionPath) as AdminCollection | undefined;
|
|
62
|
+
|
|
63
|
+
if (!collection) {
|
|
64
|
+
try {
|
|
65
|
+
collection = registry.resolvePathToCollections(collectionPath).finalCollection;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// Warn, not debug. An unresolved collection ends as a route with
|
|
68
|
+
// nothing in it, and at `debug` there was no way to tell that
|
|
69
|
+
// from a collection that legitimately has no rows.
|
|
70
|
+
console.warn(`[rebase] Could not resolve a collection for "${collectionPath}"`, e);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
49
73
|
}
|
|
50
74
|
|
|
51
75
|
if (!collection) {
|