@evenicanpm/portal-table-ui 1.6.0 → 1.8.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 +4 -4
- package/src/auth/auth-context.tsx +15 -9
- package/src/features/table/components/dashboard/table/bulk-action-button.tsx +76 -0
- package/src/features/table/components/dashboard/table/data-mask-map.ts +1 -1
- package/src/features/table/components/dashboard/table/details/details.tsx +63 -35
- package/src/features/table/components/dashboard/table/details/rows/downloads.tsx +0 -1
- package/src/features/table/components/dashboard/table/details/rows/primitive.tsx +0 -1
- package/src/features/table/components/dashboard/table/details/rows/table.tsx +0 -1
- package/src/features/table/components/dashboard/table/details/templates/invoice-details.tsx +440 -0
- package/src/features/table/components/dashboard/table/details/templates/template-registry.ts +39 -0
- package/src/features/table/components/dashboard/table/filter-render-map.tsx +2 -1
- package/src/features/table/components/dashboard/table/filters/date-filter.tsx +5 -5
- package/src/features/table/components/dashboard/table/filters/status-filter.tsx +2 -2
- package/src/features/table/components/dashboard/table/no-results.tsx +1 -1
- package/src/features/table/components/dashboard/table/table-header.tsx +41 -5
- package/src/features/table/components/dashboard/table/table-row/action-button.tsx +32 -24
- package/src/features/table/components/dashboard/table/table-row/table-row.tsx +60 -13
- package/src/features/table/components/dashboard/table/table-row-skeleton.tsx +0 -1
- package/src/features/table/components/dashboard/table/table.tsx +143 -30
- package/src/features/table/components/dashboard/table/use-select-rows.ts +53 -0
- package/src/features/table/components/dashboard/table-dashboard.tsx +2 -2
- package/src/features/table/logic/resolvers.ts +43 -0
- package/src/features/table/types/schemas.ts +39 -0
- package/src/index.ts +1 -2
- package/dist/auth/auth-context.d.ts +0 -36
- package/dist/features/settings/index.d.ts +0 -2
- package/dist/features/table/components/dashboard/index.d.ts +0 -1
- 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/index.d.ts +0 -1
- 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/index.d.ts +0 -3
- 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/details/styles.d.ts +0 -10
- 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/index.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/index.d.ts +0 -8
- package/dist/features/table/logic/index.d.ts +0 -23
- 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/handlers.d.ts +0 -53
- package/dist/features/table/types/index.d.ts +0 -69
- package/dist/features/table/types/queries.d.ts +0 -27
- package/dist/features/table/types/schemas.d.ts +0 -31
- package/dist/index.d.ts +0 -63
- package/dist/portal.d.ts +0 -38
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/features/table/components/dashboard/table/actions.ts +0 -59
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FlexBox } from "@evenicanpm/ui";
|
|
3
|
-
import { IconButton, CircularProgress } from "@mui/material";
|
|
4
|
-
import { Action } from "@evenicanpm/portal-types-schemas";
|
|
1
|
+
import { Button, CircularProgress, IconButton } from "@mui/material";
|
|
5
2
|
import { useMutation } from "@tanstack/react-query";
|
|
3
|
+
import type { UIAction } from "src/features/table/types/schemas";
|
|
6
4
|
|
|
7
5
|
interface ActionButtonProps {
|
|
8
6
|
docId: string;
|
|
9
|
-
action:
|
|
7
|
+
action: UIAction;
|
|
10
8
|
resourceName: string;
|
|
11
9
|
}
|
|
12
10
|
|
|
@@ -14,26 +12,36 @@ export function ActionButton({
|
|
|
14
12
|
action,
|
|
15
13
|
resourceName,
|
|
16
14
|
docId,
|
|
17
|
-
}:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
36
|
|
|
24
37
|
return (
|
|
25
|
-
<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
>
|
|
34
|
-
{isPending ? <CircularProgress size={26} /> : <Icon />}
|
|
35
|
-
</IconButton>
|
|
36
|
-
</span>
|
|
37
|
-
</FlexBox>
|
|
38
|
+
<Button
|
|
39
|
+
size="small"
|
|
40
|
+
variant="outlined"
|
|
41
|
+
onClick={handleClick}
|
|
42
|
+
disabled={isPending}
|
|
43
|
+
>
|
|
44
|
+
{isPending ? <CircularProgress size={18} /> : action.label}
|
|
45
|
+
</Button>
|
|
38
46
|
);
|
|
39
47
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { FlexBox } from "@evenicanpm/ui";
|
|
2
|
+
import Checkbox from "@mui/material/Checkbox";
|
|
1
3
|
import TableCell from "@mui/material/TableCell";
|
|
2
4
|
import TableRow from "@mui/material/TableRow";
|
|
5
|
+
import { styled } from "@mui/system";
|
|
3
6
|
import type React from "react";
|
|
7
|
+
import type { UIAction } from "src/features/table/types/schemas";
|
|
4
8
|
import type * as Types from "../../../../types";
|
|
5
|
-
import { styled } from "@mui/system";
|
|
6
|
-
import { Action } from "@evenicanpm/portal-types-schemas";
|
|
7
|
-
import { FlexBox } from "@evenicanpm/ui";
|
|
8
9
|
import { ActionButton } from "./action-button";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -38,10 +39,13 @@ export type DocumentTableProps = {
|
|
|
38
39
|
/** */
|
|
39
40
|
handleClick: React.Dispatch<React.SetStateAction<string | number | null>>;
|
|
40
41
|
/** Actions renderer */
|
|
41
|
-
actions:
|
|
42
|
+
actions: UIAction[];
|
|
42
43
|
/** Does this view config have actions? */
|
|
43
44
|
/** The id field for the document (used for knowing what to set selected id to) */
|
|
44
|
-
|
|
45
|
+
id: string | number;
|
|
46
|
+
isSelected: boolean;
|
|
47
|
+
onToggleSelected: (id: string | number) => void;
|
|
48
|
+
resourceName: string;
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
/**
|
|
@@ -54,9 +58,15 @@ export default function DataTableRow({
|
|
|
54
58
|
columnLogic,
|
|
55
59
|
handleClick: handleSelectDocument,
|
|
56
60
|
actions,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
id,
|
|
62
|
+
isSelected,
|
|
63
|
+
onToggleSelected,
|
|
64
|
+
resourceName,
|
|
65
|
+
}: DocumentTableProps) {
|
|
66
|
+
const visibleActions = actions.filter(
|
|
67
|
+
(a) => !a.isEnabled || a.isEnabled(row),
|
|
68
|
+
);
|
|
69
|
+
|
|
60
70
|
return (
|
|
61
71
|
<TableRow
|
|
62
72
|
hover
|
|
@@ -69,6 +79,15 @@ export default function DataTableRow({
|
|
|
69
79
|
}
|
|
70
80
|
}}
|
|
71
81
|
>
|
|
82
|
+
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
83
|
+
<Checkbox
|
|
84
|
+
checked={isSelected}
|
|
85
|
+
onChange={() => {
|
|
86
|
+
onToggleSelected(id);
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
</TableCell>
|
|
90
|
+
|
|
72
91
|
{columnLogic?.map((col, i) => {
|
|
73
92
|
// --- Cell Render --- //
|
|
74
93
|
const rawValue = row[col.id];
|
|
@@ -80,17 +99,45 @@ export default function DataTableRow({
|
|
|
80
99
|
</StyledTableCell>
|
|
81
100
|
);
|
|
82
101
|
})}
|
|
83
|
-
{
|
|
102
|
+
{visibleActions.length > 0 && (
|
|
84
103
|
<TableCell
|
|
85
|
-
key={action.Name}
|
|
86
104
|
onClick={(e) => e.stopPropagation()}
|
|
87
105
|
sx={{ whiteSpace: "nowrap" }}
|
|
88
106
|
>
|
|
89
|
-
<FlexBox
|
|
90
|
-
|
|
107
|
+
<FlexBox
|
|
108
|
+
gap={1}
|
|
109
|
+
sx={{
|
|
110
|
+
flexDirection: "row",
|
|
111
|
+
flexWrap: "nowrap",
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{actions.map((action) => {
|
|
116
|
+
const enabled = !action.isEnabled || action.isEnabled(row);
|
|
117
|
+
if (!enabled) {
|
|
118
|
+
return (
|
|
119
|
+
<span
|
|
120
|
+
key={action.label}
|
|
121
|
+
style={{
|
|
122
|
+
display: "inline-block",
|
|
123
|
+
width: action.icon ? "34px" : "42px",
|
|
124
|
+
height: "34px",
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
<ActionButton
|
|
131
|
+
key={action.label}
|
|
132
|
+
docId={String(id)}
|
|
133
|
+
action={action}
|
|
134
|
+
resourceName={resourceName}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
91
138
|
</FlexBox>
|
|
92
139
|
</TableCell>
|
|
93
|
-
)
|
|
140
|
+
)}
|
|
94
141
|
</TableRow>
|
|
95
142
|
);
|
|
96
143
|
}
|
|
@@ -9,6 +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 { BulkActionButton } from "./bulk-action-button";
|
|
12
13
|
import { dataMaskMap as dataMaskMapLocal } from "./data-mask-map";
|
|
13
14
|
import { Details } from "./details";
|
|
14
15
|
import { filterRenderMapFallback } from "./filter-render-map";
|
|
@@ -18,12 +19,15 @@ import TableHeader from "./table-header";
|
|
|
18
19
|
import { SearchField } from "./table-input";
|
|
19
20
|
import DataTableRow from "./table-row/table-row";
|
|
20
21
|
import TableRowSkeleton from "./table-row-skeleton";
|
|
22
|
+
import { useSelectRows } from "./use-select-rows";
|
|
23
|
+
|
|
24
|
+
type Id = string | number;
|
|
21
25
|
|
|
22
26
|
export type DocumentTableProps = {
|
|
23
27
|
resource: Types.Schemas.Resource;
|
|
24
28
|
pageSize: number;
|
|
25
29
|
setPageSize: React.Dispatch<React.SetStateAction<number>>;
|
|
26
|
-
viewConfigQuery
|
|
30
|
+
viewConfigQuery?: (resourceName: string) => {
|
|
27
31
|
queryKey: QueryKey;
|
|
28
32
|
queryFn: () => Promise<Types.Queries.TableConfig>;
|
|
29
33
|
};
|
|
@@ -46,7 +50,6 @@ export default function DocumentTable({
|
|
|
46
50
|
>(null);
|
|
47
51
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
48
52
|
const [searchTerm, setSearchTerm] = useState("");
|
|
49
|
-
const [searchColumn, setSearchColumn] = useState("Title");
|
|
50
53
|
|
|
51
54
|
const {
|
|
52
55
|
dataMaskMap,
|
|
@@ -58,6 +61,9 @@ export default function DocumentTable({
|
|
|
58
61
|
noResultsComponent,
|
|
59
62
|
statusColorMap,
|
|
60
63
|
statusTextMap,
|
|
64
|
+
viewConfig,
|
|
65
|
+
actions: actionRegistry,
|
|
66
|
+
bulkActions: bulkRegistry,
|
|
61
67
|
} = resource;
|
|
62
68
|
|
|
63
69
|
/**
|
|
@@ -72,7 +78,7 @@ export default function DocumentTable({
|
|
|
72
78
|
*/
|
|
73
79
|
const resolvedFilterRenderMap = {
|
|
74
80
|
...filterRenderMapFallback,
|
|
75
|
-
...
|
|
81
|
+
...filterRenderMap,
|
|
76
82
|
};
|
|
77
83
|
|
|
78
84
|
const cellRenderMapDefaults = {
|
|
@@ -92,6 +98,7 @@ export default function DocumentTable({
|
|
|
92
98
|
) => {
|
|
93
99
|
setPageSize(Number.parseInt(event.target.value, 10));
|
|
94
100
|
setPage(0);
|
|
101
|
+
clearSelection();
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
const handleChangePage = (
|
|
@@ -99,6 +106,7 @@ export default function DocumentTable({
|
|
|
99
106
|
newPage: number,
|
|
100
107
|
) => {
|
|
101
108
|
setPage(newPage);
|
|
109
|
+
clearSelection();
|
|
102
110
|
};
|
|
103
111
|
|
|
104
112
|
const handleBack = () => {
|
|
@@ -122,22 +130,34 @@ export default function DocumentTable({
|
|
|
122
130
|
*/
|
|
123
131
|
|
|
124
132
|
// --- Fetch --- //
|
|
125
|
-
const { data:
|
|
126
|
-
viewConfigQuery(resource.name)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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;
|
|
131
144
|
|
|
132
145
|
const columns = config?.Columns ?? [];
|
|
133
|
-
const actions = config?.Actions ?? [];
|
|
134
|
-
const hasActions = actions.length > 0;
|
|
135
146
|
|
|
136
147
|
// --- Transformer --- //
|
|
137
148
|
const searchableCols = transformers.getSearchableColumns(columns);
|
|
138
149
|
const filterReqs = transformers.getActiveFilters(columns);
|
|
139
150
|
const visibleCols = transformers.getVisibleColumns(columns);
|
|
140
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
|
+
|
|
141
161
|
// --- Resolver --- //
|
|
142
162
|
const filterLogic = filterReqs?.map((req) => ({
|
|
143
163
|
req,
|
|
@@ -150,28 +170,86 @@ export default function DocumentTable({
|
|
|
150
170
|
cellFn: resolvers.resolveCellUI(col, cellRenderMapDefaults),
|
|
151
171
|
}));
|
|
152
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
|
+
const resolvedActions = resolvers.resolveActions(
|
|
207
|
+
config?.Actions,
|
|
208
|
+
actionRegistry,
|
|
209
|
+
);
|
|
210
|
+
const resolvedBulkActions = resolvers.resolveBulkActions(
|
|
211
|
+
config?.BulkActions,
|
|
212
|
+
bulkRegistry,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const hasActions =
|
|
216
|
+
configPending ||
|
|
217
|
+
(resolvedActions.length > 0 &&
|
|
218
|
+
rows?.data?.some((row) =>
|
|
219
|
+
resolvedActions.some((a) => !a.isEnabled || a.isEnabled(row)),
|
|
220
|
+
)) ||
|
|
221
|
+
false;
|
|
222
|
+
const hasBulkActions = resolvedBulkActions.length > 0;
|
|
223
|
+
|
|
153
224
|
// --- Render --- //
|
|
154
225
|
if (selectedDocument) {
|
|
155
|
-
const
|
|
156
|
-
rows?.data.find((doc) => doc?.[idField] === selectedDocument)
|
|
157
|
-
|
|
226
|
+
const rowSnapshot =
|
|
227
|
+
rows?.data.find((doc) => doc?.[idField] === selectedDocument) ?? {};
|
|
228
|
+
|
|
229
|
+
const finalDetailsData =
|
|
230
|
+
hasTemplateSupport && detailsData ? detailsData : rowSnapshot;
|
|
231
|
+
|
|
158
232
|
return (
|
|
159
233
|
<Details
|
|
160
234
|
documentTypeLabel={label}
|
|
161
235
|
id={selectedDocument}
|
|
162
|
-
documentData={
|
|
236
|
+
documentData={finalDetailsData}
|
|
163
237
|
handleBack={handleBack}
|
|
164
|
-
columns={columns
|
|
238
|
+
columns={columns}
|
|
165
239
|
dataMaskMap={resolvedDataMaskMap}
|
|
166
240
|
statusColorMap={statusColorMap}
|
|
167
241
|
statusTextMap={statusTextMap}
|
|
168
242
|
detailsRenderMap={detailsRenderMap}
|
|
243
|
+
template={hasTemplateSupport ? resource.detailsTemplate : undefined}
|
|
244
|
+
loading={hasTemplateSupport ? detailsPending : false}
|
|
245
|
+
actions={resolvedActions}
|
|
246
|
+
resourceName={resource.name}
|
|
169
247
|
/>
|
|
170
248
|
);
|
|
171
249
|
}
|
|
172
250
|
|
|
173
|
-
|
|
174
|
-
const colSpan =
|
|
251
|
+
const baseCols = (visibleCols?.length ?? 0) + 1;
|
|
252
|
+
const colSpan = hasActions ? baseCols + 1 : baseCols;
|
|
175
253
|
const noResults = !rowsPending && !configPending && rows?.data?.length === 0;
|
|
176
254
|
|
|
177
255
|
return (
|
|
@@ -180,7 +258,7 @@ export default function DocumentTable({
|
|
|
180
258
|
<FlexBox
|
|
181
259
|
justifyContent={"flex-end"}
|
|
182
260
|
width="100%"
|
|
183
|
-
height=
|
|
261
|
+
height="fit-content"
|
|
184
262
|
gap={2}
|
|
185
263
|
pt={2}
|
|
186
264
|
>
|
|
@@ -196,10 +274,33 @@ export default function DocumentTable({
|
|
|
196
274
|
key={filter.req?.filterName}
|
|
197
275
|
value={filters}
|
|
198
276
|
setValue={setFilters}
|
|
199
|
-
resetPagination={() =>
|
|
277
|
+
resetPagination={() => {
|
|
278
|
+
setPage(0);
|
|
279
|
+
clearSelection();
|
|
280
|
+
}}
|
|
200
281
|
{...filter.req}
|
|
201
282
|
/>
|
|
202
283
|
))}
|
|
284
|
+
|
|
285
|
+
{hasBulkActions &&
|
|
286
|
+
resolvedBulkActions.map((action) => {
|
|
287
|
+
const selectedRows =
|
|
288
|
+
rows?.data?.filter((row) => {
|
|
289
|
+
const id = getRowId(row, idField);
|
|
290
|
+
return id !== null && selectedIds.has(id);
|
|
291
|
+
}) ?? [];
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<BulkActionButton
|
|
295
|
+
key={action.label}
|
|
296
|
+
action={action}
|
|
297
|
+
resourceName={resource.name}
|
|
298
|
+
selectedIds={Array.from(selectedIds)}
|
|
299
|
+
selectedRows={selectedRows}
|
|
300
|
+
clearSelection={clearSelection}
|
|
301
|
+
/>
|
|
302
|
+
);
|
|
303
|
+
})}
|
|
203
304
|
</FlexBox>
|
|
204
305
|
<TableContainer>
|
|
205
306
|
<Table aria-label="simple table" sx={{ tableLayout: { md: "fixed" } }}>
|
|
@@ -207,7 +308,11 @@ export default function DocumentTable({
|
|
|
207
308
|
columns={visibleCols}
|
|
208
309
|
loading={configPending}
|
|
209
310
|
hasActions={hasActions}
|
|
311
|
+
selectAllChecked={isAllOnPageSelected}
|
|
312
|
+
selectAllIndeterminate={isSomeOnPageSelected}
|
|
313
|
+
onToggleAll={toggleAllOnPage}
|
|
210
314
|
/>
|
|
315
|
+
|
|
211
316
|
<TableBody>
|
|
212
317
|
{noResults ? (
|
|
213
318
|
<TableRow>
|
|
@@ -222,16 +327,24 @@ export default function DocumentTable({
|
|
|
222
327
|
) : rowsPending ? (
|
|
223
328
|
<TableRowSkeleton pageSize={pageSize} colSpan={colSpan} />
|
|
224
329
|
) : (
|
|
225
|
-
rows?.data?.map((row
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
330
|
+
rows?.data?.map((row) => {
|
|
331
|
+
const id = getRowId(row, idField);
|
|
332
|
+
if (id === null) return null;
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
<DataTableRow
|
|
336
|
+
key={id}
|
|
337
|
+
columnLogic={columnLogic}
|
|
338
|
+
row={row}
|
|
339
|
+
handleClick={setSelectedDocument}
|
|
340
|
+
id={id}
|
|
341
|
+
actions={resolvedActions}
|
|
342
|
+
isSelected={selectedIds.has(id)}
|
|
343
|
+
onToggleSelected={(docId) => toggleOne(docId)}
|
|
344
|
+
resourceName={resource.name}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
})
|
|
235
348
|
)}
|
|
236
349
|
</TableBody>
|
|
237
350
|
</Table>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
type Id = string | number;
|
|
4
|
+
|
|
5
|
+
type Input = {
|
|
6
|
+
currentPageIds: Id[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const useSelectRows = (input: Input) => {
|
|
10
|
+
const { currentPageIds } = input;
|
|
11
|
+
|
|
12
|
+
const [selectedIds, setSelectedIds] = useState<Set<Id>>(new Set());
|
|
13
|
+
|
|
14
|
+
const clearSelection = () => setSelectedIds(new Set());
|
|
15
|
+
|
|
16
|
+
const toggleOne = (id: Id) => {
|
|
17
|
+
setSelectedIds((prev) => {
|
|
18
|
+
const next = new Set(prev);
|
|
19
|
+
if (next.has(id)) next.delete(id);
|
|
20
|
+
else next.add(id);
|
|
21
|
+
return next;
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const isAllOnPageSelected =
|
|
26
|
+
currentPageIds.length > 0 &&
|
|
27
|
+
currentPageIds.every((id) => selectedIds.has(id));
|
|
28
|
+
|
|
29
|
+
const isSomeOnPageSelected =
|
|
30
|
+
currentPageIds.some((id) => selectedIds.has(id)) && !isAllOnPageSelected;
|
|
31
|
+
|
|
32
|
+
const toggleAllOnPage = (checked: boolean) => {
|
|
33
|
+
setSelectedIds((prev) => {
|
|
34
|
+
const next = new Set(prev);
|
|
35
|
+
if (checked) {
|
|
36
|
+
for (const id of currentPageIds) next.add(id);
|
|
37
|
+
} else {
|
|
38
|
+
for (const id of currentPageIds) next.delete(id);
|
|
39
|
+
}
|
|
40
|
+
return next;
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
selectedIds,
|
|
46
|
+
setSelectedIds,
|
|
47
|
+
clearSelection,
|
|
48
|
+
toggleOne,
|
|
49
|
+
toggleAllOnPage,
|
|
50
|
+
isAllOnPageSelected,
|
|
51
|
+
isSomeOnPageSelected,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -3,10 +3,10 @@ import TabList from "@mui/lab/TabList";
|
|
|
3
3
|
import TabPanel from "@mui/lab/TabPanel";
|
|
4
4
|
import Box from "@mui/material/Box";
|
|
5
5
|
import Tab from "@mui/material/Tab";
|
|
6
|
+
import type { QueryKey } from "@tanstack/react-query";
|
|
6
7
|
import { type SyntheticEvent, useState } from "react";
|
|
7
8
|
import type * as Types from "../../types";
|
|
8
9
|
import { Table } from "./table";
|
|
9
|
-
import { QueryKey } from "@tanstack/react-query";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* The main props for top-level table entry-point
|
|
@@ -14,7 +14,7 @@ import { QueryKey } from "@tanstack/react-query";
|
|
|
14
14
|
*
|
|
15
15
|
*/
|
|
16
16
|
export interface TableProps {
|
|
17
|
-
viewConfigQuery
|
|
17
|
+
viewConfigQuery?: (resourceName: string) => {
|
|
18
18
|
queryKey: QueryKey;
|
|
19
19
|
queryFn: () => Promise<Types.Queries.TableConfig>;
|
|
20
20
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { method, multi } from "@arrows/multimethod";
|
|
2
2
|
import type { Column } from "@evenicanpm/portal-types-schemas";
|
|
3
3
|
import type * as Types from "../types";
|
|
4
|
+
import type { TableConfig } from "../types/queries";
|
|
5
|
+
import type { UIAction, UIBulkAction } from "../types/schemas";
|
|
4
6
|
import type { FilterRequest } from "./types";
|
|
5
7
|
|
|
6
8
|
/** MULTIMETHOD FOR FILTERS
|
|
@@ -66,3 +68,44 @@ export const resolveCellUI = (
|
|
|
66
68
|
|
|
67
69
|
return (value: string | number) => value;
|
|
68
70
|
};
|
|
71
|
+
|
|
72
|
+
export function resolveActions(
|
|
73
|
+
configActions: TableConfig["Actions"] | undefined,
|
|
74
|
+
registry: Record<string, UIAction> | undefined,
|
|
75
|
+
): UIAction[] {
|
|
76
|
+
if (!configActions || !registry) return [];
|
|
77
|
+
|
|
78
|
+
return configActions
|
|
79
|
+
.filter((a) => a.IsEnabled)
|
|
80
|
+
.flatMap((a) => {
|
|
81
|
+
const action = registry[a.Type];
|
|
82
|
+
if (!action) {
|
|
83
|
+
console.warn(`Unknown action type: ${a.Type}`);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
...action,
|
|
89
|
+
label: a.Name,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resolveBulkActions(
|
|
96
|
+
configBulkActions: TableConfig["BulkActions"] | undefined,
|
|
97
|
+
registry: Record<string, UIBulkAction> | undefined,
|
|
98
|
+
): UIBulkAction[] {
|
|
99
|
+
if (!configBulkActions || !registry) return [];
|
|
100
|
+
|
|
101
|
+
return configBulkActions
|
|
102
|
+
.filter((a) => a.IsEnabled)
|
|
103
|
+
.flatMap((a) => {
|
|
104
|
+
const action = registry[a.Type] as UIBulkAction;
|
|
105
|
+
if (!action) {
|
|
106
|
+
console.warn(`Unknown bulk action type: ${a.Type}`);
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
return [{ ...action, label: a.Name }];
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { TableViewParams } from "@evenicanpm/portal-types-schemas";
|
|
2
|
+
import type { SvgIconComponent } from "@mui/icons-material";
|
|
3
|
+
import type { UseMutationOptions } from "@tanstack/react-query";
|
|
4
|
+
import type { Table } from "..";
|
|
5
|
+
import type { DetailsTemplateKey } from "../components/dashboard/table/details/templates/template-registry";
|
|
2
6
|
import type {
|
|
3
7
|
CellRenderMap,
|
|
4
8
|
DataMaskMap,
|
|
@@ -21,6 +25,27 @@ export type StatusColor = "success" | "primary" | "warning" | "error";
|
|
|
21
25
|
|
|
22
26
|
export type StatusColorMap = Record<number | string, StatusColor>;
|
|
23
27
|
|
|
28
|
+
type BaseAction = {
|
|
29
|
+
label?: string;
|
|
30
|
+
icon?: SvgIconComponent;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type UIAction = BaseAction & {
|
|
34
|
+
isEnabled?: (row: Record<string, unknown>) => boolean;
|
|
35
|
+
mutation: (args: {
|
|
36
|
+
resourceName: string;
|
|
37
|
+
docId: string | number;
|
|
38
|
+
}) => UseMutationOptions<any, any, any, any>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type UIBulkAction = BaseAction & {
|
|
42
|
+
isAllowed?: (rows: Record<string, unknown>[]) => boolean;
|
|
43
|
+
mutation: (args: {
|
|
44
|
+
resourceName: string;
|
|
45
|
+
selectedIds: (string | number)[];
|
|
46
|
+
}) => UseMutationOptions<any, any, any, any>;
|
|
47
|
+
};
|
|
48
|
+
|
|
24
49
|
/**
|
|
25
50
|
* Resource Definition
|
|
26
51
|
*/
|
|
@@ -44,7 +69,21 @@ export interface Resource {
|
|
|
44
69
|
// Optional Details view
|
|
45
70
|
detailsRenderMap?: DetailsRenderMap;
|
|
46
71
|
|
|
72
|
+
detailsQueryFn?: (id: string) => {
|
|
73
|
+
queryKey: readonly unknown[];
|
|
74
|
+
queryFn: () => Promise<unknown>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
detailsTemplate?: DetailsTemplateKey;
|
|
78
|
+
|
|
47
79
|
// State Visuals
|
|
48
80
|
statusTextMap: StatusTextMap;
|
|
49
81
|
statusColorMap: StatusColorMap;
|
|
82
|
+
|
|
83
|
+
// View Config Value
|
|
84
|
+
viewConfig?: Table.Queries.TableConfig;
|
|
85
|
+
|
|
86
|
+
// Actions and Bulk Actions
|
|
87
|
+
actions?: Record<string, UIAction>;
|
|
88
|
+
bulkActions?: Record<string, UIBulkAction>;
|
|
50
89
|
}
|