@rebasepro/admin 0.13.1-canary.gf57a27e → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CollectionEditorDialog-BnfOcqM_.js → CollectionEditorDialog-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-vLXglzIy.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-DF3aiOuv.js → RouterCollectionsStudioView-CwTpdzWF.js} +4 -4
- package/dist/{RouterCollectionsStudioView-DF3aiOuv.js.map → RouterCollectionsStudioView-CwTpdzWF.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- package/dist/components/RelationSelector.d.ts +13 -0
- package/dist/data_export/export/export.d.ts +7 -0
- package/dist/data_export/export/fetch_export_data.d.ts +48 -0
- package/dist/data_export/export/index.d.ts +1 -0
- package/dist/data_import/utils/csv.d.ts +30 -0
- package/dist/data_import/utils/index.d.ts +2 -0
- package/dist/data_import/utils/save_entities.d.ts +38 -0
- package/dist/data_import/utils/transforms.d.ts +9 -0
- package/dist/editor.js +3 -0
- package/dist/editor.js.map +1 -1
- package/dist/{export-C26xQN-g.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.js.map +1 -0
- package/dist/{history-DSS-Ebwh.js → history-D5SKygpJ.js} +2 -2
- package/dist/{history-DSS-Ebwh.js.map → history-D5SKygpJ.js.map} +1 -1
- package/dist/{import-Dhx96B6x.js → import-C_4edkoD.js} +5 -4
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.js +93 -74
- package/dist/index.js.map +1 -1
- package/dist/preview/components/date_fns_locales.d.ts +9 -0
- package/dist/{util-Bt6VadeZ.js → util-C-D5yD2n.js} +567 -113
- package/dist/util-C-D5yD2n.js.map +1 -0
- package/package.json +10 -9
- 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/GeneralSettingsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
- package/src/components/ClearFilterSortButton.tsx +16 -7
- package/src/components/CollectionTableBinding/table_bindings.tsx +7 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +1 -0
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
- package/src/components/DefaultDrawer.tsx +7 -0
- package/src/components/DrawerNavigationItem.tsx +12 -0
- package/src/components/RelationSelector.tsx +52 -5
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- package/src/components/UserSelector.tsx +47 -5
- 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 +7 -1
- 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/field_bindings/BinaryFieldBinding.tsx +1 -0
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- package/src/form/field_bindings/TextFieldBinding.tsx +18 -0
- package/src/form/field_bindings/VectorFieldBinding.tsx +1 -0
- package/src/preview/components/DatePreview.tsx +15 -2
- package/src/preview/components/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- package/src/preview/components/date_fns_locales.ts +125 -0
- package/dist/CollectionEditorDialog-BnfOcqM_.js.map +0 -1
- package/dist/PropertyEditView-vLXglzIy.js.map +0 -1
- package/dist/export-C26xQN-g.js.map +0 -1
- package/dist/import-Dhx96B6x.js.map +0 -1
- package/dist/util-Bt6VadeZ.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-C-D5yD2n.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-L53WektO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-L53WektO.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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Sn as PropertyPreview, Y as useHistory, xr as getPropertyInPath$1 } from "./util-C-D5yD2n.js";
|
|
2
2
|
import * as React$1 from "react";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { ArrowLeftIcon, Chip, ErrorBoundary, HistoryIcon, IconButton, Label, Tooltip, Typography, cls, defaultBorderMixin, iconSize } from "@rebasepro/ui";
|
|
@@ -310,4 +310,4 @@ function EntityHistoryView({ entity, collection, formContext, refreshToken }) {
|
|
|
310
310
|
//#endregion
|
|
311
311
|
export { EntityHistoryView };
|
|
312
312
|
|
|
313
|
-
//# sourceMappingURL=history-
|
|
313
|
+
//# sourceMappingURL=history-D5SKygpJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history-DSS-Ebwh.js","names":[],"sources":["../src/components/history/UserChip.tsx","../src/components/history/EntityHistoryEntry.tsx","../src/components/history/EntityHistoryView.tsx"],"sourcesContent":["import { User } from \"@rebasepro/types\";\nimport { Chip, Tooltip } from \"@rebasepro/ui\";\n\nexport function UserChip({ user }: { user: User }) {\n return (\n <Tooltip title={user.email ?? user.uid}>\n <Chip size={\"small\"} className={\"flex items-center\"}>\n {user.photoURL && <img\n className={\"rounded-full w-6 h-6 mr-2\"}\n src={user.photoURL} alt={user.displayName ?? \"User picture\"}/>}\n <span>{user.displayName ?? user.email ?? user.uid}</span>\n </Chip>\n </Tooltip>\n );\n}\n","\nimport type { Property } from \"@rebasepro/types\";\nimport * as React from \"react\";\n\nimport { ArrowLeftIcon, Chip, cls, defaultBorderMixin, iconSize, Tooltip, Typography } from \"@rebasepro/ui\";\nimport { PreviewSize } from \"../../types/components/PropertyPreviewProps\";\nimport { getPropertyInPath } from \"../../util/property_utils\";\nimport { PropertyPreview } from \"../../preview/PropertyPreview\";\nimport { SkeletonPropertyComponent } from \"../../preview/property_previews/SkeletonPropertyComponent\";\nimport { useAuthController } from \"@rebasepro/app\";\nimport { UserChip } from \"./UserChip\";\nimport { HistoryEntryData } from \"../../hooks\";\nimport { getValueInPath } from \"@rebasepro/utils\";\nimport type { AdminCollection } from \"@rebasepro/admin-types\";\n\n/**\n * Recursive deep equality for primitives, arrays and plain objects.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return a === b;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== (b as unknown[]).length) return false;\n return a.every((item, i) => deepEqual(item, (b as unknown[])[i]));\n }\n\n if (Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(key => key in bObj && deepEqual(aObj[key], bObj[key]));\n}\n\nexport type EntityHistoryEntryProps = {\n size: PreviewSize;\n actions?: React.ReactNode;\n collection?: AdminCollection;\n hover?: boolean;\n entry: HistoryEntryData;\n onClick?: (e: React.SyntheticEvent) => void;\n};\n\nfunction PreviousValueView({\n previousValueInPath,\n childProperty,\n propertyKey\n}: {\n previousValueInPath: unknown;\n childProperty: Property;\n propertyKey: string;\n}) {\n if (typeof previousValueInPath === \"string\" || typeof previousValueInPath === \"number\") {\n return <Typography variant={\"caption\"} color={\"secondary\"} className=\"line-through\">\n {previousValueInPath}\n </Typography>;\n } else if (typeof previousValueInPath === \"boolean\") {\n return <Typography variant={\"caption\"} color={\"secondary\"} className=\"line-through\">\n {previousValueInPath ? \"true\" : \"false\"}\n </Typography>;\n } else {\n return <Tooltip\n side={\"left\"}\n title={<div className={\"flex flex-col gap-2\"}>\n <Typography variant={\"caption\"} color={\"secondary\"}>\n Previous value\n </Typography>\n <PropertyPreview\n propertyKey={propertyKey as string}\n value={previousValueInPath as never}\n property={childProperty as Property}\n size={\"small\"}/>\n </div>}>\n <ArrowLeftIcon size={iconSize.smallest} color={\"disabled\"} className={\"mb-1\"}/>\n </Tooltip>\n }\n}\n\n/**\n * Displays a single entity history revision entry.\n * Adapted from the entity_history plugin — now reads from backend API data.\n */\nexport function EntityHistoryEntry({\n actions,\n hover,\n collection,\n size,\n entry\n}: EntityHistoryEntryProps) {\n\n const authController = useAuthController();\n\n // The backend reports the whole row as changed, which includes the columns\n // that cannot meaningfully change: a timestamp it stamps itself on every\n // write, and the primary key, which is what identifies the row being\n // revised. Every revision was listing `updated_at: <now>` and `id: <the id\n // in the title bar>` around the one field the user actually edited.\n const changedFields = React.useMemo(() => {\n const all = entry.changed_fields;\n if (!all || !collection) return all;\n return all.filter(key => {\n const property = getPropertyInPath(collection.properties, key);\n if (!property) return true;\n if (\"isId\" in property && property.isId) return false;\n return !(property.type === \"date\" && \"autoValue\" in property && property.autoValue);\n });\n }, [entry.changed_fields, collection]);\n\n const previousValues = entry.previous_values;\n const updatedOn = new Date(entry.updated_at);\n const updatedBy = entry.updated_by;\n\n // Resolve user display\n const currentUser = authController.user;\n const userDisplay = updatedBy === currentUser?.uid\n ? currentUser\n : undefined;\n\n return <div className={\"@container/histentry w-full flex flex-col gap-2\"}>\n <div className={\"flex items-center flex-wrap gap-2\"}>\n <Typography variant={\"body2\"} color={\"secondary\"}>\n {updatedOn.toLocaleString()}\n </Typography>\n <Chip size={\"small\"}>\n {entry.action}\n </Chip>\n {!userDisplay && updatedBy && <Chip size={\"small\"}>{updatedBy}</Chip>}\n {userDisplay && <UserChip user={userDisplay}/>}\n </div>\n <div\n className={cls(\n \"bg-white dark:bg-surface-900\",\n \"min-h-[44px]\",\n \"w-full\",\n \"items-center\",\n hover ? \"hover:bg-surface-accent-50 dark:hover:bg-surface-800\" : \"\",\n size === \"small\" ? \"p-1\" : \"px-2 py-1\",\n \"flex border rounded-lg\",\n defaultBorderMixin\n )}>\n\n {actions}\n\n <div className={\"flex flex-col grow w-full m-1 shrink min-w-0\"}>\n\n {changedFields && collection && changedFields.map((key: string) => {\n const childProperty = getPropertyInPath(collection.properties, key);\n const valueInPath = entry.values ? getValueInPath(entry.values, key) : undefined;\n const previousValueInPath = previousValues ? getValueInPath(previousValues, key) : undefined;\n\n const element = childProperty\n ? <PropertyPreview\n propertyKey={key}\n value={valueInPath as never}\n property={childProperty as Property}\n size={\"small\"}/>\n : <Typography variant={\"body2\"}>\n {typeof valueInPath === \"string\" ? valueInPath : JSON.stringify(valueInPath)}\n </Typography>;\n\n // The key column used to be `min-w-[200px] w-1/5` beside a\n // `w-4/5` value — 200px of label and 80% of the row, which\n // adds up to more than the row in anything narrower than a\n // full page. In the inspector that pushed every value off\n // the right edge and wrapped ids one character per line.\n // It stacks until the entry itself is wide enough.\n return (\n <div key={\"ref_prev_\" + key}\n className=\"flex flex-col @sm/histentry:flex-row @sm/histentry:items-baseline gap-x-4 gap-y-0.5 w-full my-1.5\">\n <Typography variant={\"caption\"}\n color={\"secondary\"}\n className=\"shrink-0 break-all @sm/histentry:w-40 @sm/histentry:text-right @sm/histentry:break-normal @sm/histentry:truncate\">\n {key}\n </Typography>\n <div className=\"min-w-0 flex-1 flex flex-col items-start gap-0.5\">\n {previousValueInPath !== undefined && !deepEqual(previousValueInPath, valueInPath) &&\n <PreviousValueView previousValueInPath={previousValueInPath}\n childProperty={childProperty as Property}\n propertyKey={key}/>\n }\n {element}\n </div>\n </div>\n );\n })}\n\n {(!changedFields || changedFields.length === 0) && (\n <Typography variant={\"caption\"} color={\"secondary\"}>\n {entry.action === \"create\" ? \"Entity created\" :\n entry.action === \"delete\" ? \"Entity deleted\" : \"No field changes recorded\"}\n </Typography>\n )}\n\n </div>\n\n </div>\n </div>;\n}\n","\nimport type { EntityCustomViewParams } from \"@rebasepro/admin-types\";\nimport { useRef, useEffect } from \"react\";\nimport { cls, IconButton, Label, Tooltip, Typography } from \"@rebasepro/ui\";\nimport { ErrorBoundary, HistoryIcon } from \"@rebasepro/ui\";\nimport { EntityHistoryEntry } from \"./EntityHistoryEntry\";\nimport { useSnackbarController, useAuthController, useTranslation } from \"@rebasepro/app\";\nimport { ConfirmationDialog } from \"@rebasepro/app\";\nimport { useState } from \"react\";\nimport { useHistory } from \"../../hooks/useHistory\";\n\n/**\n * Entity history tab view. Shows a paginated list of entity revisions\n * fetched from the backend API. Supports infinite scroll and revert.\n */\nexport function EntityHistoryView<M extends Record<string, unknown>>({\n entity,\n collection,\n formContext,\n refreshToken\n}: EntityCustomViewParams<M> & {\n /** Bumped once per save, so the list picks up the revision it just made. */\n refreshToken?: number;\n}) {\n\n const { t } = useTranslation();\n const snackbarController = useSnackbarController();\n const authController = useAuthController();\n const dirty = formContext?.formex.dirty;\n\n const slug = collection.slug;\n const entityId = entity?.id;\n\n const {\n entries,\n isLoading,\n hasMore,\n loadMore,\n revert\n } = useHistory({\n slug,\n entityId,\n enabled: !!entityId,\n pageSize: 10,\n refreshToken\n });\n\n const [revertHistoryId, setRevertHistoryId] = useState<string | undefined>();\n const [isReverting, setIsReverting] = useState(false);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const loadMoreRef = useRef<HTMLDivElement>(null);\n\n // Intersection observer for infinite scroll\n useEffect(() => {\n const currentContainer = containerRef.current;\n const currentLoadMore = loadMoreRef.current;\n\n if (!currentContainer || !currentLoadMore || !hasMore || isLoading) return;\n\n const observer = new IntersectionObserver(\n (observerEntries) => {\n if (observerEntries[0].isIntersecting && hasMore && !isLoading) {\n loadMore();\n }\n },\n {\n root: currentContainer,\n rootMargin: \"0px 0px 200px 0px\",\n threshold: 0.01\n }\n );\n\n observer.observe(currentLoadMore);\n\n return () => observer.disconnect();\n }, [hasMore, isLoading, entries.length, loadMore]);\n\n if (!entity) {\n return <div className=\"flex items-center justify-center h-full\">\n <Label>{t(\"history_new_entity\") ?? \"History is only available for existing records\"}</Label>\n </div>;\n }\n\n async function doRevert(historyId: string) {\n setIsReverting(true);\n try {\n const revertedValues = await revert(historyId);\n setRevertHistoryId(undefined);\n\n // Reset the form with the reverted values so the UI updates\n // immediately without requiring a page refresh.\n if (formContext?.formex?.resetForm && revertedValues) {\n formContext.formex.resetForm({\n values: revertedValues as M,\n submitCount: 0,\n touched: {}\n });\n }\n\n snackbarController.open({\n message: t(\"history_reverted\") ?? \"Reverted to selected version\",\n type: \"info\"\n });\n } catch (error) {\n console.error(\"Error reverting entity:\", error);\n snackbarController.open({\n message: t(\"history_revert_error\") ?? \"Error reverting entity\",\n type: \"error\"\n });\n } finally {\n setIsReverting(false);\n }\n }\n\n const revertEntry = revertHistoryId\n ? entries.find(e => e.id === revertHistoryId)\n : undefined;\n\n // `p-8` plus a `mt-24` heading were sized for a full-width tab that no\n // longer exists. In the inspector panel that was 96px of nothing above the\n // first revision, and 32px of gutter on each side of a 448px column.\n return <div\n ref={containerRef}\n className={cls(\"relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-4 sm:p-5\")}>\n <div className=\"flex flex-col gap-2 max-w-6xl mx-auto w-full\">\n\n {isLoading && entries.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"flex flex-col gap-2 animate-pulse\">\n <div className=\"h-4 w-48 bg-surface-200 dark:bg-surface-700 rounded\"/>\n <div className=\"h-12 w-full bg-surface-100 dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700\"/>\n </div>\n ))}\n </div>\n )}\n\n {!isLoading && entries.length === 0 && <>\n <Label>\n {t(\"history_empty\") ?? \"No history available\"}\n </Label>\n <Typography variant={\"caption\"}>\n {t(\"history_empty_description\") ?? \"When you save a record, a new version is created and stored in the history.\"}\n </Typography>\n </>}\n\n {entries.map((entry) => (\n <div key={entry.id} className=\"flex flex-col gap-2 w-full\">\n <EntityHistoryEntry\n entry={entry}\n collection={collection}\n size={\"medium\"}\n actions={\n <Tooltip title={t(\"history_revert\") ?? \"Revert to this version\"}\n className={\"m-2 grow-0 self-start\"}>\n <IconButton\n onClick={() => {\n if (dirty) {\n snackbarController.open({\n message: t(\"history_revert_dirty\") ?? \"Please save or discard your changes before reverting\",\n type: \"warning\"\n });\n } else {\n setRevertHistoryId(entry.id);\n }\n }}>\n <HistoryIcon/>\n </IconButton>\n </Tooltip>\n }\n />\n </div>\n ))}\n\n {/* Load more sentinel */}\n {entries.length > 0 && (\n <div ref={loadMoreRef} className=\"py-4 text-center\">\n {isLoading && <Label>{t(\"loading\")}</Label>}\n {!hasMore && entries.length > 10 && <Label>{t(\"history_no_more\") ?? \"No more history\"}</Label>}\n </div>\n )}\n </div>\n\n <ErrorBoundary>\n <ConfirmationDialog\n open={Boolean(revertHistoryId)}\n onAccept={() => {\n if (revertHistoryId) doRevert(revertHistoryId);\n }}\n onCancel={() => setRevertHistoryId(undefined)}\n title={<Typography variant={\"subtitle2\"}>{t(\"history_revert_title\") ?? \"Revert data to this version?\"}</Typography>}\n body={revertEntry\n ? <div className=\"p-4\">\n <Typography variant={\"caption\"} color={\"secondary\"}>\n This will save the entity with the values from{\" \"}\n {new Date(revertEntry.updated_at).toLocaleString()}.\n A new history entry will be created for the revert.\n </Typography>\n </div>\n : null\n }\n />\n </ErrorBoundary>\n </div>;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"history-D5SKygpJ.js","names":[],"sources":["../src/components/history/UserChip.tsx","../src/components/history/EntityHistoryEntry.tsx","../src/components/history/EntityHistoryView.tsx"],"sourcesContent":["import { User } from \"@rebasepro/types\";\nimport { Chip, Tooltip } from \"@rebasepro/ui\";\n\nexport function UserChip({ user }: { user: User }) {\n return (\n <Tooltip title={user.email ?? user.uid}>\n <Chip size={\"small\"} className={\"flex items-center\"}>\n {user.photoURL && <img\n className={\"rounded-full w-6 h-6 mr-2\"}\n src={user.photoURL} alt={user.displayName ?? \"User picture\"}/>}\n <span>{user.displayName ?? user.email ?? user.uid}</span>\n </Chip>\n </Tooltip>\n );\n}\n","\nimport type { Property } from \"@rebasepro/types\";\nimport * as React from \"react\";\n\nimport { ArrowLeftIcon, Chip, cls, defaultBorderMixin, iconSize, Tooltip, Typography } from \"@rebasepro/ui\";\nimport { PreviewSize } from \"../../types/components/PropertyPreviewProps\";\nimport { getPropertyInPath } from \"../../util/property_utils\";\nimport { PropertyPreview } from \"../../preview/PropertyPreview\";\nimport { SkeletonPropertyComponent } from \"../../preview/property_previews/SkeletonPropertyComponent\";\nimport { useAuthController } from \"@rebasepro/app\";\nimport { UserChip } from \"./UserChip\";\nimport { HistoryEntryData } from \"../../hooks\";\nimport { getValueInPath } from \"@rebasepro/utils\";\nimport type { AdminCollection } from \"@rebasepro/admin-types\";\n\n/**\n * Recursive deep equality for primitives, arrays and plain objects.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a == null || b == null) return a === b;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== (b as unknown[]).length) return false;\n return a.every((item, i) => deepEqual(item, (b as unknown[])[i]));\n }\n\n if (Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n return aKeys.every(key => key in bObj && deepEqual(aObj[key], bObj[key]));\n}\n\nexport type EntityHistoryEntryProps = {\n size: PreviewSize;\n actions?: React.ReactNode;\n collection?: AdminCollection;\n hover?: boolean;\n entry: HistoryEntryData;\n onClick?: (e: React.SyntheticEvent) => void;\n};\n\nfunction PreviousValueView({\n previousValueInPath,\n childProperty,\n propertyKey\n}: {\n previousValueInPath: unknown;\n childProperty: Property;\n propertyKey: string;\n}) {\n if (typeof previousValueInPath === \"string\" || typeof previousValueInPath === \"number\") {\n return <Typography variant={\"caption\"} color={\"secondary\"} className=\"line-through\">\n {previousValueInPath}\n </Typography>;\n } else if (typeof previousValueInPath === \"boolean\") {\n return <Typography variant={\"caption\"} color={\"secondary\"} className=\"line-through\">\n {previousValueInPath ? \"true\" : \"false\"}\n </Typography>;\n } else {\n return <Tooltip\n side={\"left\"}\n title={<div className={\"flex flex-col gap-2\"}>\n <Typography variant={\"caption\"} color={\"secondary\"}>\n Previous value\n </Typography>\n <PropertyPreview\n propertyKey={propertyKey as string}\n value={previousValueInPath as never}\n property={childProperty as Property}\n size={\"small\"}/>\n </div>}>\n <ArrowLeftIcon size={iconSize.smallest} color={\"disabled\"} className={\"mb-1\"}/>\n </Tooltip>\n }\n}\n\n/**\n * Displays a single entity history revision entry.\n * Adapted from the entity_history plugin — now reads from backend API data.\n */\nexport function EntityHistoryEntry({\n actions,\n hover,\n collection,\n size,\n entry\n}: EntityHistoryEntryProps) {\n\n const authController = useAuthController();\n\n // The backend reports the whole row as changed, which includes the columns\n // that cannot meaningfully change: a timestamp it stamps itself on every\n // write, and the primary key, which is what identifies the row being\n // revised. Every revision was listing `updated_at: <now>` and `id: <the id\n // in the title bar>` around the one field the user actually edited.\n const changedFields = React.useMemo(() => {\n const all = entry.changed_fields;\n if (!all || !collection) return all;\n return all.filter(key => {\n const property = getPropertyInPath(collection.properties, key);\n if (!property) return true;\n if (\"isId\" in property && property.isId) return false;\n return !(property.type === \"date\" && \"autoValue\" in property && property.autoValue);\n });\n }, [entry.changed_fields, collection]);\n\n const previousValues = entry.previous_values;\n const updatedOn = new Date(entry.updated_at);\n const updatedBy = entry.updated_by;\n\n // Resolve user display\n const currentUser = authController.user;\n const userDisplay = updatedBy === currentUser?.uid\n ? currentUser\n : undefined;\n\n return <div className={\"@container/histentry w-full flex flex-col gap-2\"}>\n <div className={\"flex items-center flex-wrap gap-2\"}>\n <Typography variant={\"body2\"} color={\"secondary\"}>\n {updatedOn.toLocaleString()}\n </Typography>\n <Chip size={\"small\"}>\n {entry.action}\n </Chip>\n {!userDisplay && updatedBy && <Chip size={\"small\"}>{updatedBy}</Chip>}\n {userDisplay && <UserChip user={userDisplay}/>}\n </div>\n <div\n className={cls(\n \"bg-white dark:bg-surface-900\",\n \"min-h-[44px]\",\n \"w-full\",\n \"items-center\",\n hover ? \"hover:bg-surface-accent-50 dark:hover:bg-surface-800\" : \"\",\n size === \"small\" ? \"p-1\" : \"px-2 py-1\",\n \"flex border rounded-lg\",\n defaultBorderMixin\n )}>\n\n {actions}\n\n <div className={\"flex flex-col grow w-full m-1 shrink min-w-0\"}>\n\n {changedFields && collection && changedFields.map((key: string) => {\n const childProperty = getPropertyInPath(collection.properties, key);\n const valueInPath = entry.values ? getValueInPath(entry.values, key) : undefined;\n const previousValueInPath = previousValues ? getValueInPath(previousValues, key) : undefined;\n\n const element = childProperty\n ? <PropertyPreview\n propertyKey={key}\n value={valueInPath as never}\n property={childProperty as Property}\n size={\"small\"}/>\n : <Typography variant={\"body2\"}>\n {typeof valueInPath === \"string\" ? valueInPath : JSON.stringify(valueInPath)}\n </Typography>;\n\n // The key column used to be `min-w-[200px] w-1/5` beside a\n // `w-4/5` value — 200px of label and 80% of the row, which\n // adds up to more than the row in anything narrower than a\n // full page. In the inspector that pushed every value off\n // the right edge and wrapped ids one character per line.\n // It stacks until the entry itself is wide enough.\n return (\n <div key={\"ref_prev_\" + key}\n className=\"flex flex-col @sm/histentry:flex-row @sm/histentry:items-baseline gap-x-4 gap-y-0.5 w-full my-1.5\">\n <Typography variant={\"caption\"}\n color={\"secondary\"}\n className=\"shrink-0 break-all @sm/histentry:w-40 @sm/histentry:text-right @sm/histentry:break-normal @sm/histentry:truncate\">\n {key}\n </Typography>\n <div className=\"min-w-0 flex-1 flex flex-col items-start gap-0.5\">\n {previousValueInPath !== undefined && !deepEqual(previousValueInPath, valueInPath) &&\n <PreviousValueView previousValueInPath={previousValueInPath}\n childProperty={childProperty as Property}\n propertyKey={key}/>\n }\n {element}\n </div>\n </div>\n );\n })}\n\n {(!changedFields || changedFields.length === 0) && (\n <Typography variant={\"caption\"} color={\"secondary\"}>\n {entry.action === \"create\" ? \"Entity created\" :\n entry.action === \"delete\" ? \"Entity deleted\" : \"No field changes recorded\"}\n </Typography>\n )}\n\n </div>\n\n </div>\n </div>;\n}\n","\nimport type { EntityCustomViewParams } from \"@rebasepro/admin-types\";\nimport { useRef, useEffect } from \"react\";\nimport { cls, IconButton, Label, Tooltip, Typography } from \"@rebasepro/ui\";\nimport { ErrorBoundary, HistoryIcon } from \"@rebasepro/ui\";\nimport { EntityHistoryEntry } from \"./EntityHistoryEntry\";\nimport { useSnackbarController, useAuthController, useTranslation } from \"@rebasepro/app\";\nimport { ConfirmationDialog } from \"@rebasepro/app\";\nimport { useState } from \"react\";\nimport { useHistory } from \"../../hooks/useHistory\";\n\n/**\n * Entity history tab view. Shows a paginated list of entity revisions\n * fetched from the backend API. Supports infinite scroll and revert.\n */\nexport function EntityHistoryView<M extends Record<string, unknown>>({\n entity,\n collection,\n formContext,\n refreshToken\n}: EntityCustomViewParams<M> & {\n /** Bumped once per save, so the list picks up the revision it just made. */\n refreshToken?: number;\n}) {\n\n const { t } = useTranslation();\n const snackbarController = useSnackbarController();\n const authController = useAuthController();\n const dirty = formContext?.formex.dirty;\n\n const slug = collection.slug;\n const entityId = entity?.id;\n\n const {\n entries,\n isLoading,\n hasMore,\n loadMore,\n revert\n } = useHistory({\n slug,\n entityId,\n enabled: !!entityId,\n pageSize: 10,\n refreshToken\n });\n\n const [revertHistoryId, setRevertHistoryId] = useState<string | undefined>();\n const [isReverting, setIsReverting] = useState(false);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const loadMoreRef = useRef<HTMLDivElement>(null);\n\n // Intersection observer for infinite scroll\n useEffect(() => {\n const currentContainer = containerRef.current;\n const currentLoadMore = loadMoreRef.current;\n\n if (!currentContainer || !currentLoadMore || !hasMore || isLoading) return;\n\n const observer = new IntersectionObserver(\n (observerEntries) => {\n if (observerEntries[0].isIntersecting && hasMore && !isLoading) {\n loadMore();\n }\n },\n {\n root: currentContainer,\n rootMargin: \"0px 0px 200px 0px\",\n threshold: 0.01\n }\n );\n\n observer.observe(currentLoadMore);\n\n return () => observer.disconnect();\n }, [hasMore, isLoading, entries.length, loadMore]);\n\n if (!entity) {\n return <div className=\"flex items-center justify-center h-full\">\n <Label>{t(\"history_new_entity\") ?? \"History is only available for existing records\"}</Label>\n </div>;\n }\n\n async function doRevert(historyId: string) {\n setIsReverting(true);\n try {\n const revertedValues = await revert(historyId);\n setRevertHistoryId(undefined);\n\n // Reset the form with the reverted values so the UI updates\n // immediately without requiring a page refresh.\n if (formContext?.formex?.resetForm && revertedValues) {\n formContext.formex.resetForm({\n values: revertedValues as M,\n submitCount: 0,\n touched: {}\n });\n }\n\n snackbarController.open({\n message: t(\"history_reverted\") ?? \"Reverted to selected version\",\n type: \"info\"\n });\n } catch (error) {\n console.error(\"Error reverting entity:\", error);\n snackbarController.open({\n message: t(\"history_revert_error\") ?? \"Error reverting entity\",\n type: \"error\"\n });\n } finally {\n setIsReverting(false);\n }\n }\n\n const revertEntry = revertHistoryId\n ? entries.find(e => e.id === revertHistoryId)\n : undefined;\n\n // `p-8` plus a `mt-24` heading were sized for a full-width tab that no\n // longer exists. In the inspector panel that was 96px of nothing above the\n // first revision, and 32px of gutter on each side of a 448px column.\n return <div\n ref={containerRef}\n className={cls(\"relative flex-1 h-full overflow-auto w-full flex flex-col gap-4 p-4 sm:p-5\")}>\n <div className=\"flex flex-col gap-2 max-w-6xl mx-auto w-full\">\n\n {isLoading && entries.length === 0 && (\n <div className=\"flex flex-col gap-4\">\n {[1, 2, 3].map(i => (\n <div key={i} className=\"flex flex-col gap-2 animate-pulse\">\n <div className=\"h-4 w-48 bg-surface-200 dark:bg-surface-700 rounded\"/>\n <div className=\"h-12 w-full bg-surface-100 dark:bg-surface-900 rounded-lg border border-surface-200 dark:border-surface-700\"/>\n </div>\n ))}\n </div>\n )}\n\n {!isLoading && entries.length === 0 && <>\n <Label>\n {t(\"history_empty\") ?? \"No history available\"}\n </Label>\n <Typography variant={\"caption\"}>\n {t(\"history_empty_description\") ?? \"When you save a record, a new version is created and stored in the history.\"}\n </Typography>\n </>}\n\n {entries.map((entry) => (\n <div key={entry.id} className=\"flex flex-col gap-2 w-full\">\n <EntityHistoryEntry\n entry={entry}\n collection={collection}\n size={\"medium\"}\n actions={\n <Tooltip title={t(\"history_revert\") ?? \"Revert to this version\"}\n className={\"m-2 grow-0 self-start\"}>\n <IconButton\n onClick={() => {\n if (dirty) {\n snackbarController.open({\n message: t(\"history_revert_dirty\") ?? \"Please save or discard your changes before reverting\",\n type: \"warning\"\n });\n } else {\n setRevertHistoryId(entry.id);\n }\n }}>\n <HistoryIcon/>\n </IconButton>\n </Tooltip>\n }\n />\n </div>\n ))}\n\n {/* Load more sentinel */}\n {entries.length > 0 && (\n <div ref={loadMoreRef} className=\"py-4 text-center\">\n {isLoading && <Label>{t(\"loading\")}</Label>}\n {!hasMore && entries.length > 10 && <Label>{t(\"history_no_more\") ?? \"No more history\"}</Label>}\n </div>\n )}\n </div>\n\n <ErrorBoundary>\n <ConfirmationDialog\n open={Boolean(revertHistoryId)}\n onAccept={() => {\n if (revertHistoryId) doRevert(revertHistoryId);\n }}\n onCancel={() => setRevertHistoryId(undefined)}\n title={<Typography variant={\"subtitle2\"}>{t(\"history_revert_title\") ?? \"Revert data to this version?\"}</Typography>}\n body={revertEntry\n ? <div className=\"p-4\">\n <Typography variant={\"caption\"} color={\"secondary\"}>\n This will save the entity with the values from{\" \"}\n {new Date(revertEntry.updated_at).toLocaleString()}.\n A new history entry will be created for the revert.\n </Typography>\n </div>\n : null\n }\n />\n </ErrorBoundary>\n </div>;\n}\n"],"mappings":"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"}
|