@aglyn/plugins-data 1.0.0-beta.199 → 1.0.0-beta.201
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/package.json +9 -9
- package/src/lib/components/dataset-record-filter.d.ts +37 -41
- package/src/lib/components/dataset-record-filter.js +199 -72
- package/src/lib/components/dataset-record-filter.js.map +1 -1
- package/src/lib/components/host-datasets-card.component.js +77 -91
- package/src/lib/components/host-datasets-card.component.js.map +1 -1
|
@@ -21,6 +21,7 @@ import { exportShortfall, mapImportColumns, parseImportRows } from "../model/ind
|
|
|
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";
|
|
24
|
+
import ListQueryNotices, { listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
24
25
|
import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
25
26
|
import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
|
|
26
27
|
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
@@ -31,10 +32,11 @@ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, St
|
|
|
31
32
|
import { collection, deleteDoc, deleteField, doc, documentId, getCountFromServer, getDocs, limit, orderBy, query, where, setDoc, writeBatch } from "firebase/firestore";
|
|
32
33
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
33
34
|
import { useFirestore, useFirestoreCollection, useHostActivityLogger, useOrgDataScope, usePagedCollection, useScopeTokens, useUser } from "@aglyn/tenant-feature-instance";
|
|
35
|
+
import { listQueryConstraints } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
34
36
|
import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
35
37
|
import { DatasetSchemaDialog } from "./dataset-schema-dialog.component.js";
|
|
36
38
|
import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
|
|
37
|
-
import { datasetRecordFilter,
|
|
39
|
+
import { datasetRecordFilter, planRecordQuery, recordColumn } from "./dataset-record-filter.js";
|
|
38
40
|
/**
|
|
39
41
|
* How many existing records a keyed CSV import may consult.
|
|
40
42
|
*
|
|
@@ -66,7 +68,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
66
68
|
* dark-launch rule as everywhere (no plan, no gate). The host path is
|
|
67
69
|
* the pre-migration fallback for hosts not yet org-wired.
|
|
68
70
|
*/ export function HostDatasetsCard(props) {
|
|
69
|
-
var _datasets_find, _ref, _ref1, _ref2, _ref3, _ref4, _ref5
|
|
71
|
+
var _datasets_find, _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
|
|
70
72
|
const { hostId } = props;
|
|
71
73
|
// `dataScope` is null until the async org lookup settles (AGL-1061), and
|
|
72
74
|
// for a host with no owning org — there is no host fallback to act on
|
|
@@ -264,24 +266,23 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
264
266
|
* rows would run in one order within a page and another across pages.
|
|
265
267
|
* The grid's column sort is off for the same reason.
|
|
266
268
|
*
|
|
267
|
-
* ## Filtering
|
|
269
|
+
* ## Filtering: every clause and the search word are on the query
|
|
268
270
|
*
|
|
269
|
-
* The grid's Filters panel and quick search
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
271
|
+
* The grid's Filters panel and quick search are predicates on this same
|
|
272
|
+
* walk (AGL-3321), planned by `planRecordQuery` (`./dataset-record-filter`,
|
|
273
|
+
* over the shared `planListQuery`): equalities ask the record's
|
|
274
|
+
* `filterValues`, and one word-level clause — a text `contains`, a list
|
|
275
|
+
* member, or the search word — asks its `filterKeys`. Firestore's
|
|
276
|
+
* automatic single-field indexes merge them under the same document-name
|
|
277
|
+
* order, so every filter pages through every matching record exactly as
|
|
278
|
+
* the plain walk pages through all of them, with no composite index and
|
|
279
|
+
* no index deploy.
|
|
277
280
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* matches here; the caption says when the window was full. Ranges on
|
|
284
|
-
* numbers and dates are therefore always window-only.
|
|
281
|
+
* Nothing is matched over the rows already loaded. What one query cannot
|
|
282
|
+
* hold is not offered (ranges, negations, empty checks) or is refused by
|
|
283
|
+
* name above the table (a second `contains`, a `contains` beside the
|
|
284
|
+
* search), and a many-word `contains` or search says it asked the first
|
|
285
|
+
* word.
|
|
285
286
|
*/ const gridFilter = useListGridFilter({
|
|
286
287
|
selectFields: recordFilter.selectFields
|
|
287
288
|
});
|
|
@@ -291,62 +292,32 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
291
292
|
gridFilter.clauses,
|
|
292
293
|
recordFilter.fields
|
|
293
294
|
]);
|
|
294
|
-
const recordPlan = useMemo(()=>
|
|
295
|
+
const recordPlan = useMemo(()=>planRecordQuery(model, recordFilter, recordClauses, gridFilter.searchWords), [
|
|
295
296
|
model,
|
|
297
|
+
recordFilter,
|
|
296
298
|
recordClauses,
|
|
297
299
|
gridFilter.searchWords
|
|
298
300
|
]);
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
// The query's identity: a new plan is a new walk, from page one.
|
|
302
|
+
const recordPlanKey = JSON.stringify({
|
|
303
|
+
filters: recordPlan.plan.filters,
|
|
304
|
+
orderBy: recordPlan.plan.orderBy
|
|
305
|
+
});
|
|
306
|
+
const { rows: records, hasMore: hasMoreRecords, page: recordPage, setPage: setRecordPage, pageSize: recordPageSize, setPageSize: setRecordPageSize } = usePagedCollection((pageLimit)=>dataScope && (selected == null ? void 0 : selected.$id) ? query(collection(firestore, dataScope[0], dataScope[1], 'datasets', selected.$id, 'records'), ...listQueryConstraints(recordPlan.plan), limit(pageLimit)) : null, [
|
|
302
307
|
firestore,
|
|
303
308
|
dataScope,
|
|
304
309
|
selected == null ? void 0 : selected.$id,
|
|
305
|
-
|
|
306
|
-
]);
|
|
307
|
-
const { rows: pagedRecords, hasMore: hasMorePaged, page: pagedPage, setPage: setPagedPage, pageSize: recordPageSize, setPageSize: setPagedPageSize } = usePagedCollection((pageLimit)=>recordPlan.windowed ? null : recordsQuery(pageLimit), [
|
|
308
|
-
recordsQuery,
|
|
309
|
-
recordPlan.windowed
|
|
310
|
+
recordPlanKey
|
|
310
311
|
], {
|
|
311
312
|
idField: '$id'
|
|
312
313
|
});
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const windowRecords = useMemo(()=>recordPlan.windowed ? (windowDocs != null ? windowDocs : []).slice(0, RECORD_FILTER_WINDOW) : [], [
|
|
321
|
-
recordPlan.windowed,
|
|
322
|
-
windowDocs
|
|
323
|
-
]);
|
|
324
|
-
const windowFull = recordPlan.windowed && ((_ref = windowDocs == null ? void 0 : windowDocs.length) != null ? _ref : 0) > RECORD_FILTER_WINDOW;
|
|
325
|
-
const windowMatches = useMemo(()=>recordPlan.windowed ? matchRecordWindow(model, recordFilter.fields, windowRecords, recordPlan) : [], [
|
|
326
|
-
recordPlan,
|
|
327
|
-
model,
|
|
328
|
-
recordFilter.fields,
|
|
329
|
-
windowRecords
|
|
330
|
-
]);
|
|
331
|
-
const [windowPage, setWindowPage] = useState(0);
|
|
332
|
-
const [windowPageSize, setWindowPageSize] = useState(recordPageSize);
|
|
333
|
-
useEffect(()=>{
|
|
334
|
-
setWindowPage(0);
|
|
335
|
-
}, [
|
|
336
|
-
recordPlan,
|
|
337
|
-
selected == null ? void 0 : selected.$id
|
|
338
|
-
]);
|
|
339
|
-
/** Every record this card has read: the page, or the whole window. */ const records = recordPlan.windowed ? windowRecords : pagedRecords;
|
|
340
|
-
/** The rows on screen. */ const shownRecords = recordPlan.windowed ? windowMatches.slice(windowPage * windowPageSize, (windowPage + 1) * windowPageSize) : pagedRecords;
|
|
341
|
-
const recordPage = recordPlan.windowed ? windowPage : pagedPage;
|
|
342
|
-
const setRecordPage = recordPlan.windowed ? setWindowPage : setPagedPage;
|
|
343
|
-
const hasMoreRecords = recordPlan.windowed ? (windowPage + 1) * windowPageSize < windowMatches.length : hasMorePaged;
|
|
344
|
-
const setRecordPageSize = useCallback((next)=>{
|
|
345
|
-
setPagedPageSize(next);
|
|
346
|
-
setWindowPageSize(next);
|
|
347
|
-
setWindowPage(0);
|
|
348
|
-
}, [
|
|
349
|
-
setPagedPageSize
|
|
314
|
+
const recordRefusals = useMemo(()=>listQueryRefusals(recordPlan.refused, {
|
|
315
|
+
fields: recordFilter.fields,
|
|
316
|
+
headers: recordFilter.headers,
|
|
317
|
+
options: recordFilter.options
|
|
318
|
+
}), [
|
|
319
|
+
recordPlan.refused,
|
|
320
|
+
recordFilter
|
|
350
321
|
]);
|
|
351
322
|
const filteringRecords = recordClauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
352
323
|
/**
|
|
@@ -476,7 +447,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
476
447
|
]);
|
|
477
448
|
// Entries are human names — "Roast preference" → stable id
|
|
478
449
|
// `roast_preference` with the pretty name kept for headers (AGL-558).
|
|
479
|
-
const creatorEntries = parseDatasetFieldEntries((
|
|
450
|
+
const creatorEntries = parseDatasetFieldEntries((_ref = creator == null ? void 0 : creator.fields) != null ? _ref : '');
|
|
480
451
|
const creatorFields = creatorEntries.map((entry)=>entry.id);
|
|
481
452
|
const handleCreate = useCallback(async ()=>{
|
|
482
453
|
// No org, no create. The `else` that used to sit below this was the
|
|
@@ -816,6 +787,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
816
787
|
'values',
|
|
817
788
|
'referencedIds',
|
|
818
789
|
'filterKeys',
|
|
790
|
+
'filterValues',
|
|
819
791
|
'updatedAt'
|
|
820
792
|
]
|
|
821
793
|
});
|
|
@@ -1116,8 +1088,12 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1116
1088
|
const keyWindow = await getDocs(query(collection(firestore, dataScope[0], dataScope[1], 'datasets', selected.$id, 'records'), orderBy(documentId()), limit(IMPORT_KEY_WINDOW))).catch(()=>null);
|
|
1117
1089
|
for (const snapshot of (_ref1 = keyWindow == null ? void 0 : keyWindow.docs) != null ? _ref1 : []){
|
|
1118
1090
|
var _snapshot_get;
|
|
1119
|
-
const
|
|
1120
|
-
|
|
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
|
+
});
|
|
1121
1097
|
}
|
|
1122
1098
|
if (recordCount > IMPORT_KEY_WINDOW) {
|
|
1123
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.', {
|
|
@@ -1137,12 +1113,16 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1137
1113
|
...keyless
|
|
1138
1114
|
];
|
|
1139
1115
|
for (const [key, row] of deduped){
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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);
|
|
1146
1126
|
}
|
|
1147
1127
|
}
|
|
1148
1128
|
// Against the dataset's real size, not the loaded window (AGL-1716) —
|
|
@@ -1167,8 +1147,16 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1167
1147
|
values: update.values
|
|
1168
1148
|
}, datasetIntegrityUpdate(model, update.values, deleteField()), {
|
|
1169
1149
|
updatedAt: Timestamp.now()
|
|
1170
|
-
}),
|
|
1171
|
-
|
|
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
|
+
]
|
|
1172
1160
|
});
|
|
1173
1161
|
});
|
|
1174
1162
|
await batch.commit();
|
|
@@ -1320,7 +1308,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1320
1308
|
select: true,
|
|
1321
1309
|
size: "small",
|
|
1322
1310
|
label: "Dataset",
|
|
1323
|
-
value: (
|
|
1311
|
+
value: (_ref1 = selected == null ? void 0 : selected.$id) != null ? _ref1 : '',
|
|
1324
1312
|
onChange: (event)=>setSelectedId(event.target.value),
|
|
1325
1313
|
sx: {
|
|
1326
1314
|
minWidth: 180
|
|
@@ -1386,21 +1374,19 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1386
1374
|
headers: recordFilter.headers,
|
|
1387
1375
|
clauses: recordClauses,
|
|
1388
1376
|
onChange: gridFilter.setClauses,
|
|
1389
|
-
options: recordFilter.options
|
|
1390
|
-
|
|
1377
|
+
options: recordFilter.options
|
|
1378
|
+
}),
|
|
1379
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
1380
|
+
refused: recordRefusals,
|
|
1381
|
+
notices: recordPlan.notices
|
|
1391
1382
|
}),
|
|
1392
|
-
windowFull ? /*#__PURE__*/ _jsx(Typography, {
|
|
1393
|
-
variant: "caption",
|
|
1394
|
-
color: "text.secondary",
|
|
1395
|
-
children: `Filtering the first ${RECORD_FILTER_WINDOW.toLocaleString()} records` + (recordPlan.token ? ' that match the first condition' : '') + ' — one filter or one search word on its own reaches every record.'
|
|
1396
|
-
}) : null,
|
|
1397
1383
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
1398
1384
|
"aria-label": "Records",
|
|
1399
|
-
rows:
|
|
1385
|
+
rows: records,
|
|
1400
1386
|
columns: recordGridColumns,
|
|
1401
1387
|
onOpen: (id)=>setViewerId(id),
|
|
1402
|
-
// One page of the walk
|
|
1403
|
-
//
|
|
1388
|
+
// One page of the walk, turned by the footer below: the grid
|
|
1389
|
+
// neither slices nor filters it.
|
|
1404
1390
|
hideFooter: true,
|
|
1405
1391
|
filterMode: "server",
|
|
1406
1392
|
filterModel: gridFilter.filterModel,
|
|
@@ -1412,7 +1398,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1412
1398
|
/*#__PURE__*/ _jsx(ListPagination, {
|
|
1413
1399
|
page: recordPage,
|
|
1414
1400
|
pageSize: recordPageSize,
|
|
1415
|
-
rowCount:
|
|
1401
|
+
rowCount: records.length,
|
|
1416
1402
|
hasMore: hasMoreRecords,
|
|
1417
1403
|
onPageChange: setRecordPage,
|
|
1418
1404
|
onPageSizeChange: setRecordPageSize
|
|
@@ -1464,7 +1450,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1464
1450
|
children: [
|
|
1465
1451
|
/*#__PURE__*/ _jsx(TextField, {
|
|
1466
1452
|
label: "Name",
|
|
1467
|
-
value: (
|
|
1453
|
+
value: (_ref2 = creator == null ? void 0 : creator.name) != null ? _ref2 : '',
|
|
1468
1454
|
onChange: (event)=>setCreator((prev)=>prev ? _extends({}, prev, {
|
|
1469
1455
|
name: event.target.value
|
|
1470
1456
|
}) : prev),
|
|
@@ -1477,7 +1463,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1477
1463
|
}),
|
|
1478
1464
|
/*#__PURE__*/ _jsx(TextField, {
|
|
1479
1465
|
label: "Fields",
|
|
1480
|
-
value: (
|
|
1466
|
+
value: (_ref3 = creator == null ? void 0 : creator.fields) != null ? _ref3 : '',
|
|
1481
1467
|
onChange: (event)=>setCreator((prev)=>prev ? _extends({}, prev, {
|
|
1482
1468
|
fields: event.target.value
|
|
1483
1469
|
}) : prev),
|
|
@@ -1711,7 +1697,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1711
1697
|
minRows: 6,
|
|
1712
1698
|
size: "small",
|
|
1713
1699
|
label: "CSV (with header row) or JSON array",
|
|
1714
|
-
value: (
|
|
1700
|
+
value: (_ref4 = importer == null ? void 0 : importer.text) != null ? _ref4 : '',
|
|
1715
1701
|
onChange: (event)=>setImporter((prev)=>prev ? _extends({}, prev, {
|
|
1716
1702
|
text: event.target.value
|
|
1717
1703
|
}) : prev),
|
|
@@ -1724,7 +1710,7 @@ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER
|
|
|
1724
1710
|
select: true,
|
|
1725
1711
|
size: "small",
|
|
1726
1712
|
label: "Match on field (upsert)",
|
|
1727
|
-
value: (
|
|
1713
|
+
value: (_ref5 = importer == null ? void 0 : importer.keyField) != null ? _ref5 : '',
|
|
1728
1714
|
onChange: (event)=>setImporter((prev)=>prev ? _extends({}, prev, {
|
|
1729
1715
|
keyField: event.target.value
|
|
1730
1716
|
}) : prev),
|