@rebasepro/admin 0.13.0 → 0.13.1-canary.g18cfeb7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CollectionEditorDialog-CjXkWZ64.js → CollectionEditorDialog-CUFXevVg.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-CUFXevVg.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-DDUC7giL.js} +2 -2
- package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-DDUC7giL.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-vzS848Gf.js} +9 -13
- package/dist/RouterCollectionsStudioView-vzS848Gf.js.map +1 -0
- package/dist/collection_editor/serializable_types.d.ts +1 -0
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/AdditionalFieldValue.d.ts +25 -0
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
- package/dist/components/DetailViewBinding.d.ts +15 -0
- package/dist/components/DrawerNavigationGroup.d.ts +9 -6
- package/dist/components/DrawerNavigationItem.d.ts +16 -6
- package/dist/components/EditViewBinding.d.ts +30 -1
- package/dist/components/EntityDisplayHeader.d.ts +53 -0
- package/dist/components/EntityIdentityBar.d.ts +43 -1
- package/dist/components/EntityViewBinding.d.ts +60 -6
- package/dist/components/RebaseLayout.d.ts +1 -1
- package/dist/components/RebaseShell.d.ts +1 -1
- package/dist/components/RelationSelector.d.ts +8 -1
- package/dist/components/UserSelector.d.ts +8 -1
- package/dist/components/app/Scaffold.d.ts +9 -3
- package/dist/components/index.d.ts +2 -0
- package/dist/{export-J79rMoNj.js → export-5bl2EoSs.js} +2 -2
- package/dist/{export-J79rMoNj.js.map → export-5bl2EoSs.js.map} +1 -1
- package/dist/form/components/FieldBlock.d.ts +58 -4
- package/dist/form/components/FormRail.d.ts +13 -0
- package/dist/form/components/FormSections.d.ts +15 -1
- package/dist/form/components/useRailVisible.d.ts +5 -4
- package/dist/form/form_utils.d.ts +38 -1
- package/dist/{history-D7L_JlLO.js → history-BuKKAhoY.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-BuKKAhoY.js.map} +1 -1
- package/dist/hooks/useEntityDisplay.d.ts +102 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
- package/dist/{import-BvYl--U4.js → import-BhdWs34H.js} +3 -3
- package/dist/{import-BvYl--U4.js.map → import-BhdWs34H.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +152 -57
- package/dist/index.js.map +1 -1
- package/dist/preview/compact_text.d.ts +23 -0
- package/dist/preview/components/BooleanPreview.d.ts +3 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
- package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
- package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
- package/dist/routes/RebaseRoute.d.ts +1 -1
- package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
- package/dist/util/number_format.d.ts +14 -0
- package/dist/util/previews.d.ts +9 -2
- package/dist/{util-GiwPgxnL.js → util-DNG7iK0w.js} +3756 -2300
- package/dist/util-DNG7iK0w.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/components/AdditionalFieldValue.tsx +68 -0
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
- package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +723 -348
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +38 -8
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +23 -36
- package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +33 -2
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +19 -23
- package/src/components/DetailViewBinding.tsx +93 -56
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +44 -16
- package/src/components/EditViewBinding.tsx +215 -42
- package/src/components/EntityDisplayHeader.tsx +170 -0
- package/src/components/EntityIdentityBar.tsx +163 -23
- package/src/components/EntityInspector.tsx +24 -6
- package/src/components/EntityPreviewBinding.tsx +58 -18
- package/src/components/EntityViewBinding.tsx +342 -41
- package/src/components/InlineEntityPreview.tsx +10 -10
- package/src/components/RebaseLayout.tsx +2 -2
- package/src/components/RebaseShell.tsx +2 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
- package/src/components/RelationSelector.tsx +15 -4
- package/src/components/SideDialogs.tsx +15 -3
- package/src/components/SidePanelBinding.tsx +122 -66
- package/src/components/UserSelector.tsx +15 -4
- package/src/components/app/Scaffold.tsx +112 -10
- package/src/components/history/LastEditedByIndicator.tsx +7 -12
- package/src/components/index.ts +3 -0
- package/src/data_import/utils/data.ts +9 -1
- package/src/form/EntityForm.tsx +64 -52
- package/src/form/EntityFormBinding.tsx +24 -11
- package/src/form/PropertyFieldBinding.tsx +2 -2
- package/src/form/components/FieldBlock.tsx +80 -10
- package/src/form/components/FormRail.tsx +19 -2
- package/src/form/components/FormSections.tsx +32 -5
- package/src/form/components/StorageUploadProgress.tsx +9 -3
- package/src/form/components/useRailVisible.tsx +5 -4
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
- package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
- package/src/form/form_utils.ts +78 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
- package/src/hooks/useEntityDisplay.tsx +262 -0
- package/src/hooks/useEntityDisplayTitle.tsx +27 -38
- package/src/index.ts +9 -1
- package/src/preview/PropertyPreview.tsx +25 -5
- package/src/preview/compact_text.ts +68 -0
- package/src/preview/components/BooleanPreview.tsx +5 -2
- package/src/preview/components/StorageThumbnail.tsx +5 -2
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
- package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
- package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
- package/src/routes/RebaseRoute.tsx +62 -4
- package/src/types/components/PropertyPreviewProps.tsx +21 -0
- package/src/util/number_format.ts +46 -0
- package/src/util/previews.ts +8 -55
- package/src/util/property_utils.tsx +6 -1
- package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
- package/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -1,11 +1,68 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Property } from "@rebasepro/types";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
4
|
-
import
|
|
3
|
+
import type { AdditionalFieldDelegateProps, AdminCollection } from "@rebasepro/admin-types";
|
|
4
|
+
import type { ResolvedFormField, ResolvedFormSection } from "@rebasepro/app";
|
|
5
|
+
import type { FormContext } from "../types/fields";
|
|
6
|
+
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
5
7
|
import { Entity } from "@rebasepro/types";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import {
|
|
9
|
+
AlignLeftIcon,
|
|
10
|
+
cls,
|
|
11
|
+
defaultBorderMixin,
|
|
12
|
+
ErrorBoundary,
|
|
13
|
+
paperMixin
|
|
14
|
+
} from "@rebasepro/ui";
|
|
15
|
+
import { getFormFieldKeys, resolveFormLayout } from "@rebasepro/app";
|
|
16
|
+
import { getValueInPath } from "@rebasepro/utils";
|
|
17
|
+
|
|
18
|
+
import { FieldBlock, LABEL_ICON_SIZE, spanClass } from "../form/components/FieldBlock";
|
|
19
|
+
import { AdditionalFieldValue } from "./AdditionalFieldValue";
|
|
20
|
+
import { EntityDisplayHeader, HEADER_DISPLAY_ROLES } from "./EntityDisplayHeader";
|
|
21
|
+
import { FormSections } from "../form/components/FormSections";
|
|
22
|
+
import { FormRail, RecordMeta } from "../form/components/FormRail";
|
|
23
|
+
import { useEntityDisplayValues } from "../hooks/useEntityDisplay";
|
|
24
|
+
import { useRailVisible } from "../form/components/useRailVisible";
|
|
25
|
+
import { PropertyPreview } from "../preview";
|
|
26
|
+
|
|
27
|
+
/** Nothing can be in error in a view that cannot be submitted. */
|
|
28
|
+
const NO_ERRORS: ReadonlySet<string> = new Set<string>();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The fields the read-only view lays out, in render order.
|
|
32
|
+
*
|
|
33
|
+
* The same set the form uses, minus two groups. Both are dropped *before* the
|
|
34
|
+
* layout resolves rather than skipped while rendering, because the resolver has
|
|
35
|
+
* by then given each one a place on the grid — skipping later leaves that place
|
|
36
|
+
* as a hole.
|
|
37
|
+
*
|
|
38
|
+
* - The `additionalFields` when there is no form context to hand them: those
|
|
39
|
+
* entries are arbitrary components, not values, and one rendered against no
|
|
40
|
+
* context is nothing at all.
|
|
41
|
+
* - `headerKeys` — the properties {@link EntityDisplayHeader} has already
|
|
42
|
+
* rendered as the record's picture, subtitle, state and labels. Leaving one in
|
|
43
|
+
* prints its value twice on one screen.
|
|
44
|
+
*
|
|
45
|
+
* The record's **title property is not among them**, deliberately. It names the
|
|
46
|
+
* page in the identity bar, but a heading and a labelled field are different
|
|
47
|
+
* things: `Order #` is a column with a name and a description that someone opens
|
|
48
|
+
* the record to read, and dropping it because its text also appears in the
|
|
49
|
+
* breadcrumb removes a value from the record on the grounds that it is legible
|
|
50
|
+
* somewhere else. See {@link HEADER_DISPLAY_ROLES}.
|
|
51
|
+
*/
|
|
52
|
+
export function readOnlyFieldKeys(
|
|
53
|
+
collection: AdminCollection,
|
|
54
|
+
hasFormContext: boolean,
|
|
55
|
+
headerKeys?: ReadonlySet<string>
|
|
56
|
+
): string[] {
|
|
57
|
+
const keys = getFormFieldKeys(collection);
|
|
58
|
+
const additional = hasFormContext
|
|
59
|
+
? undefined
|
|
60
|
+
: new Set((collection.additionalFields ?? []).map(field => field.key));
|
|
61
|
+
|
|
62
|
+
return keys.filter(key =>
|
|
63
|
+
!additional?.has(key)
|
|
64
|
+
&& !headerKeys?.has(key));
|
|
65
|
+
}
|
|
9
66
|
|
|
10
67
|
/**
|
|
11
68
|
* @group Components
|
|
@@ -15,57 +72,301 @@ export interface EntityViewBindingProps<M extends Record<string, unknown>> {
|
|
|
15
72
|
collection: AdminCollection<M>;
|
|
16
73
|
path: string;
|
|
17
74
|
className?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Lay the record out as a page: a centred, scrolling column with the
|
|
77
|
+
* metadata rail beside it, exactly as the form does. Off for the inline
|
|
78
|
+
* uses — the delete dialog renders the record inside a box it already owns.
|
|
79
|
+
*/
|
|
80
|
+
asPage?: boolean;
|
|
81
|
+
/** Padding preset, matching the form's. Only read when `asPage`. */
|
|
82
|
+
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
83
|
+
/**
|
|
84
|
+
* Needed only to render `additionalFields`, which are arbitrary components
|
|
85
|
+
* that take a form context. Without one those entries are left out rather
|
|
86
|
+
* than rendered against a fake context.
|
|
87
|
+
*/
|
|
88
|
+
formContext?: FormContext<M>;
|
|
18
89
|
}
|
|
19
90
|
|
|
20
91
|
/**
|
|
21
92
|
* The read-only rendering of a record's values.
|
|
22
93
|
*
|
|
23
|
-
* This
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
94
|
+
* This is the same layout as {@link EntityForm}: the sections, the grid spans,
|
|
95
|
+
* the labels and the rail all come from the one resolver, so the record does
|
|
96
|
+
* not rearrange itself when you press Edit. Only the controls differ — each
|
|
97
|
+
* field renders its {@link PropertyPreview} instead of an input.
|
|
98
|
+
*
|
|
99
|
+
* It used to be a flat two-column table of every value in the record: no
|
|
100
|
+
* grouping, no widths, a label column sized for short names and a value column
|
|
101
|
+
* that wrapped emails over two lines while the pane beside it stayed empty. A
|
|
102
|
+
* boolean and a markdown body were given the same room.
|
|
103
|
+
*
|
|
104
|
+
* The synthetic `Id` row it used to prepend is gone with the same reasoning the
|
|
105
|
+
* form applies: between that row, the collection's own id property and the
|
|
106
|
+
* `path/id` chip above them, one UUID appeared three times. The id is a
|
|
107
|
+
* copyable chip in the identity bar, and the rail's record block, once each.
|
|
108
|
+
*
|
|
109
|
+
* The labels are the form's labels, icon and all: a field is the same field
|
|
110
|
+
* whether or not you are editing it. What `mode: "read"` drops is the per-field
|
|
111
|
+
* *description*, which is input help rather than part of the label — and what it
|
|
112
|
+
* adds is room. A control is a bordered box that separates itself from the next
|
|
113
|
+
* one; a value is bare text, so the rows and sections are opened up rather than
|
|
114
|
+
* inheriting spacing sized for inputs.
|
|
28
115
|
*/
|
|
29
116
|
export function EntityViewBinding<M extends Record<string, unknown>>(
|
|
30
117
|
{
|
|
31
118
|
entity,
|
|
32
119
|
collection,
|
|
33
|
-
|
|
34
|
-
|
|
120
|
+
className,
|
|
121
|
+
asPage = false,
|
|
122
|
+
openEntityMode = "full_screen",
|
|
123
|
+
formContext
|
|
35
124
|
}: EntityViewBindingProps<M>) {
|
|
36
125
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
126
|
+
// The record's name, picture, state and labels, lifted out of the grid and
|
|
127
|
+
// into a header. Resolved here rather than inside it because the fields it
|
|
128
|
+
// renders have to come *out* of the layout, and the layout is resolved here.
|
|
129
|
+
const display = useEntityDisplayValues<M>({
|
|
130
|
+
collection,
|
|
131
|
+
entity
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const headerKeys = useMemo(() => {
|
|
135
|
+
const keys = new Set<string>();
|
|
136
|
+
for (const role of HEADER_DISPLAY_ROLES) {
|
|
137
|
+
// Set only for a role filled by a property of this collection. A
|
|
138
|
+
// role filled by a resolver has no field on the grid to remove.
|
|
139
|
+
const key = display[role].source?.key;
|
|
140
|
+
if (key) keys.add(key);
|
|
141
|
+
}
|
|
142
|
+
return keys;
|
|
143
|
+
}, [display]);
|
|
144
|
+
|
|
145
|
+
// Only *whether* there is a form context changes the field list, not which
|
|
146
|
+
// one it is — so the memo turns on the boolean rather than on the context
|
|
147
|
+
// object, which is rebuilt on every keystroke of the form above it.
|
|
148
|
+
const hasFormContext = Boolean(formContext);
|
|
149
|
+
const fieldKeys = useMemo(
|
|
150
|
+
() => readOnlyFieldKeys(collection as AdminCollection, hasFormContext, headerKeys),
|
|
151
|
+
[collection, hasFormContext, headerKeys]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// `status: "existing"` — a record you are reading exists by definition, and
|
|
155
|
+
// that is what routes a generated id and the audit timestamps out of the
|
|
156
|
+
// column and into the record block.
|
|
157
|
+
const layout = useMemo(() => resolveFormLayout({
|
|
158
|
+
collection,
|
|
159
|
+
fieldKeys,
|
|
160
|
+
status: "existing"
|
|
161
|
+
}), [collection, fieldKeys]);
|
|
162
|
+
|
|
163
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
164
|
+
// Measured, not a breakpoint — see `useRailVisible`. The same reasoning as
|
|
165
|
+
// the form's: the resolver *moves* fields into the rail, so when the rail
|
|
166
|
+
// cannot be shown they have to fold back into the column.
|
|
167
|
+
const railVisible = useRailVisible(containerRef);
|
|
168
|
+
const showRail = asPage && layout.hasRail && railVisible;
|
|
169
|
+
|
|
170
|
+
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(
|
|
171
|
+
() => new Set(layout.sections.filter(section => section.collapsed).map(section => section.key))
|
|
172
|
+
);
|
|
173
|
+
const toggleSection = useCallback((key: string) => {
|
|
174
|
+
setCollapsedSections(prev => {
|
|
175
|
+
const next = new Set(prev);
|
|
176
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
177
|
+
return next;
|
|
178
|
+
});
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
const renderField = useCallback((field: ResolvedFormField): React.ReactNode => {
|
|
182
|
+
|
|
183
|
+
if (field.additional) {
|
|
184
|
+
const additionalField = collection.additionalFields?.find(f => f.key === field.key);
|
|
185
|
+
if (!additionalField || !formContext) return null;
|
|
186
|
+
|
|
187
|
+
const AdditionalFieldBuilder = additionalField.Builder;
|
|
188
|
+
const additionalFieldContext = formContext as unknown as AdditionalFieldDelegateProps["context"];
|
|
189
|
+
const child = AdditionalFieldBuilder
|
|
190
|
+
? <AdditionalFieldBuilder entity={entity} context={additionalFieldContext}/>
|
|
191
|
+
: <div className={"w-full"}>
|
|
192
|
+
<AdditionalFieldValue
|
|
193
|
+
field={additionalField}
|
|
194
|
+
entity={entity}
|
|
195
|
+
context={additionalFieldContext}/>
|
|
196
|
+
</div>;
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
// Through FieldBlock like every other cell, so a computed
|
|
200
|
+
// column is labelled at the same size, with the same gap to its
|
|
201
|
+
// box, as the property next to it on the grid.
|
|
202
|
+
<div key={`additional_${field.key}`} className={spanClass(field.span)}>
|
|
203
|
+
<FieldBlock propertyKey={field.key}
|
|
204
|
+
showLabel={true}
|
|
205
|
+
mode={"read"}
|
|
206
|
+
label={additionalField.name}
|
|
207
|
+
icon={<AlignLeftIcon size={LABEL_ICON_SIZE}/>}>
|
|
208
|
+
<div className={cls(paperMixin, "w-full min-h-12 p-4 md:p-6 overflow-x-auto no-scrollbar")}>
|
|
209
|
+
{child}
|
|
210
|
+
</div>
|
|
211
|
+
</FieldBlock>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const property = collection.properties?.[field.key] as Property | undefined;
|
|
217
|
+
if (!property) return null;
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div key={`field_${field.key}`} className={spanClass(field.span)}>
|
|
221
|
+
{/* Always labelled from here, unlike the form. The panel editors
|
|
222
|
+
that carry their own header have no header in preview — a map
|
|
223
|
+
previews as a table of its children and an array as its items,
|
|
224
|
+
so leaving the label to them would leave them unlabelled. */}
|
|
225
|
+
<FieldBlock propertyKey={field.key}
|
|
226
|
+
property={property}
|
|
227
|
+
showLabel={true}
|
|
228
|
+
mode={"read"}>
|
|
229
|
+
{/* One control's height for a single-line value, so a row of
|
|
230
|
+
short fields lines up with the taller ones beside it, and
|
|
231
|
+
anything longer simply grows. Matched to the form's row
|
|
232
|
+
rather than shrunk to the text: it is what keeps a value
|
|
233
|
+
from sitting directly on the label of the row below. */}
|
|
234
|
+
<div className={"min-h-8 flex flex-col justify-center min-w-0 text-text-primary dark:text-text-primary-dark"}>
|
|
235
|
+
<PropertyPreview propertyKey={field.key}
|
|
236
|
+
value={getValueInPath(entity.values, field.key)}
|
|
237
|
+
property={property}
|
|
238
|
+
hideLabel
|
|
239
|
+
size={"medium"}/>
|
|
240
|
+
</div>
|
|
241
|
+
</FieldBlock>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}, [collection, entity, formContext]);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* One field of a `readVariant: "summary"` section: a label/value row rather
|
|
248
|
+
* than a grid cell, with the last one emphasised.
|
|
249
|
+
*
|
|
250
|
+
* The last row is the one a summary exists for — a run of figures is read to
|
|
251
|
+
* arrive at its final line — so it is separated by a rule and set at the
|
|
252
|
+
* weight the grid gave every cell equally.
|
|
253
|
+
*/
|
|
254
|
+
const renderSummaryField = useCallback((
|
|
255
|
+
field: ResolvedFormField,
|
|
256
|
+
index: number,
|
|
257
|
+
total: number
|
|
258
|
+
): React.ReactNode => {
|
|
259
|
+
|
|
260
|
+
const property = collection.properties?.[field.key] as Property | undefined;
|
|
261
|
+
if (!property) return null;
|
|
262
|
+
|
|
263
|
+
const last = index === total - 1;
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<div key={`summary_${field.key}`}
|
|
267
|
+
className={cls(
|
|
268
|
+
"flex items-baseline justify-between gap-4 min-w-0 py-2",
|
|
269
|
+
last && total > 1 && cls("mt-1.5 pt-3 border-t", defaultBorderMixin)
|
|
270
|
+
)}>
|
|
271
|
+
<span className={cls(
|
|
272
|
+
"min-w-0 truncate",
|
|
273
|
+
last
|
|
274
|
+
? "text-sm font-semibold text-text-primary dark:text-text-primary-dark"
|
|
275
|
+
: "text-sm text-text-secondary dark:text-text-secondary-dark"
|
|
276
|
+
)}>
|
|
277
|
+
{property.name ?? field.key}
|
|
278
|
+
</span>
|
|
279
|
+
<span className={cls(
|
|
280
|
+
"shrink-0 text-right text-text-primary dark:text-text-primary-dark",
|
|
281
|
+
last ? "text-base font-semibold" : "text-sm"
|
|
282
|
+
)}>
|
|
283
|
+
<PropertyPreview propertyKey={field.key}
|
|
284
|
+
value={getValueInPath(entity.values, field.key)}
|
|
285
|
+
property={property}
|
|
286
|
+
hideLabel
|
|
287
|
+
size={"medium"}/>
|
|
288
|
+
</span>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}, [collection, entity]);
|
|
292
|
+
|
|
293
|
+
// Same fold-back as the form: with no room for the rail its fields become a
|
|
294
|
+
// trailing group rather than disappearing.
|
|
295
|
+
const sections: ResolvedFormSection[] = showRail || !layout.sidebar.length
|
|
296
|
+
? layout.sections
|
|
297
|
+
: [
|
|
298
|
+
...layout.sections,
|
|
299
|
+
{
|
|
300
|
+
key: "__rail",
|
|
301
|
+
title: "Settings",
|
|
302
|
+
collapsible: false,
|
|
303
|
+
collapsed: false,
|
|
304
|
+
fields: layout.sidebar
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const content = <>
|
|
309
|
+
{/* The title only when nothing above is already showing it. Every
|
|
310
|
+
full-page use sits under an identity bar that carries it; the
|
|
311
|
+
inline ones — the delete dialog — have no bar. */}
|
|
312
|
+
<EntityDisplayHeader entity={entity}
|
|
313
|
+
collection={collection}
|
|
314
|
+
showTitle={!asPage}
|
|
315
|
+
className={"mb-10"}/>
|
|
316
|
+
|
|
317
|
+
<FormSections sections={sections}
|
|
318
|
+
collapsed={collapsedSections}
|
|
319
|
+
onToggle={toggleSection}
|
|
320
|
+
errorKeys={NO_ERRORS}
|
|
321
|
+
renderField={renderField}
|
|
322
|
+
renderSummaryField={renderSummaryField}
|
|
323
|
+
mode={"read"}/>
|
|
324
|
+
|
|
325
|
+
{/* Folded back with the rail's fields, and for the same reason. The rail
|
|
326
|
+
renders only where it has been measured to fit, so on the split pane,
|
|
327
|
+
the side panel and the dialog this block was dropped outright and the
|
|
328
|
+
record's id was nowhere on screen but the truncated chip in the bar.
|
|
329
|
+
An id you cannot read or copy is the one piece of a record you most
|
|
330
|
+
often opened it for. */}
|
|
331
|
+
{!showRail && layout.showRecordMeta && (
|
|
332
|
+
<div className={cls("mt-10 pt-6 border-t max-w-sm", defaultBorderMixin)}>
|
|
333
|
+
<RecordMeta entity={entity as Entity<Record<string, unknown>>}/>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
</>;
|
|
337
|
+
|
|
338
|
+
if (!asPage) {
|
|
339
|
+
// `@container/col` even here: the spans resolve against the column they
|
|
340
|
+
// sit in, and inside a dialog that column is the dialog.
|
|
341
|
+
return (
|
|
342
|
+
<div ref={containerRef} className={cls("@container/col w-full min-w-0", className)}>
|
|
343
|
+
{content}
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
52
347
|
|
|
53
348
|
return (
|
|
54
|
-
<div className={"w-full "
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
349
|
+
<div ref={containerRef} className={cls("flex-1 flex flex-row w-full min-h-0", className)}>
|
|
350
|
+
|
|
351
|
+
{/* `items-start`: without it the cross-axis default stretches the
|
|
352
|
+
column to the scroller's height and its bottom padding lands at
|
|
353
|
+
the bottom of the viewport instead of after the last field. */}
|
|
354
|
+
<div className={"flex-1 min-w-0 overflow-y-auto flex justify-center items-start"}>
|
|
355
|
+
<div className={cls(
|
|
356
|
+
"@container/col w-full max-w-3xl 2xl:max-w-4xl flex flex-col",
|
|
357
|
+
openEntityMode === "dialog"
|
|
358
|
+
? "pt-5 pb-8 px-6 sm:px-8"
|
|
359
|
+
: "pt-6 pb-12 px-5 sm:px-8"
|
|
360
|
+
)}>
|
|
361
|
+
{content}
|
|
65
362
|
</div>
|
|
66
|
-
|
|
363
|
+
</div>
|
|
67
364
|
|
|
68
|
-
|
|
365
|
+
{showRail && <FormRail
|
|
366
|
+
fields={layout.sidebar}
|
|
367
|
+
showRecordMeta={layout.showRecordMeta}
|
|
368
|
+
entity={entity as Entity<Record<string, unknown>>}
|
|
369
|
+
renderField={renderField}/>}
|
|
69
370
|
</div>
|
|
70
371
|
);
|
|
71
372
|
}
|
|
@@ -3,10 +3,9 @@ import * as React from "react";
|
|
|
3
3
|
import { Entity } from "@rebasepro/types";
|
|
4
4
|
import { cls, LinkIcon, Skeleton, Tooltip } from "@rebasepro/ui";
|
|
5
5
|
import { useAnalyticsController } from "@rebasepro/app";
|
|
6
|
-
import { getValueInPath } from "@rebasepro/utils";
|
|
7
6
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
8
7
|
|
|
9
|
-
import {
|
|
8
|
+
import { useEntityTitle } from "../hooks/useEntityDisplay";
|
|
10
9
|
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
11
10
|
import { useSidePanel } from "../hooks/useSidePanel";
|
|
12
11
|
|
|
@@ -57,14 +56,15 @@ export function InlineEntityPreview({
|
|
|
57
56
|
|
|
58
57
|
const collection = collectionProp ?? collectionRegistryController.getCollection(entity.path);
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
// The id is the right fallback *here* specifically: this renders a pointer at
|
|
60
|
+
// another record, and a pointer with no label is still followable if it says
|
|
61
|
+
// which record it points at. A page heading makes the opposite choice.
|
|
62
|
+
const { value: title } = useEntityTitle({
|
|
63
|
+
collection: collection as AdminCollection,
|
|
64
|
+
entity,
|
|
65
|
+
enabled: Boolean(collection)
|
|
66
|
+
});
|
|
67
|
+
const resolvedLabel = label ?? title ?? String(entity.id);
|
|
68
68
|
|
|
69
69
|
const interactive = !disabled && (onClick !== undefined || (includeEntityLink && collection !== undefined));
|
|
70
70
|
|
|
@@ -19,7 +19,7 @@ export interface RebaseLayoutProps {
|
|
|
19
19
|
appBar?: React.ReactNode;
|
|
20
20
|
/** Custom Drawer to override the default. */
|
|
21
21
|
drawer?: React.ReactNode;
|
|
22
|
-
/** Expand the drawer while the pointer is over it. Not a load-time default — that is `defaultDrawerOpen`. */
|
|
22
|
+
/** Expand the drawer while the pointer is over it. On by default; pass `false` to opt out. Not a load-time default — that is `defaultDrawerOpen`. */
|
|
23
23
|
autoOpenDrawer?: boolean;
|
|
24
24
|
/** Start with the drawer expanded instead of collapsed to icons. Ignored on small layouts. */
|
|
25
25
|
defaultDrawerOpen?: boolean;
|
|
@@ -52,7 +52,7 @@ export function RebaseLayout(props: RebaseLayoutProps) {
|
|
|
52
52
|
title = "Rebase",
|
|
53
53
|
appBar,
|
|
54
54
|
drawer,
|
|
55
|
-
autoOpenDrawer =
|
|
55
|
+
autoOpenDrawer = true,
|
|
56
56
|
defaultDrawerOpen = false,
|
|
57
57
|
logo,
|
|
58
58
|
devViews = []
|
|
@@ -14,7 +14,7 @@ export interface RebaseShellProps {
|
|
|
14
14
|
/** Top bar. Only rendered if provided — pass `<AppBar/>` for the default one. */
|
|
15
15
|
appBar?: React.ReactNode;
|
|
16
16
|
drawer?: React.ReactNode;
|
|
17
|
-
/** Expand the drawer while the pointer is over it. Not a load-time default — that is `defaultDrawerOpen`. */
|
|
17
|
+
/** Expand the drawer while the pointer is over it. On by default; pass `false` to opt out. Not a load-time default — that is `defaultDrawerOpen`. */
|
|
18
18
|
autoOpenDrawer?: boolean;
|
|
19
19
|
/** Start with the drawer expanded instead of collapsed to icons. Ignored on small layouts. */
|
|
20
20
|
defaultDrawerOpen?: boolean;
|
|
@@ -44,7 +44,7 @@ export function RebaseShell(props: RebaseShellProps) {
|
|
|
44
44
|
title = "Rebase",
|
|
45
45
|
appBar,
|
|
46
46
|
drawer,
|
|
47
|
-
autoOpenDrawer =
|
|
47
|
+
autoOpenDrawer = true,
|
|
48
48
|
defaultDrawerOpen = false,
|
|
49
49
|
logo,
|
|
50
50
|
children
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
useData,
|
|
15
15
|
useDataTableController,
|
|
16
16
|
useLargeLayout,
|
|
17
|
-
usePermissions
|
|
17
|
+
usePermissions,
|
|
18
|
+
useTranslation
|
|
18
19
|
} from "@rebasepro/app";
|
|
19
20
|
import { ErrorView } from "@rebasepro/app";
|
|
20
21
|
import { Button, DialogActions, PlusIcon, Typography } from "@rebasepro/ui";
|
|
@@ -347,6 +348,7 @@ function EntitySelectionDialogActions({
|
|
|
347
348
|
const { canCreate } = usePermissions();
|
|
348
349
|
|
|
349
350
|
const largeLayout = useLargeLayout();
|
|
351
|
+
const { t } = useTranslation();
|
|
350
352
|
|
|
351
353
|
const onClick: MouseEventHandler<HTMLButtonElement> | undefined = onNewClick
|
|
352
354
|
? (e) => {
|
|
@@ -359,7 +361,7 @@ function EntitySelectionDialogActions({
|
|
|
359
361
|
? <Button
|
|
360
362
|
onClick={onClick}
|
|
361
363
|
startIcon={<PlusIcon/>}>
|
|
362
|
-
|
|
364
|
+
{t("add_specific", { name: collection.singularName ?? collection.name })}
|
|
363
365
|
</Button>
|
|
364
366
|
: <Button
|
|
365
367
|
onClick={onClick}>
|
|
@@ -58,7 +58,14 @@ export interface RelationSelectorProps {
|
|
|
58
58
|
|
|
59
59
|
onValueChange?: (updatedValue: EntityRelation | EntityRelation[] | undefined) => void;
|
|
60
60
|
placeholder?: React.ReactNode;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* `large` is the form's size and is on the shared 28/32/40/48 control
|
|
63
|
+
* scale, so a relation picker lines up with the text field beside it.
|
|
64
|
+
*
|
|
65
|
+
* `small` and `medium` predate that scale (42 and 56) and are kept as they
|
|
66
|
+
* are because the table cells and the filter row are built around them.
|
|
67
|
+
*/
|
|
68
|
+
size?: "small" | "medium" | "large";
|
|
62
69
|
useChips?: boolean;
|
|
63
70
|
disabled?: boolean;
|
|
64
71
|
error?: boolean; // kept for backwards compatibility (could be used for styling later)
|
|
@@ -488,9 +495,13 @@ relation } as RelationItem;
|
|
|
488
495
|
className={cls(
|
|
489
496
|
{
|
|
490
497
|
"min-h-[42px] py-1 px-2": size === "small",
|
|
491
|
-
"min-h-[56px] py-2 px-4": size === "medium"
|
|
498
|
+
"min-h-[56px] py-2 px-4": size === "medium",
|
|
499
|
+
"min-h-[48px] py-1 px-4": size === "large"
|
|
492
500
|
},
|
|
493
|
-
|
|
501
|
+
// `rounded-lg` like every other field box —
|
|
502
|
+
// `rounded-md` made this the one control in a
|
|
503
|
+
// row with a different corner.
|
|
504
|
+
"w-full select-none rounded-lg text-sm relative flex items-center",
|
|
494
505
|
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
495
506
|
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
496
507
|
className
|
|
@@ -580,7 +591,7 @@ relation } as RelationItem;
|
|
|
580
591
|
</Tooltip>
|
|
581
592
|
)}
|
|
582
593
|
<ChevronDownIcon
|
|
583
|
-
size={size === "
|
|
594
|
+
size={size === "small" ? iconSize.small : iconSize.medium}
|
|
584
595
|
className={cls("transition", isPopoverOpen ? "rotate-180" : "")}
|
|
585
596
|
/>
|
|
586
597
|
</div>
|
|
@@ -91,7 +91,17 @@ function SideDialogView({
|
|
|
91
91
|
const [blocked, setBlocked] = useState(false);
|
|
92
92
|
const [blockedNavigationMessage, setBlockedNavigationMessage] = useState<React.ReactNode | undefined>();
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// Held in a ref, read through a getter, because of *when* it is written and
|
|
95
|
+
// read. "Save and close" raises it and submits the form in one click
|
|
96
|
+
// handler, and the callback that acts on it — the form's `onSaved` — was
|
|
97
|
+
// captured before React could re-render with the new value. As state the
|
|
98
|
+
// flag was therefore always read one save late: the first "save and close"
|
|
99
|
+
// saved and left the panel open, and the *next* save closed it.
|
|
100
|
+
// Nothing renders from it, so a ref loses nothing.
|
|
101
|
+
const pendingCloseRef = React.useRef(false);
|
|
102
|
+
const setPendingClose = useCallback((value: boolean) => {
|
|
103
|
+
pendingCloseRef.current = value;
|
|
104
|
+
}, []);
|
|
95
105
|
|
|
96
106
|
const widthRef = React.useRef<string | undefined>(panel?.width);
|
|
97
107
|
const width = widthRef.current;
|
|
@@ -142,9 +152,11 @@ function SideDialogView({
|
|
|
142
152
|
setBlockedNavigationMessage,
|
|
143
153
|
width,
|
|
144
154
|
close: onCloseRequest,
|
|
145
|
-
pendingClose
|
|
155
|
+
get pendingClose() {
|
|
156
|
+
return pendingCloseRef.current;
|
|
157
|
+
},
|
|
146
158
|
setPendingClose
|
|
147
|
-
}), [blocked, setBlockedNavigationMessage, width, onCloseRequest,
|
|
159
|
+
}), [blocked, setBlockedNavigationMessage, width, onCloseRequest, setPendingClose]);
|
|
148
160
|
|
|
149
161
|
const additionalProps = panel?.additional as SidePanelBindingProps | undefined;
|
|
150
162
|
const isDialogMode = additionalProps?.collection?.openEntityMode === "dialog";
|