@aglyn/plugins-data 1.0.0-beta.195 → 1.0.0-beta.197

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.
@@ -16,21 +16,25 @@
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 { applyDatasetQuery, checkDatasetQuota, checkEntitlement, checkQuota, coerceDocumentValues, datasetIntegrityUpdate, datasetValueToInput, effectiveDatasetModel, formatDatasetValue, getCustomFieldType, modelFromFieldEntries, parseDatasetFieldEntries, parseDatasetFilter, parseDatasetSort, pluginDocsHelp, validateDocument } from "@aglyn/aglyn";
19
+ import { checkDatasetQuota, checkEntitlement, checkQuota, coerceDocumentValues, datasetIntegrityUpdate, datasetValueToInput, effectiveDatasetModel, formatDatasetValue, getCustomFieldType, modelFromFieldEntries, parseDatasetFieldEntries, pluginDocsHelp, validateDocument } from "@aglyn/aglyn";
20
20
  import { exportShortfall, mapImportColumns, parseImportRows } from "../model/index.js";
21
21
  import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
22
+ import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
22
23
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
24
+ import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
23
25
  import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
24
- import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
26
+ import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
27
+ import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
25
28
  import { useSnackbar } from "@aglyn/shared-ui-snackstack";
26
29
  import { Timestamp } from "@aglyn/shared-util-timestamp";
27
- import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TableBody, TableCell, TableHead, TableRow, TextField, Tooltip, Typography } from "@mui/material";
30
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
28
31
  import { collection, deleteDoc, deleteField, doc, documentId, getCountFromServer, getDocs, limit, orderBy, query, where, setDoc, writeBatch } from "firebase/firestore";
29
32
  import { useCallback, useEffect, useMemo, useState } from "react";
30
33
  import { useFirestore, useFirestoreCollection, useHostActivityLogger, useOrgDataScope, usePagedCollection, useScopeTokens, useUser } from "@aglyn/tenant-feature-instance";
31
34
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
32
35
  import { DatasetSchemaDialog } from "./dataset-schema-dialog.component.js";
33
36
  import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
37
+ import { datasetRecordFilter, matchRecordWindow, planRecordFilter, RECORD_FILTER_WINDOW, recordColumn } from "./dataset-record-filter.js";
34
38
  /**
35
39
  * How many existing records a keyed CSV import may consult.
36
40
  *
@@ -62,7 +66,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
62
66
  * dark-launch rule as everywhere (no plan, no gate). The host path is
63
67
  * the pre-migration fallback for hosts not yet org-wired.
64
68
  */ export function HostDatasetsCard(props) {
65
- var _datasets_find, _ref, _ref1, _ref2, _ref3, _ref4, _ref5;
69
+ var _datasets_find, _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6;
66
70
  const { hostId } = props;
67
71
  // `dataScope` is null until the async org lookup settles (AGL-1061), and
68
72
  // for a host with no owning org — there is no host fallback to act on
@@ -206,6 +210,10 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
206
210
  const fields = useMemo(()=>model.order, [
207
211
  model
208
212
  ]);
213
+ // The records grid's Filters panel, from the dataset's own schema.
214
+ const recordFilter = useMemo(()=>datasetRecordFilter(model), [
215
+ model
216
+ ]);
209
217
  // Guarded on BOTH the scope and a real selection (AGL-1440): with no
210
218
  // datasets, `selected` never resolves, and `datasets/-none-/records` is not
211
219
  // an empty read for everyone — the AGL-1044 scoped-sharing rules deny it
@@ -254,13 +262,93 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
254
262
  * `sortDatasetRecords` is deliberately NOT applied to the page. Re-sorting a
255
263
  * window of an id-ordered walk by `order` is the same lie the old code told:
256
264
  * rows would run in one order within a page and another across pages.
257
- */ 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'), orderBy(documentId()), limit(pageLimit)) : null, [
265
+ * The grid's column sort is off for the same reason.
266
+ *
267
+ * ## Filtering, and what reaches every record
268
+ *
269
+ * The grid's Filters panel and quick search narrow the walk through the
270
+ * record's `filterKeys` (`datasetFilterKeys`): a token array Firestore's
271
+ * automatic single-field index serves with `array-contains` beside the same
272
+ * document-name order, so it needs no composite index and no index
273
+ * deploy. ONE condition is served that way — the first clause a token can
274
+ * answer (`datasetFilterToken`), else the first search word — and when it is
275
+ * the only condition, the walk pages through every matching record exactly
276
+ * as it pages through all of them.
277
+ *
278
+ * Anything more — a second clause, a second word, a multi-word `contains`,
279
+ * or a clause no token answers (number and date ranges, empty checks,
280
+ * negations) — cannot be one `array-contains`. Those read a WINDOW of
281
+ * {@link RECORD_FILTER_WINDOW} records under the served condition (or the
282
+ * plain walk when none is servable), match the rest in memory, and page the
283
+ * matches here; the caption says when the window was full. Ranges on
284
+ * numbers and dates are therefore always window-only.
285
+ */ const gridFilter = useListGridFilter({
286
+ selectFields: recordFilter.selectFields
287
+ });
288
+ // A clause on a field this dataset does not have (the reader switched
289
+ // datasets) narrows nothing and is not shown.
290
+ const recordClauses = useMemo(()=>gridFilter.clauses.filter((clause)=>recordFilter.fields.some((field)=>field.column === clause.field)), [
291
+ gridFilter.clauses,
292
+ recordFilter.fields
293
+ ]);
294
+ const recordPlan = useMemo(()=>planRecordFilter(model, recordClauses, gridFilter.searchWords), [
295
+ model,
296
+ recordClauses,
297
+ gridFilter.searchWords
298
+ ]);
299
+ /** The walk, narrowed by the served token when there is one. */ const recordsQuery = useCallback((cap)=>dataScope && (selected == null ? void 0 : selected.$id) ? query(collection(firestore, dataScope[0], dataScope[1], 'datasets', selected.$id, 'records'), ...recordPlan.token ? [
300
+ where('filterKeys', 'array-contains', recordPlan.token)
301
+ ] : [], orderBy(documentId()), limit(cap)) : null, [
258
302
  firestore,
259
303
  dataScope,
260
- selected == null ? void 0 : selected.$id
304
+ selected == null ? void 0 : selected.$id,
305
+ recordPlan.token
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
+ ], {
311
+ idField: '$id'
312
+ });
313
+ // The window: one read past the cap, so "the window was full" is a fact.
314
+ const { data: windowDocs } = useFirestoreCollection(()=>recordPlan.windowed ? recordsQuery(RECORD_FILTER_WINDOW + 1) : null, [
315
+ recordsQuery,
316
+ recordPlan.windowed
261
317
  ], {
262
318
  idField: '$id'
263
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
350
+ ]);
351
+ const filteringRecords = recordClauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
264
352
  /**
265
353
  * The two HEAD-COUNTS on this card are server aggregates, not the lengths
266
354
  * of the two capped listeners above (AGL-1716, the AGL-1706 shape).
@@ -358,45 +446,6 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
358
446
  // changes nothing about which way it can be wrong.
359
447
  const datasetCount = serverDatasetCount != null ? serverDatasetCount : datasets.length;
360
448
  const recordCount = serverRecordCount && serverRecordCount.datasetId === countedDatasetId ? serverRecordCount.count : records.length;
361
- /*
362
- * NO FILTERS PANEL AND NO SEARCH over the records (AGL-3317). The walk is
363
- * cursor-paged by document name, and a record's fields live under
364
- * `values`, which the index configuration exempts from single-field
365
- * indexing — so no `where` on any field can be served, and a filter or a
366
- * search over the loaded page would present one page as the dataset. The
367
- * Filter and Sort boxes stay because they say they apply to this page, and
368
- * the Dataset select is a scope: it picks which collection is walked.
369
- */ // Query layer (AGL-181): the same evaluator the renderer uses, applied in
370
- // memory over the rows in front of the reader. That window is now one PAGE
371
- // rather than a 500-row sample (AGL-2501), which is a smaller claim and a
372
- // true one — the helper text says which, because a filter box that looks
373
- // like it searches the collection and does not is worse than one that says
374
- // so.
375
- const [filterText, setFilterText] = useState('');
376
- const [sortText, setSortText] = useState('');
377
- const visibleRecords = useMemo(()=>{
378
- const where = parseDatasetFilter(filterText);
379
- const orderBy = parseDatasetSort(sortText);
380
- if (!where && !orderBy) return records;
381
- const rows = records.map((record)=>{
382
- var _record_values;
383
- return _extends({
384
- __record: record
385
- }, (_record_values = record.values) != null ? _record_values : {});
386
- });
387
- return applyDatasetQuery(model, rows, _extends({}, where ? {
388
- where: [
389
- where
390
- ]
391
- } : {}, orderBy ? {
392
- orderBy
393
- } : {})).map((row)=>row.__record);
394
- }, [
395
- records,
396
- filterText,
397
- sortText,
398
- model
399
- ]);
400
449
  // --- Create dataset -----------------------------------------------------
401
450
  const [creator, setCreator] = useState(null);
402
451
  const handleOpenCreator = useCallback(()=>{
@@ -427,7 +476,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
427
476
  ]);
428
477
  // Entries are human names — "Roast preference" → stable id
429
478
  // `roast_preference` with the pretty name kept for headers (AGL-558).
430
- const creatorEntries = parseDatasetFieldEntries((_ref = creator == null ? void 0 : creator.fields) != null ? _ref : '');
479
+ const creatorEntries = parseDatasetFieldEntries((_ref1 = creator == null ? void 0 : creator.fields) != null ? _ref1 : '');
431
480
  const creatorFields = creatorEntries.map((entry)=>entry.id);
432
481
  const handleCreate = useCallback(async ()=>{
433
482
  // No org, no create. The `else` that used to sit below this was the
@@ -694,7 +743,8 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
694
743
  * an edit landing underneath updates the open dialog. When the row leaves
695
744
  * the page — deleted, or paged past — this answers `null` and the dialog
696
745
  * closes, which is the honest response to a record no longer in front of
697
- * this reader. Nothing here queries: `records` is the page query's answer.
746
+ * this reader. Nothing here queries: `records` is the page (or filter window)
747
+ * query's answer.
698
748
  */ const [viewerId, setViewerId] = useState(null);
699
749
  const viewerRecord = useMemo(()=>{
700
750
  var _records_find;
@@ -765,6 +815,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
765
815
  mergeFields: [
766
816
  'values',
767
817
  'referencedIds',
818
+ 'filterKeys',
768
819
  'updatedAt'
769
820
  ]
770
821
  });
@@ -1178,10 +1229,69 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1178
1229
  enqueueSnackbar,
1179
1230
  logActivity
1180
1231
  ]);
1232
+ /*
1233
+ * One column per schema field, filterable as far as `datasetRecordFilter`
1234
+ * declares it. The row OPENS the record, read-only (`onOpen`); the actions
1235
+ * column holds the keyboard's way in (`View`, a real button) beside Edit
1236
+ * and Delete, and a press anywhere in it never opens the row.
1237
+ */ const recordGridColumns = useMemo(()=>listFilterGridColumns([
1238
+ ...fields.map((fieldId)=>{
1239
+ var _ref;
1240
+ const field = model.fields[fieldId];
1241
+ return {
1242
+ field: recordColumn(fieldId),
1243
+ headerName: (_ref = field == null ? void 0 : field.name) != null ? _ref : fieldId,
1244
+ // Field descriptions (AGL-560) surface as header hints.
1245
+ description: field == null ? void 0 : field.description,
1246
+ flex: 1,
1247
+ minWidth: 140,
1248
+ sortable: false,
1249
+ renderCell: ({ row })=>{
1250
+ var _row_values, _row_values1;
1251
+ return (field == null ? void 0 : field.type) === 'reference' ? referenceLabel(fieldId, (_row_values = row.values) == null ? void 0 : _row_values[fieldId]) || '--' : field ? formatDatasetValue(field, (_row_values1 = row.values) == null ? void 0 : _row_values1[fieldId]) || '--' : '--';
1252
+ }
1253
+ };
1254
+ }),
1255
+ listActionsColumn((record)=>/*#__PURE__*/ _jsxs(Stack, {
1256
+ direction: "row",
1257
+ sx: {
1258
+ height: '100%',
1259
+ alignItems: 'center',
1260
+ justifyContent: 'flex-end'
1261
+ },
1262
+ children: [
1263
+ /*#__PURE__*/ _jsx(Button, {
1264
+ size: "small",
1265
+ onClick: ()=>setViewerId(record.$id),
1266
+ children: 'View'
1267
+ }),
1268
+ /*#__PURE__*/ _jsx(Button, {
1269
+ size: "small",
1270
+ onClick: handleOpenRecord(record),
1271
+ children: 'Edit'
1272
+ }),
1273
+ /*#__PURE__*/ _jsx(Button, {
1274
+ size: "small",
1275
+ color: "error",
1276
+ onClick: handleDeleteRecord(record),
1277
+ children: 'Delete'
1278
+ })
1279
+ ]
1280
+ }), {
1281
+ width: 220
1282
+ })
1283
+ ], recordFilter.fields, recordFilter.options, recordFilter.headers), [
1284
+ fields,
1285
+ model,
1286
+ referenceLabel,
1287
+ handleOpenRecord,
1288
+ handleDeleteRecord,
1289
+ recordFilter
1290
+ ]);
1181
1291
  return /*#__PURE__*/ _jsxs(CardDisplay, {
1182
1292
  header: 'Data',
1183
1293
  help: pluginDocsHelp('datasets', {
1184
- anchor: '#model-builder'
1294
+ anchor: '#filter-records'
1185
1295
  }),
1186
1296
  contentGutterX: true,
1187
1297
  contentGutterY: true,
@@ -1210,7 +1320,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1210
1320
  select: true,
1211
1321
  size: "small",
1212
1322
  label: "Dataset",
1213
- value: (_ref1 = selected == null ? void 0 : selected.$id) != null ? _ref1 : '',
1323
+ value: (_ref2 = selected == null ? void 0 : selected.$id) != null ? _ref2 : '',
1214
1324
  onChange: (event)=>setSelectedId(event.target.value),
1215
1325
  sx: {
1216
1326
  minWidth: 180
@@ -1269,130 +1379,40 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1269
1379
  limit: checkQuota(org, 'recordsPerDataset', recordCount).limit,
1270
1380
  noun: "record"
1271
1381
  }) : null,
1272
- selected && records.length > 0 ? /*#__PURE__*/ _jsxs(Stack, {
1273
- direction: "row",
1274
- spacing: 1,
1382
+ selected && (records.length > 0 || recordPage > 0 || filteringRecords) ? /*#__PURE__*/ _jsxs(_Fragment, {
1275
1383
  children: [
1276
- /*#__PURE__*/ _jsx(TextField, {
1277
- size: "small",
1278
- label: "Filter",
1279
- value: filterText,
1280
- onChange: (event)=>setFilterText(event.target.value),
1281
- helperText: 'e.g. price <= 20 · applies to this page',
1282
- sx: {
1283
- minWidth: 220
1284
- }
1384
+ /*#__PURE__*/ _jsx(ListFilterChips, {
1385
+ fields: recordFilter.fields,
1386
+ headers: recordFilter.headers,
1387
+ clauses: recordClauses,
1388
+ onChange: gridFilter.setClauses,
1389
+ options: recordFilter.options,
1390
+ servedField: recordPlan.windowed ? null : recordPlan.servedField
1285
1391
  }),
1286
- /*#__PURE__*/ _jsx(TextField, {
1287
- size: "small",
1288
- label: "Sort",
1289
- value: sortText,
1290
- onChange: (event)=>setSortText(event.target.value),
1291
- helperText: 'e.g. price desc',
1292
- sx: {
1293
- minWidth: 140
1294
- }
1295
- })
1296
- ]
1297
- }) : null,
1298
- selected && (records.length > 0 || recordPage > 0) ? /*#__PURE__*/ _jsxs(_Fragment, {
1299
- children: [
1300
- /*#__PURE__*/ _jsxs(ScrollTable, {
1301
- size: "small",
1302
- children: [
1303
- /*#__PURE__*/ _jsx(TableHead, {
1304
- children: /*#__PURE__*/ _jsxs(TableRow, {
1305
- children: [
1306
- fields.map((fieldId)=>{
1307
- var _field_name, _ref;
1308
- const field = model.fields[fieldId];
1309
- // Field descriptions (AGL-560) surface as header hints.
1310
- return /*#__PURE__*/ _jsx(TableCell, {
1311
- children: (field == null ? void 0 : field.description) ? /*#__PURE__*/ _jsx(Tooltip, {
1312
- title: field.description,
1313
- children: /*#__PURE__*/ _jsx("span", {
1314
- style: {
1315
- cursor: 'help'
1316
- },
1317
- children: (_field_name = field.name) != null ? _field_name : fieldId
1318
- })
1319
- }) : (_ref = field == null ? void 0 : field.name) != null ? _ref : fieldId
1320
- }, fieldId);
1321
- }),
1322
- /*#__PURE__*/ _jsx(TableCell, {
1323
- align: "right",
1324
- children: 'Actions'
1325
- })
1326
- ]
1327
- })
1328
- }),
1329
- /*#__PURE__*/ _jsx(TableBody, {
1330
- children: visibleRecords.map((record)=>/*
1331
- The row OPENS the record, read-only. Reading a record and
1332
- changing one used to be the same gesture: `Edit` was the only
1333
- way to see a document's full contents, so every look opened a
1334
- form whose primary button writes.
1335
-
1336
- The click is the mouse affordance and nothing else — a `<tr>`
1337
- cannot be made into a real activatable control without
1338
- overriding the `row` role a table depends on. The keyboard's
1339
- affordance is the `View` button in the actions cluster: a
1340
- genuine `<button>`, in the tab order, announced as one, with
1341
- the theme's focus ring. Both reach the same handler.
1342
- */ /*#__PURE__*/ _jsxs(TableRow, {
1343
- hover: true,
1344
- onClick: ()=>setViewerId(record.$id),
1345
- sx: {
1346
- cursor: 'pointer'
1347
- },
1348
- children: [
1349
- fields.map((fieldId)=>{
1350
- var _model_fields_fieldId, _record_values, _record_values1;
1351
- return /*#__PURE__*/ _jsx(TableCell, {
1352
- children: ((_model_fields_fieldId = model.fields[fieldId]) == null ? void 0 : _model_fields_fieldId.type) === 'reference' ? referenceLabel(fieldId, (_record_values = record.values) == null ? void 0 : _record_values[fieldId]) || '--' : model.fields[fieldId] ? formatDatasetValue(model.fields[fieldId], (_record_values1 = record.values) == null ? void 0 : _record_values1[fieldId]) || '--' : '--'
1353
- }, fieldId);
1354
- }),
1355
- /*#__PURE__*/ _jsxs(TableCell, {
1356
- align: "right",
1357
- sx: {
1358
- whiteSpace: 'nowrap'
1359
- },
1360
- /*
1361
- Every control in this cell is its OWN action. Without
1362
- this the row's handler fires too, so `Delete` would open
1363
- the viewer behind its own destructive write and `Edit`
1364
- would leave a read-only dialog stacked under the form —
1365
- the kind of defect nobody reports and everybody works
1366
- around.
1367
- */ onClick: (event)=>event.stopPropagation(),
1368
- children: [
1369
- /*#__PURE__*/ _jsx(Button, {
1370
- size: "small",
1371
- onClick: ()=>setViewerId(record.$id),
1372
- children: 'View'
1373
- }),
1374
- /*#__PURE__*/ _jsx(Button, {
1375
- size: "small",
1376
- onClick: handleOpenRecord(record),
1377
- children: 'Edit'
1378
- }),
1379
- /*#__PURE__*/ _jsx(Button, {
1380
- size: "small",
1381
- color: "error",
1382
- onClick: handleDeleteRecord(record),
1383
- children: 'Delete'
1384
- })
1385
- ]
1386
- })
1387
- ]
1388
- }, record.$id))
1389
- })
1390
- ]
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
+ /*#__PURE__*/ _jsx(ListTable, {
1398
+ "aria-label": "Records",
1399
+ rows: shownRecords,
1400
+ columns: recordGridColumns,
1401
+ onOpen: (id)=>setViewerId(id),
1402
+ // One page of the walk (or of the window's matches), turned by
1403
+ // the footer below: the grid neither slices nor filters it.
1404
+ hideFooter: true,
1405
+ filterMode: "server",
1406
+ filterModel: gridFilter.filterModel,
1407
+ onFilterModelChange: gridFilter.onFilterModelChange,
1408
+ quickFilter: true,
1409
+ disableColumnSorting: true,
1410
+ noRowsLabel: filteringRecords ? 'No records match these filters' : 'No records yet.'
1391
1411
  }),
1392
1412
  /*#__PURE__*/ _jsx(ListPagination, {
1393
1413
  page: recordPage,
1394
1414
  pageSize: recordPageSize,
1395
- rowCount: records.length,
1415
+ rowCount: shownRecords.length,
1396
1416
  hasMore: hasMoreRecords,
1397
1417
  onPageChange: setRecordPage,
1398
1418
  onPageSizeChange: setRecordPageSize
@@ -1444,7 +1464,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1444
1464
  children: [
1445
1465
  /*#__PURE__*/ _jsx(TextField, {
1446
1466
  label: "Name",
1447
- value: (_ref2 = creator == null ? void 0 : creator.name) != null ? _ref2 : '',
1467
+ value: (_ref3 = creator == null ? void 0 : creator.name) != null ? _ref3 : '',
1448
1468
  onChange: (event)=>setCreator((prev)=>prev ? _extends({}, prev, {
1449
1469
  name: event.target.value
1450
1470
  }) : prev),
@@ -1457,7 +1477,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1457
1477
  }),
1458
1478
  /*#__PURE__*/ _jsx(TextField, {
1459
1479
  label: "Fields",
1460
- value: (_ref3 = creator == null ? void 0 : creator.fields) != null ? _ref3 : '',
1480
+ value: (_ref4 = creator == null ? void 0 : creator.fields) != null ? _ref4 : '',
1461
1481
  onChange: (event)=>setCreator((prev)=>prev ? _extends({}, prev, {
1462
1482
  fields: event.target.value
1463
1483
  }) : prev),
@@ -1691,7 +1711,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1691
1711
  minRows: 6,
1692
1712
  size: "small",
1693
1713
  label: "CSV (with header row) or JSON array",
1694
- value: (_ref4 = importer == null ? void 0 : importer.text) != null ? _ref4 : '',
1714
+ value: (_ref5 = importer == null ? void 0 : importer.text) != null ? _ref5 : '',
1695
1715
  onChange: (event)=>setImporter((prev)=>prev ? _extends({}, prev, {
1696
1716
  text: event.target.value
1697
1717
  }) : prev),
@@ -1704,7 +1724,7 @@ import { DatasetRecordDialog } from "./dataset-record-dialog.component.js";
1704
1724
  select: true,
1705
1725
  size: "small",
1706
1726
  label: "Match on field (upsert)",
1707
- value: (_ref5 = importer == null ? void 0 : importer.keyField) != null ? _ref5 : '',
1727
+ value: (_ref6 = importer == null ? void 0 : importer.keyField) != null ? _ref6 : '',
1708
1728
  onChange: (event)=>setImporter((prev)=>prev ? _extends({}, prev, {
1709
1729
  keyField: event.target.value
1710
1730
  }) : prev),