@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.
Files changed (63) hide show
  1. package/package.json +4 -4
  2. package/src/auth/auth-context.tsx +15 -9
  3. package/src/features/table/components/dashboard/table/bulk-action-button.tsx +76 -0
  4. package/src/features/table/components/dashboard/table/data-mask-map.ts +1 -1
  5. package/src/features/table/components/dashboard/table/details/details.tsx +63 -35
  6. package/src/features/table/components/dashboard/table/details/rows/downloads.tsx +0 -1
  7. package/src/features/table/components/dashboard/table/details/rows/primitive.tsx +0 -1
  8. package/src/features/table/components/dashboard/table/details/rows/table.tsx +0 -1
  9. package/src/features/table/components/dashboard/table/details/templates/invoice-details.tsx +440 -0
  10. package/src/features/table/components/dashboard/table/details/templates/template-registry.ts +39 -0
  11. package/src/features/table/components/dashboard/table/filter-render-map.tsx +2 -1
  12. package/src/features/table/components/dashboard/table/filters/date-filter.tsx +5 -5
  13. package/src/features/table/components/dashboard/table/filters/status-filter.tsx +2 -2
  14. package/src/features/table/components/dashboard/table/no-results.tsx +1 -1
  15. package/src/features/table/components/dashboard/table/table-header.tsx +41 -5
  16. package/src/features/table/components/dashboard/table/table-row/action-button.tsx +32 -24
  17. package/src/features/table/components/dashboard/table/table-row/table-row.tsx +60 -13
  18. package/src/features/table/components/dashboard/table/table-row-skeleton.tsx +0 -1
  19. package/src/features/table/components/dashboard/table/table.tsx +143 -30
  20. package/src/features/table/components/dashboard/table/use-select-rows.ts +53 -0
  21. package/src/features/table/components/dashboard/table-dashboard.tsx +2 -2
  22. package/src/features/table/logic/resolvers.ts +43 -0
  23. package/src/features/table/types/schemas.ts +39 -0
  24. package/src/index.ts +1 -2
  25. package/dist/auth/auth-context.d.ts +0 -36
  26. package/dist/features/settings/index.d.ts +0 -2
  27. package/dist/features/table/components/dashboard/index.d.ts +0 -1
  28. package/dist/features/table/components/dashboard/table/actions.d.ts +0 -14
  29. package/dist/features/table/components/dashboard/table/data-mask-map.d.ts +0 -2
  30. package/dist/features/table/components/dashboard/table/details/details.d.ts +0 -27
  31. package/dist/features/table/components/dashboard/table/details/index.d.ts +0 -1
  32. package/dist/features/table/components/dashboard/table/details/label.d.ts +0 -5
  33. package/dist/features/table/components/dashboard/table/details/rows/downloads.d.ts +0 -6
  34. package/dist/features/table/components/dashboard/table/details/rows/index.d.ts +0 -3
  35. package/dist/features/table/components/dashboard/table/details/rows/primitive.d.ts +0 -6
  36. package/dist/features/table/components/dashboard/table/details/rows/table.d.ts +0 -17
  37. package/dist/features/table/components/dashboard/table/details/styles.d.ts +0 -10
  38. package/dist/features/table/components/dashboard/table/filter-render-map.d.ts +0 -2
  39. package/dist/features/table/components/dashboard/table/filters/date-filter.d.ts +0 -2
  40. package/dist/features/table/components/dashboard/table/filters/status-filter.d.ts +0 -2
  41. package/dist/features/table/components/dashboard/table/index.d.ts +0 -2
  42. package/dist/features/table/components/dashboard/table/no-results.d.ts +0 -2
  43. package/dist/features/table/components/dashboard/table/status-pill.d.ts +0 -8
  44. package/dist/features/table/components/dashboard/table/table-header.d.ts +0 -16
  45. package/dist/features/table/components/dashboard/table/table-input.d.ts +0 -11
  46. package/dist/features/table/components/dashboard/table/table-row/action-button.d.ts +0 -8
  47. package/dist/features/table/components/dashboard/table/table-row/table-row.d.ts +0 -32
  48. package/dist/features/table/components/dashboard/table/table-row-skeleton.d.ts +0 -6
  49. package/dist/features/table/components/dashboard/table/table.d.ts +0 -18
  50. package/dist/features/table/components/dashboard/table-dashboard.d.ts +0 -22
  51. package/dist/features/table/index.d.ts +0 -8
  52. package/dist/features/table/logic/index.d.ts +0 -23
  53. package/dist/features/table/logic/resolvers.d.ts +0 -21
  54. package/dist/features/table/logic/transformers.d.ts +0 -35
  55. package/dist/features/table/logic/types.d.ts +0 -8
  56. package/dist/features/table/types/handlers.d.ts +0 -53
  57. package/dist/features/table/types/index.d.ts +0 -69
  58. package/dist/features/table/types/queries.d.ts +0 -27
  59. package/dist/features/table/types/schemas.d.ts +0 -31
  60. package/dist/index.d.ts +0 -63
  61. package/dist/portal.d.ts +0 -38
  62. package/dist/tsconfig.tsbuildinfo +0 -1
  63. package/src/features/table/components/dashboard/table/actions.ts +0 -59
@@ -1,12 +1,10 @@
1
- import { actionIconMap, actionMutationMap } from "../actions";
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: 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
- }: Readonly<ActionButtonProps>) {
18
- // Maybe move to a resolver??
19
- // currently we only use the action type to resolve
20
- const Icon = actionIconMap?.[action.Type];
21
- const mutation = actionMutationMap?.[action.Type](resourceName, docId);
22
- const { mutate, isPending } = useMutation(mutation);
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
- <FlexBox justifyContent="flex-start" gap={1}>
26
- <span key={action.Name}>
27
- <IconButton
28
- size="small"
29
- onClick={(e) => {
30
- e.stopPropagation();
31
- mutate({ docId });
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: Action[];
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
- idField: string;
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
- idField,
58
- }: Readonly<DocumentTableProps>) {
59
- const id = row?.[idField] as string;
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
- {actions?.map((action) => (
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 justifyContent="flex-start" gap={1}>
90
- <ActionButton docId={id} action={action} resourceName={"invoice"} />
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
  }
@@ -1,7 +1,6 @@
1
1
  import { Skeleton } from "@mui/material";
2
2
  import TableCell from "@mui/material/TableCell";
3
3
  import TableRow from "@mui/material/TableRow";
4
- import type React from "react";
5
4
  import { styled } from "@mui/system";
6
5
 
7
6
  const StyledTableCell = styled(TableCell)({
@@ -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: (resourceName: string) => {
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
- ...(filterRenderMap ?? {}),
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: config, isPending: configPending } = useQuery(
126
- viewConfigQuery(resource.name),
127
- );
128
- const { data: rows, isPending: rowsPending } = useQuery(
129
- resource.rowsQueryFn({ page, pageSize, filters, searchColumn, searchTerm }),
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 selectedDocDetails =
156
- rows?.data.find((doc) => doc?.[idField] === selectedDocument) || {};
157
- // Build the logic necessary for rendering
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={selectedDocDetails}
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
- // Table body data calculations
174
- const colSpan = visibleCols?.length ? visibleCols.length + 1 : 4;
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={65}
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={() => setPage(0)}
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, i) => (
226
- <DataTableRow
227
- key={`data-table-row-${i + 1}`}
228
- columnLogic={columnLogic}
229
- row={row}
230
- handleClick={setSelectedDocument}
231
- idField={idField}
232
- actions={actions}
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: (resourceName: string) => {
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
  }
package/src/index.ts CHANGED
@@ -60,6 +60,5 @@
60
60
  */
61
61
 
62
62
  export type { Session } from "./auth/auth-context";
63
- export { Portal, type PortalProps } from "./portal";
64
-
65
63
  export { Table } from "./features/table";
64
+ export { Portal, type PortalProps } from "./portal";