@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
package/src/form/EntityForm.tsx
CHANGED
|
@@ -9,18 +9,18 @@ import { deepEqual as equal } from "fast-equals";
|
|
|
9
9
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
10
10
|
import { AlignLeftIcon, useDebouncedCallback } from "@rebasepro/ui";
|
|
11
11
|
import { getDefaultValuesFor } from "@rebasepro/common";
|
|
12
|
-
import { isReadOnly } from "@rebasepro/app";
|
|
12
|
+
import { isDisabled, 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
|
-
import { LabelWithIconAndTooltip } from "./components/LabelWithIconAndTooltip";
|
|
24
24
|
import { PropertyFieldBinding } from "./PropertyFieldBinding";
|
|
25
25
|
import { flattenKeys } from "@rebasepro/app";
|
|
26
26
|
import { ErrorFocus } from "./components/ErrorFocus";
|
|
@@ -173,15 +173,30 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
173
173
|
});
|
|
174
174
|
};
|
|
175
175
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
/**
|
|
177
|
+
* What the form opens showing, when that is not what is stored — an edit
|
|
178
|
+
* carried over from the side panel's "open full screen", or a draft
|
|
179
|
+
* restored from the local-changes backup.
|
|
180
|
+
*
|
|
181
|
+
* The merge is deliberately *not* folded into the baseline. `mergeDeep`
|
|
182
|
+
* used to produce a single `initialValues` that served as both, with a
|
|
183
|
+
* `initialDirty: true` bolted on beside it to say what the baseline could
|
|
184
|
+
* no longer show — and that flag survived exactly one render, because the
|
|
185
|
+
* record's data arriving replaced the baseline and the form recomputed
|
|
186
|
+
* dirty as false against a baseline that already contained the edit.
|
|
187
|
+
*
|
|
188
|
+
* Read once, at mount: this is a handoff, and re-reading it every render
|
|
189
|
+
* tied the form to a cache that is cleared as soon as it has been consumed.
|
|
190
|
+
*/
|
|
191
|
+
const initialModifiedValues = useRef(
|
|
192
|
+
initialDirtyValues && Object.keys(initialDirtyValues).length > 0
|
|
193
|
+
? mergeDeep(baseInitialValues, initialDirtyValues)
|
|
194
|
+
: undefined
|
|
195
|
+
).current;
|
|
181
196
|
|
|
182
197
|
const internalFormex = useCreateFormex<M>({
|
|
183
|
-
initialValues:
|
|
184
|
-
|
|
198
|
+
initialValues: baseInitialValues as M,
|
|
199
|
+
initialModifiedValues: initialModifiedValues as M | undefined,
|
|
185
200
|
debugId: `EntityForm:${path}/${entityIdProp}`,
|
|
186
201
|
initialTouched: initialDirtyValues ?
|
|
187
202
|
flattenKeys(initialDirtyValues!)
|
|
@@ -193,7 +208,9 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
193
208
|
onSubmit,
|
|
194
209
|
onReset: () => {
|
|
195
210
|
onResetProp?.();
|
|
196
|
-
|
|
211
|
+
// The stored values — a reset returns the form to them, and by now
|
|
212
|
+
// that is the baseline whether or not it was at mount.
|
|
213
|
+
onValuesModified?.(false, baseInitialValues as M);
|
|
197
214
|
},
|
|
198
215
|
onValuesChangeDeferred: onValuesChangeDeferredProp,
|
|
199
216
|
validation: async (values): Promise<Record<string, string>> => {
|
|
@@ -420,7 +437,7 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
420
437
|
disabledProp
|
|
421
438
|
|| (!autoSave && formex.isSubmitting)
|
|
422
439
|
|| isReadOnly(property)
|
|
423
|
-
||
|
|
440
|
+
|| isDisabled(property)
|
|
424
441
|
|| (!isNew && "isId" in property && Boolean(property.isId))
|
|
425
442
|
|| (isNew && isIdAndAuto)
|
|
426
443
|
);
|
|
@@ -440,25 +457,26 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
440
457
|
const child = AdditionalFieldBuilder
|
|
441
458
|
? <AdditionalFieldBuilder entity={entity} context={additionalFieldContext}/>
|
|
442
459
|
: <div className={"w-full"}>
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
460
|
+
<AdditionalFieldValue
|
|
461
|
+
field={additionalField}
|
|
462
|
+
entity={entity}
|
|
463
|
+
context={additionalFieldContext}/>
|
|
447
464
|
</div>;
|
|
448
465
|
|
|
449
466
|
return (
|
|
467
|
+
// Through FieldBlock like every other cell, so a computed
|
|
468
|
+
// column is labelled at the same size, with the same gap to its
|
|
469
|
+
// box, as the property next to it on the grid.
|
|
450
470
|
<div key={`additional_${field.key}`}
|
|
451
471
|
className={spanClass(field.span)}>
|
|
452
|
-
<
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
className={"
|
|
457
|
-
<div className={cls(paperMixin, "w-full min-h-14 p-4 md:p-6 overflow-x-auto no-scrollbar")}>
|
|
458
|
-
<ErrorBoundary>
|
|
472
|
+
<FieldBlock propertyKey={field.key}
|
|
473
|
+
showLabel={true}
|
|
474
|
+
label={additionalField.name}
|
|
475
|
+
icon={<AlignLeftIcon size={LABEL_ICON_SIZE}/>}>
|
|
476
|
+
<div className={cls(paperMixin, "w-full min-h-12 p-4 md:p-6 overflow-x-auto no-scrollbar")}>
|
|
459
477
|
{child}
|
|
460
|
-
</
|
|
461
|
-
</
|
|
478
|
+
</div>
|
|
479
|
+
</FieldBlock>
|
|
462
480
|
</div>
|
|
463
481
|
);
|
|
464
482
|
}
|
|
@@ -537,12 +555,25 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
537
555
|
}
|
|
538
556
|
];
|
|
539
557
|
|
|
540
|
-
return
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
558
|
+
return <>
|
|
559
|
+
<FormSections
|
|
560
|
+
sections={sections}
|
|
561
|
+
collapsed={collapsedSections}
|
|
562
|
+
onToggle={toggleSection}
|
|
563
|
+
errorKeys={errorKeys}
|
|
564
|
+
renderField={renderField}/>
|
|
565
|
+
|
|
566
|
+
{/* Folded back with the rail's fields, and for the same reason. The
|
|
567
|
+
rail renders only where it has been measured to fit, so on the
|
|
568
|
+
split pane, the side panel and the dialog this block was dropped
|
|
569
|
+
outright and the record's id was nowhere on screen but the
|
|
570
|
+
truncated chip in the bar. */}
|
|
571
|
+
{!showRail && layout.showRecordMeta && status === "existing" && entity && (
|
|
572
|
+
<div className={cls("mt-8 pt-5 border-t max-w-sm", defaultBorderMixin)}>
|
|
573
|
+
<RecordMeta entity={entity as Entity<Record<string, unknown>>}/>
|
|
574
|
+
</div>
|
|
575
|
+
)}
|
|
576
|
+
</>;
|
|
546
577
|
};
|
|
547
578
|
|
|
548
579
|
const formRef = useRef<HTMLDivElement>(null);
|
|
@@ -706,25 +737,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
706
737
|
);
|
|
707
738
|
}
|
|
708
739
|
|
|
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
740
|
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
741
|
useEffect(() => {
|
|
730
742
|
if (!autoSave) return;
|
|
@@ -35,8 +35,8 @@ import { EntityForm } from "./EntityForm";
|
|
|
35
35
|
import {
|
|
36
36
|
extractTouchedValues,
|
|
37
37
|
removeEmptyContainers,
|
|
38
|
-
getChanges,
|
|
39
38
|
getInitialEntityValues,
|
|
39
|
+
getUnappliedLocalChanges,
|
|
40
40
|
} from "./form_utils";
|
|
41
41
|
import { mergeDeep } from "@rebasepro/utils";
|
|
42
42
|
|
|
@@ -127,22 +127,35 @@ export function EntityFormBinding<M extends Record<string, unknown>>({
|
|
|
127
127
|
|
|
128
128
|
const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
|
|
129
129
|
|
|
130
|
+
// `initialDirtyValues` is deliberately *not* merged in here any more. This
|
|
131
|
+
// value is the form's baseline, and an edit folded into the baseline is
|
|
132
|
+
// indistinguishable from saved data — which is how an edit carried over
|
|
133
|
+
// from the side panel arrived reading "Saved". {@link EntityForm} applies
|
|
134
|
+
// it as an overlay on the values instead, so the difference survives.
|
|
135
|
+
//
|
|
136
|
+
// The auto-applied local-changes backup is still folded in, unchanged, and
|
|
137
|
+
// has the same shape of problem: a restored draft opens undirty and cannot
|
|
138
|
+
// be saved until a field is touched. Left as it is rather than changed
|
|
139
|
+
// blind, since nothing here exercises that path.
|
|
130
140
|
const computedInitialValues = useMemo(() => {
|
|
131
|
-
|
|
141
|
+
return autoApplyLocalChanges && localChangesDataRaw
|
|
132
142
|
? mergeDeep(baseInitialValues, localChangesDataRaw as Partial<M>)
|
|
133
143
|
: baseInitialValues;
|
|
134
|
-
|
|
135
|
-
}, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues, initialDirtyValues]);
|
|
144
|
+
}, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues]);
|
|
136
145
|
|
|
137
146
|
const localChangesData = useMemo(() => {
|
|
138
147
|
if (!localChangesDataRaw) return undefined;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
// What the form opens showing: the baseline plus any edit handed over by
|
|
149
|
+
// the layout this one replaced. See {@link getUnappliedLocalChanges} for
|
|
150
|
+
// why the banner is measured against that and not against the baseline.
|
|
151
|
+
const openingValues = initialDirtyValues
|
|
152
|
+
? mergeDeep(computedInitialValues, initialDirtyValues)
|
|
153
|
+
: computedInitialValues;
|
|
154
|
+
return getUnappliedLocalChanges(
|
|
155
|
+
localChangesDataRaw as Partial<M>,
|
|
156
|
+
openingValues as Partial<M>
|
|
157
|
+
);
|
|
158
|
+
}, [localChangesDataRaw, computedInitialValues, initialDirtyValues]);
|
|
146
159
|
|
|
147
160
|
const hasLocalChanges = !localChangesCleared && !!localChangesData && Object.keys(localChangesData as object).length > 0;
|
|
148
161
|
|
|
@@ -14,7 +14,7 @@ import { Field, FormexFieldProps, getIn } from "@rebasepro/forms";
|
|
|
14
14
|
import { ReadOnlyFieldBinding } from "./field_bindings/ReadOnlyFieldBinding";
|
|
15
15
|
|
|
16
16
|
import { isPropertyBuilder, resolveProperty } from "@rebasepro/common";
|
|
17
|
-
import { isHidden, isReadOnly } from "@rebasepro/app";
|
|
17
|
+
import { isDisabled, isHidden, isReadOnly } from "@rebasepro/app";
|
|
18
18
|
import { useAuthController, useCustomizationController } from "@rebasepro/app";
|
|
19
19
|
import { Typography } from "@rebasepro/ui";
|
|
20
20
|
import { getFieldConfig, getFieldId } from "../components/field_configs";
|
|
@@ -113,7 +113,7 @@ function PropertyFieldBindingInternal<M extends Record<string, unknown> = Record
|
|
|
113
113
|
}) as Property | null;
|
|
114
114
|
|
|
115
115
|
const readOnly = resolvedProperty ? isReadOnly(resolvedProperty) : true;
|
|
116
|
-
const disabled = disabledProp || readOnly ||
|
|
116
|
+
const disabled = disabledProp || readOnly || (resolvedProperty ? isDisabled(resolvedProperty) : false) || context.disabled;
|
|
117
117
|
|
|
118
118
|
if (resolvedProperty === null || isHidden(resolvedProperty)) {
|
|
119
119
|
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,71 @@ 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";
|
|
101
|
+
/**
|
|
102
|
+
* Label text, when there is no property to take it from.
|
|
103
|
+
*
|
|
104
|
+
* For additional fields, which are columns a collection computes rather
|
|
105
|
+
* than stores. They go on the same grid as the properties and so have to
|
|
106
|
+
* carry the same label — drawing their own is how they ended up a size out
|
|
107
|
+
* and a couple of pixels off from the field beside them.
|
|
108
|
+
*/
|
|
109
|
+
label?: string;
|
|
110
|
+
/** Type icon, when there is no property to derive one from. */
|
|
111
|
+
icon?: React.ReactNode;
|
|
54
112
|
children: React.ReactNode;
|
|
55
113
|
}
|
|
56
114
|
|
|
57
115
|
/**
|
|
58
116
|
* One field's worth of the form grid: label, control, description.
|
|
59
117
|
*
|
|
60
|
-
* The
|
|
61
|
-
*
|
|
62
|
-
*
|
|
118
|
+
* The same block frames the read-only rendering of a record, so a field sits in
|
|
119
|
+
* the same place, at the same width, under the same label whether or not you are
|
|
120
|
+
* editing it — see {@link EntityViewBinding}.
|
|
121
|
+
*
|
|
122
|
+
* The label is one size down from the control it names (13px against the body's
|
|
123
|
+
* 14) with its icon matched to it at 14px. Labels are scaffolding: you read them
|
|
124
|
+
* to find the value, then read the value. Setting them at body size made a
|
|
125
|
+
* column of records scan as a column of field names with data attached.
|
|
63
126
|
*/
|
|
64
127
|
export function FieldBlock({
|
|
65
128
|
propertyKey,
|
|
66
129
|
property,
|
|
67
130
|
showLabel,
|
|
131
|
+
mode = "edit",
|
|
132
|
+
label,
|
|
133
|
+
icon,
|
|
68
134
|
children
|
|
69
135
|
}: FieldBlockProps) {
|
|
70
136
|
|
|
137
|
+
const reading = mode === "read";
|
|
71
138
|
const description = property?.description?.trim();
|
|
72
139
|
const required = Boolean(property?.validation?.required);
|
|
73
140
|
|
|
@@ -82,18 +149,21 @@ export function FieldBlock({
|
|
|
82
149
|
{showLabel && (
|
|
83
150
|
<PropertyIdCopyTooltip propertyKey={propertyKey}>
|
|
84
151
|
<div className={cls(
|
|
85
|
-
"flex items-center gap-1.5
|
|
86
|
-
"text-text-secondary dark:text-text-secondary-dark"
|
|
152
|
+
"flex items-center gap-1.5 font-medium leading-tight mb-1.5",
|
|
153
|
+
"text-[13px] text-text-secondary dark:text-text-secondary-dark"
|
|
87
154
|
)}>
|
|
88
155
|
{/* Small and quiet: enough to tell a number from a
|
|
89
156
|
relation at a glance, not enough to compete with the
|
|
90
|
-
field name.
|
|
91
|
-
|
|
157
|
+
field name. Matched to the label's 13px rather than
|
|
158
|
+
left at the 16px `smallest` token, which put the
|
|
159
|
+
heaviest mark on the row on the least important
|
|
160
|
+
thing in it. */}
|
|
161
|
+
{(property || icon) && (
|
|
92
162
|
<span className={"shrink-0 text-text-disabled dark:text-text-disabled-dark"}>
|
|
93
|
-
{getIconForProperty(property,
|
|
163
|
+
{property ? getIconForProperty(property, LABEL_ICON_SIZE) : icon}
|
|
94
164
|
</span>
|
|
95
165
|
)}
|
|
96
|
-
<span className={"truncate"}>{property?.name ?? propertyKey}</span>
|
|
166
|
+
<span className={"truncate"}>{property?.name ?? label ?? propertyKey}</span>
|
|
97
167
|
{required && <span className={"text-red-500 dark:text-red-500 -ml-1"}>*</span>}
|
|
98
168
|
</div>
|
|
99
169
|
</PropertyIdCopyTooltip>
|
|
@@ -107,7 +177,7 @@ export function FieldBlock({
|
|
|
107
177
|
|
|
108
178
|
{/* Under the control, where it reads as a note about the thing you
|
|
109
179
|
just filled in rather than a subtitle of the next label. */}
|
|
110
|
-
{showLabel && description && (
|
|
180
|
+
{showLabel && description && !reading && (
|
|
111
181
|
<Typography variant={"caption"}
|
|
112
182
|
color={"disabled"}
|
|
113
183
|
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
|
|
|
@@ -7,7 +7,7 @@ import { LabelWithIconAndTooltip } from "../components/LabelWithIconAndTooltip";
|
|
|
7
7
|
import { PropertyFieldBinding } from "../PropertyFieldBinding";
|
|
8
8
|
import { ExpandablePanel, Typography } from "@rebasepro/ui";
|
|
9
9
|
import { getArrayResolvedProperties } from "@rebasepro/common";
|
|
10
|
-
import { isReadOnly } from "@rebasepro/app";
|
|
10
|
+
import { isDisabled, isReadOnly } from "@rebasepro/app";
|
|
11
11
|
import { getIconForProperty } from "../../util/property_utils";
|
|
12
12
|
import { useClearRestoreValue } from "../useClearRestoreValue";
|
|
13
13
|
import { useAuthController } from "@rebasepro/app";
|
|
@@ -65,7 +65,7 @@ export function ArrayCustomShapedFieldBinding({
|
|
|
65
65
|
</>);
|
|
66
66
|
|
|
67
67
|
const body = (resolvedProperties ?? []).map((childProperty, index) => {
|
|
68
|
-
const thisDisabled = isReadOnly(childProperty) ||
|
|
68
|
+
const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
|
|
69
69
|
const fieldProps = {
|
|
70
70
|
propertyKey: `${propertyKey}[${index}]`,
|
|
71
71
|
disabled: disabled || thisDisabled,
|
|
@@ -86,14 +86,19 @@ export function BinaryFieldBinding({
|
|
|
86
86
|
const localError = malformed ? "Not valid base64" : undefined;
|
|
87
87
|
|
|
88
88
|
return (
|
|
89
|
-
<div className="flex flex-col gap-2 w-full
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
<div className="flex flex-col gap-2 w-full">
|
|
90
|
+
{/* The row goes away with the label rather than emptying out. Left
|
|
91
|
+
unconditional it still contributed its `mb-1` and the column's
|
|
92
|
+
`gap-2`, which — on top of the `mt-2` that used to sit on the
|
|
93
|
+
wrapper — pushed this control 20px below the one next to it
|
|
94
|
+
while the two labels stayed level. */}
|
|
95
|
+
{!hideLabel && <div className="flex items-center justify-between mb-1">
|
|
96
|
+
<LabelWithIcon
|
|
92
97
|
icon={getIconForProperty(property, "small")}
|
|
93
98
|
required={property.validation?.required}
|
|
94
99
|
title={property.name ?? propertyKey}
|
|
95
|
-
/>
|
|
96
|
-
</div>
|
|
100
|
+
/>
|
|
101
|
+
</div>}
|
|
97
102
|
|
|
98
103
|
<PropertyIdCopyTooltip propertyKey={propertyKey}>
|
|
99
104
|
<div className="w-full">
|
|
@@ -13,7 +13,7 @@ import { EnumValuesChip } from "../../preview";
|
|
|
13
13
|
import { DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, getDefaultValueFor } from "@rebasepro/common";
|
|
14
14
|
import { getIconForProperty } from "../../util/property_utils";
|
|
15
15
|
import { cls, ExpandablePanel, paperMixin, Select, SelectItem, Typography } from "@rebasepro/ui";
|
|
16
|
-
import { useTranslation } from "@rebasepro/app";
|
|
16
|
+
import { isDisabled, useTranslation } from "@rebasepro/app";
|
|
17
17
|
import { useClearRestoreValue } from "../useClearRestoreValue";
|
|
18
18
|
import { ArrayContainer, ArrayEntryParams } from "../../components/ArrayContainer";
|
|
19
19
|
import { mergeDeep } from "@rebasepro/utils";
|
|
@@ -94,8 +94,8 @@ export function BlockFieldBinding({
|
|
|
94
94
|
addLabel={property.name ? t("add_to_field", { fieldName: property.name }) : t("add_entry")}
|
|
95
95
|
buildEntry={buildEntry}
|
|
96
96
|
onInternalIdAdded={setLastAddedId}
|
|
97
|
-
disabled={isSubmitting ||
|
|
98
|
-
canAddElements={!property
|
|
97
|
+
disabled={isSubmitting || isDisabled(property)}
|
|
98
|
+
canAddElements={!isDisabled(property)}
|
|
99
99
|
onValueChange={(value) => setFieldValue(propertyKey, value)}
|
|
100
100
|
newDefaultEntry={{
|
|
101
101
|
[property.oneOf!.typeField ?? DEFAULT_ONE_OF_TYPE]: firstOneOfKey,
|