@rebasepro/admin 0.13.1-canary.gef9608c → 0.14.0
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-7b-kSLno.js → CollectionEditorDialog-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-C-31uvpG.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BJylABq3.js → RouterCollectionsStudioView-CwTpdzWF.js} +9 -13
- package/dist/RouterCollectionsStudioView-CwTpdzWF.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/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
- package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
- package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +26 -0
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -6
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +1 -1
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +26 -1
- package/dist/components/DetailViewBinding.d.ts +7 -0
- package/dist/components/DrawerNavigationGroup.d.ts +4 -2
- package/dist/components/DrawerNavigationItem.d.ts +13 -5
- package/dist/components/EditViewBinding.d.ts +22 -7
- package/dist/components/EntityIdentityBar.d.ts +24 -1
- package/dist/components/RelationSelector.d.ts +13 -0
- package/dist/data_export/export/export.d.ts +7 -0
- package/dist/data_export/export/fetch_export_data.d.ts +48 -0
- package/dist/data_export/export/index.d.ts +1 -0
- package/dist/data_import/utils/csv.d.ts +30 -0
- package/dist/data_import/utils/index.d.ts +2 -0
- package/dist/data_import/utils/save_entities.d.ts +38 -0
- package/dist/data_import/utils/transforms.d.ts +9 -0
- package/dist/editor.js +3 -0
- package/dist/editor.js.map +1 -1
- package/dist/{export-C-XG3aRh.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.js.map +1 -0
- package/dist/form/form_utils.d.ts +38 -1
- package/dist/{history-DfMuvPfr.js → history-D5SKygpJ.js} +2 -2
- package/dist/{history-DfMuvPfr.js.map → history-D5SKygpJ.js.map} +1 -1
- package/dist/{import-B0GyllB0.js → import-C_4edkoD.js} +5 -4
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +96 -80
- package/dist/index.js.map +1 -1
- package/dist/preview/components/date_fns_locales.d.ts +9 -0
- package/dist/util/view_constants.d.ts +24 -0
- package/dist/util/view_mode.d.ts +18 -0
- package/dist/{util-DHNXodBo.js → util-C-D5yD2n.js} +2173 -673
- package/dist/util-C-D5yD2n.js.map +1 -0
- package/package.json +10 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +8 -3
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
- package/src/components/ClearFilterSortButton.tsx +16 -7
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +2 -2
- 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/CollectionTableBinding/table_bindings.tsx +7 -1
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +18 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +816 -335
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +45 -10
- package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
- package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
- package/src/components/CollectionViewBinding/SlotValue.tsx +96 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -9
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +3 -3
- package/src/components/CollectionViewBinding/SplitListView.tsx +36 -9
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +76 -24
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +7 -0
- package/src/components/DetailViewBinding.tsx +33 -9
- package/src/components/DrawerNavigationGroup.tsx +12 -32
- package/src/components/DrawerNavigationItem.tsx +33 -12
- package/src/components/EditViewBinding.tsx +175 -28
- package/src/components/EntityIdentityBar.tsx +109 -17
- package/src/components/EntityInspector.tsx +24 -6
- package/src/components/EntityViewBinding.tsx +4 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
- package/src/components/RelationSelector.tsx +61 -11
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- package/src/components/SidePanelBinding.tsx +36 -25
- package/src/components/UserSelector.tsx +52 -9
- package/src/components/history/LastEditedByIndicator.tsx +7 -12
- package/src/data_export/export/ExportCollectionAction.tsx +101 -48
- package/src/data_export/export/export.ts +63 -16
- package/src/data_export/export/fetch_export_data.ts +90 -0
- package/src/data_export/export/index.ts +1 -0
- package/src/data_import/components/DataNewPropertiesMapping.tsx +20 -3
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +1 -0
- package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
- package/src/data_import/import/ImportCollectionAction.tsx +13 -2
- package/src/data_import/utils/csv.ts +153 -0
- package/src/data_import/utils/data.ts +16 -2
- package/src/data_import/utils/file_to_json.ts +77 -6
- package/src/data_import/utils/index.ts +2 -0
- package/src/data_import/utils/save_entities.ts +138 -0
- package/src/data_import/utils/transforms.ts +23 -0
- package/src/editor/components/image-bubble.tsx +2 -0
- package/src/editor/selectors/link-selector.tsx +1 -0
- package/src/form/EntityForm.tsx +12 -3
- package/src/form/EntityFormBinding.tsx +8 -12
- package/src/form/PropertyFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
- package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +15 -2
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
- package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
- package/src/form/form_utils.ts +78 -0
- package/src/index.ts +9 -1
- package/src/preview/PropertyPreview.tsx +7 -3
- package/src/preview/components/DatePreview.tsx +15 -2
- package/src/preview/components/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- package/src/preview/components/date_fns_locales.ts +125 -0
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
- package/src/util/navigation_utils.ts +4 -2
- package/src/util/property_utils.tsx +6 -1
- package/src/util/view_constants.ts +27 -0
- package/src/util/view_mode.ts +36 -0
- package/dist/CollectionEditorDialog-7b-kSLno.js.map +0 -1
- package/dist/PropertyEditView-C-31uvpG.js.map +0 -1
- package/dist/RouterCollectionsStudioView-BJylABq3.js.map +0 -1
- package/dist/export-C-XG3aRh.js.map +0 -1
- package/dist/import-B0GyllB0.js.map +0 -1
- package/dist/util-DHNXodBo.js.map +0 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { CollectionAccessor, Entity, RebaseData } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
/** Rows written per request, and per transaction where the data source has one. */
|
|
4
|
+
export const IMPORT_BATCH_SIZE = 25;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An import failure that says where it stopped.
|
|
8
|
+
*
|
|
9
|
+
* "Error saving data / null value in column "sku" violates not-null constraint"
|
|
10
|
+
* on a ten-thousand-row import named no row, and the Retry button restarted at
|
|
11
|
+
* zero — where the rows already written now conflicted, so it could never
|
|
12
|
+
* succeed. Both halves of that are what this carries.
|
|
13
|
+
*/
|
|
14
|
+
export interface ImportSaveError extends Error {
|
|
15
|
+
/** Rows written and confirmed before the failure. Where a retry resumes. */
|
|
16
|
+
committed: number;
|
|
17
|
+
/** Index of the first row that was not written, 0-based. */
|
|
18
|
+
failedFrom: number;
|
|
19
|
+
/** Index after the last row of the batch that was not written. */
|
|
20
|
+
failedTo: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The row a bulk write takes: the values, with the chosen id as a column. */
|
|
24
|
+
function toBulkRow(entity: Partial<Entity<any>>): Record<string, unknown> {
|
|
25
|
+
const values = { ...(entity.values ?? {}) } as Record<string, unknown>;
|
|
26
|
+
if (entity.id !== undefined && entity.id !== null && entity.id !== "") {
|
|
27
|
+
// Exactly what single-row `create(values, id)` puts on the wire.
|
|
28
|
+
values.id = entity.id;
|
|
29
|
+
}
|
|
30
|
+
return values;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isBulkUnsupported(error: unknown): boolean {
|
|
34
|
+
if ((error as { code?: string })?.code === "BULK_UNSUPPORTED") return true;
|
|
35
|
+
const message = (error as Error)?.message ?? "";
|
|
36
|
+
return /bulk writes are not supported|does not support bulk/i.test(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isConflict(error: unknown): boolean {
|
|
40
|
+
const status = (error as { status?: number; statusCode?: number })?.status
|
|
41
|
+
?? (error as { statusCode?: number })?.statusCode;
|
|
42
|
+
if (status === 409) return true;
|
|
43
|
+
const code = (error as { code?: string })?.code;
|
|
44
|
+
return code === "CONFLICT" || code === "23505";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Write one batch, all-or-nothing wherever the data source can.
|
|
49
|
+
*
|
|
50
|
+
* `POST /<collection>/bulk` validates every row before the transaction opens,
|
|
51
|
+
* names a failure by row index, and takes the `upsert` flag the preview screen
|
|
52
|
+
* promises — none of which one `create` per row inside a `Promise.all` could
|
|
53
|
+
* offer, since `Promise.all` does not cancel its siblings and every request was
|
|
54
|
+
* its own transaction.
|
|
55
|
+
*/
|
|
56
|
+
async function writeBatch(accessor: CollectionAccessor,
|
|
57
|
+
batch: Partial<Entity<any>>[],
|
|
58
|
+
bulkUnsupported: { current: boolean },
|
|
59
|
+
written: { count: number }): Promise<void> {
|
|
60
|
+
|
|
61
|
+
if (accessor.createMany && !bulkUnsupported.current) {
|
|
62
|
+
try {
|
|
63
|
+
await accessor.createMany(batch.map(toBulkRow), { upsert: true });
|
|
64
|
+
written.count = batch.length;
|
|
65
|
+
return;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
if (!isBulkUnsupported(e)) throw e;
|
|
68
|
+
// A data source without bulk writes: remember it, so the failed
|
|
69
|
+
// round trip is not paid again on every later batch.
|
|
70
|
+
bulkUnsupported.current = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Sequential, not `Promise.all`: a rejected write does not cancel its
|
|
75
|
+
// siblings, so a parallel batch commits an indeterminate subset of itself
|
|
76
|
+
// and nobody can say which rows landed.
|
|
77
|
+
for (const entity of batch) {
|
|
78
|
+
try {
|
|
79
|
+
await accessor.create(entity.values ?? {}, entity.id);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Without `/bulk` there is no upsert, so the preview's promise is
|
|
82
|
+
// honoured the only way left: a row that collided on an id the user
|
|
83
|
+
// supplied is overwritten. A row with no id had nothing to collide
|
|
84
|
+
// on, and every other failure is still a failure.
|
|
85
|
+
if (entity.id === undefined || !isConflict(e)) throw e;
|
|
86
|
+
await accessor.update(entity.id, entity.values ?? {});
|
|
87
|
+
}
|
|
88
|
+
written.count++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write imported entities in batches, reporting what was committed.
|
|
94
|
+
*
|
|
95
|
+
* Resumable on purpose: a failure at row 4 000 leaves rows 1–3 999 in the table
|
|
96
|
+
* whatever this does, so the only useful thing to report is where to carry on
|
|
97
|
+
* from. `offset` is that point — the Retry button passes back the `committed`
|
|
98
|
+
* count of the error it is retrying, instead of starting from zero and
|
|
99
|
+
* conflicting on every row that already exists.
|
|
100
|
+
*
|
|
101
|
+
* @throws {ImportSaveError} naming the rows that were not written.
|
|
102
|
+
*/
|
|
103
|
+
export async function saveImportedEntities(dataClient: RebaseData,
|
|
104
|
+
path: string,
|
|
105
|
+
data: Partial<Entity<any>>[],
|
|
106
|
+
{
|
|
107
|
+
offset = 0,
|
|
108
|
+
batchSize = IMPORT_BATCH_SIZE,
|
|
109
|
+
onBatchCommitted = () => undefined,
|
|
110
|
+
bulkUnsupported = { current: false }
|
|
111
|
+
}: {
|
|
112
|
+
offset?: number;
|
|
113
|
+
batchSize?: number;
|
|
114
|
+
onBatchCommitted?: (rowsWritten: number) => void;
|
|
115
|
+
bulkUnsupported?: { current: boolean };
|
|
116
|
+
} = {}): Promise<void> {
|
|
117
|
+
|
|
118
|
+
console.debug("Saving imported data", offset, batchSize);
|
|
119
|
+
|
|
120
|
+
const accessor = dataClient.collection(path);
|
|
121
|
+
|
|
122
|
+
for (let start = offset; start < data.length; start += batchSize) {
|
|
123
|
+
const batch = data.slice(start, start + batchSize);
|
|
124
|
+
const written = { count: 0 };
|
|
125
|
+
try {
|
|
126
|
+
await writeBatch(accessor, batch, bulkUnsupported, written);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
throw Object.assign(e as Error, {
|
|
129
|
+
committed: start + written.count,
|
|
130
|
+
failedFrom: start + written.count,
|
|
131
|
+
failedTo: start + batch.length
|
|
132
|
+
}) as ImportSaveError;
|
|
133
|
+
}
|
|
134
|
+
onBatchCommitted(start + batch.length);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onBatchCommitted(data.length);
|
|
138
|
+
}
|
|
@@ -1,10 +1,27 @@
|
|
|
1
|
+
import { isPrototypePollutingKey, pathTraversesPrototype } from "@rebasepro/utils";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Take an object with keys of type `address.street`, `address.city` and
|
|
3
5
|
* convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
|
|
6
|
+
*
|
|
7
|
+
* Keys here are the **header row of an uploaded file**, so they are attacker
|
|
8
|
+
* data. `res["__proto__"] = …` is the prototype setter rather than an own
|
|
9
|
+
* property, so a column named `__proto__.polluted` walked out of the accumulator
|
|
10
|
+
* and wrote onto `Object.prototype` for the life of the tab — and
|
|
11
|
+
* `constructor.prototype.x` threw a `TypeError` that surfaced as an unreadable
|
|
12
|
+
* file. Such a column is refused, not sanitised, the way `setIn` settled it
|
|
13
|
+
* (`docs/bug-classes.md` class 22).
|
|
14
|
+
*
|
|
4
15
|
* @param flatObj
|
|
5
16
|
*/
|
|
6
17
|
export function unflattenObject(flatObj: { [key: string]: any }) {
|
|
7
18
|
return Object.keys(flatObj).reduce((nestedObj, key) => {
|
|
19
|
+
// The string form, so `toPath` rewrites `__proto__[0]` to a `__proto__`
|
|
20
|
+
// segment too — the bracket branch below writes through the same setter.
|
|
21
|
+
if (pathTraversesPrototype(key)) {
|
|
22
|
+
console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
|
|
23
|
+
return nestedObj;
|
|
24
|
+
}
|
|
8
25
|
let currentObj = nestedObj;
|
|
9
26
|
const keyParts = key.split(".");
|
|
10
27
|
keyParts.forEach((keyPart, i) => {
|
|
@@ -37,6 +54,12 @@ export function unflattenObject(flatObj: { [key: string]: any }) {
|
|
|
37
54
|
|
|
38
55
|
export function mapJsonParse(obj: Record<string, any>) {
|
|
39
56
|
return Object.keys(obj).reduce((acc: Record<string, any>, key) => {
|
|
57
|
+
// Same header row, same setter: `acc["__proto__"] = value` replaces the
|
|
58
|
+
// accumulator's prototype instead of adding a column.
|
|
59
|
+
if (isPrototypePollutingKey(key)) {
|
|
60
|
+
console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
|
|
61
|
+
return acc;
|
|
62
|
+
}
|
|
40
63
|
try {
|
|
41
64
|
acc[key] = JSON.parse(obj[key]);
|
|
42
65
|
} catch (e) {
|
|
@@ -157,6 +157,7 @@ export const ImageBubble = forwardRef<HTMLDivElement, ImageBubbleProps>(
|
|
|
157
157
|
>
|
|
158
158
|
<TextField
|
|
159
159
|
size={"small"}
|
|
160
|
+
aria-label={t("alt_text")}
|
|
160
161
|
placeholder={t("alt_text")}
|
|
161
162
|
value={alt}
|
|
162
163
|
onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
@@ -166,6 +167,7 @@ export const ImageBubble = forwardRef<HTMLDivElement, ImageBubbleProps>(
|
|
|
166
167
|
/>
|
|
167
168
|
<TextField
|
|
168
169
|
size={"small"}
|
|
170
|
+
aria-label={t("title")}
|
|
169
171
|
placeholder={t("title")}
|
|
170
172
|
value={title}
|
|
171
173
|
onChange={(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -9,7 +9,7 @@ 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";
|
|
@@ -437,7 +437,7 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
437
437
|
disabledProp
|
|
438
438
|
|| (!autoSave && formex.isSubmitting)
|
|
439
439
|
|| isReadOnly(property)
|
|
440
|
-
||
|
|
440
|
+
|| isDisabled(property)
|
|
441
441
|
|| (!isNew && "isId" in property && Boolean(property.isId))
|
|
442
442
|
|| (isNew && isIdAndAuto)
|
|
443
443
|
);
|
|
@@ -697,7 +697,16 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
697
697
|
<div
|
|
698
698
|
id={`form_${path}`}
|
|
699
699
|
className={cls(
|
|
700
|
-
|
|
700
|
+
// Two rules widen the column, and they answer to
|
|
701
|
+
// different things on purpose. `@7xl/form` measures
|
|
702
|
+
// the form itself: full screen on a laptop is ~1400px
|
|
703
|
+
// of form, which after the 304px rail leaves room for
|
|
704
|
+
// the wider column with real gutters either side — and
|
|
705
|
+
// it stays 768 in the split pane and the side panel,
|
|
706
|
+
// which never get that wide. `2xl` is the older
|
|
707
|
+
// viewport rule, kept so a panel on a large monitor
|
|
708
|
+
// does not get *narrower* than it is today.
|
|
709
|
+
"@container/col w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col",
|
|
701
710
|
openEntityMode === "dialog"
|
|
702
711
|
? "pt-5 pb-8 px-6 sm:px-8"
|
|
703
712
|
: "pt-6 pb-12 px-5 sm:px-8"
|
|
@@ -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
|
|
|
@@ -145,20 +145,16 @@ export function EntityFormBinding<M extends Record<string, unknown>>({
|
|
|
145
145
|
|
|
146
146
|
const localChangesData = useMemo(() => {
|
|
147
147
|
if (!localChangesDataRaw) return undefined;
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// the form, so measuring it against the baseline alone offered to
|
|
152
|
-
// restore the values already on screen.
|
|
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.
|
|
153
151
|
const openingValues = initialDirtyValues
|
|
154
152
|
? mergeDeep(computedInitialValues, initialDirtyValues)
|
|
155
153
|
: computedInitialValues;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
return cleaned;
|
|
154
|
+
return getUnappliedLocalChanges(
|
|
155
|
+
localChangesDataRaw as Partial<M>,
|
|
156
|
+
openingValues as Partial<M>
|
|
157
|
+
);
|
|
162
158
|
}, [localChangesDataRaw, computedInitialValues, initialDirtyValues]);
|
|
163
159
|
|
|
164
160
|
const hasLocalChanges = !localChangesCleared && !!localChangesData && Object.keys(localChangesData as object).length > 0;
|
|
@@ -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 <></>;
|
|
@@ -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,
|
|
@@ -139,6 +139,7 @@ export function BinaryFieldBinding({
|
|
|
139
139
|
<div className={cls("flex flex-col gap-2 p-4 rounded-xl border", defaultBorderMixin)}>
|
|
140
140
|
<TextField
|
|
141
141
|
size={size}
|
|
142
|
+
aria-label={`${property.name ?? propertyKey} base64 value`}
|
|
142
143
|
value={text}
|
|
143
144
|
onChange={onChange}
|
|
144
145
|
autoFocus={autoFocus ?? true}
|
|
@@ -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,
|
|
@@ -60,6 +60,10 @@ export function DateTimeFieldBinding({
|
|
|
60
60
|
required={property.validation?.required}
|
|
61
61
|
className={showError ? "text-red-500 dark:text-red-500" : "text-text-secondary dark:text-text-secondary-dark"}
|
|
62
62
|
title={property.name ?? propertyKey}/>}
|
|
63
|
+
// The entity form sets `hideLabel` for every field FieldBlock
|
|
64
|
+
// labels on its behalf, and FieldBlock's label is a <span> that
|
|
65
|
+
// points at nothing — so without this the control is nameless.
|
|
66
|
+
aria-label={hideLabel ? (property.name ?? propertyKey) : undefined}
|
|
63
67
|
/>
|
|
64
68
|
</PropertyIdCopyTooltip>
|
|
65
69
|
|
|
@@ -269,6 +269,7 @@ function MapKeyValueRow<T extends Record<string, unknown>>({
|
|
|
269
269
|
if (type === "string" || type === "number") {
|
|
270
270
|
return <TextField
|
|
271
271
|
key={type}
|
|
272
|
+
aria-label={fieldKey ? `${t("value")}: ${fieldKey}` : t("value")}
|
|
272
273
|
placeholder={"value"}
|
|
273
274
|
value={entryValue as string | number | undefined}
|
|
274
275
|
type={type === "number" ? "number" : "text"}
|
|
@@ -390,6 +391,7 @@ function MapKeyValueRow<T extends Record<string, unknown>>({
|
|
|
390
391
|
<div className="w-[300px] max-w-[30%]">
|
|
391
392
|
<TextField
|
|
392
393
|
value={fieldKey}
|
|
394
|
+
aria-label={t("key")}
|
|
393
395
|
placeholder={"key"}
|
|
394
396
|
disabled={disabled || (entryValue !== undefined && entryValue !== null && entryValue !== "")}
|
|
395
397
|
size={"medium"}
|
|
@@ -459,6 +461,7 @@ function ArrayKeyValueRow<T>({
|
|
|
459
461
|
function buildInput(entryValue: unknown, type: DataType) {
|
|
460
462
|
if (type === "string" || type === "number") {
|
|
461
463
|
return <TextField value={entryValue as string | number | undefined}
|
|
464
|
+
aria-label={`Value ${index + 1}`}
|
|
462
465
|
type={type === "number" ? "number" : "text"}
|
|
463
466
|
size={"medium"}
|
|
464
467
|
onChange={(event) => {
|
|
@@ -6,7 +6,7 @@ import { useTranslation } from "@rebasepro/app";
|
|
|
6
6
|
|
|
7
7
|
import { ErrorBoundary } from "@rebasepro/ui";
|
|
8
8
|
import { getIconForProperty } from "../../util/property_utils";
|
|
9
|
-
import { isHidden, isReadOnly } from "@rebasepro/app";
|
|
9
|
+
import { isDisabled, isHidden, isReadOnly } from "@rebasepro/app";
|
|
10
10
|
import { FieldHelperText } from "../components/FieldHelperText";
|
|
11
11
|
import { LabelWithIconAndTooltip } from "../components/LabelWithIconAndTooltip";
|
|
12
12
|
import { PropertyFieldBinding } from "../PropertyFieldBinding";
|
|
@@ -50,7 +50,7 @@ export function MapFieldBinding({
|
|
|
50
50
|
{Object.entries(mapProperties)
|
|
51
51
|
.filter(([_, property]) => !isHidden(property))
|
|
52
52
|
.map(([entryKey, childProperty], index) => {
|
|
53
|
-
const thisDisabled = isReadOnly(childProperty) ||
|
|
53
|
+
const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
|
|
54
54
|
const fieldBindingProps: PropertyFieldBindingProps<Record<string, unknown>> = {
|
|
55
55
|
propertyKey: `${propertyKey}.${entryKey}`,
|
|
56
56
|
disabled: disabled || thisDisabled,
|
|
@@ -45,7 +45,7 @@ export function ReadOnlyFieldBinding({
|
|
|
45
45
|
icon={getIconForProperty(property, "small")}
|
|
46
46
|
required={property.validation?.required}
|
|
47
47
|
title={property.name ?? propertyKey}
|
|
48
|
-
className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3
|
|
48
|
+
className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3"}/>
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
<div
|
|
@@ -59,13 +59,26 @@ export function ReadOnlyFieldBinding({
|
|
|
59
59
|
// so a row holding one of each had its two boxes'
|
|
60
60
|
// baselines and bottom edges disagree — the field looked
|
|
61
61
|
// misaligned rather than uneditable.
|
|
62
|
-
|
|
62
|
+
// `px-3` is what `TextField` gives its input, so a
|
|
63
|
+
// read-only value starts on the same vertical line as
|
|
64
|
+
// the text beside it. It was `px-4 md:px-6`, which in a
|
|
65
|
+
// one-column span spent a sixth of the box on empty
|
|
66
|
+
// margin and pushed short values towards the middle.
|
|
67
|
+
: "rounded-lg border border-surface-200 dark:border-surface-700 px-3 min-h-12 opacity-80"
|
|
63
68
|
)}>
|
|
64
69
|
|
|
65
70
|
<ErrorBoundary>
|
|
71
|
+
{/* The field is labelled either just above by this
|
|
72
|
+
component, or by the `FieldBlock` that asked for
|
|
73
|
+
`hideLabel` — never by the preview. `BooleanPreview`
|
|
74
|
+
prints the property name beside its checkbox, so a
|
|
75
|
+
read-only boolean read as "Procesado por LLM" over a
|
|
76
|
+
checkbox saying "Procesado por LLM", wrapped over two
|
|
77
|
+
lines in a one-column span. */}
|
|
66
78
|
<PropertyPreview propertyKey={propertyKey}
|
|
67
79
|
value={value}
|
|
68
80
|
property={property}
|
|
81
|
+
hideLabel={hideLabel || !minimalistView}
|
|
69
82
|
size={"medium"}/>
|
|
70
83
|
</ErrorBoundary>
|
|
71
84
|
|
|
@@ -11,7 +11,7 @@ import { getIconForProperty } from "../../util/property_utils";
|
|
|
11
11
|
import { PropertyFieldBinding } from "../PropertyFieldBinding";
|
|
12
12
|
import { ExpandablePanel, Typography } from "@rebasepro/ui";
|
|
13
13
|
import { useClearRestoreValue } from "../useClearRestoreValue";
|
|
14
|
-
import { useAuthController, useTranslation } from "@rebasepro/app";
|
|
14
|
+
import { isDisabled, useAuthController, useTranslation } from "@rebasepro/app";
|
|
15
15
|
import { mergeDeep } from "@rebasepro/utils";
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -96,14 +96,14 @@ export function RepeatFieldBinding({
|
|
|
96
96
|
</ErrorBoundary>;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
const canAddElements = !property
|
|
99
|
+
const canAddElements = !isDisabled(property) && !isSubmitting && !disabled && (property.admin?.canAddElements || property.admin?.canAddElements === undefined);
|
|
100
100
|
const sortable = property.admin?.sortable === undefined ? true : property.admin.sortable;
|
|
101
101
|
const arrayContainer = <ArrayContainer droppableId={propertyKey}
|
|
102
102
|
addLabel={property.name ? t("add_to_field", { fieldName: property.name }) : t("add_entry")}
|
|
103
103
|
value={(value as unknown[]) ?? []}
|
|
104
104
|
buildEntry={buildEntry}
|
|
105
105
|
onInternalIdAdded={setLastAddedId}
|
|
106
|
-
disabled={isSubmitting ||
|
|
106
|
+
disabled={isSubmitting || isDisabled(property)}
|
|
107
107
|
canAddElements={canAddElements}
|
|
108
108
|
sortable={sortable}
|
|
109
109
|
newDefaultEntry={getDefaultValueFor(property.of)}
|
|
@@ -8,7 +8,7 @@ import { useDropzone } from "react-dropzone";
|
|
|
8
8
|
import { FieldHelperText } from "../components/FieldHelperText";
|
|
9
9
|
import { LabelWithIconAndTooltip } from "../components/LabelWithIconAndTooltip";
|
|
10
10
|
|
|
11
|
-
import { isReadOnly } from "@rebasepro/app";
|
|
11
|
+
import { isDisabled, isReadOnly } from "@rebasepro/app";
|
|
12
12
|
import { getIconForProperty } from "../../util/property_utils";
|
|
13
13
|
import { useAuthController, useSnackbarController, useStorageSource, useTranslation } from "@rebasepro/app";
|
|
14
14
|
import { StorageSourceContext } from "@rebasepro/app";
|
|
@@ -74,7 +74,7 @@ export function StorageUploadFieldBinding({
|
|
|
74
74
|
const authController = useAuthController();
|
|
75
75
|
|
|
76
76
|
const storageSource = useStorageSource();
|
|
77
|
-
const disabled = isReadOnly(property) ||
|
|
77
|
+
const disabled = isReadOnly(property) || isDisabled(property) || isSubmitting || context.disabled;
|
|
78
78
|
|
|
79
79
|
const {
|
|
80
80
|
internalValue,
|
|
@@ -97,6 +97,22 @@ export function TextFieldBinding<T extends string | number>({
|
|
|
97
97
|
required={property.validation?.required || property.isId === true}
|
|
98
98
|
title={property.name ?? propertyKey}/>
|
|
99
99
|
);
|
|
100
|
+
|
|
101
|
+
// The control needs a name of its own in two cases, and between them they
|
|
102
|
+
// cover every text field in an entity form:
|
|
103
|
+
//
|
|
104
|
+
// - `hideLabel`, which the form sets for anything FieldBlock labels for it
|
|
105
|
+
// (`hideLabel: !selfLabelling`). FieldBlock draws the name in a <span>
|
|
106
|
+
// inside a <div> — not a <label>, and pointing at nothing — so suppressing
|
|
107
|
+
// the field's own label left the input with no accessible name at all and
|
|
108
|
+
// a screen reader fell back to reading its value back to you.
|
|
109
|
+
// - the multiline branch below, which never associated anything: its label
|
|
110
|
+
// is a decorative absolutely-positioned <div>, so it is unnamed whether or
|
|
111
|
+
// not the form also drew one.
|
|
112
|
+
//
|
|
113
|
+
// The single-line TextField is already handled by the kit when a label is
|
|
114
|
+
// present — it wires `aria-labelledby` — so it only needs this as a fallback.
|
|
115
|
+
const accessibleName = property.name ?? propertyKey;
|
|
100
116
|
return (<>
|
|
101
117
|
<PropertyIdCopyTooltip propertyKey={propertyKey}>
|
|
102
118
|
{isMultiline ? (
|
|
@@ -115,6 +131,7 @@ export function TextFieldBinding<T extends string | number>({
|
|
|
115
131
|
onChange={onChange}
|
|
116
132
|
autoFocus={autoFocus}
|
|
117
133
|
disabled={disabled}
|
|
134
|
+
aria-label={accessibleName}
|
|
118
135
|
className={cls(
|
|
119
136
|
"rounded-md resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3",
|
|
120
137
|
label ? "pt-8 pb-2" : "py-2",
|
|
@@ -138,6 +155,7 @@ export function TextFieldBinding<T extends string | number>({
|
|
|
138
155
|
onChange={onChange}
|
|
139
156
|
autoFocus={autoFocus}
|
|
140
157
|
label={label}
|
|
158
|
+
aria-label={label ? undefined : accessibleName}
|
|
141
159
|
type={inputType}
|
|
142
160
|
disabled={disabled}
|
|
143
161
|
endAdornment={
|
|
@@ -166,6 +166,7 @@ export function VectorFieldBinding({
|
|
|
166
166
|
<div className={cls("flex flex-col gap-2 p-4 rounded-xl border bg-surface-50/20 dark:bg-surface-800/10", defaultBorderMixin)}>
|
|
167
167
|
<TextField
|
|
168
168
|
size={size}
|
|
169
|
+
aria-label={`${property.name ?? propertyKey} vector values`}
|
|
169
170
|
value={textValue}
|
|
170
171
|
onChange={onChange}
|
|
171
172
|
autoFocus={true}
|
package/src/form/form_utils.ts
CHANGED
|
@@ -114,6 +114,84 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
|
|
|
114
114
|
return changes;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* What the local-changes backup still has to offer, given what the form is
|
|
119
|
+
* already showing — the answer the "unsaved local changes" banner is asking for.
|
|
120
|
+
* `undefined` means there is nothing left to apply, and no banner.
|
|
121
|
+
*
|
|
122
|
+
* Measured against what the form *opens showing*, not against the stored
|
|
123
|
+
* record. The banner exists to offer a draft the form is not already displaying,
|
|
124
|
+
* and an edit carried across a change of layout is in both the backup and the
|
|
125
|
+
* form: measuring against the stored values alone offered to restore the values
|
|
126
|
+
* already on screen, over an edit the user had never left.
|
|
127
|
+
*/
|
|
128
|
+
export function getUnappliedLocalChanges<M extends Record<string, unknown>>(
|
|
129
|
+
backupValues: Partial<M>,
|
|
130
|
+
openingValues: Partial<M>
|
|
131
|
+
): Partial<M> | undefined {
|
|
132
|
+
const changes = getChanges(backupValues, openingValues);
|
|
133
|
+
const cleaned = removeEmptyContainers(changes);
|
|
134
|
+
if (!cleaned || typeof cleaned !== "object" || Object.keys(cleaned).length === 0) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return cleaned as Partial<M>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What travels when the same record changes layout — the split's "hide list",
|
|
142
|
+
* full screen's "show list", the side panel's "open full screen". One mounted
|
|
143
|
+
* form is replaced by another showing the same record, so the edit in progress
|
|
144
|
+
* has to be handed over; left to the local-changes backup alone, the new form
|
|
145
|
+
* met it as a draft from a closed tab and raised the "unsaved local changes"
|
|
146
|
+
* banner over changes the user had made a second earlier and never left.
|
|
147
|
+
*
|
|
148
|
+
* Returns `undefined` when there is nothing to carry, which is its own answer: a
|
|
149
|
+
* record nobody edited must open in the next layout exactly as clean as it was.
|
|
150
|
+
*
|
|
151
|
+
* Only what was edited here travels, keyed by `touched` — the same subset the
|
|
152
|
+
* backup stores. Carrying the whole record instead marks every field touched in
|
|
153
|
+
* the receiving form, which lights up every empty required field as an error and
|
|
154
|
+
* writes the entire record back out as a "local change".
|
|
155
|
+
*/
|
|
156
|
+
export function getEditHandoffValues<M extends Record<string, unknown>>({
|
|
157
|
+
status,
|
|
158
|
+
dirty,
|
|
159
|
+
values,
|
|
160
|
+
touched,
|
|
161
|
+
storedValues
|
|
162
|
+
}: {
|
|
163
|
+
status: EntityStatus;
|
|
164
|
+
/** Whether `values` differ from what is stored. */
|
|
165
|
+
dirty: boolean;
|
|
166
|
+
values: Partial<M>;
|
|
167
|
+
touched: Record<string, boolean>;
|
|
168
|
+
/** The stored record, for the fallback below. Absent for a new one. */
|
|
169
|
+
storedValues?: Partial<M>;
|
|
170
|
+
}): Partial<M> | undefined {
|
|
171
|
+
|
|
172
|
+
if (!values) return undefined;
|
|
173
|
+
|
|
174
|
+
// A record with nothing stored behind it carries whole: there is no
|
|
175
|
+
// baseline to fall back to, so anything not carried is simply lost.
|
|
176
|
+
if (status === "new" || status === "copy") {
|
|
177
|
+
return Object.keys(values).length > 0 ? values : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!dirty) return undefined;
|
|
181
|
+
|
|
182
|
+
const touchedValues = removeEmptyContainers(extractTouchedValues(values, touched ?? {})) as Partial<M> | undefined;
|
|
183
|
+
if (touchedValues && Object.keys(touchedValues).length > 0) {
|
|
184
|
+
return touchedValues;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Dirty with nothing touched — a value set programmatically, by a plugin or
|
|
188
|
+
// a custom field that writes through `setFieldValue` without the blur that
|
|
189
|
+
// marks it. Diffing against the stored record still finds it, and dropping
|
|
190
|
+
// it here would hand over a form that opens clean over an edit.
|
|
191
|
+
const changes = getChanges(values, storedValues ?? {});
|
|
192
|
+
return Object.keys(changes).length > 0 ? changes : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
117
195
|
export function getInitialEntityValues<M extends Record<string, unknown>>(
|
|
118
196
|
authController: AuthController,
|
|
119
197
|
collection: AdminCollection,
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,12 @@ export {
|
|
|
44
44
|
Drawer,
|
|
45
45
|
DefaultDrawer,
|
|
46
46
|
DrawerFooterActions,
|
|
47
|
+
// The two rows the `Shell.DrawerNavigation*` overrides replace. Public so an
|
|
48
|
+
// app can wrap the stock ones — pass different props, add a context — instead of
|
|
49
|
+
// reimplementing a nav row and drifting from the framework's hover, active and
|
|
50
|
+
// tooltip behaviour on the next release.
|
|
51
|
+
DrawerNavigationItem,
|
|
52
|
+
DrawerNavigationGroup,
|
|
47
53
|
AdminModeSyncer,
|
|
48
54
|
// ContentHomePage is lazy-loaded — not re-exported here
|
|
49
55
|
RebaseAdmin,
|
|
@@ -60,7 +66,9 @@ export type {
|
|
|
60
66
|
EntityViewBindingProps,
|
|
61
67
|
SelectionProps,
|
|
62
68
|
SelectableTableProps,
|
|
63
|
-
CollectionPanelProps
|
|
69
|
+
CollectionPanelProps,
|
|
70
|
+
DrawerNavigationGroupProps,
|
|
71
|
+
DrawerNavigationItemProps
|
|
64
72
|
} from "./components";
|
|
65
73
|
|
|
66
74
|
export * from "./hooks";
|