@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.
- package/package.json +6 -5
- package/src/auth/auth-context.tsx +92 -0
- package/src/features/table/components/dashboard/table/bulk-action-button.tsx +77 -0
- package/src/features/table/components/dashboard/table/data-mask-map.ts +26 -0
- package/src/features/table/components/dashboard/table/details/details.tsx +185 -0
- package/src/features/table/components/dashboard/table/details/label.tsx +26 -0
- package/src/features/table/components/dashboard/table/details/rows/downloads.tsx +25 -0
- package/src/features/table/components/dashboard/table/details/rows/primitive.tsx +25 -0
- package/src/features/table/components/dashboard/table/details/rows/table.tsx +73 -0
- package/{dist/features/table/components/dashboard/table/details/styles.d.ts → src/features/table/components/dashboard/table/details/styles.ts} +10 -2
- package/src/features/table/components/dashboard/table/details/templates/invoice-details.tsx +407 -0
- package/src/features/table/components/dashboard/table/details/templates/template-registry.ts +36 -0
- package/src/features/table/components/dashboard/table/filter-render-map.tsx +10 -0
- package/src/features/table/components/dashboard/table/filters/date-filter.tsx +31 -0
- package/src/features/table/components/dashboard/table/filters/status-filter.tsx +36 -0
- package/src/features/table/components/dashboard/table/no-results.tsx +26 -0
- package/src/features/table/components/dashboard/table/status-pill.tsx +18 -0
- package/src/features/table/components/dashboard/table/table-header.tsx +98 -0
- package/src/features/table/components/dashboard/table/table-input.tsx +94 -0
- package/src/features/table/components/dashboard/table/table-row/action-button.tsx +47 -0
- package/src/features/table/components/dashboard/table/table-row/table-row.tsx +144 -0
- package/src/features/table/components/dashboard/table/table-row-skeleton.tsx +35 -0
- package/src/features/table/components/dashboard/table/table.tsx +361 -0
- package/src/features/table/components/dashboard/table/use-select-rows.ts +53 -0
- package/src/features/table/components/dashboard/table-dashboard.tsx +71 -0
- package/{dist/features/table/index.d.ts → src/features/table/index.ts} +7 -1
- package/src/features/table/logic/resolvers.ts +111 -0
- package/src/features/table/logic/transformers.ts +56 -0
- package/src/features/table/logic/types.ts +8 -0
- package/{dist/features/table/types/handlers.d.ts → src/features/table/types/handlers.ts} +30 -12
- package/{dist/features/table/types/index.d.ts → src/features/table/types/index.ts} +1 -0
- package/{dist/features/table/types/queries.d.ts → src/features/table/types/queries.ts} +10 -3
- package/src/features/table/types/schemas.ts +89 -0
- package/{dist/index.d.ts → src/index.ts} +2 -1
- package/src/portal.tsx +68 -0
- package/dist/auth/auth-context.d.ts +0 -36
- package/dist/features/settings/index.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/actions.d.ts +0 -14
- package/dist/features/table/components/dashboard/table/data-mask-map.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/details/details.d.ts +0 -27
- package/dist/features/table/components/dashboard/table/details/label.d.ts +0 -5
- package/dist/features/table/components/dashboard/table/details/rows/downloads.d.ts +0 -6
- package/dist/features/table/components/dashboard/table/details/rows/primitive.d.ts +0 -6
- package/dist/features/table/components/dashboard/table/details/rows/table.d.ts +0 -17
- package/dist/features/table/components/dashboard/table/filter-render-map.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/filters/date-filter.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/filters/status-filter.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/no-results.d.ts +0 -2
- package/dist/features/table/components/dashboard/table/status-pill.d.ts +0 -8
- package/dist/features/table/components/dashboard/table/table-header.d.ts +0 -16
- package/dist/features/table/components/dashboard/table/table-input.d.ts +0 -11
- package/dist/features/table/components/dashboard/table/table-row/action-button.d.ts +0 -8
- package/dist/features/table/components/dashboard/table/table-row/table-row.d.ts +0 -32
- package/dist/features/table/components/dashboard/table/table-row-skeleton.d.ts +0 -6
- package/dist/features/table/components/dashboard/table/table.d.ts +0 -18
- package/dist/features/table/components/dashboard/table-dashboard.d.ts +0 -22
- package/dist/features/table/logic/resolvers.d.ts +0 -21
- package/dist/features/table/logic/transformers.d.ts +0 -35
- package/dist/features/table/logic/types.d.ts +0 -8
- package/dist/features/table/types/schemas.d.ts +0 -31
- package/dist/portal.d.ts +0 -38
- package/dist/tsconfig.tsbuildinfo +0 -1
- /package/{dist/features/table/components/dashboard/index.d.ts → src/features/table/components/dashboard/index.tsx} +0 -0
- /package/{dist/features/table/components/dashboard/table/details/index.d.ts → src/features/table/components/dashboard/table/details/index.ts} +0 -0
- /package/{dist/features/table/components/dashboard/table/details/rows/index.d.ts → src/features/table/components/dashboard/table/details/rows/index.ts} +0 -0
- /package/{dist/features/table/components/dashboard/table/index.d.ts → src/features/table/components/dashboard/table/index.ts} +0 -0
- /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
|
+
}
|