@rebasepro/admin 0.13.0 → 0.13.1-canary.g1822133
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-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.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/AdditionalFieldValue.d.ts +25 -0
- package/dist/components/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
- 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 +63 -0
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- 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 +21 -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/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-J79rMoNj.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.js.map +1 -0
- 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-D5SKygpJ.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-D5SKygpJ.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-C_4edkoD.js} +6 -5
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +237 -123
- 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/components/date_fns_locales.d.ts +9 -0
- 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/view_constants.d.ts +24 -0
- package/dist/util/view_mode.d.ts +18 -0
- package/dist/{util-GiwPgxnL.js → util-C-D5yD2n.js} +4161 -1809
- 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/AdditionalFieldValue.tsx +68 -0
- package/src/components/ClearFilterSortButton.tsx +16 -7
- 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/CollectionTableBinding/table_bindings.tsx +7 -1
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +822 -354
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +52 -12
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +51 -39
- package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
- package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
- package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +47 -7
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +23 -6
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +26 -23
- package/src/components/DetailViewBinding.tsx +109 -62
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +56 -16
- package/src/components/EditViewBinding.tsx +233 -50
- 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 +344 -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 +76 -15
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- package/src/components/SideDialogs.tsx +15 -3
- package/src/components/SidePanelBinding.tsx +122 -66
- package/src/components/UserSelector.tsx +67 -13
- 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_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 +74 -53
- 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 +11 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- 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 +21 -2
- 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/TextFieldBinding.tsx +18 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +7 -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/DatePreview.tsx +15 -2
- package/src/preview/components/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- package/src/preview/components/StorageThumbnail.tsx +5 -2
- package/src/preview/components/date_fns_locales.ts +125 -0
- 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/navigation_utils.ts +4 -2
- package/src/util/number_format.ts +46 -0
- package/src/util/previews.ts +8 -55
- 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-CjXkWZ64.js.map +0 -1
- package/dist/PropertyEditView-W8I3YV89.js.map +0 -1
- package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
- package/dist/export-J79rMoNj.js.map +0 -1
- package/dist/import-BvYl--U4.js.map +0 -1
- package/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import ExcelJS from "exceljs";
|
|
2
1
|
import { getWorksheetHeaders } from "./file_headers";
|
|
3
2
|
import { mapJsonParse, unflattenObject } from "./transforms";
|
|
3
|
+
import { parseCsvToObjects } from "./csv";
|
|
4
|
+
import { isPrototypePollutingKey } from "@rebasepro/utils";
|
|
4
5
|
|
|
5
6
|
// Re-export for backwards compat (tests import from this file)
|
|
6
7
|
export { unflattenObject } from "./transforms";
|
|
@@ -10,9 +11,78 @@ type ConversionResult = {
|
|
|
10
11
|
propertiesOrder: string[]
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* ExcelJS, fetched the first time somebody actually opens a workbook.
|
|
16
|
+
*
|
|
17
|
+
* It was a top-level `import ExcelJS from "exceljs"`, and that one line put
|
|
18
|
+
* 940 kB of spreadsheet reader into the admin's entry chunk — preloaded on the
|
|
19
|
+
* login screen, before anyone has authenticated, let alone clicked Import.
|
|
20
|
+
* `CollectionViewActions` lazy-loads the import and export actions and says so
|
|
21
|
+
* in a comment, but the package's own barrel (`src/index.ts` re-exports
|
|
22
|
+
* `./data_import`) puts this module back in the entry's static graph, so the
|
|
23
|
+
* `lazy()` bought nothing. A static import inside a module the barrel reaches
|
|
24
|
+
* is eager no matter what the component above it does; the only thing that
|
|
25
|
+
* makes a dependency lazy is importing it lazily, here.
|
|
26
|
+
*/
|
|
27
|
+
type ExcelJsModule = typeof import("exceljs");
|
|
28
|
+
|
|
29
|
+
let excelJsModule: Promise<ExcelJsModule> | undefined;
|
|
30
|
+
|
|
31
|
+
function loadExcelJs(): Promise<ExcelJsModule> {
|
|
32
|
+
// exceljs is CommonJS, so the namespace a bundler hands back wraps the real
|
|
33
|
+
// module under `default`. Native ESM (and the type declarations) put the
|
|
34
|
+
// members at the top level. Accept either.
|
|
35
|
+
excelJsModule ??= import("exceljs").then(mod => (mod as { default?: ExcelJsModule }).default ?? mod);
|
|
36
|
+
return excelJsModule;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether this file is delimited text rather than a workbook.
|
|
41
|
+
*
|
|
42
|
+
* Browsers report `text/csv`, `application/csv` or nothing at all for the same
|
|
43
|
+
* `.csv`, so the extension is the reliable half of the test.
|
|
44
|
+
*/
|
|
45
|
+
function isCsvFile(file: File): boolean {
|
|
46
|
+
const name = (file.name ?? "").toLowerCase();
|
|
47
|
+
if (name.endsWith(".csv") || name.endsWith(".tsv")) return true;
|
|
48
|
+
return file.type === "text/csv" || file.type === "application/csv";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Turn the parsed cells of one sheet/file into the entity-shaped rows the import maps. */
|
|
52
|
+
function toImportRows(rows: Array<Record<string, unknown>>): object[] {
|
|
53
|
+
return rows.map(mapJsonParse).map(unflattenObject);
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
export function convertFileToJson(file: File): Promise<ConversionResult> {
|
|
14
57
|
return new Promise((resolve, reject) => {
|
|
15
|
-
if (file
|
|
58
|
+
if (isCsvFile(file)) {
|
|
59
|
+
// Not the ExcelJS branch: `workbook.xlsx.load` is a zip reader, and
|
|
60
|
+
// every CSV — the format this admin exports by default — died on it
|
|
61
|
+
// with "Can't find end of central directory : is this a zip file ?".
|
|
62
|
+
console.debug("Converting CSV file to JSON", file.name);
|
|
63
|
+
const reader = new FileReader();
|
|
64
|
+
reader.onload = function (e) {
|
|
65
|
+
try {
|
|
66
|
+
const { headers, data } = parseCsvToObjects(e.target?.result as string);
|
|
67
|
+
if (headers.length === 0) {
|
|
68
|
+
reject(new Error("The CSV file is empty"));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
resolve({
|
|
72
|
+
data: toImportRows(data),
|
|
73
|
+
propertiesOrder: headers
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error("Error parsing CSV file", err);
|
|
77
|
+
reject(err);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
reader.onerror = () => reject(reader.error ?? new Error("Could not read the file"));
|
|
81
|
+
// Explicit UTF-8: the pre-ExcelJS implementation passed
|
|
82
|
+
// `codepage: 65001` for the same reason, and the browser's default
|
|
83
|
+
// guess mangles accented and CJK text.
|
|
84
|
+
reader.readAsText(file, "utf-8");
|
|
85
|
+
} else if (file.type === "application/json") {
|
|
16
86
|
console.debug("Converting JSON file to JSON", file.name);
|
|
17
87
|
const reader = new FileReader();
|
|
18
88
|
reader.onload = function (e) {
|
|
@@ -40,6 +110,7 @@ export function convertFileToJson(file: File): Promise<ConversionResult> {
|
|
|
40
110
|
reader.onload = async function (e) {
|
|
41
111
|
try {
|
|
42
112
|
const buffer = e.target?.result as ArrayBuffer;
|
|
113
|
+
const ExcelJS = await loadExcelJs();
|
|
43
114
|
const workbook = new ExcelJS.Workbook();
|
|
44
115
|
await workbook.xlsx.load(buffer);
|
|
45
116
|
const worksheet = workbook.worksheets[0];
|
|
@@ -57,17 +128,17 @@ export function convertFileToJson(file: File): Promise<ConversionResult> {
|
|
|
57
128
|
const obj: Record<string, any> = {};
|
|
58
129
|
row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
|
|
59
130
|
const header = headers[colNumber - 1];
|
|
60
|
-
|
|
131
|
+
// A `__proto__` header would be the prototype setter
|
|
132
|
+
// here rather than a column; refused, as in `csv.ts`.
|
|
133
|
+
if (header && !isPrototypePollutingKey(header)) {
|
|
61
134
|
obj[header] = cell.value;
|
|
62
135
|
}
|
|
63
136
|
});
|
|
64
137
|
parsedData.push(obj);
|
|
65
138
|
});
|
|
66
139
|
|
|
67
|
-
const cleanedData = parsedData.map(mapJsonParse);
|
|
68
|
-
const jsonData = cleanedData.map(unflattenObject);
|
|
69
140
|
resolve({
|
|
70
|
-
data:
|
|
141
|
+
data: toImportRows(parsedData),
|
|
71
142
|
propertiesOrder: headers
|
|
72
143
|
});
|
|
73
144
|
} catch (err) {
|
|
@@ -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,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);
|
|
@@ -666,7 +697,16 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
666
697
|
<div
|
|
667
698
|
id={`form_${path}`}
|
|
668
699
|
className={cls(
|
|
669
|
-
|
|
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",
|
|
670
710
|
openEntityMode === "dialog"
|
|
671
711
|
? "pt-5 pb-8 px-6 sm:px-8"
|
|
672
712
|
: "pt-6 pb-12 px-5 sm:px-8"
|
|
@@ -706,25 +746,6 @@ export function EntityForm<M extends Record<string, unknown>>({
|
|
|
706
746
|
);
|
|
707
747
|
}
|
|
708
748
|
|
|
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
749
|
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
750
|
useEffect(() => {
|
|
730
751
|
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 <></>;
|