@aglyn/plugins-data 1.0.0-beta.221 → 1.0.0-beta.222
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/README.md +9 -6
- package/package.json +10 -10
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/lib/artifact/dataset-schema-artifact.server.d.ts +57 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js +308 -0
- package/src/lib/artifact/dataset-schema-artifact.server.js.map +1 -0
- package/src/lib/artifact/dataset-schema.d.ts +136 -0
- package/src/lib/artifact/dataset-schema.js +232 -0
- package/src/lib/artifact/dataset-schema.js.map +1 -0
- package/src/lib/components/dataset-record-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-record-dialog.component.js +1 -1
- package/src/lib/components/dataset-record-dialog.component.js.map +1 -1
- package/src/lib/components/dataset-record-filter.d.ts +1 -1
- package/src/lib/components/dataset-record-filter.js +2 -2
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/dataset-schema-dialog.component.d.ts +1 -1
- package/src/lib/components/dataset-schema-dialog.component.js +55 -23
- package/src/lib/components/dataset-schema-dialog.component.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +91 -403
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +12 -0
- package/src/lib/constants/bundle-common.js +13 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +16 -1
- package/src/lib/declarations.console-server.js +34 -2
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.server.d.ts +13 -0
- package/src/lib/declarations.server.js +48 -2
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/form-target/dataset-form-record-target.server.js +5 -3
- package/src/lib/form-target/dataset-form-record-target.server.js.map +1 -1
- package/src/lib/model/dataset-models.d.ts +392 -0
- package/src/lib/model/dataset-models.js +806 -0
- package/src/lib/model/dataset-models.js.map +1 -0
- package/src/lib/model/dataset-record-view.d.ts +1 -1
- package/src/lib/model/dataset-record-view.js +1 -2
- package/src/lib/model/dataset-record-view.js.map +1 -1
- package/src/lib/model/dataset-record.js +2 -2
- package/src/lib/model/dataset-record.js.map +1 -1
- package/src/lib/model/datasets.d.ts +162 -0
- package/src/lib/model/datasets.js +227 -0
- package/src/lib/model/datasets.js.map +1 -0
- package/src/lib/plugin.d.ts +4 -0
- package/src/lib/plugin.js +40 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/record-pages/record-page-live-paths.server.d.ts +55 -0
- package/src/lib/record-pages/record-page-live-paths.server.js +83 -0
- package/src/lib/record-pages/record-page-live-paths.server.js.map +1 -0
- package/src/lib/record-pages/record-page-read.server.d.ts +70 -0
- package/src/lib/record-pages/record-page-read.server.js +175 -0
- package/src/lib/record-pages/record-page-read.server.js.map +1 -0
- package/src/lib/record-pages/record-page-resolver.server.d.ts +37 -0
- package/src/lib/record-pages/record-page-resolver.server.js +131 -0
- package/src/lib/record-pages/record-page-resolver.server.js.map +1 -0
- package/src/lib/record-pages/record-page-sitemap.server.d.ts +32 -0
- package/src/lib/record-pages/record-page-sitemap.server.js +102 -0
- package/src/lib/record-pages/record-page-sitemap.server.js.map +1 -0
- package/src/lib/record-pages/record-page-source.d.ts +42 -0
- package/src/lib/record-pages/record-page-source.js +176 -0
- package/src/lib/record-pages/record-page-source.js.map +1 -0
- package/src/lib/record-pages/record-pages-route.d.ts +42 -0
- package/src/lib/record-pages/record-pages-route.js +348 -0
- package/src/lib/record-pages/record-pages-route.js.map +1 -0
- package/src/lib/record-pages/record-pages.d.ts +218 -0
- package/src/lib/record-pages/record-pages.js +351 -0
- package/src/lib/record-pages/record-pages.js.map +1 -0
- package/src/lib/record-pages/record-template-row-chip.component.d.ts +28 -0
- package/src/lib/record-pages/record-template-row-chip.component.js +87 -0
- package/src/lib/record-pages/record-template-row-chip.component.js.map +1 -0
- package/src/lib/record-pages/record-template-section.component.d.ts +30 -0
- package/src/lib/record-pages/record-template-section.component.js +491 -0
- package/src/lib/record-pages/record-template-section.component.js.map +1 -0
- package/src/lib/repeat/dataset-repeat-rows.d.ts +2 -1
- package/src/lib/repeat/dataset-repeat-rows.js +30 -12
- package/src/lib/repeat/dataset-repeat-rows.js.map +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.d.ts +1 -1
- package/src/lib/repeat/dataset-repeat-rows.server.js +23 -12
- package/src/lib/repeat/dataset-repeat-rows.server.js.map +1 -1
- package/src/lib/repeat/repeat-record-pages.d.ts +48 -0
- package/src/lib/repeat/repeat-record-pages.js +64 -0
- package/src/lib/repeat/repeat-record-pages.js.map +1 -0
- package/src/lib/server/announce-dataset-records.d.ts +1 -1
- package/src/lib/server/announce-dataset-records.js +1 -1
- package/src/lib/server/announce-dataset-records.js.map +1 -1
- package/src/lib/server/api-v1/datasets.js +5 -3
- package/src/lib/server/api-v1/datasets.js.map +1 -1
- package/src/lib/server/dataset-figures.js +2 -2
- package/src/lib/server/dataset-figures.js.map +1 -1
- package/src/lib/server/dataset-live-pages.d.ts +147 -0
- package/src/lib/server/dataset-live-pages.js +436 -0
- package/src/lib/server/dataset-live-pages.js.map +1 -0
- package/src/lib/server/dataset-steps.server.d.ts +45 -0
- package/src/lib/server/dataset-steps.server.js +268 -0
- package/src/lib/server/dataset-steps.server.js.map +1 -0
- package/src/lib/server/datasets-route.d.ts +2 -0
- package/src/lib/server/datasets-route.js +102 -6
- package/src/lib/server/datasets-route.js.map +1 -1
- package/src/lib/server/resolve-dataset.d.ts +40 -0
- package/src/lib/server/resolve-dataset.js +64 -0
- package/src/lib/server/resolve-dataset.js.map +1 -0
- package/src/lib/server.d.ts +3 -2
- package/src/lib/server.js +8 -9
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-bundle/datasets-package.d.ts +42 -0
- package/src/lib/site-bundle/datasets-package.js +95 -0
- package/src/lib/site-bundle/datasets-package.js.map +1 -0
- package/src/lib/site-bundle/datasets-site-bundle.server.js +2 -2
- package/src/lib/site-bundle/datasets-site-bundle.server.js.map +1 -1
- package/src/lib/{model/index.js → tenant-api.d.ts} +12 -7
- package/src/lib/tenant-api.js +30 -0
- package/src/lib/tenant-api.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-key.d.ts +30 -0
- package/src/lib/transfer/dataset-transfer-key.js +31 -0
- package/src/lib/transfer/dataset-transfer-key.js.map +1 -0
- package/src/lib/transfer/dataset-transfer-model.d.ts +146 -0
- package/src/lib/transfer/dataset-transfer-model.js +536 -0
- package/src/lib/transfer/dataset-transfer-model.js.map +1 -0
- package/src/lib/{model/index.d.ts → transfer/dataset-transfer-resource.d.ts} +2 -6
- package/src/lib/transfer/dataset-transfer-resource.js +41 -0
- package/src/lib/transfer/dataset-transfer-resource.js.map +1 -0
- package/src/lib/transfer/dataset-transfer.server.d.ts +69 -0
- package/src/lib/transfer/dataset-transfer.server.js +930 -0
- package/src/lib/transfer/dataset-transfer.server.js.map +1 -0
- package/src/lib/model/dataset-csv.d.ts +0 -36
- package/src/lib/model/dataset-csv.js +0 -85
- package/src/lib/model/dataset-csv.js.map +0 -1
- package/src/lib/model/dataset-io.d.ts +0 -44
- package/src/lib/model/dataset-io.js +0 -85
- package/src/lib/model/dataset-io.js.map +0 -1
- package/src/lib/model/index.js.map +0 -1
- package/src/lib/server/datasets-export-route.d.ts +0 -57
- package/src/lib/server/datasets-export-route.js +0 -229
- package/src/lib/server/datasets-export-route.js.map +0 -1
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
18
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { TransferResumeImport } from "@aglyn/aglyn/app-utils/transfer-resume-import";
|
|
20
|
+
import { checkDatasetQuota, checkEntitlement, checkQuota, getCustomFieldType, pluginDocsHelp, useTransferLauncher } from "@aglyn/aglyn";
|
|
21
21
|
import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
22
22
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
23
23
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
@@ -29,7 +29,7 @@ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filt
|
|
|
29
29
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
30
30
|
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
31
31
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
32
|
-
import { collection, deleteDoc, deleteField, doc,
|
|
32
|
+
import { collection, deleteDoc, deleteField, doc, getCountFromServer, getDocs, limit, query, where, setDoc, writeBatch } from "firebase/firestore";
|
|
33
33
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
34
34
|
import { useFirestore, useFirestoreCollection, useHostActivityLogger, useOrgDataScope, usePagedCollection, useScopeTokens, useUser } from "@aglyn/tenant-feature-instance";
|
|
35
35
|
import { listQueryConstraints } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
@@ -37,19 +37,10 @@ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
|
37
37
|
import { DatasetSchemaDialog } from "./dataset-schema-dialog.component.js";
|
|
38
38
|
import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
|
|
39
39
|
import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-record-filter.js";
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* page can answer — so it reads its own window, once, when a key field is
|
|
45
|
-
* chosen. The window is bounded because `recordsPerDataset` is unlimited on
|
|
46
|
-
* the agency plan and an unbounded client read is a browser hang behind a
|
|
47
|
-
* dialog button; it is the same ceiling the listener used to carry, moved from
|
|
48
|
-
* every mount of the card to the one moment it is needed.
|
|
49
|
-
*
|
|
50
|
-
* A dataset larger than this is not silently mis-imported: the import says
|
|
51
|
-
* which keys it could consult, measured against the server's exact count.
|
|
52
|
-
*/ const IMPORT_KEY_WINDOW = 500;
|
|
40
|
+
import { coerceDocumentValues, datasetIntegrityUpdate, datasetValueToInput, effectiveDatasetModel, formatDatasetValue, modelFromFieldEntries, validateDocument } from "../model/dataset-models.js";
|
|
41
|
+
import { fillRecordAddresses } from "../record-pages/record-pages.js";
|
|
42
|
+
import { datasetDisplayName, parseDatasetFieldEntries } from "../model/datasets.js";
|
|
43
|
+
import { datasetTransferResourceKey } from "../transfer/dataset-transfer-key.js";
|
|
53
44
|
/**
|
|
54
45
|
* The sentence that says who a NEW dataset is shared with, for the card and
|
|
55
46
|
* for the site Data page's banner above it.
|
|
@@ -68,7 +59,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
68
59
|
* dark-launch rule as everywhere (no plan, no gate). The host path is
|
|
69
60
|
* the pre-migration fallback for hosts not yet org-wired.
|
|
70
61
|
*/ export function HostDatasetsCard(props) {
|
|
71
|
-
var _datasets_find, _ref, _ref1, _ref2, _ref3
|
|
62
|
+
var _datasets_find, _ref, _ref1, _ref2, _ref3;
|
|
72
63
|
const { hostId } = props;
|
|
73
64
|
// `dataScope` is null until the async org lookup settles (AGL-1061), and
|
|
74
65
|
// for a host with no owning org — there is no host fallback to act on
|
|
@@ -195,10 +186,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
195
186
|
});
|
|
196
187
|
const datasets = useMemo(()=>[
|
|
197
188
|
...datasetDocs != null ? datasetDocs : []
|
|
198
|
-
].sort((a, b)=>
|
|
199
|
-
var _a_displayName, _b_displayName;
|
|
200
|
-
return String((_a_displayName = a.displayName) != null ? _a_displayName : '').localeCompare(String((_b_displayName = b.displayName) != null ? _b_displayName : ''));
|
|
201
|
-
}), [
|
|
189
|
+
].sort((a, b)=>datasetDisplayName(a).localeCompare(datasetDisplayName(b))), [
|
|
202
190
|
datasetDocs
|
|
203
191
|
]);
|
|
204
192
|
const [selectedId, setSelectedId] = useState(null);
|
|
@@ -507,10 +495,8 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
507
495
|
// takes the same API-only route (AGL-1050) — the host-scope client
|
|
508
496
|
// write that used to stand in for it is gone.
|
|
509
497
|
if (!a || !b || !orgId) return;
|
|
510
|
-
const fieldFor = (target, fieldId)=>{
|
|
511
|
-
|
|
512
|
-
return {
|
|
513
|
-
name: String((_target_displayName = target.displayName) != null ? _target_displayName : fieldId),
|
|
498
|
+
const fieldFor = (target, fieldId)=>({
|
|
499
|
+
name: datasetDisplayName(target) || fieldId,
|
|
514
500
|
type: 'reference',
|
|
515
501
|
required: true,
|
|
516
502
|
reference: {
|
|
@@ -518,10 +504,9 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
518
504
|
displayFieldId: effectiveDatasetModel(target).order[0],
|
|
519
505
|
onDelete: 'setNull'
|
|
520
506
|
}
|
|
521
|
-
};
|
|
522
|
-
};
|
|
507
|
+
});
|
|
523
508
|
const payload = {
|
|
524
|
-
displayName: `${a
|
|
509
|
+
displayName: `${datasetDisplayName(a)} ↔ ${datasetDisplayName(b)}`,
|
|
525
510
|
fields: [
|
|
526
511
|
'aRef',
|
|
527
512
|
'bRef'
|
|
@@ -570,7 +555,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
570
555
|
if (!selected) return;
|
|
571
556
|
const confirmed = await confirm({
|
|
572
557
|
title: 'Delete this collection?',
|
|
573
|
-
description: `"${selected
|
|
558
|
+
description: `"${datasetDisplayName(selected)}"` + // What is actually being destroyed, not what is loaded (AGL-1716):
|
|
574
559
|
// a 40,000-row dataset used to warn about "its 500 documents".
|
|
575
560
|
(recordCount ? ` and its ${recordCount} document${recordCount === 1 ? '' : 's'}` : '') + ' stop resolving in repeatable components and bindings that ' + 'reference it.',
|
|
576
561
|
confirmationText: 'Delete',
|
|
@@ -619,7 +604,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
619
604
|
logActivity('Deleted dataset', {
|
|
620
605
|
type: 'content',
|
|
621
606
|
id: selected.$id,
|
|
622
|
-
name: selected
|
|
607
|
+
name: datasetDisplayName(selected)
|
|
623
608
|
});
|
|
624
609
|
}, [
|
|
625
610
|
selected,
|
|
@@ -765,7 +750,9 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
765
750
|
]);
|
|
766
751
|
const handleSaveRecord = useCallback(async ()=>{
|
|
767
752
|
if (!editor || !selected || !dataScope) return;
|
|
768
|
-
|
|
753
|
+
// The editor holds the record's WHOLE values, so an address it already
|
|
754
|
+
// has stays and an empty one fills in from its source (AGL-3475).
|
|
755
|
+
const coerced = fillRecordAddresses(model, coerceDocumentValues(model, editor.values));
|
|
769
756
|
const errors = validateDocument(model, coerced);
|
|
770
757
|
if (Object.keys(errors).length) {
|
|
771
758
|
return void setEditor((prev)=>prev ? _extends({}, prev, {
|
|
@@ -821,7 +808,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
821
808
|
logActivity(editor.id ? 'Updated record' : 'Added record', {
|
|
822
809
|
type: 'content',
|
|
823
810
|
id: selected.$id,
|
|
824
|
-
name: selected
|
|
811
|
+
name: datasetDisplayName(selected)
|
|
825
812
|
});
|
|
826
813
|
}, [
|
|
827
814
|
editor,
|
|
@@ -880,7 +867,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
880
867
|
try {
|
|
881
868
|
snapshot = await getDocs(query(collection(firestore, dataScope[0], dataScope[1], 'datasets', other.$id, 'records'), where('referencedIds', 'array-contains', record.$id)));
|
|
882
869
|
} catch (unused) {
|
|
883
|
-
return void enqueueSnackbar(`Cannot delete: "${other
|
|
870
|
+
return void enqueueSnackbar(`Cannot delete: "${datasetDisplayName(other)}" could not be checked for ` + 'references, so removing this record could break it. Nothing ' + 'was deleted.', {
|
|
884
871
|
variant: 'error'
|
|
885
872
|
});
|
|
886
873
|
}
|
|
@@ -895,7 +882,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
895
882
|
return ((_otherModel_fields_fieldId = otherModel.fields[fieldId]) == null ? void 0 : (_otherModel_fields_fieldId_reference = _otherModel_fields_fieldId.reference) == null ? void 0 : _otherModel_fields_fieldId_reference.onDelete) === 'restrict';
|
|
896
883
|
});
|
|
897
884
|
if (restricted) {
|
|
898
|
-
return void enqueueSnackbar(`Cannot delete: referenced by ${hits.length} document` + `${hits.length === 1 ? '' : 's'} in "${other
|
|
885
|
+
return void enqueueSnackbar(`Cannot delete: referenced by ${hits.length} document` + `${hits.length === 1 ? '' : 's'} in "${datasetDisplayName(other)}"`, {
|
|
899
886
|
variant: 'warning',
|
|
900
887
|
persist: false
|
|
901
888
|
});
|
|
@@ -946,276 +933,69 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
946
933
|
announceRecords,
|
|
947
934
|
enqueueSnackbar
|
|
948
935
|
]);
|
|
949
|
-
// CSV/JSON round-tripping (AGL-182).
|
|
950
|
-
const download = useCallback((name, mime, text)=>{
|
|
951
|
-
const url = URL.createObjectURL(new Blob([
|
|
952
|
-
text
|
|
953
|
-
], {
|
|
954
|
-
type: mime
|
|
955
|
-
}));
|
|
956
|
-
const anchor = document.createElement('a');
|
|
957
|
-
anchor.href = url;
|
|
958
|
-
anchor.download = name;
|
|
959
|
-
anchor.click();
|
|
960
|
-
URL.revokeObjectURL(url);
|
|
961
|
-
}, []);
|
|
962
|
-
const [exporting, setExporting] = useState(null);
|
|
963
936
|
/**
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
const { promised, received, short } = exportShortfall(response.headers.get('X-Aglyn-Export-Rows'), text, format);
|
|
997
|
-
if (short) {
|
|
998
|
-
enqueueSnackbar(`Export incomplete — ${received} of ${promised} documents ` + 'arrived. Nothing was saved; try again.', {
|
|
999
|
-
variant: 'error'
|
|
1000
|
-
});
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
const base = String((_selected_displayName = selected.displayName) != null ? _selected_displayName : 'collection').replace(/[^A-Za-z0-9_-]+/g, '-').toLowerCase() || 'collection';
|
|
1004
|
-
download(`${base}.${format}`, format === 'csv' ? 'text/csv' : 'application/json', text);
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
enqueueSnackbar(error instanceof Error ? error.message : 'Export failed', {
|
|
1007
|
-
variant: 'error'
|
|
937
|
+
* Import and export (AGL-3530) open the console's import wizard and export
|
|
938
|
+
* dialog on this dataset (`data.dataset:<datasetId>`): the file is read,
|
|
939
|
+
* matched and written on the server, through the same record writes as
|
|
940
|
+
* every other create and update, and the export streams the fields the
|
|
941
|
+
* person picks. Outside the console shell there is no launcher, and no
|
|
942
|
+
* Import or Export to offer. Inside it, Import is offered to a member who
|
|
943
|
+
* may import (Manage data) and Export to any member who can see the
|
|
944
|
+
* dataset (AGL-3546) — the transfer routes' own rule.
|
|
945
|
+
*/ const transfer = useTransferLauncher();
|
|
946
|
+
const transferTarget = (selected == null ? void 0 : selected.$id) ? {
|
|
947
|
+
resource: datasetTransferResourceKey(selected.$id),
|
|
948
|
+
scope: 'org'
|
|
949
|
+
} : null;
|
|
950
|
+
const canImport = Boolean(transfer && transferTarget && transfer.can('import', transferTarget));
|
|
951
|
+
const canExport = Boolean(transfer && transferTarget && transfer.can('export', transferTarget));
|
|
952
|
+
const handleImport = useCallback(()=>{
|
|
953
|
+
if (!transfer || !(selected == null ? void 0 : selected.$id)) return;
|
|
954
|
+
const dataset = {
|
|
955
|
+
id: selected.$id,
|
|
956
|
+
name: datasetDisplayName(selected)
|
|
957
|
+
};
|
|
958
|
+
transfer.openImport({
|
|
959
|
+
resource: datasetTransferResourceKey(dataset.id),
|
|
960
|
+
scope: 'org',
|
|
961
|
+
title: dataset.name ? `Import into ${dataset.name}` : 'Import records',
|
|
962
|
+
onFinished: ()=>{
|
|
963
|
+
// An import moves the record count, and the aggregate is a one-shot.
|
|
964
|
+
setRecordCountEpoch((epoch)=>epoch + 1);
|
|
965
|
+
logActivity('Imported records', {
|
|
966
|
+
type: 'content',
|
|
967
|
+
id: dataset.id,
|
|
968
|
+
name: dataset.name
|
|
1008
969
|
});
|
|
1009
|
-
} finally{
|
|
1010
|
-
setExporting(null);
|
|
1011
|
-
}
|
|
1012
|
-
}, [
|
|
1013
|
-
selected,
|
|
1014
|
-
orgId,
|
|
1015
|
-
user,
|
|
1016
|
-
exporting,
|
|
1017
|
-
download,
|
|
1018
|
-
enqueueSnackbar
|
|
1019
|
-
]);
|
|
1020
|
-
// keyField (wave v6): optional unique key — matching rows update the
|
|
1021
|
-
// existing record instead of appending a duplicate.
|
|
1022
|
-
const [importer, setImporter] = useState(null);
|
|
1023
|
-
const importPreview = useMemo(()=>{
|
|
1024
|
-
if (!importer) return null;
|
|
1025
|
-
const rows = parseImportRows(importer.text);
|
|
1026
|
-
if (!rows || !rows.length) return null;
|
|
1027
|
-
const { mapping, unmatched } = mapImportColumns(model, Object.keys(rows[0]));
|
|
1028
|
-
const prepared = rows.map((raw)=>{
|
|
1029
|
-
const input = {};
|
|
1030
|
-
for (const [column, fieldId] of Object.entries(mapping)){
|
|
1031
|
-
var _raw_column;
|
|
1032
|
-
input[fieldId] = (_raw_column = raw[column]) != null ? _raw_column : '';
|
|
1033
970
|
}
|
|
1034
|
-
const values = coerceDocumentValues(model, input);
|
|
1035
|
-
const errors = validateDocument(model, values);
|
|
1036
|
-
return {
|
|
1037
|
-
values,
|
|
1038
|
-
errors,
|
|
1039
|
-
valid: !Object.keys(errors).length
|
|
1040
|
-
};
|
|
1041
971
|
});
|
|
1042
|
-
return {
|
|
1043
|
-
prepared,
|
|
1044
|
-
unmatched,
|
|
1045
|
-
valid: prepared.filter((row)=>row.valid).length
|
|
1046
|
-
};
|
|
1047
972
|
}, [
|
|
1048
|
-
|
|
1049
|
-
|
|
973
|
+
transfer,
|
|
974
|
+
selected,
|
|
975
|
+
logActivity
|
|
1050
976
|
]);
|
|
1051
|
-
const
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
if (keyField) {
|
|
1066
|
-
var _ref1;
|
|
1067
|
-
/*
|
|
1068
|
-
* The key index is its OWN read, not the table's window.
|
|
1069
|
-
*
|
|
1070
|
-
* It used to be built from the card's live listener, which is now a
|
|
1071
|
-
* PAGE — and a page cannot answer "does this key already exist". An
|
|
1072
|
-
* upsert that consults ten rows would turn nearly every update into a
|
|
1073
|
-
* create: duplicate rows, and quota spent on them.
|
|
1074
|
-
*
|
|
1075
|
-
* So it is read here, once, at the moment a key field is actually
|
|
1076
|
-
* chosen — which is also strictly cheaper than the listener it replaces,
|
|
1077
|
-
* since that one paid for five hundred documents on every mount of a
|
|
1078
|
-
* card nobody had scrolled.
|
|
1079
|
-
*
|
|
1080
|
-
* ⚠️ STILL BOUNDED, and the bound is honest rather than hidden. An
|
|
1081
|
-
* unbounded client read is what AGL-2335 refused for the export, for the
|
|
1082
|
-
* same reason: `recordsPerDataset` is unlimited on the agency plan, so
|
|
1083
|
-
* "read them all" is a browser hang behind a dialog button. Past the
|
|
1084
|
-
* ceiling the reader is TOLD which keys could be consulted instead of
|
|
1085
|
-
* being handed silent duplicates — `recordCount` is the server
|
|
1086
|
-
* aggregate, so that comparison is exact.
|
|
1087
|
-
*/ const existingByKey = new Map();
|
|
1088
|
-
const keyWindow = await getDocs(query(collection(firestore, dataScope[0], dataScope[1], 'datasets', selected.$id, 'records'), orderBy(documentId()), limit(IMPORT_KEY_WINDOW))).catch(()=>null);
|
|
1089
|
-
for (const snapshot of (_ref1 = keyWindow == null ? void 0 : keyWindow.docs) != null ? _ref1 : []){
|
|
1090
|
-
var _snapshot_get;
|
|
1091
|
-
const values = (_snapshot_get = snapshot.get('values')) != null ? _snapshot_get : {};
|
|
1092
|
-
const key = keyOf(values);
|
|
1093
|
-
if (key) existingByKey.set(key, {
|
|
1094
|
-
id: snapshot.id,
|
|
1095
|
-
values
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
if (recordCount > IMPORT_KEY_WINDOW) {
|
|
1099
|
-
enqueueSnackbar(`Matching on "${keyField}" read the first ` + `${IMPORT_KEY_WINDOW.toLocaleString()} of ${recordCount.toLocaleString()} ` + 'records — rows keyed beyond that will be added rather than updated.', {
|
|
1100
|
-
variant: 'warning',
|
|
1101
|
-
persist: true
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
const deduped = new Map();
|
|
1105
|
-
const keyless = [];
|
|
1106
|
-
for (const row of validRows){
|
|
1107
|
-
const key = keyOf(row.values);
|
|
1108
|
-
if (key) deduped.set(key, row);
|
|
1109
|
-
else keyless.push(row);
|
|
1110
|
-
}
|
|
1111
|
-
updates = [];
|
|
1112
|
-
creates = [
|
|
1113
|
-
...keyless
|
|
1114
|
-
];
|
|
1115
|
-
for (const [key, row] of deduped){
|
|
1116
|
-
const existing = existingByKey.get(key);
|
|
1117
|
-
// The import's columns over the record's own: a column the file
|
|
1118
|
-
// does not carry keeps its value, and the filter fields are derived
|
|
1119
|
-
// from the WHOLE record, not the columns imported.
|
|
1120
|
-
if (existing) {
|
|
1121
|
-
updates.push({
|
|
1122
|
-
id: existing.id,
|
|
1123
|
-
values: _extends({}, existing.values, row.values)
|
|
1124
|
-
});
|
|
1125
|
-
} else creates.push(row);
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
// Against the dataset's real size, not the loaded window (AGL-1716) —
|
|
1129
|
-
// `room` subtracts this number from the limit, so an understated count
|
|
1130
|
-
// did not merely fail to refuse, it INFLATED the slots offered.
|
|
1131
|
-
const quota = checkQuota(org, 'recordsPerDataset', recordCount + creates.length - 1);
|
|
1132
|
-
const room = quota.allowed ? creates.length : Math.max(0, Number((_quota_limit = quota.limit) != null ? _quota_limit : 0) - recordCount);
|
|
1133
|
-
const toWrite = creates.slice(0, room);
|
|
1134
|
-
if (!toWrite.length && !updates.length) {
|
|
1135
|
-
return void enqueueSnackbar(`Record limit reached (${quota.limit}) — see Billing to upgrade`, {
|
|
1136
|
-
variant: 'warning',
|
|
1137
|
-
persist: false
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
// Updates stay client-direct (no quota consumed), chunked under
|
|
1141
|
-
// Firestore's 500-writes/batch limit.
|
|
1142
|
-
for(let start = 0; start < updates.length; start += 400){
|
|
1143
|
-
const batch = writeBatch(firestore);
|
|
1144
|
-
updates.slice(start, start + 400).forEach((update)=>{
|
|
1145
|
-
const ref = doc(firestore, dataScope[0], dataScope[1], 'datasets', selected.$id, 'records', update.id);
|
|
1146
|
-
batch.set(ref, _extends({
|
|
1147
|
-
values: update.values
|
|
1148
|
-
}, datasetIntegrityUpdate(model, update.values, deleteField()), {
|
|
1149
|
-
updatedAt: Timestamp.now()
|
|
1150
|
-
}), // Each named field replaced whole — `merge: true` would fold the
|
|
1151
|
-
// `filterValues` map into the stored one and keep a cleared value.
|
|
1152
|
-
{
|
|
1153
|
-
mergeFields: [
|
|
1154
|
-
'values',
|
|
1155
|
-
'referencedIds',
|
|
1156
|
-
'filterKeys',
|
|
1157
|
-
'filterValues',
|
|
1158
|
-
'updatedAt'
|
|
1159
|
-
]
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
await batch.commit();
|
|
1163
|
-
}
|
|
1164
|
-
// Creates go through the quota-enforcing API (AGL-473). The legacy
|
|
1165
|
-
// host-scope client batch that used to be the `else` here is gone
|
|
1166
|
-
// (AGL-1050) — it was the largest of the quota bypasses, since a CSV
|
|
1167
|
-
// import could create records by the hundred without ever asking.
|
|
1168
|
-
try {
|
|
1169
|
-
if (toWrite.length) {
|
|
1170
|
-
await callDatasetApi({
|
|
1171
|
-
action: 'import-records',
|
|
1172
|
-
datasetId: selected.$id,
|
|
1173
|
-
records: toWrite.map((row)=>({
|
|
1174
|
-
values: row.values
|
|
1175
|
-
}))
|
|
1176
|
-
});
|
|
1177
|
-
} else if (updates.length) {
|
|
1178
|
-
// An import that only UPDATED existing rows never reaches the server
|
|
1179
|
-
// leg, so it has to announce for itself — otherwise the one import
|
|
1180
|
-
// shape that changes every row of a dataset is the one that refreshes
|
|
1181
|
-
// nothing (AGL-3113).
|
|
1182
|
-
await announceRecords(selected.$id);
|
|
977
|
+
const handleExport = useCallback(()=>{
|
|
978
|
+
if (!transfer || !(selected == null ? void 0 : selected.$id)) return;
|
|
979
|
+
const name = datasetDisplayName(selected);
|
|
980
|
+
transfer.openExport(_extends({
|
|
981
|
+
resource: datasetTransferResourceKey(selected.$id),
|
|
982
|
+
scope: 'org',
|
|
983
|
+
title: name ? `Export ${name}` : 'Export records'
|
|
984
|
+
}, filteringRecords ? {
|
|
985
|
+
filter: {
|
|
986
|
+
label: 'The records the table’s filters and search show',
|
|
987
|
+
value: {
|
|
988
|
+
clauses: recordClauses,
|
|
989
|
+
search: gridFilter.searchWords
|
|
990
|
+
}
|
|
1183
991
|
}
|
|
1184
|
-
}
|
|
1185
|
-
var _ref2;
|
|
1186
|
-
return void enqueueSnackbar((_ref2 = error == null ? void 0 : error.message) != null ? _ref2 : 'Import failed', {
|
|
1187
|
-
variant: 'warning',
|
|
1188
|
-
persist: false
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
setImporter(null);
|
|
1192
|
-
// An import moves the record count and the aggregate is a one-shot —
|
|
1193
|
-
// the listener refreshes the ROWS for free, the count has to be asked
|
|
1194
|
-
// again or the next import prices its room off a pre-import number.
|
|
1195
|
-
setRecordCountEpoch((epoch)=>epoch + 1);
|
|
1196
|
-
const skippedInvalid = importPreview.prepared.length - validRows.length;
|
|
1197
|
-
const skippedQuota = creates.length - toWrite.length;
|
|
1198
|
-
enqueueSnackbar(`Imported ${toWrite.length} new` + (updates.length ? `, updated ${updates.length}` : '') + (skippedInvalid ? `, ${skippedInvalid} invalid skipped` : '') + (skippedQuota ? `, ${skippedQuota} over the record limit` : ''), {
|
|
1199
|
-
variant: skippedInvalid || skippedQuota ? 'warning' : 'success'
|
|
1200
|
-
});
|
|
1201
|
-
logActivity('Imported records', {
|
|
1202
|
-
type: 'content',
|
|
1203
|
-
id: selected.$id,
|
|
1204
|
-
name: selected.displayName
|
|
1205
|
-
});
|
|
992
|
+
} : {}));
|
|
1206
993
|
}, [
|
|
994
|
+
transfer,
|
|
1207
995
|
selected,
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
model,
|
|
1212
|
-
recordCount,
|
|
1213
|
-
firestore,
|
|
1214
|
-
dataScope,
|
|
1215
|
-
callDatasetApi,
|
|
1216
|
-
announceRecords,
|
|
1217
|
-
enqueueSnackbar,
|
|
1218
|
-
logActivity
|
|
996
|
+
filteringRecords,
|
|
997
|
+
recordClauses,
|
|
998
|
+
gridFilter.searchWords
|
|
1219
999
|
]);
|
|
1220
1000
|
/*
|
|
1221
1001
|
* One column per schema field, filterable as far as `datasetRecordFilter`
|
|
@@ -1315,7 +1095,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
1315
1095
|
},
|
|
1316
1096
|
children: datasets.map((item)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
1317
1097
|
value: item.$id,
|
|
1318
|
-
children: item
|
|
1098
|
+
children: datasetDisplayName(item)
|
|
1319
1099
|
}, item.$id))
|
|
1320
1100
|
}),
|
|
1321
1101
|
/*#__PURE__*/ _jsx(Button, {
|
|
@@ -1329,29 +1109,22 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
1329
1109
|
onClick: ()=>setSchemaOpen(true),
|
|
1330
1110
|
children: 'Schema'
|
|
1331
1111
|
}),
|
|
1332
|
-
/*#__PURE__*/ _jsx(
|
|
1112
|
+
transferTarget ? /*#__PURE__*/ _jsx(TransferResumeImport, _extends({
|
|
1113
|
+
target: transferTarget
|
|
1114
|
+
}, selected ? {
|
|
1115
|
+
title: `Import into ${datasetDisplayName(selected)}`
|
|
1116
|
+
} : {}, {
|
|
1117
|
+
onFinished: ()=>setRecordCountEpoch((epoch)=>epoch + 1)
|
|
1118
|
+
})) : null,
|
|
1119
|
+
canImport ? /*#__PURE__*/ _jsx(Button, {
|
|
1333
1120
|
size: "small",
|
|
1334
|
-
onClick:
|
|
1335
|
-
text: '',
|
|
1336
|
-
keyField: ''
|
|
1337
|
-
}),
|
|
1121
|
+
onClick: handleImport,
|
|
1338
1122
|
children: 'Import'
|
|
1339
|
-
}),
|
|
1340
|
-
recordCount || records.length ? /*#__PURE__*/
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
disabled: Boolean(exporting),
|
|
1345
|
-
onClick: handleExport('csv'),
|
|
1346
|
-
children: exporting === 'csv' ? 'Exporting…' : 'CSV'
|
|
1347
|
-
}),
|
|
1348
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
1349
|
-
size: "small",
|
|
1350
|
-
disabled: Boolean(exporting),
|
|
1351
|
-
onClick: handleExport('json'),
|
|
1352
|
-
children: exporting === 'json' ? 'Exporting…' : 'JSON'
|
|
1353
|
-
})
|
|
1354
|
-
]
|
|
1123
|
+
}) : null,
|
|
1124
|
+
canExport && (recordCount || records.length) ? /*#__PURE__*/ _jsx(Button, {
|
|
1125
|
+
size: "small",
|
|
1126
|
+
onClick: handleExport,
|
|
1127
|
+
children: 'Export'
|
|
1355
1128
|
}) : null,
|
|
1356
1129
|
/*#__PURE__*/ _jsx(Button, {
|
|
1357
1130
|
size: "small",
|
|
@@ -1676,91 +1449,6 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
1676
1449
|
})
|
|
1677
1450
|
]
|
|
1678
1451
|
}),
|
|
1679
|
-
/*#__PURE__*/ _jsxs(Dialog, {
|
|
1680
|
-
open: Boolean(importer),
|
|
1681
|
-
onClose: ()=>setImporter(null),
|
|
1682
|
-
maxWidth: "sm",
|
|
1683
|
-
fullWidth: true,
|
|
1684
|
-
children: [
|
|
1685
|
-
/*#__PURE__*/ _jsx(DialogTitle, {
|
|
1686
|
-
children: 'Import records'
|
|
1687
|
-
}),
|
|
1688
|
-
/*#__PURE__*/ _jsxs(DialogContent, {
|
|
1689
|
-
sx: {
|
|
1690
|
-
display: 'flex',
|
|
1691
|
-
flexDirection: 'column',
|
|
1692
|
-
gap: 2
|
|
1693
|
-
},
|
|
1694
|
-
children: [
|
|
1695
|
-
/*#__PURE__*/ _jsx(TextField, {
|
|
1696
|
-
multiline: true,
|
|
1697
|
-
minRows: 6,
|
|
1698
|
-
size: "small",
|
|
1699
|
-
label: "CSV (with header row) or JSON array",
|
|
1700
|
-
value: (_ref4 = importer == null ? void 0 : importer.text) != null ? _ref4 : '',
|
|
1701
|
-
onChange: (event)=>setImporter((prev)=>prev ? _extends({}, prev, {
|
|
1702
|
-
text: event.target.value
|
|
1703
|
-
}) : prev),
|
|
1704
|
-
sx: {
|
|
1705
|
-
mt: 1
|
|
1706
|
-
},
|
|
1707
|
-
helperText: "Columns match by field id or display name"
|
|
1708
|
-
}),
|
|
1709
|
-
/*#__PURE__*/ _jsxs(TextField, {
|
|
1710
|
-
select: true,
|
|
1711
|
-
size: "small",
|
|
1712
|
-
label: "Match on field (upsert)",
|
|
1713
|
-
value: (_ref5 = importer == null ? void 0 : importer.keyField) != null ? _ref5 : '',
|
|
1714
|
-
onChange: (event)=>setImporter((prev)=>prev ? _extends({}, prev, {
|
|
1715
|
-
keyField: event.target.value
|
|
1716
|
-
}) : prev),
|
|
1717
|
-
helperText: 'Rows whose value matches an existing record update it ' + 'instead of appending a duplicate.',
|
|
1718
|
-
sx: {
|
|
1719
|
-
maxWidth: 280
|
|
1720
|
-
},
|
|
1721
|
-
children: [
|
|
1722
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
1723
|
-
value: "",
|
|
1724
|
-
children: 'None — always append'
|
|
1725
|
-
}),
|
|
1726
|
-
model.order.map((fieldId)=>{
|
|
1727
|
-
var _ref;
|
|
1728
|
-
var _model_fields_fieldId;
|
|
1729
|
-
return /*#__PURE__*/ _jsx(MenuItem, {
|
|
1730
|
-
value: fieldId,
|
|
1731
|
-
children: (_ref = (_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.name) != null ? _ref : fieldId
|
|
1732
|
-
}, fieldId);
|
|
1733
|
-
})
|
|
1734
|
-
]
|
|
1735
|
-
}),
|
|
1736
|
-
importPreview ? /*#__PURE__*/ _jsx(Typography, {
|
|
1737
|
-
variant: "body2",
|
|
1738
|
-
color: "text.secondary",
|
|
1739
|
-
children: `${importPreview.prepared.length} rows parsed · ` + `${importPreview.valid} valid` + (importPreview.prepared.length - importPreview.valid ? ` · ${importPreview.prepared.length - importPreview.valid} invalid (skipped)` : '') + (importPreview.unmatched.length ? ` · unmatched columns: ${importPreview.unmatched.join(', ')}` : '')
|
|
1740
|
-
}) : (importer == null ? void 0 : importer.text.trim()) ? /*#__PURE__*/ _jsx(Typography, {
|
|
1741
|
-
variant: "body2",
|
|
1742
|
-
color: "warning.main",
|
|
1743
|
-
children: 'Nothing parseable yet — CSV needs a header row.'
|
|
1744
|
-
}) : null
|
|
1745
|
-
]
|
|
1746
|
-
}),
|
|
1747
|
-
/*#__PURE__*/ _jsxs(DialogActions, {
|
|
1748
|
-
children: [
|
|
1749
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
1750
|
-
onClick: ()=>setImporter(null),
|
|
1751
|
-
children: 'Cancel'
|
|
1752
|
-
}),
|
|
1753
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
1754
|
-
variant: "contained",
|
|
1755
|
-
color: "primary",
|
|
1756
|
-
disabled: !(importPreview == null ? void 0 : importPreview.valid),
|
|
1757
|
-
onClick: handleImport,
|
|
1758
|
-
children: 'Import valid rows'
|
|
1759
|
-
})
|
|
1760
|
-
]
|
|
1761
|
-
})
|
|
1762
|
-
]
|
|
1763
|
-
}),
|
|
1764
1452
|
/*#__PURE__*/ _jsxs(Dialog, {
|
|
1765
1453
|
open: Boolean(joiner),
|
|
1766
1454
|
onClose: ()=>setJoiner(null),
|
|
@@ -1800,7 +1488,7 @@ import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-re
|
|
|
1800
1488
|
}) : prev),
|
|
1801
1489
|
children: datasets.map((item)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
1802
1490
|
value: item.$id,
|
|
1803
|
-
children: item
|
|
1491
|
+
children: datasetDisplayName(item)
|
|
1804
1492
|
}, item.$id))
|
|
1805
1493
|
}, side);
|
|
1806
1494
|
})
|