@evenicanpm/portal-table-ui 1.5.0 → 1.7.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.
Files changed (67) hide show
  1. package/package.json +6 -5
  2. package/src/auth/auth-context.tsx +92 -0
  3. package/src/features/table/components/dashboard/table/bulk-action-button.tsx +77 -0
  4. package/src/features/table/components/dashboard/table/data-mask-map.ts +26 -0
  5. package/src/features/table/components/dashboard/table/details/details.tsx +185 -0
  6. package/src/features/table/components/dashboard/table/details/label.tsx +26 -0
  7. package/src/features/table/components/dashboard/table/details/rows/downloads.tsx +25 -0
  8. package/src/features/table/components/dashboard/table/details/rows/primitive.tsx +25 -0
  9. package/src/features/table/components/dashboard/table/details/rows/table.tsx +73 -0
  10. package/{dist/features/table/components/dashboard/table/details/styles.d.ts → src/features/table/components/dashboard/table/details/styles.ts} +10 -2
  11. package/src/features/table/components/dashboard/table/details/templates/invoice-details.tsx +407 -0
  12. package/src/features/table/components/dashboard/table/details/templates/template-registry.ts +36 -0
  13. package/src/features/table/components/dashboard/table/filter-render-map.tsx +10 -0
  14. package/src/features/table/components/dashboard/table/filters/date-filter.tsx +31 -0
  15. package/src/features/table/components/dashboard/table/filters/status-filter.tsx +36 -0
  16. package/src/features/table/components/dashboard/table/no-results.tsx +26 -0
  17. package/src/features/table/components/dashboard/table/status-pill.tsx +18 -0
  18. package/src/features/table/components/dashboard/table/table-header.tsx +98 -0
  19. package/src/features/table/components/dashboard/table/table-input.tsx +94 -0
  20. package/src/features/table/components/dashboard/table/table-row/action-button.tsx +47 -0
  21. package/src/features/table/components/dashboard/table/table-row/table-row.tsx +144 -0
  22. package/src/features/table/components/dashboard/table/table-row-skeleton.tsx +35 -0
  23. package/src/features/table/components/dashboard/table/table.tsx +361 -0
  24. package/src/features/table/components/dashboard/table/use-select-rows.ts +53 -0
  25. package/src/features/table/components/dashboard/table-dashboard.tsx +71 -0
  26. package/{dist/features/table/index.d.ts → src/features/table/index.ts} +7 -1
  27. package/src/features/table/logic/resolvers.ts +111 -0
  28. package/src/features/table/logic/transformers.ts +56 -0
  29. package/src/features/table/logic/types.ts +8 -0
  30. package/{dist/features/table/types/handlers.d.ts → src/features/table/types/handlers.ts} +30 -12
  31. package/{dist/features/table/types/index.d.ts → src/features/table/types/index.ts} +1 -0
  32. package/{dist/features/table/types/queries.d.ts → src/features/table/types/queries.ts} +10 -3
  33. package/src/features/table/types/schemas.ts +89 -0
  34. package/{dist/index.d.ts → src/index.ts} +2 -1
  35. package/src/portal.tsx +68 -0
  36. package/dist/auth/auth-context.d.ts +0 -36
  37. package/dist/features/settings/index.d.ts +0 -2
  38. package/dist/features/table/components/dashboard/table/actions.d.ts +0 -14
  39. package/dist/features/table/components/dashboard/table/data-mask-map.d.ts +0 -2
  40. package/dist/features/table/components/dashboard/table/details/details.d.ts +0 -27
  41. package/dist/features/table/components/dashboard/table/details/label.d.ts +0 -5
  42. package/dist/features/table/components/dashboard/table/details/rows/downloads.d.ts +0 -6
  43. package/dist/features/table/components/dashboard/table/details/rows/primitive.d.ts +0 -6
  44. package/dist/features/table/components/dashboard/table/details/rows/table.d.ts +0 -17
  45. package/dist/features/table/components/dashboard/table/filter-render-map.d.ts +0 -2
  46. package/dist/features/table/components/dashboard/table/filters/date-filter.d.ts +0 -2
  47. package/dist/features/table/components/dashboard/table/filters/status-filter.d.ts +0 -2
  48. package/dist/features/table/components/dashboard/table/no-results.d.ts +0 -2
  49. package/dist/features/table/components/dashboard/table/status-pill.d.ts +0 -8
  50. package/dist/features/table/components/dashboard/table/table-header.d.ts +0 -16
  51. package/dist/features/table/components/dashboard/table/table-input.d.ts +0 -11
  52. package/dist/features/table/components/dashboard/table/table-row/action-button.d.ts +0 -8
  53. package/dist/features/table/components/dashboard/table/table-row/table-row.d.ts +0 -32
  54. package/dist/features/table/components/dashboard/table/table-row-skeleton.d.ts +0 -6
  55. package/dist/features/table/components/dashboard/table/table.d.ts +0 -18
  56. package/dist/features/table/components/dashboard/table-dashboard.d.ts +0 -22
  57. package/dist/features/table/logic/resolvers.d.ts +0 -21
  58. package/dist/features/table/logic/transformers.d.ts +0 -35
  59. package/dist/features/table/logic/types.d.ts +0 -8
  60. package/dist/features/table/types/schemas.d.ts +0 -31
  61. package/dist/portal.d.ts +0 -38
  62. package/dist/tsconfig.tsbuildinfo +0 -1
  63. /package/{dist/features/table/components/dashboard/index.d.ts → src/features/table/components/dashboard/index.tsx} +0 -0
  64. /package/{dist/features/table/components/dashboard/table/details/index.d.ts → src/features/table/components/dashboard/table/details/index.ts} +0 -0
  65. /package/{dist/features/table/components/dashboard/table/details/rows/index.d.ts → src/features/table/components/dashboard/table/details/rows/index.ts} +0 -0
  66. /package/{dist/features/table/components/dashboard/table/index.d.ts → src/features/table/components/dashboard/table/index.ts} +0 -0
  67. /package/{dist/features/table/logic/index.d.ts → src/features/table/logic/index.ts} +0 -0
@@ -0,0 +1,94 @@
1
+ import type { Column } from "@evenicanpm/portal-types-schemas";
2
+ import { SearchInput } from "@evenicanpm/ui";
3
+ import { MenuItem, Select } from "@mui/material";
4
+ import React from "react";
5
+
6
+ interface SearchProps {
7
+ searchTerm: string;
8
+ setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
9
+ setColumn: React.Dispatch<React.SetStateAction<string>>;
10
+ selectedColumn: string;
11
+ searchCols: Column[];
12
+ }
13
+
14
+ const ColumnSelect = ({
15
+ selected,
16
+ handleChange,
17
+ searchCols,
18
+ }: {
19
+ selected: string;
20
+ handleChange: React.Dispatch<React.SetStateAction<string>>;
21
+ searchCols: Column[];
22
+ }) => {
23
+ return (
24
+ <Select
25
+ id="demo-simple-select"
26
+ value={selected}
27
+ sx={{
28
+ maxWidth: "150px",
29
+ "& .MuiOutlinedInput-notchedOutline": { border: "none" },
30
+ "& .MuiSelect-select": {
31
+ paddingTop: 0,
32
+ paddingBottom: 0,
33
+ display: "flex",
34
+ alignItems: "center",
35
+ },
36
+ "&:hover": { bgcolor: "rgba(0, 0, 0, 0.04)" }, // Subtle hover state
37
+ }}
38
+ onChange={(e) => handleChange(e.target.value)}
39
+ >
40
+ {searchCols?.map((col) => (
41
+ <MenuItem key={col?.Data} value={col?.Data}>
42
+ {col?.Label}
43
+ </MenuItem>
44
+ ))}
45
+ </Select>
46
+ );
47
+ };
48
+
49
+ const SearchField = React.memo(
50
+ ({
51
+ searchTerm,
52
+ setSearchTerm,
53
+ searchCols,
54
+ selectedColumn,
55
+ setColumn,
56
+ }: SearchProps) => {
57
+ return (
58
+ <SearchInput
59
+ sx={{
60
+ width: "100%",
61
+ pr: 0,
62
+ boderRight: "none",
63
+ "& .MuiOutlinedInput-notchedOutline": {
64
+ borderRight: "none",
65
+ borderTopRightRadius: 0,
66
+ borderBottomRightRadius: 0,
67
+ },
68
+ // Target the hover state too, otherwise the thick line reappears on hover
69
+ "&:hover .MuiOutlinedInput-notchedOutline": {
70
+ borderRight: "none",
71
+ },
72
+ // Target focus state
73
+ "&.Mui-focused .MuiOutlinedInput-notchedOutline": {
74
+ borderRight: "none",
75
+ },
76
+ }}
77
+ value={searchTerm}
78
+ onChange={(e) => {
79
+ setSearchTerm(e.target.value);
80
+ }}
81
+ placeholder={`Seach by ${selectedColumn}`}
82
+ endAdornment={
83
+ <ColumnSelect
84
+ selected={selectedColumn}
85
+ searchCols={searchCols}
86
+ handleChange={setColumn}
87
+ />
88
+ }
89
+ />
90
+ );
91
+ },
92
+ );
93
+
94
+ export { SearchField };
@@ -0,0 +1,47 @@
1
+ import { Button, CircularProgress, IconButton } from "@mui/material";
2
+ import { useMutation } from "@tanstack/react-query";
3
+ import type { UIAction } from "src/features/table/types/schemas";
4
+
5
+ interface ActionButtonProps {
6
+ docId: string;
7
+ action: UIAction;
8
+ resourceName: string;
9
+ }
10
+
11
+ export function ActionButton({
12
+ action,
13
+ resourceName,
14
+ docId,
15
+ }: ActionButtonProps) {
16
+ const { mutate, isPending } = useMutation(
17
+ action.mutation({
18
+ resourceName,
19
+ docId,
20
+ }),
21
+ );
22
+
23
+ const handleClick = (e: React.MouseEvent) => {
24
+ e.stopPropagation();
25
+ mutate({ docId });
26
+ };
27
+
28
+ if (action.icon) {
29
+ const Icon = action.icon;
30
+ return (
31
+ <IconButton size="small" onClick={handleClick} disabled={isPending}>
32
+ {isPending ? <CircularProgress size={26} /> : Icon ? <Icon /> : null}
33
+ </IconButton>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <Button
39
+ size="small"
40
+ variant="outlined"
41
+ onClick={handleClick}
42
+ disabled={isPending}
43
+ >
44
+ {isPending ? <CircularProgress size={18} /> : action.label}
45
+ </Button>
46
+ );
47
+ }
@@ -0,0 +1,144 @@
1
+ import { Action } from "@evenicanpm/portal-types-schemas";
2
+ import { FlexBox } from "@evenicanpm/ui";
3
+ import Checkbox from "@mui/material/Checkbox";
4
+ import TableCell from "@mui/material/TableCell";
5
+ import TableRow from "@mui/material/TableRow";
6
+ import { styled } from "@mui/system";
7
+ import type React from "react";
8
+ import type { UIAction } from "src/features/table/types/schemas";
9
+ import type * as Types from "../../../../types";
10
+ import { ActionButton } from "./action-button";
11
+
12
+ /**
13
+ * This is what we transorm the column data into for convenient rendering.
14
+ */
15
+ type ColumnLogic =
16
+ | {
17
+ id: string;
18
+ mask: string | undefined;
19
+ maskFn: any;
20
+ cellFn:
21
+ | ((
22
+ value: string | number | string[],
23
+ ) => React.ReactElement<
24
+ any,
25
+ string | React.JSXElementConstructor<any>
26
+ >)
27
+ | ((value: string | number) => string | number);
28
+ }[]
29
+ | undefined;
30
+
31
+ const StyledTableCell = styled(TableCell)({
32
+ height: "75px",
33
+ });
34
+
35
+ export type DocumentTableProps = {
36
+ /** The row to render */
37
+ row: Types.Queries.Row;
38
+ /** The render logic for each column ie data mask functions, cell maps*/
39
+ columnLogic: ColumnLogic;
40
+ /** */
41
+ handleClick: React.Dispatch<React.SetStateAction<string | number | null>>;
42
+ /** Actions renderer */
43
+ actions: UIAction[];
44
+ /** Does this view config have actions? */
45
+ /** The id field for the document (used for knowing what to set selected id to) */
46
+ id: string | number;
47
+ isSelected: boolean;
48
+ onToggleSelected: (id: string | number) => void;
49
+ resourceName: string;
50
+ };
51
+
52
+ /**
53
+ * A single row (document) in the table. Takes the pre-constructed column logic
54
+ * and executes all of the cell renders and data map functions. Also handles
55
+ * rendering the action icons.
56
+ */
57
+ export default function DataTableRow({
58
+ row,
59
+ columnLogic,
60
+ handleClick: handleSelectDocument,
61
+ actions,
62
+ id,
63
+ isSelected,
64
+ onToggleSelected,
65
+ resourceName,
66
+ }: DocumentTableProps) {
67
+ const visibleActions = actions.filter(
68
+ (a) => !a.isEnabled || a.isEnabled(row),
69
+ );
70
+
71
+ return (
72
+ <TableRow
73
+ hover
74
+ sx={{ cursor: "pointer" }}
75
+ onClick={() => {
76
+ // type narrowing to make sure we don't use a complex object
77
+ // as the id in state
78
+ if (typeof id === "string" || typeof id === "number") {
79
+ handleSelectDocument(id);
80
+ }
81
+ }}
82
+ >
83
+ <TableCell onClick={(e) => e.stopPropagation()}>
84
+ <Checkbox
85
+ checked={isSelected}
86
+ onChange={() => {
87
+ onToggleSelected(id);
88
+ }}
89
+ />
90
+ </TableCell>
91
+
92
+ {columnLogic?.map((col, i) => {
93
+ // --- Cell Render --- //
94
+ const rawValue = row[col.id];
95
+ const maskedValue = col.maskFn(rawValue, col?.mask);
96
+ const cellValue = col.cellFn(maskedValue);
97
+ return (
98
+ <StyledTableCell key={rawValue + String(i)}>
99
+ {cellValue}
100
+ </StyledTableCell>
101
+ );
102
+ })}
103
+ {visibleActions.length > 0 && (
104
+ <TableCell
105
+ onClick={(e) => e.stopPropagation()}
106
+ sx={{ whiteSpace: "nowrap" }}
107
+ >
108
+ <FlexBox
109
+ gap={1}
110
+ sx={{
111
+ flexDirection: "row",
112
+ flexWrap: "nowrap",
113
+ alignItems: "center",
114
+ }}
115
+ >
116
+ {actions.map((action) => {
117
+ const enabled = !action.isEnabled || action.isEnabled(row);
118
+ if (!enabled) {
119
+ return (
120
+ <span
121
+ key={action.label}
122
+ style={{
123
+ display: "inline-block",
124
+ width: action.icon ? "34px" : "42px",
125
+ height: "34px",
126
+ }}
127
+ />
128
+ );
129
+ }
130
+ return (
131
+ <ActionButton
132
+ key={action.label}
133
+ docId={String(id)}
134
+ action={action}
135
+ resourceName={resourceName}
136
+ />
137
+ );
138
+ })}
139
+ </FlexBox>
140
+ </TableCell>
141
+ )}
142
+ </TableRow>
143
+ );
144
+ }
@@ -0,0 +1,35 @@
1
+ import { Skeleton } from "@mui/material";
2
+ import TableCell from "@mui/material/TableCell";
3
+ import TableRow from "@mui/material/TableRow";
4
+ import { styled } from "@mui/system";
5
+
6
+ const StyledTableCell = styled(TableCell)({
7
+ height: "75px",
8
+ });
9
+
10
+ export type TableRowSkeletonProps = {
11
+ pageSize: number;
12
+ colSpan: number;
13
+ };
14
+
15
+ export default function TableRowSkeleton({
16
+ pageSize,
17
+ colSpan,
18
+ }: Readonly<TableRowSkeletonProps>) {
19
+ const rowKeys = Array.from({ length: pageSize }, (_, i) => `row-${i}`);
20
+ const cellKeys = Array.from({ length: colSpan }, (_, i) => `cell-${i}`);
21
+
22
+ return (
23
+ <>
24
+ {rowKeys.map((rowKey) => (
25
+ <TableRow key={rowKey}>
26
+ {cellKeys.map((cellKey) => (
27
+ <StyledTableCell key={`${rowKey}-${cellKey}`}>
28
+ <Skeleton />
29
+ </StyledTableCell>
30
+ ))}
31
+ </TableRow>
32
+ ))}
33
+ </>
34
+ );
35
+ }
@@ -0,0 +1,361 @@
1
+ import { FlexBox, ResponsiveTablePagination } from "@evenicanpm/ui";
2
+ import Table from "@mui/material/Table";
3
+ import TableBody from "@mui/material/TableBody";
4
+ import TableCell from "@mui/material/TableCell";
5
+ import TableContainer from "@mui/material/TableContainer";
6
+ import TableRow from "@mui/material/TableRow";
7
+ import { type QueryKey, useQuery } from "@tanstack/react-query";
8
+ import type React from "react";
9
+ import { useState } from "react";
10
+ import { resolvers, transformers } from "../../../logic";
11
+ import type * as Types from "../../../types";
12
+ import { BulkActionButton } from "./bulk-action-button";
13
+ import { dataMaskMap as dataMaskMapLocal } from "./data-mask-map";
14
+ import { Details } from "./details";
15
+ import { filterRenderMapFallback } from "./filter-render-map";
16
+ import { NoResultsComponent as NoResultsComponentLocal } from "./no-results";
17
+ import { StatusPill } from "./status-pill";
18
+ import TableHeader from "./table-header";
19
+ import { SearchField } from "./table-input";
20
+ import DataTableRow from "./table-row/table-row";
21
+ import TableRowSkeleton from "./table-row-skeleton";
22
+ import { useSelectRows } from "./use-select-rows";
23
+
24
+ type Id = string | number;
25
+
26
+ export type DocumentTableProps = {
27
+ resource: Types.Schemas.Resource;
28
+ pageSize: number;
29
+ setPageSize: React.Dispatch<React.SetStateAction<number>>;
30
+ viewConfigQuery?: (resourceName: string) => {
31
+ queryKey: QueryKey;
32
+ queryFn: () => Promise<Types.Queries.TableConfig>;
33
+ };
34
+ };
35
+
36
+ /**
37
+ *
38
+ * Table handles display and caches it's own data.
39
+ *
40
+ */
41
+ export default function DocumentTable({
42
+ resource,
43
+ pageSize,
44
+ setPageSize,
45
+ viewConfigQuery,
46
+ }: Readonly<DocumentTableProps>) {
47
+ const [page, setPage] = useState(0);
48
+ const [selectedDocument, setSelectedDocument] = useState<
49
+ string | number | null
50
+ >(null);
51
+ const [filters, setFilters] = useState<Record<string, string>>({});
52
+ const [searchTerm, setSearchTerm] = useState("");
53
+
54
+ const {
55
+ dataMaskMap,
56
+ cellRenderMap: columnRenderMap,
57
+ filterRenderMap,
58
+ detailsRenderMap,
59
+ idField,
60
+ label,
61
+ noResultsComponent,
62
+ statusColorMap,
63
+ statusTextMap,
64
+ viewConfig,
65
+ actions: actionRegistry,
66
+ bulkActions: bulkRegistry,
67
+ } = resource;
68
+
69
+ /**
70
+ *
71
+ * This is not simply a default map because it passes special props
72
+ * specific to status... Once we have more of these types of
73
+ * functions that return special renders that are not general
74
+ * resolvers we can create a new file.
75
+ *
76
+ * Render would be something like... If no column resolver -> try
77
+ * resolving these as opposed to just overwriting column renders
78
+ */
79
+ const resolvedFilterRenderMap = {
80
+ ...filterRenderMapFallback,
81
+ ...filterRenderMap,
82
+ };
83
+
84
+ const cellRenderMapDefaults = {
85
+ ...columnRenderMap,
86
+ Status: (value: string | number | string[]) => (
87
+ <StatusPill
88
+ status={value}
89
+ statusColorMap={statusColorMap}
90
+ statusTextMap={statusTextMap}
91
+ />
92
+ ),
93
+ };
94
+
95
+ // --- State Handlers --- //
96
+ const handleChangePageSize = (
97
+ event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
98
+ ) => {
99
+ setPageSize(Number.parseInt(event.target.value, 10));
100
+ setPage(0);
101
+ clearSelection();
102
+ };
103
+
104
+ const handleChangePage = (
105
+ _event: React.MouseEvent<HTMLButtonElement> | null,
106
+ newPage: number,
107
+ ) => {
108
+ setPage(newPage);
109
+ clearSelection();
110
+ };
111
+
112
+ const handleBack = () => {
113
+ setSelectedDocument(null);
114
+ };
115
+
116
+ const resolvedNoResults = (noResultsComponent ?? NoResultsComponentLocal)({
117
+ label,
118
+ });
119
+
120
+ const resolvedDataMaskMap = dataMaskMap ?? dataMaskMapLocal;
121
+
122
+ /**
123
+ * Data pipeline:
124
+ * FETCH -> TRANFORMER -> RESOLVER
125
+ * Keep the JSX dumb.
126
+ *
127
+ * Tested performance with console.time,
128
+ * calculation rendering around 0.02,
129
+ * no need to memoize yet.
130
+ */
131
+
132
+ // --- Fetch --- //
133
+ const { data: fetchedConfig, isPending: fetchedConfigPending } = useQuery({
134
+ ...(viewConfigQuery?.(resource.name) ?? {
135
+ queryKey: [],
136
+ queryFn: () => Promise.resolve(undefined),
137
+ }),
138
+ enabled: !!viewConfigQuery && !viewConfig,
139
+ });
140
+
141
+ const config = viewConfig ?? fetchedConfig;
142
+
143
+ const configPending = !viewConfig ? fetchedConfigPending : false;
144
+
145
+ const columns = config?.Columns ?? [];
146
+
147
+ // --- Transformer --- //
148
+ const searchableCols = transformers.getSearchableColumns(columns);
149
+ const filterReqs = transformers.getActiveFilters(columns);
150
+ const visibleCols = transformers.getVisibleColumns(columns);
151
+
152
+ // Depends on searchableCols, must be declared after transformer
153
+ const [searchColumn, setSearchColumn] = useState(
154
+ () => searchableCols?.[0]?.Data ?? "Title",
155
+ );
156
+
157
+ const { data: rows, isPending: rowsPending } = useQuery(
158
+ resource.rowsQueryFn({ page, pageSize, filters, searchColumn, searchTerm }),
159
+ );
160
+
161
+ // --- Resolver --- //
162
+ const filterLogic = filterReqs?.map((req) => ({
163
+ req,
164
+ FilterComponent: resolvers.resolveFilterUI(req, resolvedFilterRenderMap),
165
+ }));
166
+ const columnLogic = visibleCols?.map((col) => ({
167
+ id: col.Data,
168
+ mask: col?.DataMask,
169
+ maskFn: resolvers.resolveDataMask(col, resolvedDataMaskMap),
170
+ cellFn: resolvers.resolveCellUI(col, cellRenderMapDefaults),
171
+ }));
172
+
173
+ const getRowId = (row: Types.Queries.Row, field: string): Id | null => {
174
+ const v = row?.[field];
175
+ if (typeof v === "string" || typeof v === "number") return v;
176
+ return null;
177
+ };
178
+
179
+ const currentPageIds: Id[] =
180
+ rows?.data
181
+ ?.map((r) => getRowId(r, idField))
182
+ .filter((v): v is Id => v !== null) ?? [];
183
+
184
+ const {
185
+ selectedIds,
186
+ clearSelection,
187
+ toggleOne,
188
+ toggleAllOnPage,
189
+ isAllOnPageSelected,
190
+ isSomeOnPageSelected,
191
+ } = useSelectRows({ currentPageIds });
192
+
193
+ const hasTemplateSupport =
194
+ !!resource.detailsQueryFn && !!resource.detailsTemplate;
195
+
196
+ const { data: detailsData, isPending: detailsPending } = useQuery(
197
+ hasTemplateSupport && selectedDocument
198
+ ? resource.detailsQueryFn!(String(selectedDocument))
199
+ : {
200
+ queryKey: [],
201
+ queryFn: async () => undefined,
202
+ enabled: false,
203
+ },
204
+ );
205
+
206
+ // --- Render --- //
207
+ if (selectedDocument) {
208
+ const rowSnapshot =
209
+ rows?.data.find((doc) => doc?.[idField] === selectedDocument) ?? {};
210
+
211
+ const finalDetailsData =
212
+ hasTemplateSupport && detailsData ? detailsData : rowSnapshot;
213
+
214
+ return (
215
+ <Details
216
+ documentTypeLabel={label}
217
+ id={selectedDocument}
218
+ documentData={finalDetailsData}
219
+ handleBack={handleBack}
220
+ columns={columns}
221
+ dataMaskMap={resolvedDataMaskMap}
222
+ statusColorMap={statusColorMap}
223
+ statusTextMap={statusTextMap}
224
+ detailsRenderMap={detailsRenderMap}
225
+ template={hasTemplateSupport ? resource.detailsTemplate : undefined}
226
+ loading={hasTemplateSupport ? detailsPending : false}
227
+ />
228
+ );
229
+ }
230
+
231
+ const resolvedActions = resolvers.resolveActions(
232
+ config?.Actions,
233
+ actionRegistry,
234
+ );
235
+ const resolvedBulkActions = resolvers.resolveBulkActions(
236
+ config?.BulkActions,
237
+ bulkRegistry,
238
+ );
239
+
240
+ const hasActions =
241
+ configPending ||
242
+ (resolvedActions.length > 0 &&
243
+ rows?.data?.some((row) =>
244
+ resolvedActions.some((a) => !a.isEnabled || a.isEnabled(row)),
245
+ )) ||
246
+ false;
247
+ const hasBulkActions = resolvedBulkActions.length > 0;
248
+
249
+ const baseCols = (visibleCols?.length ?? 0) + 1;
250
+ const colSpan = hasActions ? baseCols + 1 : baseCols;
251
+ const noResults = !rowsPending && !configPending && rows?.data?.length === 0;
252
+
253
+ return (
254
+ <>
255
+ <h1>{label}</h1>
256
+ <FlexBox
257
+ justifyContent={"flex-end"}
258
+ width="100%"
259
+ height="fit-content"
260
+ gap={2}
261
+ pt={2}
262
+ >
263
+ <SearchField
264
+ searchTerm={searchTerm}
265
+ setSearchTerm={setSearchTerm}
266
+ setColumn={setSearchColumn}
267
+ searchCols={searchableCols || []}
268
+ selectedColumn={searchColumn}
269
+ />
270
+ {filterLogic?.map((filter) => (
271
+ <filter.FilterComponent
272
+ key={filter.req?.filterName}
273
+ value={filters}
274
+ setValue={setFilters}
275
+ resetPagination={() => {
276
+ setPage(0);
277
+ clearSelection();
278
+ }}
279
+ {...filter.req}
280
+ />
281
+ ))}
282
+
283
+ {hasBulkActions &&
284
+ resolvedBulkActions.map((action) => {
285
+ const selectedRows =
286
+ rows?.data?.filter((row) => {
287
+ const id = getRowId(row, idField);
288
+ return id !== null && selectedIds.has(id);
289
+ }) ?? [];
290
+
291
+ return (
292
+ <BulkActionButton
293
+ key={action.label}
294
+ action={action}
295
+ resourceName={resource.name}
296
+ selectedIds={Array.from(selectedIds)}
297
+ selectedRows={selectedRows}
298
+ clearSelection={clearSelection}
299
+ />
300
+ );
301
+ })}
302
+ </FlexBox>
303
+ <TableContainer>
304
+ <Table aria-label="simple table" sx={{ tableLayout: { md: "fixed" } }}>
305
+ <TableHeader
306
+ columns={visibleCols}
307
+ loading={configPending}
308
+ hasActions={hasActions}
309
+ selectAllChecked={isAllOnPageSelected}
310
+ selectAllIndeterminate={isSomeOnPageSelected}
311
+ onToggleAll={toggleAllOnPage}
312
+ />
313
+
314
+ <TableBody>
315
+ {noResults ? (
316
+ <TableRow>
317
+ <TableCell
318
+ colSpan={colSpan || 1}
319
+ align="center"
320
+ sx={{ py: 6, color: "text.secondary" }}
321
+ >
322
+ {resolvedNoResults}
323
+ </TableCell>
324
+ </TableRow>
325
+ ) : rowsPending ? (
326
+ <TableRowSkeleton pageSize={pageSize} colSpan={colSpan} />
327
+ ) : (
328
+ rows?.data?.map((row) => {
329
+ const id = getRowId(row, idField);
330
+ if (id === null) return null;
331
+
332
+ return (
333
+ <DataTableRow
334
+ key={id}
335
+ columnLogic={columnLogic}
336
+ row={row}
337
+ handleClick={setSelectedDocument}
338
+ id={id}
339
+ actions={resolvedActions}
340
+ isSelected={selectedIds.has(id)}
341
+ onToggleSelected={(docId) => toggleOne(docId)}
342
+ resourceName={resource.name}
343
+ />
344
+ );
345
+ })
346
+ )}
347
+ </TableBody>
348
+ </Table>
349
+ </TableContainer>
350
+ <ResponsiveTablePagination
351
+ count={rows?.items || 0}
352
+ page={page}
353
+ component="div"
354
+ rowsPerPage={pageSize}
355
+ rowsPerPageOptions={[5, 10]}
356
+ onPageChange={handleChangePage}
357
+ onRowsPerPageChange={handleChangePageSize}
358
+ />
359
+ </>
360
+ );
361
+ }