@evenicanpm/portal-table-ui 2.4.2 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evenicanpm/portal-table-ui",
3
- "version": "2.4.2",
3
+ "version": "2.5.0",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -39,7 +39,7 @@
39
39
  "@mui/lab": "^7.0.1-beta.19",
40
40
  "date-fns": "^4.1.0",
41
41
  "zod": "^4.4.3",
42
- "@evenicanpm/ui": "2.4.2"
42
+ "@evenicanpm/ui": "2.5.0"
43
43
  },
44
44
  "scripts": {
45
45
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -8,6 +8,7 @@ interface BulkActionButtonProps {
8
8
  selectedIds: (string | number)[];
9
9
  clearSelection: () => void;
10
10
  selectedRows: Types.Queries.Row[];
11
+ selectionRequired: boolean;
11
12
  }
12
13
  export function BulkActionButton({
13
14
  action,
@@ -15,13 +16,15 @@ export function BulkActionButton({
15
16
  selectedIds,
16
17
  clearSelection,
17
18
  selectedRows,
19
+ selectionRequired,
18
20
  }: BulkActionButtonProps) {
19
21
  const mutationOptions = action.mutation({
20
22
  resourceName,
21
23
  selectedIds,
22
24
  });
23
25
 
24
- const allowed = !action.isAllowed || action.isAllowed(selectedRows);
26
+ const isAllowed = action.isAllowed ? action.isAllowed(selectedRows) : true;
27
+ const allowed = !selectionRequired || isAllowed;
25
28
  const { mutate, isPending } = useMutation(mutationOptions);
26
29
 
27
30
  const handleClick = () => {
@@ -6,7 +6,6 @@ import { Box, Button } from "@mui/material";
6
6
  import Grid from "@mui/material/Grid2";
7
7
  import { resolvers, transformers } from "../../../../logic";
8
8
  import type * as Types from "../../../../types";
9
- import type { Column } from "../../../../types";
10
9
  import type { StatusColorMap, StatusTextMap } from "../../../../types/schemas";
11
10
  import { StatusPill } from "../status-pill";
12
11
  import { DetailsRowLabel } from "./label";
@@ -29,7 +28,7 @@ interface DetailsProps {
29
28
  /** Data mask map */
30
29
  dataMaskMap: Types.Handlers.DataMaskMap;
31
30
  /** Table columns **/
32
- columns: Column[];
31
+ columns: Types.Column[];
33
32
  /** */
34
33
  statusColorMap: StatusColorMap;
35
34
  /** */
@@ -104,7 +103,7 @@ const Details = ({
104
103
  "@media print": { display: "inline-block" },
105
104
  }}
106
105
  >
107
- {documentTypeLabel.slice(0, documentTypeLabel.length - 1)} #{id}
106
+ {documentTypeLabel.slice(0, -1)} #{id}
108
107
  </H6>
109
108
  <Subtitle sx={{ "@media print": { display: "none" } }}>
110
109
  DETAILS
@@ -50,6 +50,10 @@ const formatDate = (dateString?: string): string => {
50
50
  }
51
51
  };
52
52
 
53
+ function handleOnClick() {
54
+ globalThis.print();
55
+ }
56
+
53
57
  /**
54
58
  * InvoiceType enum type.
55
59
  */
@@ -269,10 +273,6 @@ export const InvoiceDetailsTemplate = ({
269
273
  );
270
274
  }
271
275
 
272
- function handleOnClick() {
273
- window.print();
274
- }
275
-
276
276
  return (
277
277
  <Box
278
278
  sx={{
@@ -14,6 +14,15 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({
14
14
  minWidth: 140,
15
15
  }));
16
16
 
17
+ const headerSkeletonKeys = [
18
+ "header-skeleton-1",
19
+ "header-skeleton-2",
20
+ "header-skeleton-3",
21
+ "header-skeleton-4",
22
+ "header-skeleton-5",
23
+ "header-skeleton-6",
24
+ ] as const;
25
+
17
26
  interface TableHeaderProps {
18
27
  hasActions: boolean;
19
28
  columns: (Column & { ColumnOrder: number })[] | undefined;
@@ -35,7 +44,7 @@ export default function TableHeader({
35
44
  selectAllChecked,
36
45
  selectAllIndeterminate,
37
46
  onToggleAll,
38
- }: TableHeaderProps) {
47
+ }: Readonly<TableHeaderProps>) {
39
48
  if (loading) {
40
49
  return (
41
50
  <TableHead>
@@ -48,8 +57,8 @@ export default function TableHeader({
48
57
  sx={{ bgcolor: "rgba(255,255,255,0.1)" }}
49
58
  />
50
59
  </StyledTableCell>
51
- {Array.from({ length: 6 })?.map((_col, i) => (
52
- <StyledTableCell key={String(i)}>
60
+ {headerSkeletonKeys.map((key) => (
61
+ <StyledTableCell key={key}>
53
62
  <Skeleton
54
63
  variant="rounded"
55
64
  width={100}
@@ -12,7 +12,7 @@ export function ActionButton({
12
12
  action,
13
13
  resourceName,
14
14
  docId,
15
- }: ActionButtonProps) {
15
+ }: Readonly<ActionButtonProps>) {
16
16
  const { mutate, isPending } = useMutation(
17
17
  action.mutation({
18
18
  resourceName,
@@ -30,9 +30,11 @@ export function ActionButton({
30
30
 
31
31
  if (action.icon) {
32
32
  const Icon = action.icon;
33
+ const iconContent = isPending ? <CircularProgress size={26} /> : <Icon />;
34
+
33
35
  return (
34
36
  <IconButton size="small" onClick={handleClick} disabled={isPending}>
35
- {isPending ? <CircularProgress size={26} /> : Icon ? <Icon /> : null}
37
+ {iconContent}
36
38
  </IconButton>
37
39
  );
38
40
  }
@@ -54,7 +54,7 @@ export default function DataTableRow({
54
54
  isSelected,
55
55
  onToggleSelected,
56
56
  resourceName,
57
- }: DocumentTableProps) {
57
+ }: Readonly<DocumentTableProps>) {
58
58
  const visibleActions = actions.filter(
59
59
  (a) => !a.isEnabled || a.isEnabled(row),
60
60
  );
@@ -80,16 +80,12 @@ export default function DataTableRow({
80
80
  />
81
81
  </TableCell>
82
82
 
83
- {columnLogic?.map((col, i) => {
83
+ {columnLogic?.map((col) => {
84
84
  // --- Cell Render --- //
85
85
  const rawValue = row[col.id];
86
86
  const maskedValue = col.maskFn(rawValue, col?.mask);
87
87
  const cellValue = col.cellFn(maskedValue);
88
- return (
89
- <StyledTableCell key={rawValue + String(i)}>
90
- {cellValue}
91
- </StyledTableCell>
92
- );
88
+ return <StyledTableCell key={col?.id}>{cellValue}</StyledTableCell>;
93
89
  })}
94
90
  {visibleActions.length > 0 && (
95
91
  <TableCell
@@ -9,7 +9,7 @@ import type React from "react";
9
9
  import { useState } from "react";
10
10
  import { resolvers, transformers } from "../../../logic";
11
11
  import type * as Types from "../../../types";
12
- import type { Column } from "../../../types";
12
+ import type { StatusColorMap, StatusTextMap } from "../../../types/schemas";
13
13
  import { BulkActionButton } from "./bulk-action-button";
14
14
  import { dataMaskMap as dataMaskMapLocal } from "./data-mask-map";
15
15
  import { Details } from "./details";
@@ -24,6 +24,257 @@ import { useSelectRows } from "./use-select-rows";
24
24
 
25
25
  type Id = string | number;
26
26
 
27
+ type ColumnLogic =
28
+ | {
29
+ id: string;
30
+ mask: string | undefined;
31
+ maskFn: (value: unknown, mask?: string) => string | number | string[];
32
+ cellFn: (value: string | number | string[]) => React.ReactNode;
33
+ }[]
34
+ | undefined;
35
+
36
+ type StatusCellProps = {
37
+ value: string | number | string[];
38
+ statusColorMap: StatusColorMap;
39
+ statusTextMap: StatusTextMap;
40
+ };
41
+
42
+ function StatusCell({
43
+ value,
44
+ statusColorMap,
45
+ statusTextMap,
46
+ }: Readonly<StatusCellProps>) {
47
+ return (
48
+ <StatusPill
49
+ status={value}
50
+ statusColorMap={statusColorMap}
51
+ statusTextMap={statusTextMap}
52
+ />
53
+ );
54
+ }
55
+
56
+ function createStatusCellRenderer(
57
+ statusColorMap: StatusColorMap,
58
+ statusTextMap: StatusTextMap,
59
+ ) {
60
+ return (value: string | number | string[]) => (
61
+ <StatusCell
62
+ value={value}
63
+ statusColorMap={statusColorMap}
64
+ statusTextMap={statusTextMap}
65
+ />
66
+ );
67
+ }
68
+
69
+ function getRowId(row: Types.Queries.Row, field: string): Id | null {
70
+ const v = row?.[field];
71
+ if (typeof v === "string" || typeof v === "number") return v;
72
+ return null;
73
+ }
74
+
75
+ type DocumentTableBodyProps = {
76
+ noResults: boolean;
77
+ rowsPending: boolean;
78
+ colSpan: number;
79
+ resolvedNoResults: React.ReactNode;
80
+ pageSize: number;
81
+ rows: Types.Queries.TableRows | undefined;
82
+ idField: string;
83
+ columnLogic: ColumnLogic;
84
+ resolvedActions: Types.Schemas.UIAction[];
85
+ selectedIds: Set<Id>;
86
+ onSelectDocument: React.Dispatch<
87
+ React.SetStateAction<string | number | null>
88
+ >;
89
+ onToggleSelected: (id: Id) => void;
90
+ resourceName: string;
91
+ };
92
+
93
+ function DocumentTableBody({
94
+ noResults,
95
+ rowsPending,
96
+ colSpan,
97
+ resolvedNoResults,
98
+ pageSize,
99
+ rows,
100
+ idField,
101
+ columnLogic,
102
+ resolvedActions,
103
+ selectedIds,
104
+ onSelectDocument,
105
+ onToggleSelected,
106
+ resourceName,
107
+ }: Readonly<DocumentTableBodyProps>) {
108
+ if (noResults) {
109
+ return (
110
+ <TableRow>
111
+ <TableCell
112
+ colSpan={colSpan || 1}
113
+ align="center"
114
+ sx={{ py: 6, color: "text.secondary" }}
115
+ >
116
+ {resolvedNoResults}
117
+ </TableCell>
118
+ </TableRow>
119
+ );
120
+ }
121
+
122
+ if (rowsPending) {
123
+ return <TableRowSkeleton pageSize={pageSize} colSpan={colSpan} />;
124
+ }
125
+
126
+ return (
127
+ <>
128
+ {rows?.data?.map((row) => {
129
+ const id = getRowId(row, idField);
130
+ if (id === null) return null;
131
+
132
+ return (
133
+ <DataTableRow
134
+ key={id}
135
+ columnLogic={columnLogic}
136
+ row={row}
137
+ handleClick={onSelectDocument}
138
+ id={id}
139
+ actions={resolvedActions}
140
+ isSelected={selectedIds.has(id)}
141
+ onToggleSelected={onToggleSelected}
142
+ resourceName={resourceName}
143
+ />
144
+ );
145
+ })}
146
+ </>
147
+ );
148
+ }
149
+
150
+ function resolveDetailsQueryOptions(
151
+ resource: Types.Schemas.Resource,
152
+ hasTemplateSupport: boolean,
153
+ selectedDocument: string | number | null,
154
+ ) {
155
+ if (hasTemplateSupport && selectedDocument && resource.detailsQueryFn) {
156
+ return resource.detailsQueryFn(String(selectedDocument));
157
+ }
158
+
159
+ return {
160
+ queryKey: [],
161
+ queryFn: async () => undefined,
162
+ enabled: false,
163
+ };
164
+ }
165
+
166
+ type DocumentTableDetailsViewProps = {
167
+ selectedDocument: string | number;
168
+ rows: Types.Queries.TableRows | undefined;
169
+ idField: string;
170
+ hasTemplateSupport: boolean;
171
+ detailsData: unknown;
172
+ detailsPending: boolean;
173
+ label: string;
174
+ columns: Types.Column[];
175
+ resolvedDataMaskMap: Types.Handlers.DataMaskMap;
176
+ statusColorMap: StatusColorMap;
177
+ statusTextMap: StatusTextMap;
178
+ detailsRenderMap: Types.Handlers.DetailsRenderMap | undefined;
179
+ detailsTemplate: Types.Schemas.Resource["detailsTemplate"];
180
+ resolvedActions: Types.Schemas.UIAction[];
181
+ resourceName: string;
182
+ onBack: () => void;
183
+ };
184
+
185
+ function DocumentTableDetailsView({
186
+ selectedDocument,
187
+ rows,
188
+ idField,
189
+ hasTemplateSupport,
190
+ detailsData,
191
+ detailsPending,
192
+ label,
193
+ columns,
194
+ resolvedDataMaskMap,
195
+ statusColorMap,
196
+ statusTextMap,
197
+ detailsRenderMap,
198
+ detailsTemplate,
199
+ resolvedActions,
200
+ resourceName,
201
+ onBack,
202
+ }: Readonly<DocumentTableDetailsViewProps>) {
203
+ const rowSnapshot =
204
+ rows?.data.find((doc) => doc?.[idField] === selectedDocument) ?? {};
205
+
206
+ return (
207
+ <Details
208
+ documentTypeLabel={label}
209
+ id={selectedDocument}
210
+ documentData={
211
+ hasTemplateSupport ? (detailsData ?? rowSnapshot) : undefined
212
+ }
213
+ row={rowSnapshot}
214
+ handleBack={onBack}
215
+ columns={columns}
216
+ dataMaskMap={resolvedDataMaskMap}
217
+ statusColorMap={statusColorMap}
218
+ statusTextMap={statusTextMap}
219
+ detailsRenderMap={detailsRenderMap}
220
+ template={hasTemplateSupport ? detailsTemplate : undefined}
221
+ loading={hasTemplateSupport ? detailsPending : false}
222
+ actions={resolvedActions}
223
+ resourceName={resourceName}
224
+ />
225
+ );
226
+ }
227
+
228
+ type BulkActionButtonsProps = {
229
+ hasBulkActions: boolean;
230
+ resolvedBulkActions: Types.Schemas.UIBulkAction[];
231
+ rows: Types.Queries.TableRows | undefined;
232
+ idField: string;
233
+ selectedIds: Set<Id>;
234
+ resourceName: string;
235
+ clearSelection: () => void;
236
+ selectionRequired: boolean;
237
+ };
238
+
239
+ function BulkActionButtons({
240
+ hasBulkActions,
241
+ resolvedBulkActions,
242
+ rows,
243
+ idField,
244
+ selectedIds,
245
+ resourceName,
246
+ clearSelection,
247
+ selectionRequired,
248
+ }: Readonly<BulkActionButtonsProps>) {
249
+ if (!hasBulkActions) {
250
+ return null;
251
+ }
252
+
253
+ return (
254
+ <>
255
+ {resolvedBulkActions.map((action) => {
256
+ const selectedRows =
257
+ rows?.data?.filter((row) => {
258
+ const id = getRowId(row, idField);
259
+ return id !== null && selectedIds.has(id);
260
+ }) ?? [];
261
+
262
+ return (
263
+ <BulkActionButton
264
+ key={action.label}
265
+ action={action}
266
+ resourceName={resourceName}
267
+ selectionRequired={selectionRequired}
268
+ selectedIds={Array.from(selectedIds)}
269
+ selectedRows={selectedRows}
270
+ clearSelection={clearSelection}
271
+ />
272
+ );
273
+ })}
274
+ </>
275
+ );
276
+ }
277
+
27
278
  export type DocumentTableProps = {
28
279
  resource: Types.Schemas.Resource;
29
280
  pageSize: number;
@@ -58,9 +309,9 @@ export default function DocumentTable({
58
309
  >(null);
59
310
 
60
311
  const selectedDocument =
61
- selectedDocumentProp !== undefined
62
- ? selectedDocumentProp
63
- : selectedDocumentLocal;
312
+ selectedDocumentProp === undefined
313
+ ? selectedDocumentLocal
314
+ : selectedDocumentProp;
64
315
  const setSelectedDocument =
65
316
  setSelectedDocumentProp ?? setSelectedDocumentLocal;
66
317
  const [filters, setFilters] = useState<Record<string, string>>({});
@@ -98,13 +349,7 @@ export default function DocumentTable({
98
349
 
99
350
  const cellRenderMapDefaults = {
100
351
  ...columnRenderMap,
101
- Status: (value: string | number | string[]) => (
102
- <StatusPill
103
- status={value}
104
- statusColorMap={statusColorMap}
105
- statusTextMap={statusTextMap}
106
- />
107
- ),
352
+ Status: createStatusCellRenderer(statusColorMap, statusTextMap),
108
353
  };
109
354
 
110
355
  // --- State Handlers --- //
@@ -155,7 +400,7 @@ export default function DocumentTable({
155
400
 
156
401
  const config = viewConfig ?? fetchedConfig;
157
402
 
158
- const configPending = !viewConfig ? fetchedConfigPending : false;
403
+ const configPending = viewConfig ? false : fetchedConfigPending;
159
404
 
160
405
  const columns = config?.Columns ?? [];
161
406
 
@@ -165,8 +410,8 @@ export default function DocumentTable({
165
410
  const visibleCols = transformers.getVisibleColumns(columns);
166
411
 
167
412
  // Depends on searchableCols, must be declared after transformer
168
- const [searchColumn, setSearchColumn] = useState<Column>(
169
- () => searchableCols?.[0] as Column,
413
+ const [searchColumn, setSearchColumn] = useState<Types.Column>(
414
+ () => searchableCols?.[0] as Types.Column,
170
415
  );
171
416
 
172
417
  // always derive a safe usable column
@@ -199,16 +444,10 @@ export default function DocumentTable({
199
444
  cellFn: resolvers.resolveCellUI(col, cellRenderMapDefaults),
200
445
  }));
201
446
 
202
- const getRowId = (row: Types.Queries.Row, field: string): Id | null => {
203
- const v = row?.[field];
204
- if (typeof v === "string" || typeof v === "number") return v;
205
- return null;
206
- };
447
+ const getRowIdForField = (row: Types.Queries.Row) => getRowId(row, idField);
207
448
 
208
449
  const currentPageIds: Id[] =
209
- rows?.data
210
- ?.map((r) => getRowId(r, idField))
211
- .filter((v): v is Id => v !== null) ?? [];
450
+ rows?.data?.map(getRowIdForField).filter((v): v is Id => v !== null) ?? [];
212
451
 
213
452
  const {
214
453
  selectedIds,
@@ -223,13 +462,7 @@ export default function DocumentTable({
223
462
  !!resource.detailsQueryFn && !!resource.detailsTemplate;
224
463
 
225
464
  const { data: detailsData, isPending: detailsPending } = useQuery(
226
- hasTemplateSupport && selectedDocument && resource.detailsQueryFn
227
- ? resource.detailsQueryFn(String(selectedDocument))
228
- : {
229
- queryKey: [],
230
- queryFn: async () => undefined,
231
- enabled: false,
232
- },
465
+ resolveDetailsQueryOptions(resource, hasTemplateSupport, selectedDocument),
233
466
  );
234
467
 
235
468
  const resolvedActions = resolvers.resolveActions(
@@ -255,29 +488,24 @@ export default function DocumentTable({
255
488
 
256
489
  // --- Render --- //
257
490
  if (selectedDocument) {
258
- const rowSnapshot =
259
- rows?.data.find((doc) => doc?.[idField] === selectedDocument) ?? {};
260
-
261
- const shouldUseDetailsData = hasTemplateSupport;
262
-
263
491
  return (
264
- <Details
265
- documentTypeLabel={label}
266
- id={selectedDocument}
267
- documentData={
268
- shouldUseDetailsData ? (detailsData ?? rowSnapshot) : undefined
269
- }
270
- row={rowSnapshot}
271
- handleBack={handleBack}
492
+ <DocumentTableDetailsView
493
+ selectedDocument={selectedDocument}
494
+ rows={rows}
495
+ idField={idField}
496
+ hasTemplateSupport={hasTemplateSupport}
497
+ detailsData={detailsData}
498
+ detailsPending={detailsPending}
499
+ label={label}
272
500
  columns={columns}
273
- dataMaskMap={resolvedDataMaskMap}
501
+ resolvedDataMaskMap={resolvedDataMaskMap}
274
502
  statusColorMap={statusColorMap}
275
503
  statusTextMap={statusTextMap}
276
504
  detailsRenderMap={detailsRenderMap}
277
- template={hasTemplateSupport ? resource.detailsTemplate : undefined}
278
- loading={hasTemplateSupport ? detailsPending : false}
279
- actions={resolvedActions}
505
+ detailsTemplate={resource.detailsTemplate}
506
+ resolvedActions={resolvedActions}
280
507
  resourceName={resource.name}
508
+ onBack={handleBack}
281
509
  />
282
510
  );
283
511
  }
@@ -316,25 +544,16 @@ export default function DocumentTable({
316
544
  />
317
545
  ))}
318
546
 
319
- {hasBulkActions &&
320
- resolvedBulkActions.map((action) => {
321
- const selectedRows =
322
- rows?.data?.filter((row) => {
323
- const id = getRowId(row, idField);
324
- return id !== null && selectedIds.has(id);
325
- }) ?? [];
326
-
327
- return (
328
- <BulkActionButton
329
- key={action.label}
330
- action={action}
331
- resourceName={resource.name}
332
- selectedIds={Array.from(selectedIds)}
333
- selectedRows={selectedRows}
334
- clearSelection={clearSelection}
335
- />
336
- );
337
- })}
547
+ <BulkActionButtons
548
+ hasBulkActions={hasBulkActions}
549
+ selectionRequired={config?.SelectionRequired ?? true}
550
+ resolvedBulkActions={resolvedBulkActions}
551
+ rows={rows}
552
+ idField={idField}
553
+ selectedIds={selectedIds}
554
+ resourceName={resource.name}
555
+ clearSelection={clearSelection}
556
+ />
338
557
  </FlexBox>
339
558
  <TableContainer className="invoice-list-table-container">
340
559
  <Table
@@ -352,38 +571,21 @@ export default function DocumentTable({
352
571
  />
353
572
 
354
573
  <TableBody>
355
- {noResults ? (
356
- <TableRow>
357
- <TableCell
358
- colSpan={colSpan || 1}
359
- align="center"
360
- sx={{ py: 6, color: "text.secondary" }}
361
- >
362
- {resolvedNoResults}
363
- </TableCell>
364
- </TableRow>
365
- ) : rowsPending ? (
366
- <TableRowSkeleton pageSize={pageSize} colSpan={colSpan} />
367
- ) : (
368
- rows?.data?.map((row) => {
369
- const id = getRowId(row, idField);
370
- if (id === null) return null;
371
-
372
- return (
373
- <DataTableRow
374
- key={id}
375
- columnLogic={columnLogic}
376
- row={row}
377
- handleClick={setSelectedDocument}
378
- id={id}
379
- actions={resolvedActions}
380
- isSelected={selectedIds.has(id)}
381
- onToggleSelected={(docId) => toggleOne(docId)}
382
- resourceName={resource.name}
383
- />
384
- );
385
- })
386
- )}
574
+ <DocumentTableBody
575
+ noResults={noResults}
576
+ rowsPending={rowsPending}
577
+ colSpan={colSpan}
578
+ resolvedNoResults={resolvedNoResults}
579
+ pageSize={pageSize}
580
+ rows={rows}
581
+ idField={idField}
582
+ columnLogic={columnLogic}
583
+ resolvedActions={resolvedActions}
584
+ selectedIds={selectedIds}
585
+ onSelectDocument={setSelectedDocument}
586
+ onToggleSelected={toggleOne}
587
+ resourceName={resource.name}
588
+ />
387
589
  </TableBody>
388
590
  </Table>
389
591
  </TableContainer>
@@ -6,15 +6,14 @@ import type { ViewConfig } from "../types";
6
6
  *
7
7
  */
8
8
 
9
+ export type RowValue = string | number | string[] | Record<string, string>[];
10
+
9
11
  /**
10
12
  *
11
13
  * A single row in the data table response object.
12
14
  *
13
15
  */
14
- export type Row = Record<
15
- string,
16
- string | number | string[] | Record<string, string>[] | unknown
17
- >;
16
+ export type Row = Record<string, RowValue>;
18
17
 
19
18
  /**
20
19
  *