@mini_7/material-react-table-mcp 4.0.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/README.md +68 -0
- package/data/MIGRATION.md +136 -0
- package/data/api.json +5379 -0
- package/data/examples/advanced.tsx +309 -0
- package/data/examples/aggregation-and-grouping.tsx +153 -0
- package/data/examples/aggregation-multi.tsx +105 -0
- package/data/examples/alternate-column-filtering.tsx +49 -0
- package/data/examples/alternate-detail-panel.tsx +81 -0
- package/data/examples/alternate-pagination.tsx +23 -0
- package/data/examples/basic.tsx +109 -0
- package/data/examples/chart-detail-panel.tsx +89 -0
- package/data/examples/column-actions-space.tsx +44 -0
- package/data/examples/column-alignment.tsx +70 -0
- package/data/examples/custom-column-actions.tsx +109 -0
- package/data/examples/custom-column-filtering-ui.tsx +89 -0
- package/data/examples/custom-headless.tsx +125 -0
- package/data/examples/custom-top-toolbar.tsx +88 -0
- package/data/examples/customize-display-columns.tsx +85 -0
- package/data/examples/customize-filter-components.tsx +101 -0
- package/data/examples/customize-filter-modes.tsx +73 -0
- package/data/examples/customize-filter-variants.tsx +116 -0
- package/data/examples/customize-global-filter-component.tsx +52 -0
- package/data/examples/customize-remove-column-grouping.tsx +94 -0
- package/data/examples/customize-row-selection.tsx +54 -0
- package/data/examples/customize-table-styles.tsx +100 -0
- package/data/examples/disable-column-actions.tsx +37 -0
- package/data/examples/disable-column-hiding.tsx +90 -0
- package/data/examples/disable-density-toggle.tsx +49 -0
- package/data/examples/dynamic-columns.tsx +189 -0
- package/data/examples/editing-crud-cell.tsx +377 -0
- package/data/examples/editing-crud-modal.tsx +370 -0
- package/data/examples/editing-crud-row.tsx +333 -0
- package/data/examples/editing-crud-table.tsx +369 -0
- package/data/examples/editing-crud-tree.tsx +416 -0
- package/data/examples/enable-cell-actions.tsx +76 -0
- package/data/examples/enable-click-to-copy.tsx +39 -0
- package/data/examples/enable-column-grouping.tsx +120 -0
- package/data/examples/enable-column-ordering.tsx +48 -0
- package/data/examples/enable-column-pinning.tsx +69 -0
- package/data/examples/enable-column-resizing.tsx +56 -0
- package/data/examples/enable-column-virtualization.tsx +27 -0
- package/data/examples/enable-detail-panel-conditionally.tsx +78 -0
- package/data/examples/enable-detail-panel-virtualized.tsx +80 -0
- package/data/examples/enable-expanding-tree.tsx +110 -0
- package/data/examples/enable-filter-facet-values.tsx +65 -0
- package/data/examples/enable-row-dragging.tsx +137 -0
- package/data/examples/enable-row-numbers-original.tsx +48 -0
- package/data/examples/enable-row-numbers-static.tsx +48 -0
- package/data/examples/enable-row-ordering.tsx +57 -0
- package/data/examples/enable-row-pinning-select.tsx +76 -0
- package/data/examples/enable-row-pinning-static.tsx +50 -0
- package/data/examples/enable-row-pinning-sticky.tsx +69 -0
- package/data/examples/enable-row-selection.tsx +96 -0
- package/data/examples/enable-row-virtualization.tsx +101 -0
- package/data/examples/enable-sticky-header.tsx +59 -0
- package/data/examples/expanding-tree-expanded.tsx +121 -0
- package/data/examples/expanding-tree-flat-parse.tsx +125 -0
- package/data/examples/expanding-tree-root-expanded.tsx +151 -0
- package/data/examples/export-to-csv.tsx +114 -0
- package/data/examples/export-to-pdf.tsx +104 -0
- package/data/examples/external-toolbar.tsx +108 -0
- package/data/examples/font-awesome-icons.tsx +106 -0
- package/data/examples/infinite-scrolling.tsx +208 -0
- package/data/examples/lazy-detail-panel.tsx +262 -0
- package/data/examples/lazy-sub-rows.tsx +190 -0
- package/data/examples/linear-progress.tsx +66 -0
- package/data/examples/loading.tsx +50 -0
- package/data/examples/localization-i18n-ar.tsx +65 -0
- package/data/examples/localization-i18n-az.tsx +60 -0
- package/data/examples/localization-i18n-bg.tsx +60 -0
- package/data/examples/localization-i18n-cs.tsx +60 -0
- package/data/examples/localization-i18n-da.tsx +60 -0
- package/data/examples/localization-i18n-de.tsx +60 -0
- package/data/examples/localization-i18n-el.tsx +60 -0
- package/data/examples/localization-i18n-en.tsx +60 -0
- package/data/examples/localization-i18n-es.tsx +60 -0
- package/data/examples/localization-i18n-et.tsx +60 -0
- package/data/examples/localization-i18n-fa.tsx +66 -0
- package/data/examples/localization-i18n-fi.tsx +60 -0
- package/data/examples/localization-i18n-fr.tsx +60 -0
- package/data/examples/localization-i18n-he.tsx +66 -0
- package/data/examples/localization-i18n-hr.tsx +60 -0
- package/data/examples/localization-i18n-hu.tsx +60 -0
- package/data/examples/localization-i18n-hy.tsx +65 -0
- package/data/examples/localization-i18n-id.tsx +60 -0
- package/data/examples/localization-i18n-it.tsx +60 -0
- package/data/examples/localization-i18n-ja.tsx +60 -0
- package/data/examples/localization-i18n-ko.tsx +60 -0
- package/data/examples/localization-i18n-nl.tsx +60 -0
- package/data/examples/localization-i18n-no.tsx +60 -0
- package/data/examples/localization-i18n-np.tsx +60 -0
- package/data/examples/localization-i18n-pl.tsx +60 -0
- package/data/examples/localization-i18n-pt-BR.tsx +60 -0
- package/data/examples/localization-i18n-pt.tsx +60 -0
- package/data/examples/localization-i18n-ro.tsx +60 -0
- package/data/examples/localization-i18n-ru.tsx +60 -0
- package/data/examples/localization-i18n-sk.tsx +60 -0
- package/data/examples/localization-i18n-sr-Cyrl-RS.tsx +60 -0
- package/data/examples/localization-i18n-sr-Latn-RS.tsx +60 -0
- package/data/examples/localization-i18n-sv.tsx +60 -0
- package/data/examples/localization-i18n-tr.tsx +60 -0
- package/data/examples/localization-i18n-uk.tsx +60 -0
- package/data/examples/localization-i18n-vi.tsx +60 -0
- package/data/examples/localization-i18n-zh-hans.tsx +60 -0
- package/data/examples/localization-i18n-zh-hant.tsx +60 -0
- package/data/examples/manual-selection.tsx +105 -0
- package/data/examples/minimal.tsx +78 -0
- package/data/examples/mui-theme.tsx +136 -0
- package/data/examples/multi-sorting.tsx +58 -0
- package/data/examples/persistent-state.tsx +184 -0
- package/data/examples/react-query.tsx +182 -0
- package/data/examples/remote.tsx +150 -0
- package/data/examples/row-actions-buttons.tsx +89 -0
- package/data/examples/row-actions-menu-items.tsx +64 -0
- package/data/examples/single-row-selection.tsx +98 -0
- package/data/examples/virtualized.tsx +138 -0
- package/data/guides/accessibility.md +119 -0
- package/data/guides/aggregation.md +210 -0
- package/data/guides/async-loading.md +132 -0
- package/data/guides/best-practices.md +363 -0
- package/data/guides/cell-actions.md +139 -0
- package/data/guides/click-to-copy.md +67 -0
- package/data/guides/column-actions.md +69 -0
- package/data/guides/column-filtering.md +366 -0
- package/data/guides/column-grouping.md +192 -0
- package/data/guides/column-hiding.md +133 -0
- package/data/guides/column-ordering-dnd.md +99 -0
- package/data/guides/column-pinning.md +72 -0
- package/data/guides/column-resizing.md +157 -0
- package/data/guides/column-size.md +162 -0
- package/data/guides/customize-components.md +395 -0
- package/data/guides/customize-icons.md +35 -0
- package/data/guides/data-columns.md +292 -0
- package/data/guides/density-toggle.md +46 -0
- package/data/guides/detail-panel.md +195 -0
- package/data/guides/display-columns.md +135 -0
- package/data/guides/editing.md +367 -0
- package/data/guides/expanding-sub-rows.md +194 -0
- package/data/guides/full-screen-toggle.md +63 -0
- package/data/guides/global-filtering.md +237 -0
- package/data/guides/localization.md +57 -0
- package/data/guides/memoization.md +157 -0
- package/data/guides/pagination.md +171 -0
- package/data/guides/row-actions.md +116 -0
- package/data/guides/row-numbers.md +24 -0
- package/data/guides/row-ordering-dnd.md +51 -0
- package/data/guides/row-pinning.md +96 -0
- package/data/guides/row-selection.md +237 -0
- package/data/guides/sorting.md +190 -0
- package/data/guides/state-management.md +165 -0
- package/data/guides/sticky-header.md +41 -0
- package/data/guides/table-event-listeners.md +103 -0
- package/data/guides/toolbar-customization.md +251 -0
- package/data/guides/virtualization.md +183 -0
- package/data/index.json +1930 -0
- package/data/reference/mrt-components.md +183 -0
- package/data/reference/mrt-hooks.md +125 -0
- package/data/skills/composable-components/SKILL.md +189 -0
- package/data/skills/customization/SKILL.md +190 -0
- package/data/skills/drag-and-drop-ordering/SKILL.md +227 -0
- package/data/skills/editing/SKILL.md +243 -0
- package/data/skills/filtering/SKILL.md +213 -0
- package/data/skills/getting-started/SKILL.md +213 -0
- package/data/skills/localization/SKILL.md +182 -0
- package/data/skills/migrate-v3-to-v4/SKILL.md +161 -0
- package/data/skills/state-and-server-data/SKILL.md +200 -0
- package/data/skills/virtualization/SKILL.md +203 -0
- package/dist/data.js +137 -0
- package/dist/index.js +232 -0
- package/package.json +50 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { lazy, Suspense, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
MRT_EditActionButtons,
|
|
4
|
+
MaterialReactTable,
|
|
5
|
+
// createRow,
|
|
6
|
+
type MRT_ColumnDef,
|
|
7
|
+
type MRT_Row,
|
|
8
|
+
type MRT_TableOptions,
|
|
9
|
+
useMaterialReactTable,
|
|
10
|
+
} from 'material-react-table';
|
|
11
|
+
import {
|
|
12
|
+
Box,
|
|
13
|
+
Button,
|
|
14
|
+
DialogActions,
|
|
15
|
+
DialogContent,
|
|
16
|
+
DialogTitle,
|
|
17
|
+
IconButton,
|
|
18
|
+
Tooltip,
|
|
19
|
+
} from '@mui/material';
|
|
20
|
+
import {
|
|
21
|
+
QueryClient,
|
|
22
|
+
QueryClientProvider,
|
|
23
|
+
useMutation,
|
|
24
|
+
useQuery,
|
|
25
|
+
useQueryClient,
|
|
26
|
+
} from '@tanstack/react-query';
|
|
27
|
+
import { type User, fakeData, usStates } from './makeData';
|
|
28
|
+
import EditIcon from '@mui/icons-material/Edit';
|
|
29
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
30
|
+
|
|
31
|
+
const Example = () => {
|
|
32
|
+
const [validationErrors, setValidationErrors] = useState<
|
|
33
|
+
Record<string, string | undefined>
|
|
34
|
+
>({});
|
|
35
|
+
|
|
36
|
+
const columns = useMemo<MRT_ColumnDef<User>[]>(
|
|
37
|
+
() => [
|
|
38
|
+
{
|
|
39
|
+
accessorKey: 'id',
|
|
40
|
+
header: 'Id',
|
|
41
|
+
enableEditing: false,
|
|
42
|
+
size: 80,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
accessorKey: 'firstName',
|
|
46
|
+
header: 'First Name',
|
|
47
|
+
muiEditTextFieldProps: {
|
|
48
|
+
required: true,
|
|
49
|
+
error: !!validationErrors?.firstName,
|
|
50
|
+
helperText: validationErrors?.firstName,
|
|
51
|
+
//remove any previous validation errors when user focuses on the input
|
|
52
|
+
onFocus: () =>
|
|
53
|
+
setValidationErrors({
|
|
54
|
+
...validationErrors,
|
|
55
|
+
firstName: undefined,
|
|
56
|
+
}),
|
|
57
|
+
//optionally add validation checking for onBlur or onChange
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
accessorKey: 'lastName',
|
|
62
|
+
header: 'Last Name',
|
|
63
|
+
muiEditTextFieldProps: {
|
|
64
|
+
required: true,
|
|
65
|
+
error: !!validationErrors?.lastName,
|
|
66
|
+
helperText: validationErrors?.lastName,
|
|
67
|
+
//remove any previous validation errors when user focuses on the input
|
|
68
|
+
onFocus: () =>
|
|
69
|
+
setValidationErrors({
|
|
70
|
+
...validationErrors,
|
|
71
|
+
lastName: undefined,
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
accessorKey: 'email',
|
|
77
|
+
header: 'Email',
|
|
78
|
+
muiEditTextFieldProps: {
|
|
79
|
+
type: 'email',
|
|
80
|
+
required: true,
|
|
81
|
+
error: !!validationErrors?.email,
|
|
82
|
+
helperText: validationErrors?.email,
|
|
83
|
+
//remove any previous validation errors when user focuses on the input
|
|
84
|
+
onFocus: () =>
|
|
85
|
+
setValidationErrors({
|
|
86
|
+
...validationErrors,
|
|
87
|
+
email: undefined,
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
accessorKey: 'state',
|
|
93
|
+
header: 'State',
|
|
94
|
+
editVariant: 'select',
|
|
95
|
+
editSelectOptions: usStates,
|
|
96
|
+
muiEditTextFieldProps: {
|
|
97
|
+
select: true,
|
|
98
|
+
error: !!validationErrors?.state,
|
|
99
|
+
helperText: validationErrors?.state,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
[validationErrors],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
//call CREATE hook
|
|
107
|
+
const { mutateAsync: createUser, isPending: isCreatingUser } =
|
|
108
|
+
useCreateUser();
|
|
109
|
+
//call READ hook
|
|
110
|
+
const {
|
|
111
|
+
data: fetchedUsers = [],
|
|
112
|
+
isError: isLoadingUsersError,
|
|
113
|
+
isFetching: isFetchingUsers,
|
|
114
|
+
isLoading: isLoadingUsers,
|
|
115
|
+
} = useGetUsers();
|
|
116
|
+
//call UPDATE hook
|
|
117
|
+
const { mutateAsync: updateUser, isPending: isUpdatingUser } =
|
|
118
|
+
useUpdateUser();
|
|
119
|
+
//call DELETE hook
|
|
120
|
+
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
|
121
|
+
useDeleteUser();
|
|
122
|
+
|
|
123
|
+
//CREATE action
|
|
124
|
+
const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({
|
|
125
|
+
values,
|
|
126
|
+
table,
|
|
127
|
+
}) => {
|
|
128
|
+
const newValidationErrors = validateUser(values);
|
|
129
|
+
if (Object.values(newValidationErrors).some((error) => error)) {
|
|
130
|
+
setValidationErrors(newValidationErrors);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setValidationErrors({});
|
|
134
|
+
await createUser(values);
|
|
135
|
+
table.setCreatingRow(null); //exit creating mode
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
//UPDATE action
|
|
139
|
+
const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({
|
|
140
|
+
values,
|
|
141
|
+
table,
|
|
142
|
+
}) => {
|
|
143
|
+
const newValidationErrors = validateUser(values);
|
|
144
|
+
if (Object.values(newValidationErrors).some((error) => error)) {
|
|
145
|
+
setValidationErrors(newValidationErrors);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
setValidationErrors({});
|
|
149
|
+
await updateUser(values);
|
|
150
|
+
table.setEditingRow(null); //exit editing mode
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
//DELETE action
|
|
154
|
+
const openDeleteConfirmModal = (row: MRT_Row<User>) => {
|
|
155
|
+
if (window.confirm('Are you sure you want to delete this user?')) {
|
|
156
|
+
deleteUser(row.original.id);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const table = useMaterialReactTable({
|
|
161
|
+
columns,
|
|
162
|
+
data: fetchedUsers,
|
|
163
|
+
createDisplayMode: 'modal', //default ('row', and 'custom' are also available)
|
|
164
|
+
editDisplayMode: 'modal', //default ('row', 'cell', 'table', and 'custom' are also available)
|
|
165
|
+
enableEditing: true,
|
|
166
|
+
getRowId: (row) => row.id,
|
|
167
|
+
muiToolbarAlertBannerProps: isLoadingUsersError
|
|
168
|
+
? {
|
|
169
|
+
color: 'error',
|
|
170
|
+
children: 'Error loading data',
|
|
171
|
+
}
|
|
172
|
+
: undefined,
|
|
173
|
+
muiTableContainerProps: {
|
|
174
|
+
sx: {
|
|
175
|
+
minHeight: '500px',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
onCreatingRowCancel: () => setValidationErrors({}),
|
|
179
|
+
onCreatingRowSave: handleCreateUser,
|
|
180
|
+
onEditingRowCancel: () => setValidationErrors({}),
|
|
181
|
+
onEditingRowSave: handleSaveUser,
|
|
182
|
+
//optionally customize modal content
|
|
183
|
+
renderCreateRowDialogContent: ({ table, row, internalEditComponents }) => (
|
|
184
|
+
<>
|
|
185
|
+
<DialogTitle variant="h3">Create New User</DialogTitle>
|
|
186
|
+
<DialogContent
|
|
187
|
+
sx={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
|
|
188
|
+
>
|
|
189
|
+
{internalEditComponents} {/* or render custom edit components here */}
|
|
190
|
+
</DialogContent>
|
|
191
|
+
<DialogActions>
|
|
192
|
+
<MRT_EditActionButtons variant="text" table={table} row={row} />
|
|
193
|
+
</DialogActions>
|
|
194
|
+
</>
|
|
195
|
+
),
|
|
196
|
+
//optionally customize modal content
|
|
197
|
+
renderEditRowDialogContent: ({ table, row, internalEditComponents }) => (
|
|
198
|
+
<>
|
|
199
|
+
<DialogTitle variant="h3">Edit User</DialogTitle>
|
|
200
|
+
<DialogContent
|
|
201
|
+
sx={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}
|
|
202
|
+
>
|
|
203
|
+
{internalEditComponents} {/* or render custom edit components here */}
|
|
204
|
+
</DialogContent>
|
|
205
|
+
<DialogActions>
|
|
206
|
+
<MRT_EditActionButtons variant="text" table={table} row={row} />
|
|
207
|
+
</DialogActions>
|
|
208
|
+
</>
|
|
209
|
+
),
|
|
210
|
+
renderRowActions: ({ row, table }) => (
|
|
211
|
+
<Box sx={{ display: 'flex', gap: '1rem' }}>
|
|
212
|
+
<Tooltip title="Edit">
|
|
213
|
+
<IconButton onClick={() => table.setEditingRow(row)}>
|
|
214
|
+
<EditIcon />
|
|
215
|
+
</IconButton>
|
|
216
|
+
</Tooltip>
|
|
217
|
+
<Tooltip title="Delete">
|
|
218
|
+
<IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>
|
|
219
|
+
<DeleteIcon />
|
|
220
|
+
</IconButton>
|
|
221
|
+
</Tooltip>
|
|
222
|
+
</Box>
|
|
223
|
+
),
|
|
224
|
+
renderTopToolbarCustomActions: ({ table }) => (
|
|
225
|
+
<Button
|
|
226
|
+
variant="contained"
|
|
227
|
+
onClick={() => {
|
|
228
|
+
table.setCreatingRow(true); //simplest way to open the create row modal with no default values
|
|
229
|
+
//or you can pass in a row object to set default values with the `createRow` helper function
|
|
230
|
+
// table.setCreatingRow(
|
|
231
|
+
// createRow(table, {
|
|
232
|
+
// //optionally pass in default values for the new row, useful for nested data or other complex scenarios
|
|
233
|
+
// }),
|
|
234
|
+
// );
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
Create New User
|
|
238
|
+
</Button>
|
|
239
|
+
),
|
|
240
|
+
state: {
|
|
241
|
+
isLoading: isLoadingUsers,
|
|
242
|
+
isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
|
|
243
|
+
showAlertBanner: isLoadingUsersError,
|
|
244
|
+
showProgressBars: isFetchingUsers,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return <MaterialReactTable table={table} />;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
//CREATE hook (post new user to api)
|
|
252
|
+
function useCreateUser() {
|
|
253
|
+
const queryClient = useQueryClient();
|
|
254
|
+
return useMutation({
|
|
255
|
+
mutationFn: async (user: User) => {
|
|
256
|
+
//send api update request here
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
258
|
+
return Promise.resolve();
|
|
259
|
+
},
|
|
260
|
+
//client side optimistic update
|
|
261
|
+
onMutate: (newUserInfo: User) => {
|
|
262
|
+
queryClient.setQueryData(
|
|
263
|
+
['users'],
|
|
264
|
+
(prevUsers: any) =>
|
|
265
|
+
[
|
|
266
|
+
...prevUsers,
|
|
267
|
+
{
|
|
268
|
+
...newUserInfo,
|
|
269
|
+
id: (Math.random() + 1).toString(36).substring(7),
|
|
270
|
+
},
|
|
271
|
+
] as User[],
|
|
272
|
+
);
|
|
273
|
+
},
|
|
274
|
+
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
//READ hook (get users from api)
|
|
279
|
+
function useGetUsers() {
|
|
280
|
+
return useQuery<User[]>({
|
|
281
|
+
queryKey: ['users'],
|
|
282
|
+
queryFn: async () => {
|
|
283
|
+
//send api request here
|
|
284
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
285
|
+
return Promise.resolve(fakeData);
|
|
286
|
+
},
|
|
287
|
+
refetchOnWindowFocus: false,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
//UPDATE hook (put user in api)
|
|
292
|
+
function useUpdateUser() {
|
|
293
|
+
const queryClient = useQueryClient();
|
|
294
|
+
return useMutation({
|
|
295
|
+
mutationFn: async (user: User) => {
|
|
296
|
+
//send api update request here
|
|
297
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
298
|
+
return Promise.resolve();
|
|
299
|
+
},
|
|
300
|
+
//client side optimistic update
|
|
301
|
+
onMutate: (newUserInfo: User) => {
|
|
302
|
+
queryClient.setQueryData(['users'], (prevUsers: any) =>
|
|
303
|
+
prevUsers?.map((prevUser: User) =>
|
|
304
|
+
prevUser.id === newUserInfo.id ? newUserInfo : prevUser,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
//DELETE hook (delete user in api)
|
|
313
|
+
function useDeleteUser() {
|
|
314
|
+
const queryClient = useQueryClient();
|
|
315
|
+
return useMutation({
|
|
316
|
+
mutationFn: async (userId: string) => {
|
|
317
|
+
//send api update request here
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
319
|
+
return Promise.resolve();
|
|
320
|
+
},
|
|
321
|
+
//client side optimistic update
|
|
322
|
+
onMutate: (userId: string) => {
|
|
323
|
+
queryClient.setQueryData(['users'], (prevUsers: any) =>
|
|
324
|
+
prevUsers?.filter((user: User) => user.id !== userId),
|
|
325
|
+
);
|
|
326
|
+
},
|
|
327
|
+
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
//react query setup in App.tsx
|
|
332
|
+
const ReactQueryDevtoolsProduction = lazy(() =>
|
|
333
|
+
import('@tanstack/react-query-devtools/build/modern/production.js').then(
|
|
334
|
+
(d) => ({
|
|
335
|
+
default: d.ReactQueryDevtools,
|
|
336
|
+
}),
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const queryClient = new QueryClient();
|
|
341
|
+
|
|
342
|
+
export default function App() {
|
|
343
|
+
return (
|
|
344
|
+
<QueryClientProvider client={queryClient}>
|
|
345
|
+
<Example />
|
|
346
|
+
<Suspense fallback={null}>
|
|
347
|
+
<ReactQueryDevtoolsProduction />
|
|
348
|
+
</Suspense>
|
|
349
|
+
</QueryClientProvider>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const validateRequired = (value: string) => !!value.length;
|
|
354
|
+
const validateEmail = (email: string) =>
|
|
355
|
+
!!email.length &&
|
|
356
|
+
email
|
|
357
|
+
.toLowerCase()
|
|
358
|
+
.match(
|
|
359
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
function validateUser(user: User) {
|
|
363
|
+
return {
|
|
364
|
+
firstName: !validateRequired(user.firstName)
|
|
365
|
+
? 'First Name is Required'
|
|
366
|
+
: '',
|
|
367
|
+
lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
|
|
368
|
+
email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { lazy, Suspense, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
MaterialReactTable,
|
|
4
|
+
// createRow,
|
|
5
|
+
type MRT_ColumnDef,
|
|
6
|
+
type MRT_Row,
|
|
7
|
+
type MRT_TableOptions,
|
|
8
|
+
useMaterialReactTable,
|
|
9
|
+
} from 'material-react-table';
|
|
10
|
+
import { Box, Button, IconButton, Tooltip } from '@mui/material';
|
|
11
|
+
import {
|
|
12
|
+
QueryClient,
|
|
13
|
+
QueryClientProvider,
|
|
14
|
+
useMutation,
|
|
15
|
+
useQuery,
|
|
16
|
+
useQueryClient,
|
|
17
|
+
} from '@tanstack/react-query';
|
|
18
|
+
import { type User, fakeData, usStates } from './makeData';
|
|
19
|
+
import EditIcon from '@mui/icons-material/Edit';
|
|
20
|
+
import DeleteIcon from '@mui/icons-material/Delete';
|
|
21
|
+
|
|
22
|
+
const Example = () => {
|
|
23
|
+
const [validationErrors, setValidationErrors] = useState<
|
|
24
|
+
Record<string, string | undefined>
|
|
25
|
+
>({});
|
|
26
|
+
|
|
27
|
+
const columns = useMemo<MRT_ColumnDef<User>[]>(
|
|
28
|
+
() => [
|
|
29
|
+
{
|
|
30
|
+
accessorKey: 'id',
|
|
31
|
+
header: 'Id',
|
|
32
|
+
enableEditing: false,
|
|
33
|
+
size: 80,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
accessorKey: 'firstName',
|
|
37
|
+
header: 'First Name',
|
|
38
|
+
muiEditTextFieldProps: {
|
|
39
|
+
required: true,
|
|
40
|
+
error: !!validationErrors?.firstName,
|
|
41
|
+
helperText: validationErrors?.firstName,
|
|
42
|
+
//remove any previous validation errors when user focuses on the input
|
|
43
|
+
onFocus: () =>
|
|
44
|
+
setValidationErrors({
|
|
45
|
+
...validationErrors,
|
|
46
|
+
firstName: undefined,
|
|
47
|
+
}),
|
|
48
|
+
//optionally add validation checking for onBlur or onChange
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
accessorKey: 'lastName',
|
|
53
|
+
header: 'Last Name',
|
|
54
|
+
muiEditTextFieldProps: {
|
|
55
|
+
required: true,
|
|
56
|
+
error: !!validationErrors?.lastName,
|
|
57
|
+
helperText: validationErrors?.lastName,
|
|
58
|
+
//remove any previous validation errors when user focuses on the input
|
|
59
|
+
onFocus: () =>
|
|
60
|
+
setValidationErrors({
|
|
61
|
+
...validationErrors,
|
|
62
|
+
lastName: undefined,
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
accessorKey: 'email',
|
|
68
|
+
header: 'Email',
|
|
69
|
+
muiEditTextFieldProps: {
|
|
70
|
+
type: 'email',
|
|
71
|
+
required: true,
|
|
72
|
+
error: !!validationErrors?.email,
|
|
73
|
+
helperText: validationErrors?.email,
|
|
74
|
+
//remove any previous validation errors when user focuses on the input
|
|
75
|
+
onFocus: () =>
|
|
76
|
+
setValidationErrors({
|
|
77
|
+
...validationErrors,
|
|
78
|
+
email: undefined,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
accessorKey: 'state',
|
|
84
|
+
header: 'State',
|
|
85
|
+
editVariant: 'select',
|
|
86
|
+
editSelectOptions: usStates,
|
|
87
|
+
muiEditTextFieldProps: {
|
|
88
|
+
select: true,
|
|
89
|
+
error: !!validationErrors?.state,
|
|
90
|
+
helperText: validationErrors?.state,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
[validationErrors],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
//call CREATE hook
|
|
98
|
+
const { mutateAsync: createUser, isPending: isCreatingUser } =
|
|
99
|
+
useCreateUser();
|
|
100
|
+
//call READ hook
|
|
101
|
+
const {
|
|
102
|
+
data: fetchedUsers = [],
|
|
103
|
+
isError: isLoadingUsersError,
|
|
104
|
+
isFetching: isFetchingUsers,
|
|
105
|
+
isLoading: isLoadingUsers,
|
|
106
|
+
} = useGetUsers();
|
|
107
|
+
//call UPDATE hook
|
|
108
|
+
const { mutateAsync: updateUser, isPending: isUpdatingUser } =
|
|
109
|
+
useUpdateUser();
|
|
110
|
+
//call DELETE hook
|
|
111
|
+
const { mutateAsync: deleteUser, isPending: isDeletingUser } =
|
|
112
|
+
useDeleteUser();
|
|
113
|
+
|
|
114
|
+
//CREATE action
|
|
115
|
+
const handleCreateUser: MRT_TableOptions<User>['onCreatingRowSave'] = async ({
|
|
116
|
+
values,
|
|
117
|
+
table,
|
|
118
|
+
}) => {
|
|
119
|
+
const newValidationErrors = validateUser(values);
|
|
120
|
+
if (Object.values(newValidationErrors).some((error) => error)) {
|
|
121
|
+
setValidationErrors(newValidationErrors);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setValidationErrors({});
|
|
125
|
+
await createUser(values);
|
|
126
|
+
table.setCreatingRow(null); //exit creating mode
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
//UPDATE action
|
|
130
|
+
const handleSaveUser: MRT_TableOptions<User>['onEditingRowSave'] = async ({
|
|
131
|
+
values,
|
|
132
|
+
table,
|
|
133
|
+
}) => {
|
|
134
|
+
const newValidationErrors = validateUser(values);
|
|
135
|
+
if (Object.values(newValidationErrors).some((error) => error)) {
|
|
136
|
+
setValidationErrors(newValidationErrors);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setValidationErrors({});
|
|
140
|
+
await updateUser(values);
|
|
141
|
+
table.setEditingRow(null); //exit editing mode
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
//DELETE action
|
|
145
|
+
const openDeleteConfirmModal = (row: MRT_Row<User>) => {
|
|
146
|
+
if (window.confirm('Are you sure you want to delete this user?')) {
|
|
147
|
+
deleteUser(row.original.id);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const table = useMaterialReactTable({
|
|
152
|
+
columns,
|
|
153
|
+
data: fetchedUsers,
|
|
154
|
+
createDisplayMode: 'row', // ('modal', and 'custom' are also available)
|
|
155
|
+
editDisplayMode: 'row', // ('modal', 'cell', 'table', and 'custom' are also available)
|
|
156
|
+
enableEditing: true,
|
|
157
|
+
getRowId: (row) => row.id,
|
|
158
|
+
muiToolbarAlertBannerProps: isLoadingUsersError
|
|
159
|
+
? {
|
|
160
|
+
color: 'error',
|
|
161
|
+
children: 'Error loading data',
|
|
162
|
+
}
|
|
163
|
+
: undefined,
|
|
164
|
+
muiTableContainerProps: {
|
|
165
|
+
sx: {
|
|
166
|
+
minHeight: '500px',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
onCreatingRowCancel: () => setValidationErrors({}),
|
|
170
|
+
onCreatingRowSave: handleCreateUser,
|
|
171
|
+
onEditingRowCancel: () => setValidationErrors({}),
|
|
172
|
+
onEditingRowSave: handleSaveUser,
|
|
173
|
+
renderRowActions: ({ row, table }) => (
|
|
174
|
+
<Box sx={{ display: 'flex', gap: '1rem' }}>
|
|
175
|
+
<Tooltip title="Edit">
|
|
176
|
+
<IconButton onClick={() => table.setEditingRow(row)}>
|
|
177
|
+
<EditIcon />
|
|
178
|
+
</IconButton>
|
|
179
|
+
</Tooltip>
|
|
180
|
+
<Tooltip title="Delete">
|
|
181
|
+
<IconButton color="error" onClick={() => openDeleteConfirmModal(row)}>
|
|
182
|
+
<DeleteIcon />
|
|
183
|
+
</IconButton>
|
|
184
|
+
</Tooltip>
|
|
185
|
+
</Box>
|
|
186
|
+
),
|
|
187
|
+
renderTopToolbarCustomActions: ({ table }) => (
|
|
188
|
+
<Button
|
|
189
|
+
variant="contained"
|
|
190
|
+
onClick={() => {
|
|
191
|
+
table.setCreatingRow(true); //simplest way to open the create row modal with no default values
|
|
192
|
+
//or you can pass in a row object to set default values with the `createRow` helper function
|
|
193
|
+
// table.setCreatingRow(
|
|
194
|
+
// createRow(table, {
|
|
195
|
+
// //optionally pass in default values for the new row, useful for nested data or other complex scenarios
|
|
196
|
+
// }),
|
|
197
|
+
// );
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
Create New User
|
|
201
|
+
</Button>
|
|
202
|
+
),
|
|
203
|
+
state: {
|
|
204
|
+
isLoading: isLoadingUsers,
|
|
205
|
+
isSaving: isCreatingUser || isUpdatingUser || isDeletingUser,
|
|
206
|
+
showAlertBanner: isLoadingUsersError,
|
|
207
|
+
showProgressBars: isFetchingUsers,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return <MaterialReactTable table={table} />;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
//CREATE hook (post new user to api)
|
|
215
|
+
function useCreateUser() {
|
|
216
|
+
const queryClient = useQueryClient();
|
|
217
|
+
return useMutation({
|
|
218
|
+
mutationFn: async (user: User) => {
|
|
219
|
+
//send api update request here
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
221
|
+
return Promise.resolve();
|
|
222
|
+
},
|
|
223
|
+
//client side optimistic update
|
|
224
|
+
onMutate: (newUserInfo: User) => {
|
|
225
|
+
queryClient.setQueryData(
|
|
226
|
+
['users'],
|
|
227
|
+
(prevUsers: any) =>
|
|
228
|
+
[
|
|
229
|
+
...prevUsers,
|
|
230
|
+
{
|
|
231
|
+
...newUserInfo,
|
|
232
|
+
id: (Math.random() + 1).toString(36).substring(7),
|
|
233
|
+
},
|
|
234
|
+
] as User[],
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
//READ hook (get users from api)
|
|
242
|
+
function useGetUsers() {
|
|
243
|
+
return useQuery<User[]>({
|
|
244
|
+
queryKey: ['users'],
|
|
245
|
+
queryFn: async () => {
|
|
246
|
+
//send api request here
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
248
|
+
return Promise.resolve(fakeData);
|
|
249
|
+
},
|
|
250
|
+
refetchOnWindowFocus: false,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//UPDATE hook (put user in api)
|
|
255
|
+
function useUpdateUser() {
|
|
256
|
+
const queryClient = useQueryClient();
|
|
257
|
+
return useMutation({
|
|
258
|
+
mutationFn: async (user: User) => {
|
|
259
|
+
//send api update request here
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
261
|
+
return Promise.resolve();
|
|
262
|
+
},
|
|
263
|
+
//client side optimistic update
|
|
264
|
+
onMutate: (newUserInfo: User) => {
|
|
265
|
+
queryClient.setQueryData(['users'], (prevUsers: any) =>
|
|
266
|
+
prevUsers?.map((prevUser: User) =>
|
|
267
|
+
prevUser.id === newUserInfo.id ? newUserInfo : prevUser,
|
|
268
|
+
),
|
|
269
|
+
);
|
|
270
|
+
},
|
|
271
|
+
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//DELETE hook (delete user in api)
|
|
276
|
+
function useDeleteUser() {
|
|
277
|
+
const queryClient = useQueryClient();
|
|
278
|
+
return useMutation({
|
|
279
|
+
mutationFn: async (userId: string) => {
|
|
280
|
+
//send api update request here
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, 1000)); //fake api call
|
|
282
|
+
return Promise.resolve();
|
|
283
|
+
},
|
|
284
|
+
//client side optimistic update
|
|
285
|
+
onMutate: (userId: string) => {
|
|
286
|
+
queryClient.setQueryData(['users'], (prevUsers: any) =>
|
|
287
|
+
prevUsers?.filter((user: User) => user.id !== userId),
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
// onSettled: () => queryClient.invalidateQueries({ queryKey: ['users'] }), //refetch users after mutation, disabled for demo
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//react query setup in App.tsx
|
|
295
|
+
const ReactQueryDevtoolsProduction = lazy(() =>
|
|
296
|
+
import('@tanstack/react-query-devtools/build/modern/production.js').then(
|
|
297
|
+
(d) => ({
|
|
298
|
+
default: d.ReactQueryDevtools,
|
|
299
|
+
}),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
const queryClient = new QueryClient();
|
|
304
|
+
|
|
305
|
+
export default function App() {
|
|
306
|
+
return (
|
|
307
|
+
<QueryClientProvider client={queryClient}>
|
|
308
|
+
<Example />
|
|
309
|
+
<Suspense fallback={null}>
|
|
310
|
+
<ReactQueryDevtoolsProduction />
|
|
311
|
+
</Suspense>
|
|
312
|
+
</QueryClientProvider>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const validateRequired = (value: string) => !!value.length;
|
|
317
|
+
const validateEmail = (email: string) =>
|
|
318
|
+
!!email.length &&
|
|
319
|
+
email
|
|
320
|
+
.toLowerCase()
|
|
321
|
+
.match(
|
|
322
|
+
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
function validateUser(user: User) {
|
|
326
|
+
return {
|
|
327
|
+
firstName: !validateRequired(user.firstName)
|
|
328
|
+
? 'First Name is Required'
|
|
329
|
+
: '',
|
|
330
|
+
lastName: !validateRequired(user.lastName) ? 'Last Name is Required' : '',
|
|
331
|
+
email: !validateEmail(user.email) ? 'Incorrect Email Format' : '',
|
|
332
|
+
};
|
|
333
|
+
}
|