@rebasepro/admin 0.13.0 → 0.13.1-canary.g06dbe5b
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-C3DbFNqp.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-C3DbFNqp.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-DFH8rXRN.js} +5 -5
- package/dist/PropertyEditView-DFH8rXRN.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-DiUDOu4b.js} +9 -13
- package/dist/RouterCollectionsStudioView-DiUDOu4b.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 +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/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/{export-J79rMoNj.js → export-Bd5i6Uvm.js} +175 -39
- package/dist/export-Bd5i6Uvm.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-D9R6LUrH.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-D9R6LUrH.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-pt_j-0G3.js} +6 -5
- package/dist/import-pt_j-0G3.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +158 -60
- 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/view_constants.d.ts +24 -0
- package/dist/util/view_mode.d.ts +18 -0
- package/dist/{util-GiwPgxnL.js → util-nX_-JRuF.js} +3984 -1774
- package/dist/util-nX_-JRuF.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/CollectionsStudioView.tsx +9 -11
- 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/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 +51 -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 +19 -23
- package/src/components/DetailViewBinding.tsx +109 -62
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +44 -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 +35 -10
- 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/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 +51 -5
- 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/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 +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 +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/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/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- 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/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/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,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import {
|
|
2
|
+
import { sn as useAdminContext } from "./util-nX_-JRuF.js";
|
|
3
3
|
import React, { useCallback } from "react";
|
|
4
4
|
import { Alert, BooleanSwitchWithLabel, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, DownloadIcon, IconButton, Label, RadioGroup, RadioGroupItem, Tooltip, iconSize } from "@rebasepro/ui";
|
|
5
5
|
import { getDefaultValuesFor } from "@rebasepro/common";
|
|
@@ -84,6 +84,22 @@ function getHeaders(property, propertyKey, prefix = "") {
|
|
|
84
84
|
key: currentKey
|
|
85
85
|
}];
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Read whatever a `date` column actually arrived as into a `Date`.
|
|
89
|
+
*
|
|
90
|
+
* The driver/WebSocket path revives `{ __type: "date" }` into a `Date`; the REST
|
|
91
|
+
* path — which is the one the admin is on — leaves an ISO string, and an epoch
|
|
92
|
+
* number is what a re-imported export carries. Returns `undefined` for anything
|
|
93
|
+
* that is not a date, so the caller can pass the raw value through.
|
|
94
|
+
*/
|
|
95
|
+
function toExportableDate(inputValue) {
|
|
96
|
+
if (inputValue instanceof Date) return Number.isNaN(inputValue.getTime()) ? void 0 : inputValue;
|
|
97
|
+
if (typeof inputValue === "number" && Number.isFinite(inputValue)) return new Date(inputValue);
|
|
98
|
+
if (typeof inputValue === "string" && inputValue.trim() !== "") {
|
|
99
|
+
const parsed = new Date(inputValue);
|
|
100
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
87
103
|
function processValueForExport(inputValue, property, exportType, dateExportType) {
|
|
88
104
|
let value;
|
|
89
105
|
if (property.type === "map" && property.properties) value = processValuesForExport(inputValue, property.properties, exportType, dateExportType);
|
|
@@ -94,8 +110,10 @@ function processValueForExport(inputValue, property, exportType, dateExportType)
|
|
|
94
110
|
else if (property.type === "reference" && inputValue && typeof inputValue === "object" && "isEntityReference" in inputValue && typeof inputValue.isEntityReference === "function" && inputValue.isEntityReference()) {
|
|
95
111
|
const ref = inputValue ? inputValue : void 0;
|
|
96
112
|
value = ref ? ref.fullPath : null;
|
|
97
|
-
} else if (property.type === "date"
|
|
98
|
-
|
|
113
|
+
} else if (property.type === "date") {
|
|
114
|
+
const date = toExportableDate(inputValue);
|
|
115
|
+
value = date ? dateExportType === "timestamp" ? date.getTime() : date.toISOString() : inputValue;
|
|
116
|
+
} else value = inputValue;
|
|
99
117
|
return value;
|
|
100
118
|
}
|
|
101
119
|
function processValuesForExport(inputValues, properties, exportType, dateExportType) {
|
|
@@ -112,12 +130,39 @@ function processValuesForExport(inputValues, properties, exportType, dateExportT
|
|
|
112
130
|
...updatedValues
|
|
113
131
|
};
|
|
114
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Characters that make a spreadsheet read a cell as a formula rather than text.
|
|
135
|
+
*
|
|
136
|
+
* Quoting is CSV *escaping* and is not enough: Excel, LibreOffice Calc and
|
|
137
|
+
* Sheets strip the CSV quoting at parse time and then evaluate a cell whose
|
|
138
|
+
* first character is one of these — `=HYPERLINK(…)` exfiltrates neighbouring
|
|
139
|
+
* cells on click, `=cmd|'…'!A0` is worse.
|
|
140
|
+
*/
|
|
141
|
+
var FORMULA_TRIGGERS = [
|
|
142
|
+
"=",
|
|
143
|
+
"+",
|
|
144
|
+
"-",
|
|
145
|
+
"@",
|
|
146
|
+
" ",
|
|
147
|
+
"\r"
|
|
148
|
+
];
|
|
149
|
+
/**
|
|
150
|
+
* Neutralise a cell that a spreadsheet would otherwise evaluate, by prefixing a
|
|
151
|
+
* single quote — the standard OWASP mitigation. The apostrophe is consumed by
|
|
152
|
+
* the spreadsheet as "treat the rest as text", so the value reads back intact.
|
|
153
|
+
*/
|
|
154
|
+
function escapeCsvFormula(value) {
|
|
155
|
+
const first = value.charAt(0);
|
|
156
|
+
if (!FORMULA_TRIGGERS.includes(first)) return value;
|
|
157
|
+
if ((first === "-" || first === "+") && value.trim() !== "" && Number.isFinite(Number(value))) return value;
|
|
158
|
+
return "'" + value;
|
|
159
|
+
}
|
|
160
|
+
function toCSVCell(v) {
|
|
161
|
+
if (v === null || v === void 0) return "";
|
|
162
|
+
return "\"" + escapeCsvFormula(Array.isArray(v) ? JSON.stringify(v) : String(v)).replaceAll("\"", "\"\"") + "\"";
|
|
163
|
+
}
|
|
115
164
|
function entryToCSVRow(entry) {
|
|
116
|
-
return entry.map((
|
|
117
|
-
if (v === null || v === void 0) return "";
|
|
118
|
-
if (Array.isArray(v)) return "\"" + JSON.stringify(v).replaceAll("\"", "\"\"") + "\"";
|
|
119
|
-
return "\"" + String(v).replaceAll("\"", "\"\"") + "\"";
|
|
120
|
-
}).join(",") + "\r\n";
|
|
165
|
+
return entry.map(toCSVCell).join(",") + "\r\n";
|
|
121
166
|
}
|
|
122
167
|
function downloadBlob(content, filename, contentType) {
|
|
123
168
|
const blob = new Blob(content, { type: contentType });
|
|
@@ -130,12 +175,62 @@ function downloadBlob(content, filename, contentType) {
|
|
|
130
175
|
function downloadDataAsCsv(data, name) {
|
|
131
176
|
if (data.length === 0) return;
|
|
132
177
|
const headers = Object.keys(data[0]);
|
|
133
|
-
downloadBlob([[headers.join(","), ...data.map((row) => headers.map((header) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
178
|
+
downloadBlob([[headers.join(","), ...data.map((row) => headers.map((header) => toCSVCell(row[header])).join(","))].join("\r\n")], `${name}.csv`, "text/csv");
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/data_export/export/fetch_export_data.ts
|
|
182
|
+
/**
|
|
183
|
+
* Rows asked for per request.
|
|
184
|
+
*
|
|
185
|
+
* Kept at or below the server's `MAX_LIST_LIMIT`, which is now the largest page
|
|
186
|
+
* a read will serve *and refuses* anything larger — it used to clamp silently,
|
|
187
|
+
* and asking for 100 000 while receiving 1 000 looked exactly like a collection
|
|
188
|
+
* with 1 000 rows in it. The walk still never trusts the page size it asked for:
|
|
189
|
+
* it advances by the rows it was actually handed.
|
|
190
|
+
*/
|
|
191
|
+
var EXPORT_PAGE_SIZE = 500;
|
|
192
|
+
/**
|
|
193
|
+
* Most rows one browser-side export will materialise.
|
|
194
|
+
*
|
|
195
|
+
* Everything here is built in memory and handed to a Blob, so there is a
|
|
196
|
+
* ceiling whether or not it is named. Naming it means going over it **throws**
|
|
197
|
+
* instead of writing a short file that reads like the whole collection.
|
|
198
|
+
*/
|
|
199
|
+
var MAX_EXPORT_ROWS = 1e5;
|
|
200
|
+
/**
|
|
201
|
+
* Read **every** row matching a query, one page at a time.
|
|
202
|
+
*
|
|
203
|
+
* `find({})` sends no `limit`, and an absent limit is resolved server-side to
|
|
204
|
+
* `DEFAULT_LIST_LIMIT` — 50 rows. The export used to hand those 50 rows back as
|
|
205
|
+
* `<collection>.csv` with nothing saying it was a sample, next to a dialog
|
|
206
|
+
* warning that the collection had 100 000 documents.
|
|
207
|
+
*
|
|
208
|
+
* Termination is driven by the server's `meta.hasMore` where it is reported and
|
|
209
|
+
* by an empty page otherwise — never by comparing a page's length against the
|
|
210
|
+
* requested limit, since a final page that happens to be exactly full is
|
|
211
|
+
* indistinguishable that way and a walk that stops there drops rows.
|
|
212
|
+
*
|
|
213
|
+
* @throws When more than `maxRows` rows match. A truncated export is worse than
|
|
214
|
+
* no export: it is indistinguishable from a complete one.
|
|
215
|
+
*/
|
|
216
|
+
async function fetchAllEntitiesForExport({ accessor, params, pageSize = 500, maxRows = MAX_EXPORT_ROWS, onProgress }) {
|
|
217
|
+
const rows = [];
|
|
218
|
+
let offset = 0;
|
|
219
|
+
for (;;) {
|
|
220
|
+
const res = await accessor.find({
|
|
221
|
+
...params,
|
|
222
|
+
limit: pageSize,
|
|
223
|
+
offset
|
|
224
|
+
});
|
|
225
|
+
const page = res?.data ?? [];
|
|
226
|
+
rows.push(...page);
|
|
227
|
+
onProgress?.(rows.length, res?.meta?.total);
|
|
228
|
+
if (rows.length > maxRows) throw new Error(`This export would contain more than ${maxRows.toLocaleString()} rows, which is more than the browser can build in one file. Filter the collection down, or export it from the server.`);
|
|
229
|
+
if (page.length === 0) break;
|
|
230
|
+
if (res?.meta && !res.meta.hasMore) break;
|
|
231
|
+
offset += page.length;
|
|
232
|
+
}
|
|
233
|
+
return rows;
|
|
139
234
|
}
|
|
140
235
|
//#endregion
|
|
141
236
|
//#region src/data_export/export/BasicExportAction.tsx
|
|
@@ -275,6 +370,18 @@ function BasicExportAction({ data, properties, propertiesOrder }) {
|
|
|
275
370
|
//#endregion
|
|
276
371
|
//#region src/data_export/export/ExportCollectionAction.tsx
|
|
277
372
|
var DOCS_LIMIT = 500;
|
|
373
|
+
/**
|
|
374
|
+
* Additional-field builders run per row and may do I/O, so a `Promise.all` over
|
|
375
|
+
* the whole export is one request per row all at once. That was bounded only by
|
|
376
|
+
* the 50-row cap the export used to have; now that the read is paginated, it is
|
|
377
|
+
* bounded here instead.
|
|
378
|
+
*/
|
|
379
|
+
var ADDITIONAL_FIELDS_CONCURRENCY = 50;
|
|
380
|
+
async function mapInChunks(items, fn, chunkSize = ADDITIONAL_FIELDS_CONCURRENCY) {
|
|
381
|
+
const results = [];
|
|
382
|
+
for (let i = 0; i < items.length; i += chunkSize) results.push(...await Promise.all(items.slice(i, i + chunkSize).map(fn)));
|
|
383
|
+
return results;
|
|
384
|
+
}
|
|
278
385
|
function ExportCollectionAction({ collection, path, collectionEntitiesCount, onAnalyticsEvent, exportAllowed, notAllowedView }) {
|
|
279
386
|
const { t } = useTranslation();
|
|
280
387
|
const exportConfig = typeof collection.exportable === "object" ? collection.exportable : void 0;
|
|
@@ -285,6 +392,7 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
285
392
|
const [dateExportType, setDateExportType] = React.useState("string");
|
|
286
393
|
const context = useAdminContext();
|
|
287
394
|
const dataClient = useData();
|
|
395
|
+
const tooManyToExport = collectionEntitiesCount !== void 0 && collectionEntitiesCount > 1e5;
|
|
288
396
|
const canExport = !exportAllowed || exportAllowed({
|
|
289
397
|
collectionEntitiesCount: collectionEntitiesCount ?? 0,
|
|
290
398
|
path,
|
|
@@ -292,6 +400,7 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
292
400
|
});
|
|
293
401
|
const [dataLoading, setDataLoading] = React.useState(false);
|
|
294
402
|
const [dataLoadingError, setDataLoadingError] = React.useState();
|
|
403
|
+
const [progress, setProgress] = React.useState({ loaded: 0 });
|
|
295
404
|
const [open, setOpen] = React.useState(false);
|
|
296
405
|
const handleClickOpen = useCallback(() => {
|
|
297
406
|
setOpen(true);
|
|
@@ -302,7 +411,7 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
302
411
|
const fetchAdditionalFields = useCallback(async (entities) => {
|
|
303
412
|
const additionalExportFields = exportConfig?.additionalFields;
|
|
304
413
|
const additionalFields = collection.additionalFields;
|
|
305
|
-
const resolvedExportColumnsValues = additionalExportFields ? await
|
|
414
|
+
const resolvedExportColumnsValues = additionalExportFields ? await mapInChunks(entities, async (entity) => {
|
|
306
415
|
return (await Promise.all(additionalExportFields.map(async (column) => {
|
|
307
416
|
return { [column.key]: await column.builder({
|
|
308
417
|
entity,
|
|
@@ -312,8 +421,8 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
312
421
|
...a,
|
|
313
422
|
...b
|
|
314
423
|
}), {});
|
|
315
|
-
})
|
|
316
|
-
const resolvedColumnsValues = additionalFields ? await
|
|
424
|
+
}) : [];
|
|
425
|
+
const resolvedColumnsValues = additionalFields ? await mapInChunks(entities, async (entity) => {
|
|
317
426
|
return (await Promise.all(additionalFields.map(async (field) => {
|
|
318
427
|
if (!field.value) return {};
|
|
319
428
|
return { [field.key]: await field.value({
|
|
@@ -324,28 +433,33 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
324
433
|
...a,
|
|
325
434
|
...b
|
|
326
435
|
}), {});
|
|
327
|
-
})
|
|
436
|
+
}) : [];
|
|
328
437
|
return [...resolvedExportColumnsValues, ...resolvedColumnsValues];
|
|
329
438
|
}, [exportConfig?.additionalFields]);
|
|
330
439
|
const doDownload = useCallback(async (collection, exportConfig) => {
|
|
331
440
|
onAnalyticsEvent?.("export_collection", { collection: collection.slug });
|
|
332
441
|
setDataLoading(true);
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
442
|
+
setDataLoadingError(void 0);
|
|
443
|
+
setProgress({ loaded: 0 });
|
|
444
|
+
try {
|
|
445
|
+
const data = await fetchAllEntitiesForExport({
|
|
446
|
+
accessor: dataClient.collection(path),
|
|
447
|
+
onProgress: (loaded, total) => setProgress({
|
|
448
|
+
loaded,
|
|
449
|
+
total
|
|
450
|
+
})
|
|
451
|
+
});
|
|
336
452
|
const additionalData = await fetchAdditionalFields(data);
|
|
337
453
|
const additionalHeaders = [...exportConfig?.additionalFields?.map((column) => column.key) ?? [], ...collection.additionalFields?.map((field) => field.key) ?? []];
|
|
454
|
+
const defaultValues = includeUndefinedValues ? getDefaultValuesFor(collection.properties) : void 0;
|
|
338
455
|
downloadEntitiesExport({
|
|
339
|
-
data:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
...
|
|
343
|
-
values
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
}) : data,
|
|
456
|
+
data: defaultValues ? data.map((entity) => ({
|
|
457
|
+
...entity,
|
|
458
|
+
values: {
|
|
459
|
+
...defaultValues,
|
|
460
|
+
...entity.values
|
|
461
|
+
}
|
|
462
|
+
})) : data,
|
|
349
463
|
additionalData,
|
|
350
464
|
properties: collection.properties,
|
|
351
465
|
propertiesOrder: collection.propertiesOrder,
|
|
@@ -356,10 +470,14 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
356
470
|
dateExportType
|
|
357
471
|
});
|
|
358
472
|
onAnalyticsEvent?.("export_collection_success", { collection: collection.slug });
|
|
359
|
-
|
|
473
|
+
return true;
|
|
474
|
+
} catch (e) {
|
|
360
475
|
console.error("Error loading export data", e);
|
|
361
476
|
setDataLoadingError(e);
|
|
362
|
-
|
|
477
|
+
return false;
|
|
478
|
+
} finally {
|
|
479
|
+
setDataLoading(false);
|
|
480
|
+
}
|
|
363
481
|
}, [
|
|
364
482
|
onAnalyticsEvent,
|
|
365
483
|
dataClient,
|
|
@@ -371,8 +489,9 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
371
489
|
dateExportType
|
|
372
490
|
]);
|
|
373
491
|
const onOkClicked = useCallback(() => {
|
|
374
|
-
doDownload(collection, exportConfig)
|
|
375
|
-
|
|
492
|
+
doDownload(collection, exportConfig).then((downloaded) => {
|
|
493
|
+
if (downloaded) handleClose();
|
|
494
|
+
});
|
|
376
495
|
}, [
|
|
377
496
|
doDownload,
|
|
378
497
|
collection,
|
|
@@ -401,10 +520,17 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
401
520
|
className: "flex flex-col gap-4 my-4",
|
|
402
521
|
children: [
|
|
403
522
|
/* @__PURE__ */ jsx("div", { children: t("download_table_csv") }),
|
|
404
|
-
collectionEntitiesCount !== void 0 && collectionEntitiesCount > DOCS_LIMIT && /* @__PURE__ */ jsx(Alert, {
|
|
523
|
+
collectionEntitiesCount !== void 0 && collectionEntitiesCount > DOCS_LIMIT && !tooManyToExport && /* @__PURE__ */ jsx(Alert, {
|
|
405
524
|
color: "warning",
|
|
406
525
|
children: /* @__PURE__ */ jsx("div", { children: t("large_number_of_documents", { count: collectionEntitiesCount.toString() }) })
|
|
407
526
|
}),
|
|
527
|
+
tooManyToExport && /* @__PURE__ */ jsx(Alert, {
|
|
528
|
+
color: "error",
|
|
529
|
+
children: /* @__PURE__ */ jsx("div", { children: t("too_many_documents_to_export", {
|
|
530
|
+
count: (collectionEntitiesCount ?? 0).toString(),
|
|
531
|
+
limit: 1e5.toString()
|
|
532
|
+
}) })
|
|
533
|
+
}),
|
|
408
534
|
/* @__PURE__ */ jsxs("div", {
|
|
409
535
|
className: "flex flex-row gap-4",
|
|
410
536
|
children: [/* @__PURE__ */ jsx("div", {
|
|
@@ -482,11 +608,16 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
482
608
|
onValueChange: setIncludeUndefinedValues,
|
|
483
609
|
label: t("include_undefined_values")
|
|
484
610
|
}),
|
|
611
|
+
dataLoadingError && /* @__PURE__ */ jsx(Alert, {
|
|
612
|
+
color: "error",
|
|
613
|
+
children: /* @__PURE__ */ jsx("div", { children: dataLoadingError.message })
|
|
614
|
+
}),
|
|
485
615
|
!canExport && notAllowedView
|
|
486
616
|
]
|
|
487
617
|
}),
|
|
488
618
|
/* @__PURE__ */ jsxs(DialogActions, { children: [
|
|
489
619
|
dataLoading && /* @__PURE__ */ jsx(CircularProgress, { size: "smallest" }),
|
|
620
|
+
dataLoading && /* @__PURE__ */ jsx(Label, { children: progress.total !== void 0 ? `${progress.loaded} / ${progress.total}` : `${progress.loaded}` }),
|
|
490
621
|
/* @__PURE__ */ jsx(Button, {
|
|
491
622
|
onClick: handleClose,
|
|
492
623
|
variant: "text",
|
|
@@ -494,7 +625,7 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
494
625
|
}),
|
|
495
626
|
/* @__PURE__ */ jsx(Button, {
|
|
496
627
|
onClick: onOkClicked,
|
|
497
|
-
disabled: dataLoading || !canExport,
|
|
628
|
+
disabled: dataLoading || !canExport || tooManyToExport,
|
|
498
629
|
children: t("download")
|
|
499
630
|
})
|
|
500
631
|
] })
|
|
@@ -505,14 +636,19 @@ function ExportCollectionAction({ collection, path, collectionEntitiesCount, onA
|
|
|
505
636
|
//#region src/data_export/export/index.ts
|
|
506
637
|
var export_exports = /* @__PURE__ */ __exportAll({
|
|
507
638
|
BasicExportAction: () => BasicExportAction,
|
|
639
|
+
EXPORT_PAGE_SIZE: () => 500,
|
|
508
640
|
ExportCollectionAction: () => ExportCollectionAction,
|
|
641
|
+
MAX_EXPORT_ROWS: () => MAX_EXPORT_ROWS,
|
|
509
642
|
downloadBlob: () => downloadBlob,
|
|
510
643
|
downloadDataAsCsv: () => downloadDataAsCsv,
|
|
511
644
|
downloadEntitiesExport: () => downloadEntitiesExport,
|
|
645
|
+
entryToCSVRow: () => entryToCSVRow,
|
|
646
|
+
escapeCsvFormula: () => escapeCsvFormula,
|
|
647
|
+
fetchAllEntitiesForExport: () => fetchAllEntitiesForExport,
|
|
512
648
|
getEntityCSVExportableData: () => getEntityCSVExportableData,
|
|
513
649
|
getEntityJsonExportableData: () => getEntityJsonExportableData
|
|
514
650
|
});
|
|
515
651
|
//#endregion
|
|
516
|
-
export {
|
|
652
|
+
export { MAX_EXPORT_ROWS as a, downloadDataAsCsv as c, escapeCsvFormula as d, getEntityCSVExportableData as f, EXPORT_PAGE_SIZE as i, downloadEntitiesExport as l, ExportCollectionAction as n, fetchAllEntitiesForExport as o, getEntityJsonExportableData as p, BasicExportAction as r, downloadBlob as s, export_exports as t, entryToCSVRow as u };
|
|
517
653
|
|
|
518
|
-
//# sourceMappingURL=export-
|
|
654
|
+
//# sourceMappingURL=export-Bd5i6Uvm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-Bd5i6Uvm.js","names":[],"sources":["../src/data_export/export/export.ts","../src/data_export/export/fetch_export_data.ts","../src/data_export/export/BasicExportAction.tsx","../src/data_export/export/ExportCollectionAction.tsx","../src/data_export/export/index.ts"],"sourcesContent":["import { Entity, EntityReference, Properties, Property } from \"@rebasepro/types\";\nimport { type ArrayValuesCount, getArrayValuesCount, getValueInPath } from \"@rebasepro/utils\";\n\ninterface Header {\n key: string;\n label: string;\n}\n\nexport interface DownloadEntitiesExportParams<M extends Record<string, unknown>> {\n data: Entity<M>[];\n additionalData: Record<string, unknown>[] | undefined;\n properties: Properties;\n propertiesOrder: string[] | undefined;\n name: string;\n flattenArrays: boolean;\n additionalHeaders: string[] | undefined;\n exportType: \"csv\" | \"json\";\n dateExportType: \"timestamp\" | \"string\";\n}\n\nexport function downloadEntitiesExport<M extends Record<string, unknown>>({\n data,\n additionalData,\n properties,\n propertiesOrder,\n name,\n flattenArrays,\n additionalHeaders,\n exportType,\n dateExportType\n}: DownloadEntitiesExportParams<M>\n) {\n\n console.debug(\"Downloading export\", {\n dataLength: data.length,\n properties,\n exportType,\n dateExportType\n });\n\n if (exportType === \"csv\") {\n const arrayValuesCount = flattenArrays ? getArrayValuesCount(data.map(d => d.values)) : {};\n const headers = getExportHeaders(properties, propertiesOrder, additionalHeaders, arrayValuesCount);\n const exportableData = getEntityCSVExportableData(data, additionalData, properties, headers, dateExportType);\n const headersData = entryToCSVRow(headers.map(h => h.label));\n const csvData = exportableData.map(entry => entryToCSVRow(entry));\n downloadBlob([headersData, ...csvData], `${name}.csv`, \"text/csv\");\n } else {\n const exportableData = getEntityJsonExportableData(data, additionalData, properties, dateExportType);\n const json = JSON.stringify(exportableData, null, 2);\n downloadBlob([json], `${name}.json`, \"application/json\");\n }\n}\n\nexport function getEntityCSVExportableData(data: Entity<Record<string, unknown>>[],\n additionalData: Record<string, unknown>[] | undefined,\n properties: Properties,\n headers: Header[],\n dateExportType: \"timestamp\" | \"string\"\n) {\n\n const mergedData: Record<string, unknown>[] = data.map(e => ({\n id: e.id,\n ...processValuesForExport(e.values, properties, \"csv\", dateExportType)\n }));\n\n if (additionalData) {\n additionalData.forEach((additional, index) => {\n mergedData[index] = { ...mergedData[index],\n...additional };\n });\n }\n\n return mergedData && mergedData.map((entry) => {\n return headers.map((header) => getValueInPath(entry, header.key));\n });\n}\n\nexport function getEntityJsonExportableData(data: Entity<Record<string, unknown>>[],\n additionalData: Record<string, unknown>[] | undefined,\n properties: Properties,\n dateExportType: \"timestamp\" | \"string\"\n) {\n\n const mergedData: Record<string, unknown>[] = data.map(e => ({\n id: e.id,\n ...processValuesForExport(e.values, properties, \"json\", dateExportType)\n }));\n\n if (additionalData) {\n additionalData.forEach((additional, index) => {\n mergedData[index] = { ...mergedData[index],\n...additional };\n });\n }\n\n return mergedData;\n}\n\nfunction getExportHeaders<M extends Record<string, unknown>>(properties: Properties,\n propertiesOrder: string[] | undefined,\n additionalHeaders: string[] | undefined,\n arrayValuesCount?: ArrayValuesCount): Header[] {\n\n const headers: Header[] = [\n {\n label: \"id\",\n key: \"id\"\n },\n ...(propertiesOrder ?? Object.keys(properties))\n .flatMap((childKey) => {\n const property = properties[childKey];\n if (!property) {\n console.warn(\"Property not found\", childKey, properties);\n return [];\n }\n if (arrayValuesCount && arrayValuesCount[childKey] > 1) {\n return Array.from({ length: arrayValuesCount[childKey] },\n (_, i) => getHeaders(property as Property, `${childKey}[${i}]`, \"\"))\n .flat();\n } else {\n return getHeaders(property as Property, childKey, \"\");\n }\n })\n ];\n\n if (additionalHeaders) {\n headers.push(...additionalHeaders.map(h => ({\n label: h,\n key: h\n })));\n }\n\n return headers;\n}\n\n/**\n * Get headers for property. There could be more than one header per property\n * @param property\n * @param propertyKey\n * @param prefix\n */\nfunction getHeaders(property: Property, propertyKey: string, prefix = \"\"): Header[] {\n const currentKey = prefix ? `${prefix}.${propertyKey}` : propertyKey;\n if (property.type === \"map\" && property.properties) {\n return Object.entries(property.properties)\n .map(([childKey, p]) => getHeaders(p, childKey, currentKey))\n .flat();\n } else {\n return [{\n label: currentKey,\n key: currentKey\n }];\n }\n}\n\n/**\n * Read whatever a `date` column actually arrived as into a `Date`.\n *\n * The driver/WebSocket path revives `{ __type: \"date\" }` into a `Date`; the REST\n * path — which is the one the admin is on — leaves an ISO string, and an epoch\n * number is what a re-imported export carries. Returns `undefined` for anything\n * that is not a date, so the caller can pass the raw value through.\n */\nfunction toExportableDate(inputValue: unknown): Date | undefined {\n if (inputValue instanceof Date) return Number.isNaN(inputValue.getTime()) ? undefined : inputValue;\n if (typeof inputValue === \"number\" && Number.isFinite(inputValue)) return new Date(inputValue);\n if (typeof inputValue === \"string\" && inputValue.trim() !== \"\") {\n const parsed = new Date(inputValue);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed;\n }\n return undefined;\n}\n\nfunction processValueForExport(inputValue: unknown,\n property: Property,\n exportType: \"csv\" | \"json\",\n dateExportType: \"timestamp\" | \"string\"\n): unknown {\n\n let value: unknown;\n if (property.type === \"map\" && property.properties) {\n value = processValuesForExport(inputValue as Record<string, unknown>, property.properties as Properties, exportType, dateExportType);\n } else if (property.type === \"array\") {\n if (property.of && Array.isArray(inputValue)) {\n if (Array.isArray(property.of)) {\n value = property.of.map((p, i) => processValueForExport(inputValue[i], p, exportType, dateExportType));\n } else if (property.of.type === \"map\") {\n value = exportType === \"csv\"\n ? inputValue.map((e) => JSON.stringify(e))\n : inputValue.map((e) => processValueForExport(e, property.of as Property, exportType, dateExportType));\n ;\n } else {\n value = inputValue.map((e) => processValueForExport(e, property.of as Property, exportType, dateExportType));\n }\n } else {\n value = inputValue;\n }\n } else if (property.type === \"reference\" && inputValue && typeof inputValue === \"object\" && \"isEntityReference\" in inputValue && typeof (inputValue as EntityReference).isEntityReference === \"function\" && (inputValue as EntityReference).isEntityReference()) {\n const ref = inputValue ? inputValue as EntityReference : undefined;\n value = ref ? ref.fullPath : null;\n } else if (property.type === \"date\") {\n // Not `instanceof Date`: the admin reads over REST, and the REST row\n // pipeline hands dates back as the database wrote them — a string. That\n // made this branch dead on every live export, so both radio options\n // produced identical files. Normalise here, where the declared type says\n // the column is a date, and leave anything unparseable untouched rather\n // than exporting an invalid date as `null`.\n const date = toExportableDate(inputValue);\n value = date\n ? (dateExportType === \"timestamp\" ? date.getTime() : date.toISOString())\n : inputValue;\n } else {\n value = inputValue;\n }\n\n return value;\n}\n\nfunction processValuesForExport<M extends Record<string, unknown>>\n (inputValues: Record<string, unknown>,\n properties: Properties,\n exportType: \"csv\" | \"json\",\n dateExportType: \"timestamp\" | \"string\"\n ): Record<string, unknown> {\n const updatedValues = Object.entries(properties)\n .map(([key, property]) => {\n const inputValue = inputValues && (inputValues)[key];\n const updatedValue = processValueForExport(inputValue, property as Property, exportType, dateExportType);\n if (updatedValue === undefined) return {};\n return ({ [key]: updatedValue });\n })\n .reduce((a, b) => ({ ...a,\n...b }), {}) as Record<string, unknown>;\n return { ...inputValues,\n...updatedValues };\n}\n\n/**\n * Characters that make a spreadsheet read a cell as a formula rather than text.\n *\n * Quoting is CSV *escaping* and is not enough: Excel, LibreOffice Calc and\n * Sheets strip the CSV quoting at parse time and then evaluate a cell whose\n * first character is one of these — `=HYPERLINK(…)` exfiltrates neighbouring\n * cells on click, `=cmd|'…'!A0` is worse.\n */\nconst FORMULA_TRIGGERS = [\"=\", \"+\", \"-\", \"@\", \"\\t\", \"\\r\"];\n\n/**\n * Neutralise a cell that a spreadsheet would otherwise evaluate, by prefixing a\n * single quote — the standard OWASP mitigation. The apostrophe is consumed by\n * the spreadsheet as \"treat the rest as text\", so the value reads back intact.\n */\nexport function escapeCsvFormula(value: string): string {\n const first = value.charAt(0);\n if (!FORMULA_TRIGGERS.includes(first)) return value;\n // A signed number is not a formula, and prefixing it would turn every\n // negative amount in the file into text. `-1+1` is still neutralised,\n // because it is not a number.\n if ((first === \"-\" || first === \"+\") && value.trim() !== \"\" && Number.isFinite(Number(value))) return value;\n return \"'\" + value;\n}\n\nfunction toCSVCell(v: unknown): string {\n if (v === null || v === undefined) return \"\";\n const s = Array.isArray(v) ? JSON.stringify(v) : String(v);\n return \"\\\"\" + escapeCsvFormula(s).replaceAll(\"\\\"\", \"\\\"\\\"\") + \"\\\"\";\n}\n\nexport function entryToCSVRow(entry: unknown[]) {\n return entry\n .map(toCSVCell)\n .join(\",\") + \"\\r\\n\";\n}\n\nexport function downloadBlob(content: BlobPart[], filename: string, contentType: string) {\n const blob = new Blob(content, { type: contentType });\n const url = URL.createObjectURL(blob);\n const pom = document.createElement(\"a\");\n pom.href = url;\n pom.setAttribute(\"download\", filename);\n pom.click();\n}\n\nexport function downloadDataAsCsv(data: object[], name: string) {\n if (data.length === 0) return;\n\n const headers = Object.keys(data[0]);\n const csvContent = [\n headers.join(\",\"),\n ...data.map(row => headers.map(header => toCSVCell((row as Record<string, unknown>)[header])).join(\",\"))\n ].join(\"\\r\\n\");\n\n downloadBlob([csvContent], `${name}.csv`, \"text/csv\");\n}\n","import { Entity, FindParams, FindResponse } from \"@rebasepro/types\";\n\n/**\n * Rows asked for per request.\n *\n * Kept at or below the server's `MAX_LIST_LIMIT`, which is now the largest page\n * a read will serve *and refuses* anything larger — it used to clamp silently,\n * and asking for 100 000 while receiving 1 000 looked exactly like a collection\n * with 1 000 rows in it. The walk still never trusts the page size it asked for:\n * it advances by the rows it was actually handed.\n */\nexport const EXPORT_PAGE_SIZE = 500;\n\n/**\n * Most rows one browser-side export will materialise.\n *\n * Everything here is built in memory and handed to a Blob, so there is a\n * ceiling whether or not it is named. Naming it means going over it **throws**\n * instead of writing a short file that reads like the whole collection.\n */\nexport const MAX_EXPORT_ROWS = 100_000;\n\nexport interface FetchAllForExportParams<M extends Record<string, unknown>> {\n /** The collection accessor to read through — `dataClient.collection(path)`. */\n accessor: { find(params?: FindParams<M>): Promise<FindResponse<M>> };\n /** Query to export. The window (`limit` / `offset` / `page`) is owned by the walk. */\n params?: Omit<FindParams<M>, \"limit\" | \"offset\" | \"page\">;\n pageSize?: number;\n maxRows?: number;\n /** Called after every page with the rows so far, and the total when the server reports one. */\n onProgress?: (loaded: number, total: number | undefined) => void;\n}\n\n/**\n * Read **every** row matching a query, one page at a time.\n *\n * `find({})` sends no `limit`, and an absent limit is resolved server-side to\n * `DEFAULT_LIST_LIMIT` — 50 rows. The export used to hand those 50 rows back as\n * `<collection>.csv` with nothing saying it was a sample, next to a dialog\n * warning that the collection had 100 000 documents.\n *\n * Termination is driven by the server's `meta.hasMore` where it is reported and\n * by an empty page otherwise — never by comparing a page's length against the\n * requested limit, since a final page that happens to be exactly full is\n * indistinguishable that way and a walk that stops there drops rows.\n *\n * @throws When more than `maxRows` rows match. A truncated export is worse than\n * no export: it is indistinguishable from a complete one.\n */\nexport async function fetchAllEntitiesForExport<M extends Record<string, unknown>>({\n accessor,\n params,\n pageSize = EXPORT_PAGE_SIZE,\n maxRows = MAX_EXPORT_ROWS,\n onProgress\n}: FetchAllForExportParams<M>): Promise<Entity<M>[]> {\n\n const rows: Entity<M>[] = [];\n let offset = 0;\n\n for (;;) {\n const res = await accessor.find({\n ...params,\n limit: pageSize,\n offset\n } as FindParams<M>);\n\n const page = res?.data ?? [];\n rows.push(...page);\n onProgress?.(rows.length, res?.meta?.total);\n\n if (rows.length > maxRows) {\n throw new Error(\n `This export would contain more than ${maxRows.toLocaleString()} rows, which is more than the ` +\n \"browser can build in one file. Filter the collection down, or export it from the server.\"\n );\n }\n\n // An empty page ends the walk whatever the metadata says, so a server\n // that keeps answering `hasMore` cannot spin forever.\n if (page.length === 0) break;\n if (res?.meta && !res.meta.hasMore) break;\n\n // By rows received, not by `pageSize`: a server whose ceiling is lower\n // than the page we asked for would otherwise skip everything it clamped.\n offset += page.length;\n }\n\n return rows;\n}\n","\nimport React, { useCallback } from \"react\";\n\nimport { useTranslation } from \"@rebasepro/app\";\nimport { Entity, Properties } from \"@rebasepro/types\";\nimport {\n BooleanSwitchWithLabel,\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n DownloadIcon,\n IconButton,\n Label,\n RadioGroup,\n RadioGroupItem,\n Tooltip,\n Typography\n} from \"@rebasepro/ui\";\nimport { downloadEntitiesExport } from \"./export\";\n\nexport type BasicExportActionProps = {\n data: Entity<any>[];\n properties: Properties;\n propertiesOrder?: string[];\n}\n\nexport function BasicExportAction({\n data,\n properties,\n propertiesOrder\n}: BasicExportActionProps) {\n\n const { t } = useTranslation();\n const dateRef = React.useRef<Date>(new Date());\n const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);\n const [exportType, setExportType] = React.useState<\"csv\" | \"json\">(\"csv\");\n const [dateExportType, setDateExportType] = React.useState<\"timestamp\" | \"string\">(\"string\");\n\n const [open, setOpen] = React.useState(false);\n\n const handleClickOpen = useCallback(() => {\n setOpen(true);\n }, [setOpen]);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n }, [setOpen]);\n\n const onOkClicked = useCallback(() => {\n downloadEntitiesExport({\n data,\n additionalData: [],\n properties,\n propertiesOrder,\n name: \"export.csv\",\n flattenArrays,\n additionalHeaders: [],\n exportType,\n dateExportType\n });\n handleClose();\n }, []);\n\n return <>\n\n <Tooltip title={\"Export\"}\n asChild={true}>\n <IconButton\n size={\"small\"}\n color={\"primary\"} onClick={handleClickOpen}>\n <DownloadIcon\n size={\"small\"}/>\n </IconButton>\n </Tooltip>\n\n <Dialog\n open={open}\n onOpenChange={setOpen}\n maxWidth={\"xl\"}>\n\n <DialogTitle variant={\"h6\"}>{t(\"export_data\")}</DialogTitle>\n\n <DialogContent className={\"flex flex-col gap-4 my-4\"}>\n\n <div>{t(\"download_table_csv\")}</div>\n\n <div className={\"flex flex-row gap-4\"}>\n <div className={\"p-4 flex flex-col\"}>\n <RadioGroup value={exportType} onValueChange={(v) => setExportType(v as \"csv\" | \"json\")}>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"csv\" id=\"radio-csv\"/>\n <Label htmlFor=\"radio-csv\">{t(\"csv\")}</Label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"json\" id=\"radio-json\"/>\n <Label htmlFor=\"radio-json\">{t(\"json\")}</Label>\n </div>\n </RadioGroup>\n </div>\n\n <div className={\"p-4 flex flex-col\"}>\n <RadioGroup value={dateExportType} onValueChange={(v) => setDateExportType(v as \"timestamp\" | \"string\")}>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"timestamp\" id=\"radio-timestamp\"/>\n <Label htmlFor=\"radio-timestamp\">{t(\"dates_as_timestamps\")} ({dateRef.current.getTime()})</Label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"string\" id=\"radio-string\"/>\n <Label htmlFor=\"radio-string\">{t(\"dates_as_strings\")} ({dateRef.current.toISOString()})</Label>\n </div>\n </RadioGroup>\n </div>\n </div>\n\n <BooleanSwitchWithLabel\n size={\"small\"}\n disabled={exportType !== \"csv\"}\n value={flattenArrays}\n onValueChange={setFlattenArrays}\n label={t(\"flatten_arrays\")}/>\n\n </DialogContent>\n\n <DialogActions>\n\n <Button onClick={handleClose}\n variant={\"text\"}>\n {t(\"cancel\")}\n </Button>\n\n <Button onClick={onOkClicked}>\n {t(\"download\")}\n </Button>\n\n </DialogActions>\n\n </Dialog>\n\n </>;\n}\n","\nimport React, { useCallback } from \"react\";\n\nimport {\n useAuthController,\n useCustomizationController,\n useData,\n useTranslation\n} from \"@rebasepro/app\";\nimport { useAdminContext } from \"../../hooks\";\nimport { Entity, User } from \"@rebasepro/types\";\nimport { CollectionActionsProps, ExportConfig, RebaseContext, AdminCollection } from \"@rebasepro/admin-types\";\nimport { getDefaultValuesFor } from \"@rebasepro/common\";\nimport {\n Alert,\n BooleanSwitchWithLabel,\n Button,\n CircularProgress,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n DownloadIcon,\n IconButton,\n iconSize,\n Label,\n RadioGroup,\n RadioGroupItem,\n Tooltip\n} from \"@rebasepro/ui\";\nimport { downloadEntitiesExport } from \"./export\";\nimport { fetchAllEntitiesForExport, MAX_EXPORT_ROWS } from \"./fetch_export_data\";\n\nconst DOCS_LIMIT = 500;\n\n/**\n * Additional-field builders run per row and may do I/O, so a `Promise.all` over\n * the whole export is one request per row all at once. That was bounded only by\n * the 50-row cap the export used to have; now that the read is paginated, it is\n * bounded here instead.\n */\nconst ADDITIONAL_FIELDS_CONCURRENCY = 50;\n\nasync function mapInChunks<T, R>(items: T[], fn: (item: T) => Promise<R>, chunkSize = ADDITIONAL_FIELDS_CONCURRENCY): Promise<R[]> {\n const results: R[] = [];\n for (let i = 0; i < items.length; i += chunkSize) {\n results.push(...await Promise.all(items.slice(i, i + chunkSize).map(fn)));\n }\n return results;\n}\n\nexport function ExportCollectionAction<M extends Record<string, unknown>, USER extends User>({\n collection,\n path,\n collectionEntitiesCount,\n onAnalyticsEvent,\n exportAllowed,\n notAllowedView\n}: CollectionActionsProps<M, USER, AdminCollection<M>> & {\n exportAllowed?: (props: { collectionEntitiesCount: number, path: string, collection: AdminCollection }) => boolean;\n notAllowedView?: React.ReactNode;\n onAnalyticsEvent?: (event: string, params?: any) => void;\n}) {\n\n const { t } = useTranslation();\n\n const exportConfig = typeof collection.exportable === \"object\" ? collection.exportable : undefined;\n\n const dateRef = React.useRef<Date>(new Date());\n\n const [includeUndefinedValues, setIncludeUndefinedValues] = React.useState<boolean>(false);\n const [flattenArrays, setFlattenArrays] = React.useState<boolean>(true);\n const [exportType, setExportType] = React.useState<\"csv\" | \"json\">(\"csv\");\n const [dateExportType, setDateExportType] = React.useState<\"timestamp\" | \"string\">(\"string\");\n\n const context = useAdminContext<USER>();\n const dataClient = useData();\n\n // Said before the download starts, not discovered halfway through it: the\n // walk refuses rather than writing a short file, so the dialog has to name\n // the ceiling it is about to hit.\n const tooManyToExport = collectionEntitiesCount !== undefined && collectionEntitiesCount > MAX_EXPORT_ROWS;\n\n const canExport = !exportAllowed || exportAllowed({\n collectionEntitiesCount: collectionEntitiesCount ?? 0,\n path,\n collection\n });\n\n const [dataLoading, setDataLoading] = React.useState<boolean>(false);\n const [dataLoadingError, setDataLoadingError] = React.useState<Error | undefined>();\n const [progress, setProgress] = React.useState<{ loaded: number, total?: number }>({ loaded: 0 });\n\n const [open, setOpen] = React.useState(false);\n\n const handleClickOpen = useCallback(() => {\n setOpen(true);\n }, [setOpen]);\n\n const handleClose = useCallback(() => {\n setOpen(false);\n }, [setOpen]);\n\n const fetchAdditionalFields = useCallback(async (entities: Entity<M>[]) => {\n\n const additionalExportFields = exportConfig?.additionalFields;\n const additionalFields = collection.additionalFields;\n\n const resolvedExportColumnsValues: Record<string, any>[] = additionalExportFields\n ? await mapInChunks(entities, async (entity) => {\n return (await Promise.all(additionalExportFields.map(async (column) => {\n return {\n [column.key]: await column.builder({\n entity,\n context: context as RebaseContext\n })\n };\n }))).reduce((a, b) => ({ ...a,\n...b }), {});\n })\n : [];\n\n const resolvedColumnsValues: Record<string, any>[] = additionalFields\n ? await mapInChunks(entities, async (entity) => {\n return (await Promise.all(additionalFields\n .map(async (field) => {\n if (!field.value)\n return {};\n return {\n [field.key]: await field.value({\n entity,\n context: context as RebaseContext\n })\n };\n }))).reduce((a, b) => ({ ...a,\n...b }), {});\n })\n : [];\n return [...resolvedExportColumnsValues, ...resolvedColumnsValues];\n }, [exportConfig?.additionalFields]);\n\n const doDownload = useCallback(async (collection: AdminCollection<M>,\n exportConfig: ExportConfig<any> | undefined): Promise<boolean> => {\n\n onAnalyticsEvent?.(\"export_collection\", {\n collection: collection.slug\n });\n setDataLoading(true);\n setDataLoadingError(undefined);\n setProgress({ loaded: 0 });\n try {\n // Paginated, not `find({})`: an absent limit resolves to 50 rows\n // server-side, so the export used to be the first page of the\n // collection under a filename that read like all of it.\n const data = await fetchAllEntitiesForExport<M>({\n accessor: dataClient.collection(path) as { find: (params?: any) => Promise<any> },\n onProgress: (loaded, total) => setProgress({ loaded,\n total })\n });\n const additionalData = await fetchAdditionalFields(data);\n const additionalHeaders = [\n ...exportConfig?.additionalFields?.map(column => column.key) ?? [],\n ...collection.additionalFields?.map(field => field.key) ?? []\n ];\n\n const defaultValues = includeUndefinedValues ? getDefaultValuesFor(collection.properties) : undefined;\n const dataWithDefaults = defaultValues\n ? data.map(entity => ({\n ...entity,\n values: { ...defaultValues,\n...entity.values }\n }))\n : data;\n downloadEntitiesExport({\n data: dataWithDefaults,\n additionalData,\n properties: collection.properties,\n propertiesOrder: collection.propertiesOrder,\n name: collection.name,\n flattenArrays,\n additionalHeaders,\n exportType,\n dateExportType\n });\n onAnalyticsEvent?.(\"export_collection_success\", {\n collection: collection.slug\n });\n return true;\n } catch (e) {\n console.error(\"Error loading export data\", e);\n setDataLoadingError(e as Error);\n return false;\n } finally {\n setDataLoading(false);\n }\n\n }, [onAnalyticsEvent, dataClient, path, fetchAdditionalFields, includeUndefinedValues, flattenArrays, exportType, dateExportType]);\n\n const onOkClicked = useCallback(() => {\n // The dialog stays open until the walk finishes: it is the only place a\n // multi-page export reports progress, or reports that it failed.\n doDownload(collection, exportConfig).then((downloaded) => {\n if (downloaded) handleClose();\n });\n }, [doDownload, collection, exportConfig, handleClose]);\n\n return <>\n\n <Tooltip title={\"Export\"}\n asChild={true}>\n <IconButton\n size={\"small\"}\n color={\"primary\"}\n onClick={handleClickOpen}>\n <DownloadIcon size={iconSize.small}/>\n </IconButton>\n </Tooltip>\n\n <Dialog\n open={open}\n onOpenChange={setOpen}\n maxWidth={\"xl\"}>\n\n <DialogTitle variant={\"h6\"}>{t(\"export_data\")}</DialogTitle>\n\n <DialogContent className={\"flex flex-col gap-4 my-4\"}>\n\n <div>{t(\"download_table_csv\")}</div>\n\n {collectionEntitiesCount !== undefined && collectionEntitiesCount > DOCS_LIMIT && !tooManyToExport &&\n <Alert color={\"warning\"}>\n <div>\n {t(\"large_number_of_documents\", { count: collectionEntitiesCount.toString() })}\n </div>\n </Alert>}\n\n {tooManyToExport &&\n <Alert color={\"error\"}>\n <div>\n {t(\"too_many_documents_to_export\", {\n count: (collectionEntitiesCount ?? 0).toString(),\n limit: MAX_EXPORT_ROWS.toString()\n })}\n </div>\n </Alert>}\n\n <div className={\"flex flex-row gap-4\"}>\n <div className={\"p-4 flex flex-col\"}>\n <RadioGroup value={exportType} onValueChange={(v) => setExportType(v as \"csv\" | \"json\")}>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"csv\" id=\"radio-csv\"/>\n <Label htmlFor=\"radio-csv\">{t(\"csv\")}</Label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"json\" id=\"radio-json\"/>\n <Label htmlFor=\"radio-json\">{t(\"json\")}</Label>\n </div>\n </RadioGroup>\n </div>\n\n <div className={\"p-4 flex flex-col\"}>\n <RadioGroup value={dateExportType} onValueChange={(v) => setDateExportType(v as \"timestamp\" | \"string\")}>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"timestamp\" id=\"radio-timestamp\"/>\n <Label htmlFor=\"radio-timestamp\">{t(\"dates_as_timestamps\")} ({dateRef.current.getTime()})</Label>\n </div>\n <div className=\"flex items-center gap-2\">\n <RadioGroupItem value=\"string\" id=\"radio-string\"/>\n <Label htmlFor=\"radio-string\">{t(\"dates_as_strings\")} ({dateRef.current.toISOString()})</Label>\n </div>\n </RadioGroup>\n </div>\n </div>\n\n <BooleanSwitchWithLabel\n size={\"small\"}\n disabled={exportType !== \"csv\"}\n value={flattenArrays}\n onValueChange={setFlattenArrays}\n label={t(\"flatten_arrays\")}/>\n\n <BooleanSwitchWithLabel\n size={\"small\"}\n value={includeUndefinedValues}\n onValueChange={setIncludeUndefinedValues}\n label={t(\"include_undefined_values\")}/>\n\n {dataLoadingError && <Alert color={\"error\"}>\n <div>{dataLoadingError.message}</div>\n </Alert>}\n\n {!canExport && notAllowedView}\n\n </DialogContent>\n\n <DialogActions>\n\n {dataLoading && <CircularProgress size={\"smallest\"}/>}\n\n {dataLoading && <Label>\n {progress.total !== undefined\n ? `${progress.loaded} / ${progress.total}`\n : `${progress.loaded}`}\n </Label>}\n\n <Button onClick={handleClose}\n variant={\"text\"}>\n {t(\"cancel\")}\n </Button>\n\n <Button onClick={onOkClicked}\n disabled={dataLoading || !canExport || tooManyToExport}>\n {t(\"download\")}\n </Button>\n\n </DialogActions>\n\n </Dialog>\n\n </>;\n}\n","export * from \"./export\";\nexport * from \"./fetch_export_data\";\nexport * from \"./BasicExportAction\";\nexport * from \"./ExportCollectionAction\";\n"],"mappings":"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"}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { Property } from "@rebasepro/types";
|
|
2
|
+
import type { PropertySpan } from "@rebasepro/admin-types";
|
|
2
3
|
import React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Grid placement for a span.
|
|
6
|
+
*
|
|
7
|
+
* Written out as literal class strings rather than composed, because Tailwind
|
|
8
|
+
* scans source text: a template literal would produce classes that exist at
|
|
9
|
+
* runtime and were never generated into the stylesheet.
|
|
10
|
+
*
|
|
11
|
+
* Below the column breakpoint every field takes the single available column, so
|
|
12
|
+
* the side panel, the dialog and mobile all get a plain stack.
|
|
13
|
+
*
|
|
14
|
+
* Lives here rather than in the form, because the read-only rendering of a
|
|
15
|
+
* record places its fields on the same grid — a record that changes shape when
|
|
16
|
+
* you press Edit is the bug this whole layout exists to avoid.
|
|
17
|
+
*/
|
|
18
|
+
export declare function spanClass(span: PropertySpan): string;
|
|
3
19
|
/**
|
|
4
20
|
* Does this property's editor supply its own heading?
|
|
5
21
|
*
|
|
@@ -13,18 +29,56 @@ import React from "react";
|
|
|
13
29
|
* in step: the self-labelling types are exactly the ones that take a full row.
|
|
14
30
|
*/
|
|
15
31
|
export declare function isSelfLabellingProperty(property: Property | undefined): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The type icon in a field label, in px.
|
|
34
|
+
*
|
|
35
|
+
* Matched to the label's 13px rather than taken from `iconSize.smallest` (16),
|
|
36
|
+
* which is sized for buttons. Exported so the surfaces that draw a label
|
|
37
|
+
* themselves — the additional-field cards, the rail — can stay in step.
|
|
38
|
+
*/
|
|
39
|
+
export declare const LABEL_ICON_SIZE = 14;
|
|
16
40
|
export interface FieldBlockProps {
|
|
17
41
|
propertyKey: string;
|
|
18
42
|
property?: Property;
|
|
19
43
|
/** Rendered by the form rather than by the field, unless self-labelling. */
|
|
20
44
|
showLabel: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Which of the two renderings this block frames.
|
|
47
|
+
*
|
|
48
|
+
* The **label is identical in both** — same size, same weight, same colour,
|
|
49
|
+
* same type icon, same required marker. A field is the same field whether or
|
|
50
|
+
* not you are editing it, and a record whose labels restyle themselves under
|
|
51
|
+
* Edit reads as two different screens.
|
|
52
|
+
*
|
|
53
|
+
* What `"read"` drops is the *description*, which is not part of the label:
|
|
54
|
+
* it is input help. "Sum of all line items before tax and shipping" under a
|
|
55
|
+
* number that is visibly a sum tells a reader nothing, and a line of it
|
|
56
|
+
* under every field is what a record has instead of room.
|
|
57
|
+
*/
|
|
58
|
+
mode?: "edit" | "read";
|
|
59
|
+
/**
|
|
60
|
+
* Label text, when there is no property to take it from.
|
|
61
|
+
*
|
|
62
|
+
* For additional fields, which are columns a collection computes rather
|
|
63
|
+
* than stores. They go on the same grid as the properties and so have to
|
|
64
|
+
* carry the same label — drawing their own is how they ended up a size out
|
|
65
|
+
* and a couple of pixels off from the field beside them.
|
|
66
|
+
*/
|
|
67
|
+
label?: string;
|
|
68
|
+
/** Type icon, when there is no property to derive one from. */
|
|
69
|
+
icon?: React.ReactNode;
|
|
21
70
|
children: React.ReactNode;
|
|
22
71
|
}
|
|
23
72
|
/**
|
|
24
73
|
* One field's worth of the form grid: label, control, description.
|
|
25
74
|
*
|
|
26
|
-
* The
|
|
27
|
-
*
|
|
28
|
-
*
|
|
75
|
+
* The same block frames the read-only rendering of a record, so a field sits in
|
|
76
|
+
* the same place, at the same width, under the same label whether or not you are
|
|
77
|
+
* editing it — see {@link EntityViewBinding}.
|
|
78
|
+
*
|
|
79
|
+
* The label is one size down from the control it names (13px against the body's
|
|
80
|
+
* 14) with its icon matched to it at 14px. Labels are scaffolding: you read them
|
|
81
|
+
* to find the value, then read the value. Setting them at body size made a
|
|
82
|
+
* column of records scan as a column of field names with data attached.
|
|
29
83
|
*/
|
|
30
|
-
export declare function FieldBlock({ propertyKey, property, showLabel, children }: FieldBlockProps): React.JSX.Element;
|
|
84
|
+
export declare function FieldBlock({ propertyKey, property, showLabel, mode, label, icon, children }: FieldBlockProps): React.JSX.Element;
|
|
@@ -16,3 +16,16 @@ export interface FormRailProps {
|
|
|
16
16
|
* visible while you scroll.
|
|
17
17
|
*/
|
|
18
18
|
export declare function FormRail({ fields, showRecordMeta, entity, renderField }: FormRailProps): React.JSX.Element | null;
|
|
19
|
+
/**
|
|
20
|
+
* The record's id and audit timestamps.
|
|
21
|
+
*
|
|
22
|
+
* Exported because the rail is not the only place it renders. The rail is shown
|
|
23
|
+
* only when it has been *measured* to fit, and every layout narrower than that —
|
|
24
|
+
* the split pane, the side panel, the dialog — folds its contents back into the
|
|
25
|
+
* column. The fields folded back but this block did not, so on three of the four
|
|
26
|
+
* layouts a record simply had no id on screen: the only copy of it was the
|
|
27
|
+
* truncated `e166a394…b422` chip in the identity bar.
|
|
28
|
+
*/
|
|
29
|
+
export declare function RecordMeta({ entity }: {
|
|
30
|
+
entity: Entity<Record<string, unknown>>;
|
|
31
|
+
}): React.JSX.Element;
|
|
@@ -12,5 +12,19 @@ export interface FormSectionsProps {
|
|
|
12
12
|
*/
|
|
13
13
|
errorKeys: ReadonlySet<string>;
|
|
14
14
|
renderField: (field: ResolvedFormField) => React.ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Which rendering this is. `"read"` opens the gaps up rather than closing
|
|
17
|
+
* them: a control is a bordered 40px box that separates itself from the next
|
|
18
|
+
* one, and a value is bare text that does not. The form can also afford
|
|
19
|
+
* tighter rows because a description usually sits between them; with that
|
|
20
|
+
* gone, the same numbers ran a record's values together into one block.
|
|
21
|
+
*/
|
|
22
|
+
mode?: "edit" | "read";
|
|
23
|
+
/**
|
|
24
|
+
* Renders one field as a summary row, for a section that asked for
|
|
25
|
+
* `readVariant: "summary"`. Only the read view supplies it; without it such
|
|
26
|
+
* a section falls back to the grid rather than to nothing.
|
|
27
|
+
*/
|
|
28
|
+
renderSummaryField?: (field: ResolvedFormField, index: number, total: number) => React.ReactNode;
|
|
15
29
|
}
|
|
16
|
-
export declare function FormSections({ sections, collapsed, onToggle, errorKeys, renderField }: FormSectionsProps): React.JSX.Element;
|
|
30
|
+
export declare function FormSections({ sections, collapsed, onToggle, errorKeys, renderField, mode, renderSummaryField }: FormSectionsProps): React.JSX.Element;
|
|
@@ -2,10 +2,11 @@ import React from "react";
|
|
|
2
2
|
/**
|
|
3
3
|
* Below this the form is a single column and the rail is not shown.
|
|
4
4
|
*
|
|
5
|
-
* Roughly the
|
|
6
|
-
* four-up row
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Roughly the 320px rail plus a content column still wide enough to hold a
|
|
6
|
+
* four-up row — at the threshold that leaves ~680px, which is four columns of
|
|
7
|
+
* ~145px once the gutters are taken out. Above it the rail is worth having even
|
|
8
|
+
* in the split view; below it — a narrow split pane, the side panel, the dialog,
|
|
9
|
+
* a phone — its fields fold back into the column.
|
|
9
10
|
*/
|
|
10
11
|
export declare const RAIL_MIN_WIDTH = 1000;
|
|
11
12
|
/**
|