@evenicanpm/portal-table-ui 2.4.2 → 2.5.1

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.1",
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.1"
43
43
  },
44
44
  "scripts": {
45
45
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -45,7 +45,7 @@ export function BulkActionButton({
45
45
  <IconButton
46
46
  size="small"
47
47
  onClick={handleClick}
48
- disabled={isPending || selectedIds.length === 0 || !allowed}
48
+ disabled={isPending || !allowed}
49
49
  sx={{
50
50
  border: 1,
51
51
  borderRadius: 1,
@@ -70,7 +70,7 @@ export function BulkActionButton({
70
70
  onClick={handleClick}
71
71
  size="large"
72
72
  sx={{ textWrap: "nowrap", minHeight: "fit-content" }}
73
- disabled={isPending || selectedIds.length === 0 || !allowed}
73
+ disabled={isPending || !allowed}
74
74
  >
75
75
  {isPending ? (
76
76
  <CircularProgress size={22} />
@@ -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,254 @@ 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
+ };
237
+
238
+ function BulkActionButtons({
239
+ hasBulkActions,
240
+ resolvedBulkActions,
241
+ rows,
242
+ idField,
243
+ selectedIds,
244
+ resourceName,
245
+ clearSelection,
246
+ }: Readonly<BulkActionButtonsProps>) {
247
+ if (!hasBulkActions) {
248
+ return null;
249
+ }
250
+
251
+ return (
252
+ <>
253
+ {resolvedBulkActions.map((action) => {
254
+ const selectedRows =
255
+ rows?.data?.filter((row) => {
256
+ const id = getRowId(row, idField);
257
+ return id !== null && selectedIds.has(id);
258
+ }) ?? [];
259
+
260
+ return (
261
+ <BulkActionButton
262
+ key={action.label}
263
+ action={action}
264
+ resourceName={resourceName}
265
+ selectedIds={Array.from(selectedIds)}
266
+ selectedRows={selectedRows}
267
+ clearSelection={clearSelection}
268
+ />
269
+ );
270
+ })}
271
+ </>
272
+ );
273
+ }
274
+
27
275
  export type DocumentTableProps = {
28
276
  resource: Types.Schemas.Resource;
29
277
  pageSize: number;
@@ -58,9 +306,9 @@ export default function DocumentTable({
58
306
  >(null);
59
307
 
60
308
  const selectedDocument =
61
- selectedDocumentProp !== undefined
62
- ? selectedDocumentProp
63
- : selectedDocumentLocal;
309
+ selectedDocumentProp === undefined
310
+ ? selectedDocumentLocal
311
+ : selectedDocumentProp;
64
312
  const setSelectedDocument =
65
313
  setSelectedDocumentProp ?? setSelectedDocumentLocal;
66
314
  const [filters, setFilters] = useState<Record<string, string>>({});
@@ -98,13 +346,7 @@ export default function DocumentTable({
98
346
 
99
347
  const cellRenderMapDefaults = {
100
348
  ...columnRenderMap,
101
- Status: (value: string | number | string[]) => (
102
- <StatusPill
103
- status={value}
104
- statusColorMap={statusColorMap}
105
- statusTextMap={statusTextMap}
106
- />
107
- ),
349
+ Status: createStatusCellRenderer(statusColorMap, statusTextMap),
108
350
  };
109
351
 
110
352
  // --- State Handlers --- //
@@ -153,9 +395,9 @@ export default function DocumentTable({
153
395
  enabled: !!viewConfigQuery && !viewConfig,
154
396
  });
155
397
 
156
- const config = viewConfig ?? fetchedConfig;
398
+ const config: Types.ViewConfig | undefined = viewConfig ?? fetchedConfig;
157
399
 
158
- const configPending = !viewConfig ? fetchedConfigPending : false;
400
+ const configPending = viewConfig ? false : fetchedConfigPending;
159
401
 
160
402
  const columns = config?.Columns ?? [];
161
403
 
@@ -165,8 +407,8 @@ export default function DocumentTable({
165
407
  const visibleCols = transformers.getVisibleColumns(columns);
166
408
 
167
409
  // Depends on searchableCols, must be declared after transformer
168
- const [searchColumn, setSearchColumn] = useState<Column>(
169
- () => searchableCols?.[0] as Column,
410
+ const [searchColumn, setSearchColumn] = useState<Types.Column>(
411
+ () => searchableCols?.[0] as Types.Column,
170
412
  );
171
413
 
172
414
  // always derive a safe usable column
@@ -199,16 +441,10 @@ export default function DocumentTable({
199
441
  cellFn: resolvers.resolveCellUI(col, cellRenderMapDefaults),
200
442
  }));
201
443
 
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
- };
444
+ const getRowIdForField = (row: Types.Queries.Row) => getRowId(row, idField);
207
445
 
208
446
  const currentPageIds: Id[] =
209
- rows?.data
210
- ?.map((r) => getRowId(r, idField))
211
- .filter((v): v is Id => v !== null) ?? [];
447
+ rows?.data?.map(getRowIdForField).filter((v): v is Id => v !== null) ?? [];
212
448
 
213
449
  const {
214
450
  selectedIds,
@@ -223,13 +459,7 @@ export default function DocumentTable({
223
459
  !!resource.detailsQueryFn && !!resource.detailsTemplate;
224
460
 
225
461
  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
- },
462
+ resolveDetailsQueryOptions(resource, hasTemplateSupport, selectedDocument),
233
463
  );
234
464
 
235
465
  const resolvedActions = resolvers.resolveActions(
@@ -255,29 +485,24 @@ export default function DocumentTable({
255
485
 
256
486
  // --- Render --- //
257
487
  if (selectedDocument) {
258
- const rowSnapshot =
259
- rows?.data.find((doc) => doc?.[idField] === selectedDocument) ?? {};
260
-
261
- const shouldUseDetailsData = hasTemplateSupport;
262
-
263
488
  return (
264
- <Details
265
- documentTypeLabel={label}
266
- id={selectedDocument}
267
- documentData={
268
- shouldUseDetailsData ? (detailsData ?? rowSnapshot) : undefined
269
- }
270
- row={rowSnapshot}
271
- handleBack={handleBack}
489
+ <DocumentTableDetailsView
490
+ selectedDocument={selectedDocument}
491
+ rows={rows}
492
+ idField={idField}
493
+ hasTemplateSupport={hasTemplateSupport}
494
+ detailsData={detailsData}
495
+ detailsPending={detailsPending}
496
+ label={label}
272
497
  columns={columns}
273
- dataMaskMap={resolvedDataMaskMap}
498
+ resolvedDataMaskMap={resolvedDataMaskMap}
274
499
  statusColorMap={statusColorMap}
275
500
  statusTextMap={statusTextMap}
276
501
  detailsRenderMap={detailsRenderMap}
277
- template={hasTemplateSupport ? resource.detailsTemplate : undefined}
278
- loading={hasTemplateSupport ? detailsPending : false}
279
- actions={resolvedActions}
502
+ detailsTemplate={resource.detailsTemplate}
503
+ resolvedActions={resolvedActions}
280
504
  resourceName={resource.name}
505
+ onBack={handleBack}
281
506
  />
282
507
  );
283
508
  }
@@ -316,25 +541,15 @@ export default function DocumentTable({
316
541
  />
317
542
  ))}
318
543
 
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
- })}
544
+ <BulkActionButtons
545
+ hasBulkActions={hasBulkActions}
546
+ resolvedBulkActions={resolvedBulkActions}
547
+ rows={rows}
548
+ idField={idField}
549
+ selectedIds={selectedIds}
550
+ resourceName={resource.name}
551
+ clearSelection={clearSelection}
552
+ />
338
553
  </FlexBox>
339
554
  <TableContainer className="invoice-list-table-container">
340
555
  <Table
@@ -352,38 +567,21 @@ export default function DocumentTable({
352
567
  />
353
568
 
354
569
  <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
- )}
570
+ <DocumentTableBody
571
+ noResults={noResults}
572
+ rowsPending={rowsPending}
573
+ colSpan={colSpan}
574
+ resolvedNoResults={resolvedNoResults}
575
+ pageSize={pageSize}
576
+ rows={rows}
577
+ idField={idField}
578
+ columnLogic={columnLogic}
579
+ resolvedActions={resolvedActions}
580
+ selectedIds={selectedIds}
581
+ onSelectDocument={setSelectedDocument}
582
+ onToggleSelected={toggleOne}
583
+ resourceName={resource.name}
584
+ />
387
585
  </TableBody>
388
586
  </Table>
389
587
  </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
  *