@micha.bigler/ui-core-micha 2.4.5 → 2.5.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.
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
|
3
|
-
import { Box, Typography, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TextField, } from '@mui/material';
|
|
4
|
-
import { DataGrid } from '@mui/x-data-grid';
|
|
3
|
+
import { Box, Typography, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TextField, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableSortLabel, TablePagination, } from '@mui/material';
|
|
5
4
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
6
5
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
7
6
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
@@ -15,6 +14,10 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
15
14
|
const [error, setError] = useState(null);
|
|
16
15
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
17
16
|
const [searchQuery, setSearchQuery] = useState('');
|
|
17
|
+
const [sortField, setSortField] = useState('email');
|
|
18
|
+
const [sortDir, setSortDir] = useState('asc');
|
|
19
|
+
const [page, setPage] = useState(0);
|
|
20
|
+
const [pageSize, setPageSize] = useState(25);
|
|
18
21
|
const controlSx = { minWidth: 140 };
|
|
19
22
|
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
20
23
|
const loadUsers = useCallback(async () => {
|
|
@@ -42,6 +45,9 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
42
45
|
useEffect(() => {
|
|
43
46
|
loadUsers();
|
|
44
47
|
}, [loadUsers, refreshTrigger]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
setPage(0);
|
|
50
|
+
}, [searchQuery]);
|
|
45
51
|
const handleDelete = async (userId) => {
|
|
46
52
|
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?')))
|
|
47
53
|
return;
|
|
@@ -211,16 +217,23 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
211
217
|
field: 'email',
|
|
212
218
|
headerName: t('Auth.EMAIL_LABEL', 'Email'),
|
|
213
219
|
minWidth: 220,
|
|
214
|
-
maxWidth: 340,
|
|
215
220
|
flex: 1,
|
|
221
|
+
sortable: true,
|
|
222
|
+
align: 'left',
|
|
223
|
+
headerAlign: 'left',
|
|
224
|
+
valueGetter: (row) => { var _a; return (_a = row === null || row === void 0 ? void 0 : row.email) !== null && _a !== void 0 ? _a : ''; },
|
|
225
|
+
renderCell: (row) => row.email,
|
|
216
226
|
},
|
|
217
227
|
{
|
|
218
228
|
field: 'name',
|
|
219
229
|
headerName: t('Profile.NAME_LABEL', 'Name'),
|
|
220
230
|
minWidth: 180,
|
|
221
|
-
maxWidth: 260,
|
|
222
231
|
flex: 0.9,
|
|
223
|
-
|
|
232
|
+
sortable: true,
|
|
233
|
+
align: 'left',
|
|
234
|
+
headerAlign: 'left',
|
|
235
|
+
valueGetter: (row) => getUserDisplayName(row),
|
|
236
|
+
renderCell: (row) => getUserDisplayName(row),
|
|
224
237
|
},
|
|
225
238
|
];
|
|
226
239
|
if (showNewColumn) {
|
|
@@ -228,14 +241,14 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
228
241
|
field: 'is_new',
|
|
229
242
|
headerName: t('UserList.NEW', 'New'),
|
|
230
243
|
minWidth: 90,
|
|
231
|
-
maxWidth: 110,
|
|
232
244
|
flex: 0.35,
|
|
245
|
+
sortable: true,
|
|
233
246
|
align: 'center',
|
|
234
247
|
headerAlign: 'center',
|
|
235
|
-
valueGetter: (
|
|
236
|
-
renderCell: (
|
|
237
|
-
var _a
|
|
238
|
-
return renderBooleanStatusIcon(Boolean(((
|
|
248
|
+
valueGetter: (row) => { var _a; return Boolean(((_a = row === null || row === void 0 ? void 0 : row.profile) === null || _a === void 0 ? void 0 : _a.is_new) || (row === null || row === void 0 ? void 0 : row.is_new)); },
|
|
249
|
+
renderCell: (row) => {
|
|
250
|
+
var _a;
|
|
251
|
+
return renderBooleanStatusIcon(Boolean(((_a = row === null || row === void 0 ? void 0 : row.profile) === null || _a === void 0 ? void 0 : _a.is_new) || (row === null || row === void 0 ? void 0 : row.is_new)), t('Common.YES', 'Yes'), t('Common.NO', 'No'));
|
|
239
252
|
},
|
|
240
253
|
});
|
|
241
254
|
}
|
|
@@ -244,14 +257,14 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
244
257
|
field: 'successful_login',
|
|
245
258
|
headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
|
|
246
259
|
minWidth: 120,
|
|
247
|
-
maxWidth: 150,
|
|
248
260
|
flex: 0.4,
|
|
261
|
+
sortable: true,
|
|
249
262
|
align: 'center',
|
|
250
263
|
headerAlign: 'center',
|
|
251
|
-
valueGetter: (
|
|
252
|
-
renderCell: (
|
|
253
|
-
var _a
|
|
254
|
-
return renderBooleanStatusIcon(Boolean((
|
|
264
|
+
valueGetter: (row) => { var _a; return Boolean((_a = row === null || row === void 0 ? void 0 : row.successful_login) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.last_login); },
|
|
265
|
+
renderCell: (row) => {
|
|
266
|
+
var _a;
|
|
267
|
+
return renderBooleanStatusIcon(Boolean((_a = row === null || row === void 0 ? void 0 : row.successful_login) !== null && _a !== void 0 ? _a : row === null || row === void 0 ? void 0 : row.last_login), t('Common.YES', 'Yes'), t('Common.NO', 'No'));
|
|
255
268
|
},
|
|
256
269
|
});
|
|
257
270
|
}
|
|
@@ -260,28 +273,26 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
260
273
|
field: 'role',
|
|
261
274
|
headerName: t('UserList.ROLE', 'Role'),
|
|
262
275
|
minWidth: 180,
|
|
263
|
-
maxWidth: 240,
|
|
264
276
|
flex: 0.7,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
},
|
|
277
|
+
sortable: true,
|
|
278
|
+
align: 'left',
|
|
279
|
+
headerAlign: 'left',
|
|
280
|
+
valueGetter: (row) => (row === null || row === void 0 ? void 0 : row.role) || 'none',
|
|
281
|
+
renderCell: (row) => (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(row), children: _jsx(Select, { value: row.role || 'none', onChange: (event) => handleChangeRole(row.id, event.target.value), variant: "outlined", children: roles.map((role) => _jsx(MenuItem, { value: role, children: role }, role)) }) })),
|
|
270
282
|
});
|
|
271
283
|
}
|
|
272
284
|
const mappedExtraColumns = visibleExtraColumns.map((column) => ({
|
|
273
285
|
field: `extra:${column.key}`,
|
|
274
286
|
headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
|
|
275
287
|
minWidth: Number(column.minWidth) || 180,
|
|
276
|
-
maxWidth: Number(column.maxWidth) || 320,
|
|
277
288
|
flex: Number(column.flex) || 0.9,
|
|
278
289
|
sortable: column.sortable !== false,
|
|
279
290
|
align: column.align || 'left',
|
|
280
291
|
headerAlign: column.align || 'left',
|
|
281
|
-
valueGetter: (
|
|
282
|
-
renderCell: (
|
|
283
|
-
user:
|
|
284
|
-
canEdit: canEdit(
|
|
292
|
+
valueGetter: (row) => getExtraColumnSortValue(column, row),
|
|
293
|
+
renderCell: (row) => column.renderCell({
|
|
294
|
+
user: row,
|
|
295
|
+
canEdit: canEdit(row),
|
|
285
296
|
currentUser,
|
|
286
297
|
extraContext,
|
|
287
298
|
t,
|
|
@@ -296,30 +307,27 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
296
307
|
field: 'actions',
|
|
297
308
|
headerName: t('Common.ACTIONS', 'Actions'),
|
|
298
309
|
minWidth: Math.max(220, 110 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 110),
|
|
299
|
-
maxWidth: Math.max(360, 120 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 120),
|
|
300
310
|
flex: 1.1,
|
|
301
311
|
sortable: false,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}), showDeleteAction && (_jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(user.id), disabled: !canDelete(user), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) }))] }));
|
|
322
|
-
},
|
|
312
|
+
align: 'left',
|
|
313
|
+
headerAlign: 'left',
|
|
314
|
+
valueGetter: null,
|
|
315
|
+
renderCell: (row) => (_jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', py: 0.5 }, children: [visibleRowActions.map((action) => {
|
|
316
|
+
const actionId = `${action.key}:${row.id}`;
|
|
317
|
+
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
318
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
319
|
+
? action.disabled({
|
|
320
|
+
user: row,
|
|
321
|
+
canEdit: canEdit(row),
|
|
322
|
+
currentUser,
|
|
323
|
+
extraContext,
|
|
324
|
+
t,
|
|
325
|
+
})
|
|
326
|
+
: false;
|
|
327
|
+
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, row), disabled: isBusy || isDisabled, sx: actionButtonSx, children: typeof action.label === 'function'
|
|
328
|
+
? action.label({ user: row, t, currentUser, canEdit: canEdit(row) })
|
|
329
|
+
: action.label }, `${action.key}-${row.id}`));
|
|
330
|
+
}), showDeleteAction && (_jsx(Tooltip, { title: t('Common.DELETE', 'Delete'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(row.id), disabled: !canDelete(row), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) }))] })),
|
|
323
331
|
};
|
|
324
332
|
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
325
333
|
}, [
|
|
@@ -339,27 +347,29 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
339
347
|
canEdit,
|
|
340
348
|
canDelete,
|
|
341
349
|
]);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
350
|
+
const sortedUsers = useMemo(() => {
|
|
351
|
+
const col = columns.find((c) => c.field === sortField);
|
|
352
|
+
return [...filteredUsers].sort((a, b) => {
|
|
353
|
+
var _a, _b;
|
|
354
|
+
const aVal = (col === null || col === void 0 ? void 0 : col.valueGetter) ? col.valueGetter(a) : ((_a = a[sortField]) !== null && _a !== void 0 ? _a : '');
|
|
355
|
+
const bVal = (col === null || col === void 0 ? void 0 : col.valueGetter) ? col.valueGetter(b) : ((_b = b[sortField]) !== null && _b !== void 0 ? _b : '');
|
|
356
|
+
const cmp = String(aVal !== null && aVal !== void 0 ? aVal : '').localeCompare(String(bVal !== null && bVal !== void 0 ? bVal : ''), undefined, { numeric: true, sensitivity: 'base' });
|
|
357
|
+
return sortDir === 'asc' ? cmp : -cmp;
|
|
358
|
+
});
|
|
359
|
+
}, [filteredUsers, sortField, sortDir, columns]);
|
|
360
|
+
const pagedUsers = useMemo(() => sortedUsers.slice(page * pageSize, page * pageSize + pageSize), [sortedUsers, page, pageSize]);
|
|
361
|
+
const handleSortClick = (field) => {
|
|
362
|
+
if (sortField === field) {
|
|
363
|
+
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
setSortField(field);
|
|
367
|
+
setSortDir('asc');
|
|
368
|
+
}
|
|
369
|
+
setPage(0);
|
|
370
|
+
};
|
|
371
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(error) }), loading && (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })), !loading && (_jsxs(Box, { sx: { width: '100%' }, children: [_jsx(TableContainer, { sx: { border: 1, borderColor: 'divider', borderRadius: 1 }, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { sx: { bgcolor: 'action.hover' }, children: _jsx(TableRow, { children: columns.map((col) => (_jsx(TableCell, { align: col.headerAlign || col.align || 'left', sortDirection: sortField === col.field ? sortDir : false, sx: { minWidth: col.minWidth, fontWeight: 600, whiteSpace: 'nowrap' }, children: col.sortable !== false ? (_jsx(TableSortLabel, { active: sortField === col.field, direction: sortField === col.field ? sortDir : 'asc', onClick: () => handleSortClick(col.field), children: col.headerName })) : (col.headerName) }, col.field))) }) }), _jsx(TableBody, { children: pagedUsers.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: columns.length, align: "center", sx: { py: 4, color: 'text.secondary' }, children: t('UserList.NO_USERS', 'No users found.') }) })) : (pagedUsers.map((row) => (_jsx(TableRow, { hover: true, children: columns.map((col) => (_jsx(TableCell, { align: col.align || 'left', sx: { verticalAlign: 'top', py: 1 }, children: col.renderCell(row) }, col.field))) }, row.id)))) })] }) }), _jsx(TablePagination, { component: "div", count: sortedUsers.length, page: page, onPageChange: (_, newPage) => setPage(newPage), rowsPerPage: pageSize, onRowsPerPageChange: (event) => {
|
|
372
|
+
setPageSize(Number(event.target.value));
|
|
373
|
+
setPage(0);
|
|
374
|
+
}, rowsPerPageOptions: [10, 25, 50, 100] })] }))] }));
|
|
365
375
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micha.bigler/ui-core-micha",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"@emotion/react": "^11.14.0",
|
|
13
13
|
"@emotion/styled": "^11.14.1",
|
|
14
14
|
"@mui/material": "^7.3.11",
|
|
15
|
-
"@mui/x-data-grid": "^8.28.6",
|
|
16
15
|
"axios": "^1.16.1",
|
|
17
16
|
"i18next": "^25.10.10 || ^26.0.0",
|
|
18
17
|
"qrcode.react": "^4.2.0",
|
|
@@ -10,8 +10,15 @@ import {
|
|
|
10
10
|
CircularProgress,
|
|
11
11
|
Alert,
|
|
12
12
|
TextField,
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableContainer,
|
|
17
|
+
TableHead,
|
|
18
|
+
TableRow,
|
|
19
|
+
TableSortLabel,
|
|
20
|
+
TablePagination,
|
|
13
21
|
} from '@mui/material';
|
|
14
|
-
import { DataGrid } from '@mui/x-data-grid';
|
|
15
22
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
16
23
|
import CancelIcon from '@mui/icons-material/Cancel';
|
|
17
24
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
@@ -20,7 +27,7 @@ import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
|
|
|
20
27
|
|
|
21
28
|
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
22
29
|
|
|
23
|
-
export function UserListComponent({
|
|
30
|
+
export function UserListComponent({
|
|
24
31
|
roles = DEFAULT_ROLES,
|
|
25
32
|
currentUser,
|
|
26
33
|
extraColumns = [],
|
|
@@ -42,6 +49,10 @@ export function UserListComponent({
|
|
|
42
49
|
const [error, setError] = useState(null);
|
|
43
50
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
44
51
|
const [searchQuery, setSearchQuery] = useState('');
|
|
52
|
+
const [sortField, setSortField] = useState('email');
|
|
53
|
+
const [sortDir, setSortDir] = useState('asc');
|
|
54
|
+
const [page, setPage] = useState(0);
|
|
55
|
+
const [pageSize, setPageSize] = useState(25);
|
|
45
56
|
const controlSx = { minWidth: 140 };
|
|
46
57
|
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
47
58
|
|
|
@@ -69,6 +80,10 @@ export function UserListComponent({
|
|
|
69
80
|
loadUsers();
|
|
70
81
|
}, [loadUsers, refreshTrigger]);
|
|
71
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setPage(0);
|
|
85
|
+
}, [searchQuery]);
|
|
86
|
+
|
|
72
87
|
const handleDelete = async (userId) => {
|
|
73
88
|
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?'))) return;
|
|
74
89
|
try {
|
|
@@ -106,14 +121,14 @@ export function UserListComponent({
|
|
|
106
121
|
const defaultCanEdit = (targetUser) => {
|
|
107
122
|
if (!currentUser) return false;
|
|
108
123
|
if (currentUser.is_superuser) return true;
|
|
109
|
-
|
|
124
|
+
|
|
110
125
|
const myRole = currentUser.role || 'none';
|
|
111
126
|
const targetRole = targetUser.role || 'none';
|
|
112
127
|
|
|
113
128
|
if (myRole === 'admin') return true;
|
|
114
|
-
|
|
129
|
+
|
|
115
130
|
if (myRole === 'teacher') {
|
|
116
|
-
if (targetUser.id === currentUser.id) return false;
|
|
131
|
+
if (targetUser.id === currentUser.id) return false;
|
|
117
132
|
if (['teacher', 'admin', 'supervisor'].includes(targetRole)) return false;
|
|
118
133
|
return true;
|
|
119
134
|
}
|
|
@@ -267,16 +282,23 @@ export function UserListComponent({
|
|
|
267
282
|
field: 'email',
|
|
268
283
|
headerName: t('Auth.EMAIL_LABEL', 'Email'),
|
|
269
284
|
minWidth: 220,
|
|
270
|
-
maxWidth: 340,
|
|
271
285
|
flex: 1,
|
|
286
|
+
sortable: true,
|
|
287
|
+
align: 'left',
|
|
288
|
+
headerAlign: 'left',
|
|
289
|
+
valueGetter: (row) => row?.email ?? '',
|
|
290
|
+
renderCell: (row) => row.email,
|
|
272
291
|
},
|
|
273
292
|
{
|
|
274
293
|
field: 'name',
|
|
275
294
|
headerName: t('Profile.NAME_LABEL', 'Name'),
|
|
276
295
|
minWidth: 180,
|
|
277
|
-
maxWidth: 260,
|
|
278
296
|
flex: 0.9,
|
|
279
|
-
|
|
297
|
+
sortable: true,
|
|
298
|
+
align: 'left',
|
|
299
|
+
headerAlign: 'left',
|
|
300
|
+
valueGetter: (row) => getUserDisplayName(row),
|
|
301
|
+
renderCell: (row) => getUserDisplayName(row),
|
|
280
302
|
},
|
|
281
303
|
];
|
|
282
304
|
|
|
@@ -285,13 +307,13 @@ export function UserListComponent({
|
|
|
285
307
|
field: 'is_new',
|
|
286
308
|
headerName: t('UserList.NEW', 'New'),
|
|
287
309
|
minWidth: 90,
|
|
288
|
-
maxWidth: 110,
|
|
289
310
|
flex: 0.35,
|
|
311
|
+
sortable: true,
|
|
290
312
|
align: 'center',
|
|
291
313
|
headerAlign: 'center',
|
|
292
|
-
valueGetter: (
|
|
293
|
-
renderCell: (
|
|
294
|
-
Boolean(
|
|
314
|
+
valueGetter: (row) => Boolean(row?.profile?.is_new || row?.is_new),
|
|
315
|
+
renderCell: (row) => renderBooleanStatusIcon(
|
|
316
|
+
Boolean(row?.profile?.is_new || row?.is_new),
|
|
295
317
|
t('Common.YES', 'Yes'),
|
|
296
318
|
t('Common.NO', 'No'),
|
|
297
319
|
),
|
|
@@ -303,13 +325,13 @@ export function UserListComponent({
|
|
|
303
325
|
field: 'successful_login',
|
|
304
326
|
headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
|
|
305
327
|
minWidth: 120,
|
|
306
|
-
maxWidth: 150,
|
|
307
328
|
flex: 0.4,
|
|
329
|
+
sortable: true,
|
|
308
330
|
align: 'center',
|
|
309
331
|
headerAlign: 'center',
|
|
310
|
-
valueGetter: (
|
|
311
|
-
renderCell: (
|
|
312
|
-
Boolean(
|
|
332
|
+
valueGetter: (row) => Boolean(row?.successful_login ?? row?.last_login),
|
|
333
|
+
renderCell: (row) => renderBooleanStatusIcon(
|
|
334
|
+
Boolean(row?.successful_login ?? row?.last_login),
|
|
313
335
|
t('Common.YES', 'Yes'),
|
|
314
336
|
t('Common.NO', 'No'),
|
|
315
337
|
),
|
|
@@ -321,23 +343,22 @@ export function UserListComponent({
|
|
|
321
343
|
field: 'role',
|
|
322
344
|
headerName: t('UserList.ROLE', 'Role'),
|
|
323
345
|
minWidth: 180,
|
|
324
|
-
maxWidth: 240,
|
|
325
346
|
flex: 0.7,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
</
|
|
338
|
-
</
|
|
339
|
-
|
|
340
|
-
|
|
347
|
+
sortable: true,
|
|
348
|
+
align: 'left',
|
|
349
|
+
headerAlign: 'left',
|
|
350
|
+
valueGetter: (row) => row?.role || 'none',
|
|
351
|
+
renderCell: (row) => (
|
|
352
|
+
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(row)}>
|
|
353
|
+
<Select
|
|
354
|
+
value={row.role || 'none'}
|
|
355
|
+
onChange={(event) => handleChangeRole(row.id, event.target.value)}
|
|
356
|
+
variant="outlined"
|
|
357
|
+
>
|
|
358
|
+
{roles.map((role) => <MenuItem key={role} value={role}>{role}</MenuItem>)}
|
|
359
|
+
</Select>
|
|
360
|
+
</FormControl>
|
|
361
|
+
),
|
|
341
362
|
});
|
|
342
363
|
}
|
|
343
364
|
|
|
@@ -345,16 +366,15 @@ export function UserListComponent({
|
|
|
345
366
|
field: `extra:${column.key}`,
|
|
346
367
|
headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
|
|
347
368
|
minWidth: Number(column.minWidth) || 180,
|
|
348
|
-
maxWidth: Number(column.maxWidth) || 320,
|
|
349
369
|
flex: Number(column.flex) || 0.9,
|
|
350
370
|
sortable: column.sortable !== false,
|
|
351
371
|
align: column.align || 'left',
|
|
352
372
|
headerAlign: column.align || 'left',
|
|
353
|
-
valueGetter: (
|
|
354
|
-
renderCell: (
|
|
373
|
+
valueGetter: (row) => getExtraColumnSortValue(column, row),
|
|
374
|
+
renderCell: (row) =>
|
|
355
375
|
column.renderCell({
|
|
356
|
-
user:
|
|
357
|
-
canEdit: canEdit(
|
|
376
|
+
user: row,
|
|
377
|
+
canEdit: canEdit(row),
|
|
358
378
|
currentUser,
|
|
359
379
|
extraContext,
|
|
360
380
|
t,
|
|
@@ -372,65 +392,61 @@ export function UserListComponent({
|
|
|
372
392
|
field: 'actions',
|
|
373
393
|
headerName: t('Common.ACTIONS', 'Actions'),
|
|
374
394
|
minWidth: Math.max(220, 110 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 110),
|
|
375
|
-
maxWidth: Math.max(360, 120 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 120),
|
|
376
395
|
flex: 1.1,
|
|
377
396
|
sortable: false,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
397
|
+
align: 'left',
|
|
398
|
+
headerAlign: 'left',
|
|
399
|
+
valueGetter: null,
|
|
400
|
+
renderCell: (row) => (
|
|
401
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', py: 0.5 }}>
|
|
402
|
+
{visibleRowActions.map((action) => {
|
|
403
|
+
const actionId = `${action.key}:${row.id}`;
|
|
404
|
+
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
405
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
406
|
+
? action.disabled({
|
|
407
|
+
user: row,
|
|
408
|
+
canEdit: canEdit(row),
|
|
409
|
+
currentUser,
|
|
410
|
+
extraContext,
|
|
411
|
+
t,
|
|
412
|
+
})
|
|
413
|
+
: false;
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<Button
|
|
417
|
+
key={`${action.key}-${row.id}`}
|
|
418
|
+
size="small"
|
|
419
|
+
variant="outlined"
|
|
420
|
+
onClick={() => runRowAction(action, row)}
|
|
421
|
+
disabled={isBusy || isDisabled}
|
|
422
|
+
sx={actionButtonSx}
|
|
423
|
+
>
|
|
424
|
+
{typeof action.label === 'function'
|
|
425
|
+
? action.label({ user: row, t, currentUser, canEdit: canEdit(row) })
|
|
426
|
+
: action.label}
|
|
427
|
+
</Button>
|
|
428
|
+
);
|
|
429
|
+
})}
|
|
430
|
+
|
|
431
|
+
{showDeleteAction && (
|
|
432
|
+
<Tooltip title={t('Common.DELETE', 'Delete')}>
|
|
433
|
+
<span>
|
|
399
434
|
<Button
|
|
400
|
-
key={`${action.key}-${user.id}`}
|
|
401
435
|
size="small"
|
|
402
436
|
variant="outlined"
|
|
403
|
-
|
|
404
|
-
|
|
437
|
+
color="error"
|
|
438
|
+
startIcon={<DeleteIcon />}
|
|
439
|
+
onClick={() => handleDelete(row.id)}
|
|
440
|
+
disabled={!canDelete(row)}
|
|
405
441
|
sx={actionButtonSx}
|
|
406
442
|
>
|
|
407
|
-
{
|
|
408
|
-
? action.label({ user, t, currentUser, canEdit: canEdit(user) })
|
|
409
|
-
: action.label}
|
|
443
|
+
{t('Common.DELETE', 'Delete')}
|
|
410
444
|
</Button>
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
<span>
|
|
417
|
-
<Button
|
|
418
|
-
size="small"
|
|
419
|
-
variant="outlined"
|
|
420
|
-
color="error"
|
|
421
|
-
startIcon={<DeleteIcon />}
|
|
422
|
-
onClick={() => handleDelete(user.id)}
|
|
423
|
-
disabled={!canDelete(user)}
|
|
424
|
-
sx={actionButtonSx}
|
|
425
|
-
>
|
|
426
|
-
{t('Common.DELETE', 'Delete')}
|
|
427
|
-
</Button>
|
|
428
|
-
</span>
|
|
429
|
-
</Tooltip>
|
|
430
|
-
)}
|
|
431
|
-
</Box>
|
|
432
|
-
);
|
|
433
|
-
},
|
|
445
|
+
</span>
|
|
446
|
+
</Tooltip>
|
|
447
|
+
)}
|
|
448
|
+
</Box>
|
|
449
|
+
),
|
|
434
450
|
};
|
|
435
451
|
|
|
436
452
|
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
@@ -452,6 +468,31 @@ export function UserListComponent({
|
|
|
452
468
|
canDelete,
|
|
453
469
|
]);
|
|
454
470
|
|
|
471
|
+
const sortedUsers = useMemo(() => {
|
|
472
|
+
const col = columns.find((c) => c.field === sortField);
|
|
473
|
+
return [...filteredUsers].sort((a, b) => {
|
|
474
|
+
const aVal = col?.valueGetter ? col.valueGetter(a) : (a[sortField] ?? '');
|
|
475
|
+
const bVal = col?.valueGetter ? col.valueGetter(b) : (b[sortField] ?? '');
|
|
476
|
+
const cmp = String(aVal ?? '').localeCompare(String(bVal ?? ''), undefined, { numeric: true, sensitivity: 'base' });
|
|
477
|
+
return sortDir === 'asc' ? cmp : -cmp;
|
|
478
|
+
});
|
|
479
|
+
}, [filteredUsers, sortField, sortDir, columns]);
|
|
480
|
+
|
|
481
|
+
const pagedUsers = useMemo(
|
|
482
|
+
() => sortedUsers.slice(page * pageSize, page * pageSize + pageSize),
|
|
483
|
+
[sortedUsers, page, pageSize],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const handleSortClick = (field) => {
|
|
487
|
+
if (sortField === field) {
|
|
488
|
+
setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
489
|
+
} else {
|
|
490
|
+
setSortField(field);
|
|
491
|
+
setSortDir('asc');
|
|
492
|
+
}
|
|
493
|
+
setPage(0);
|
|
494
|
+
};
|
|
495
|
+
|
|
455
496
|
return (
|
|
456
497
|
<Box>
|
|
457
498
|
<Typography variant="h6" gutterBottom>{t('UserList.TITLE', 'All Users')}</Typography>
|
|
@@ -466,7 +507,7 @@ export function UserListComponent({
|
|
|
466
507
|
onChange={(event) => setSearchQuery(event.target.value)}
|
|
467
508
|
/>
|
|
468
509
|
</Box>
|
|
469
|
-
|
|
510
|
+
|
|
470
511
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{t(error)}</Alert>}
|
|
471
512
|
|
|
472
513
|
{loading && (
|
|
@@ -476,40 +517,73 @@ export function UserListComponent({
|
|
|
476
517
|
)}
|
|
477
518
|
|
|
478
519
|
{!loading && (
|
|
479
|
-
<Box sx={{ width: '100%'
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
520
|
+
<Box sx={{ width: '100%' }}>
|
|
521
|
+
<TableContainer sx={{ border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
|
522
|
+
<Table size="small">
|
|
523
|
+
<TableHead sx={{ bgcolor: 'action.hover' }}>
|
|
524
|
+
<TableRow>
|
|
525
|
+
{columns.map((col) => (
|
|
526
|
+
<TableCell
|
|
527
|
+
key={col.field}
|
|
528
|
+
align={col.headerAlign || col.align || 'left'}
|
|
529
|
+
sortDirection={sortField === col.field ? sortDir : false}
|
|
530
|
+
sx={{ minWidth: col.minWidth, fontWeight: 600, whiteSpace: 'nowrap' }}
|
|
531
|
+
>
|
|
532
|
+
{col.sortable !== false ? (
|
|
533
|
+
<TableSortLabel
|
|
534
|
+
active={sortField === col.field}
|
|
535
|
+
direction={sortField === col.field ? sortDir : 'asc'}
|
|
536
|
+
onClick={() => handleSortClick(col.field)}
|
|
537
|
+
>
|
|
538
|
+
{col.headerName}
|
|
539
|
+
</TableSortLabel>
|
|
540
|
+
) : (
|
|
541
|
+
col.headerName
|
|
542
|
+
)}
|
|
543
|
+
</TableCell>
|
|
544
|
+
))}
|
|
545
|
+
</TableRow>
|
|
546
|
+
</TableHead>
|
|
547
|
+
<TableBody>
|
|
548
|
+
{pagedUsers.length === 0 ? (
|
|
549
|
+
<TableRow>
|
|
550
|
+
<TableCell
|
|
551
|
+
colSpan={columns.length}
|
|
552
|
+
align="center"
|
|
553
|
+
sx={{ py: 4, color: 'text.secondary' }}
|
|
554
|
+
>
|
|
555
|
+
{t('UserList.NO_USERS', 'No users found.')}
|
|
556
|
+
</TableCell>
|
|
557
|
+
</TableRow>
|
|
558
|
+
) : (
|
|
559
|
+
pagedUsers.map((row) => (
|
|
560
|
+
<TableRow key={row.id} hover>
|
|
561
|
+
{columns.map((col) => (
|
|
562
|
+
<TableCell
|
|
563
|
+
key={col.field}
|
|
564
|
+
align={col.align || 'left'}
|
|
565
|
+
sx={{ verticalAlign: 'top', py: 1 }}
|
|
566
|
+
>
|
|
567
|
+
{col.renderCell(row)}
|
|
568
|
+
</TableCell>
|
|
569
|
+
))}
|
|
570
|
+
</TableRow>
|
|
571
|
+
))
|
|
572
|
+
)}
|
|
573
|
+
</TableBody>
|
|
574
|
+
</Table>
|
|
575
|
+
</TableContainer>
|
|
576
|
+
<TablePagination
|
|
577
|
+
component="div"
|
|
578
|
+
count={sortedUsers.length}
|
|
579
|
+
page={page}
|
|
580
|
+
onPageChange={(_, newPage) => setPage(newPage)}
|
|
581
|
+
rowsPerPage={pageSize}
|
|
582
|
+
onRowsPerPageChange={(event) => {
|
|
583
|
+
setPageSize(Number(event.target.value));
|
|
584
|
+
setPage(0);
|
|
512
585
|
}}
|
|
586
|
+
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
513
587
|
/>
|
|
514
588
|
</Box>
|
|
515
589
|
)}
|