@micha.bigler/ui-core-micha 2.1.20 → 2.2.1
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/dist/auth/AuthContext.js +4 -0
- package/dist/auth/authApi.js +45 -3
- package/dist/components/AccessCodeManager.js +39 -3
- package/dist/components/AuthFactorRequirementCard.js +49 -0
- package/dist/components/BulkInviteCsvTab.js +2 -2
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/QrSignupManager.js +81 -0
- package/dist/components/RegistrationMethodsManager.js +91 -0
- package/dist/components/UserInviteComponent.js +2 -4
- package/dist/components/UserListComponent.js +130 -105
- package/dist/index.js +3 -0
- package/dist/pages/AccountPage.js +6 -2
- package/dist/pages/LoginPage.js +31 -25
- package/dist/pages/PasswordInvitePage.js +6 -1
- package/dist/pages/SignUpPage.js +76 -16
- package/package.json +2 -1
- package/src/auth/AuthContext.jsx +4 -0
- package/src/auth/authApi.jsx +51 -3
- package/src/components/AccessCodeManager.jsx +71 -8
- package/src/components/AuthFactorRequirementCard.jsx +74 -0
- package/src/components/BulkInviteCsvTab.jsx +2 -2
- package/src/components/LoginForm.jsx +7 -6
- package/src/components/QrSignupManager.jsx +128 -0
- package/src/components/RegistrationMethodsManager.jsx +184 -0
- package/src/components/UserInviteComponent.jsx +2 -4
- package/src/components/UserListComponent.jsx +216 -246
- package/src/index.js +3 -0
- package/src/pages/AccountPage.jsx +23 -1
- package/src/pages/LoginPage.jsx +43 -23
- package/src/pages/PasswordInvitePage.jsx +6 -1
- package/src/pages/SignUpPage.jsx +145 -30
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import React, { useMemo, useState, useEffect } from 'react';
|
|
1
|
+
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
Box,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Box,
|
|
4
|
+
Typography,
|
|
5
|
+
FormControl,
|
|
6
|
+
Select,
|
|
7
|
+
MenuItem,
|
|
8
|
+
Button,
|
|
9
|
+
Tooltip,
|
|
10
|
+
CircularProgress,
|
|
11
|
+
Alert,
|
|
12
|
+
TextField,
|
|
7
13
|
} from '@mui/material';
|
|
14
|
+
import { DataGrid } from '@mui/x-data-grid';
|
|
8
15
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
9
16
|
import { useTranslation } from 'react-i18next';
|
|
10
17
|
import { fetchUsersList, deleteUser, updateUserRole, updateUserSupportStatus } from '../auth/authApi';
|
|
@@ -26,17 +33,13 @@ export function UserListComponent({
|
|
|
26
33
|
const [error, setError] = useState(null);
|
|
27
34
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
28
35
|
const [searchQuery, setSearchQuery] = useState('');
|
|
29
|
-
const [sortConfig, setSortConfig] = useState({ key: 'id', direction: 'asc' });
|
|
30
|
-
const cellSx = { verticalAlign: 'middle' };
|
|
31
36
|
const controlSx = { minWidth: 140 };
|
|
32
37
|
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
33
38
|
|
|
34
|
-
const loadUsers = async () => {
|
|
39
|
+
const loadUsers = useCallback(async () => {
|
|
35
40
|
setLoading(true);
|
|
36
41
|
setError(null);
|
|
37
42
|
try {
|
|
38
|
-
// FIX: Removed apiUrl parameter.
|
|
39
|
-
// fetchUsersList uses USERS_BASE from authConfig internally.
|
|
40
43
|
const data = await fetchUsersList();
|
|
41
44
|
const list = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : []);
|
|
42
45
|
// Keep row identity stable even if backend returns unordered results.
|
|
@@ -51,17 +54,15 @@ export function UserListComponent({
|
|
|
51
54
|
} finally {
|
|
52
55
|
setLoading(false);
|
|
53
56
|
}
|
|
54
|
-
};
|
|
57
|
+
}, []);
|
|
55
58
|
|
|
56
59
|
useEffect(() => {
|
|
57
60
|
loadUsers();
|
|
58
|
-
|
|
59
|
-
}, [refreshTrigger]); // Dependency on apiUrl removed
|
|
61
|
+
}, [loadUsers, refreshTrigger]);
|
|
60
62
|
|
|
61
63
|
const handleDelete = async (userId) => {
|
|
62
64
|
if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?'))) return;
|
|
63
65
|
try {
|
|
64
|
-
// FIX: Removed apiUrl parameter.
|
|
65
66
|
await deleteUser(userId);
|
|
66
67
|
setUsers(prev => prev.filter(u => u.id !== userId));
|
|
67
68
|
} catch (err) {
|
|
@@ -71,7 +72,6 @@ export function UserListComponent({
|
|
|
71
72
|
|
|
72
73
|
const handleChangeRole = async (userId, newRole) => {
|
|
73
74
|
try {
|
|
74
|
-
// FIX: Removed apiUrl parameter.
|
|
75
75
|
await updateUserRole(userId, newRole);
|
|
76
76
|
await loadUsers();
|
|
77
77
|
} catch (err) {
|
|
@@ -81,7 +81,6 @@ export function UserListComponent({
|
|
|
81
81
|
|
|
82
82
|
const handleToggleSupporter = async (userId, newValue) => {
|
|
83
83
|
try {
|
|
84
|
-
// FIX: Removed apiUrl parameter.
|
|
85
84
|
await updateUserSupportStatus(userId, newValue);
|
|
86
85
|
await loadUsers();
|
|
87
86
|
} catch (err) {
|
|
@@ -117,7 +116,7 @@ export function UserListComponent({
|
|
|
117
116
|
extraContext,
|
|
118
117
|
t,
|
|
119
118
|
reloadUsers: loadUsers,
|
|
120
|
-
}), [currentUser, extraContext, t]);
|
|
119
|
+
}), [currentUser, extraContext, t, loadUsers]);
|
|
121
120
|
|
|
122
121
|
const visibleExtraColumns = useMemo(
|
|
123
122
|
() =>
|
|
@@ -172,45 +171,6 @@ export function UserListComponent({
|
|
|
172
171
|
return getExtraColumnSortValue(column, user);
|
|
173
172
|
};
|
|
174
173
|
|
|
175
|
-
const normalizeSortValue = (value) => {
|
|
176
|
-
if (value === null || value === undefined) return '';
|
|
177
|
-
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
178
|
-
if (typeof value === 'number') return value;
|
|
179
|
-
if (value instanceof Date) return value.getTime();
|
|
180
|
-
return String(value).toLocaleLowerCase();
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const compareSortValues = (a, b) => {
|
|
184
|
-
const av = normalizeSortValue(a);
|
|
185
|
-
const bv = normalizeSortValue(b);
|
|
186
|
-
|
|
187
|
-
if (typeof av === 'number' && typeof bv === 'number') {
|
|
188
|
-
return av - bv;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return String(av).localeCompare(String(bv), undefined, {
|
|
192
|
-
numeric: true,
|
|
193
|
-
sensitivity: 'base',
|
|
194
|
-
});
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const getSortValueByKey = (user, sortKey) => {
|
|
198
|
-
if (sortKey === 'id') return Number(user?.id ?? 0);
|
|
199
|
-
if (sortKey === 'email') return user?.email || '';
|
|
200
|
-
if (sortKey === 'name') return getUserDisplayName(user);
|
|
201
|
-
if (sortKey === 'role') return user?.role || '';
|
|
202
|
-
if (sortKey === 'is_support_agent') return Boolean(user?.is_support_agent);
|
|
203
|
-
|
|
204
|
-
if (sortKey.startsWith('extra:')) {
|
|
205
|
-
const extraKey = sortKey.slice('extra:'.length);
|
|
206
|
-
const column = visibleExtraColumns.find((col) => String(col.key) === extraKey);
|
|
207
|
-
if (!column) return '';
|
|
208
|
-
return getExtraColumnSortValue(column, user);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return user?.[sortKey] ?? '';
|
|
212
|
-
};
|
|
213
|
-
|
|
214
174
|
const toSearchText = (value) => {
|
|
215
175
|
if (value === null || value === undefined) return '';
|
|
216
176
|
if (Array.isArray(value)) return value.map((item) => toSearchText(item)).join(' ');
|
|
@@ -238,37 +198,13 @@ export function UserListComponent({
|
|
|
238
198
|
|
|
239
199
|
const normalizedSearch = searchQuery.trim().toLocaleLowerCase();
|
|
240
200
|
|
|
241
|
-
const
|
|
242
|
-
|
|
201
|
+
const filteredUsers = useMemo(() => {
|
|
202
|
+
return users.filter((user) => {
|
|
243
203
|
if (!normalizedSearch) return true;
|
|
244
204
|
const bucket = getSearchBucketForUser(user);
|
|
245
205
|
return bucket.some((entry) => entry.includes(normalizedSearch));
|
|
246
206
|
});
|
|
247
|
-
|
|
248
|
-
const sorted = [...filtered].sort((a, b) => {
|
|
249
|
-
const aValue = getSortValueByKey(a, sortConfig.key);
|
|
250
|
-
const bValue = getSortValueByKey(b, sortConfig.key);
|
|
251
|
-
const cmp = compareSortValues(aValue, bValue);
|
|
252
|
-
if (cmp !== 0) return sortConfig.direction === 'asc' ? cmp : -cmp;
|
|
253
|
-
|
|
254
|
-
// Stable fallback
|
|
255
|
-
return Number(a?.id ?? 0) - Number(b?.id ?? 0);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
return sorted;
|
|
259
|
-
}, [users, normalizedSearch, sortConfig, visibleExtraColumns, currentUser, extraContext, t]);
|
|
260
|
-
|
|
261
|
-
const handleSort = (columnKey) => {
|
|
262
|
-
setSortConfig((prev) => {
|
|
263
|
-
if (prev.key === columnKey) {
|
|
264
|
-
return {
|
|
265
|
-
key: columnKey,
|
|
266
|
-
direction: prev.direction === 'asc' ? 'desc' : 'asc',
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
return { key: columnKey, direction: 'asc' };
|
|
270
|
-
});
|
|
271
|
-
};
|
|
207
|
+
}, [users, normalizedSearch, visibleExtraColumns, currentUser, extraContext, t]);
|
|
272
208
|
|
|
273
209
|
const runRowAction = async (action, user) => {
|
|
274
210
|
const actionId = `${action.key}:${user.id}`;
|
|
@@ -290,6 +226,162 @@ export function UserListComponent({
|
|
|
290
226
|
}
|
|
291
227
|
};
|
|
292
228
|
|
|
229
|
+
const columns = useMemo(() => {
|
|
230
|
+
const baseColumns = [
|
|
231
|
+
{
|
|
232
|
+
field: 'email',
|
|
233
|
+
headerName: t('Auth.EMAIL_LABEL', 'Email'),
|
|
234
|
+
minWidth: 240,
|
|
235
|
+
flex: 1.2,
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
field: 'name',
|
|
239
|
+
headerName: t('Profile.NAME_LABEL', 'Name'),
|
|
240
|
+
minWidth: 220,
|
|
241
|
+
flex: 1,
|
|
242
|
+
valueGetter: (_value, row) => getUserDisplayName(row),
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
field: 'role',
|
|
246
|
+
headerName: t('UserList.ROLE', 'Role'),
|
|
247
|
+
minWidth: 180,
|
|
248
|
+
flex: 0.8,
|
|
249
|
+
valueGetter: (_value, row) => row?.role || 'none',
|
|
250
|
+
renderCell: (params) => {
|
|
251
|
+
const user = params.row;
|
|
252
|
+
return (
|
|
253
|
+
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(user)}>
|
|
254
|
+
<Select
|
|
255
|
+
value={user.role || 'none'}
|
|
256
|
+
onChange={(event) => handleChangeRole(user.id, event.target.value)}
|
|
257
|
+
variant="outlined"
|
|
258
|
+
>
|
|
259
|
+
{roles.map((role) => <MenuItem key={role} value={role}>{role}</MenuItem>)}
|
|
260
|
+
</Select>
|
|
261
|
+
</FormControl>
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
field: 'is_support_agent',
|
|
267
|
+
headerName: t('UserList.SUPPORTER', 'Support Agent'),
|
|
268
|
+
minWidth: 190,
|
|
269
|
+
flex: 0.8,
|
|
270
|
+
valueGetter: (_value, row) => Boolean(row?.is_support_agent),
|
|
271
|
+
renderCell: (params) => {
|
|
272
|
+
const user = params.row;
|
|
273
|
+
return (
|
|
274
|
+
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(user)}>
|
|
275
|
+
<Select
|
|
276
|
+
value={user.is_support_agent ? 'yes' : 'no'}
|
|
277
|
+
onChange={(event) => handleToggleSupporter(user.id, event.target.value === 'yes')}
|
|
278
|
+
variant="outlined"
|
|
279
|
+
>
|
|
280
|
+
<MenuItem value="yes">{t('Common.YES', 'Yes')}</MenuItem>
|
|
281
|
+
<MenuItem value="no">{t('Common.NO', 'No')}</MenuItem>
|
|
282
|
+
</Select>
|
|
283
|
+
</FormControl>
|
|
284
|
+
);
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
const mappedExtraColumns = visibleExtraColumns.map((column) => ({
|
|
290
|
+
field: `extra:${column.key}`,
|
|
291
|
+
headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
|
|
292
|
+
minWidth: Number(column.minWidth) || 180,
|
|
293
|
+
flex: Number(column.flex) || 0.9,
|
|
294
|
+
sortable: column.sortable !== false,
|
|
295
|
+
align: column.align || 'left',
|
|
296
|
+
headerAlign: column.align || 'left',
|
|
297
|
+
valueGetter: (_value, row) => getExtraColumnSortValue(column, row),
|
|
298
|
+
renderCell: (params) =>
|
|
299
|
+
column.renderCell({
|
|
300
|
+
user: params.row,
|
|
301
|
+
canEdit: canEdit(params.row),
|
|
302
|
+
currentUser,
|
|
303
|
+
extraContext,
|
|
304
|
+
t,
|
|
305
|
+
reloadUsers: loadUsers,
|
|
306
|
+
}),
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
const actionColumn = {
|
|
310
|
+
field: 'actions',
|
|
311
|
+
headerName: t('Common.ACTIONS', 'Actions'),
|
|
312
|
+
minWidth: Math.max(220, 110 + visibleRowActions.length * 110),
|
|
313
|
+
flex: 1.4,
|
|
314
|
+
sortable: false,
|
|
315
|
+
filterable: false,
|
|
316
|
+
disableColumnMenu: true,
|
|
317
|
+
renderCell: (params) => {
|
|
318
|
+
const user = params.row;
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', py: 0.5 }}>
|
|
322
|
+
{visibleRowActions.map((action) => {
|
|
323
|
+
const actionId = `${action.key}:${user.id}`;
|
|
324
|
+
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
325
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
326
|
+
? action.disabled({
|
|
327
|
+
user,
|
|
328
|
+
canEdit: canEdit(user),
|
|
329
|
+
currentUser,
|
|
330
|
+
extraContext,
|
|
331
|
+
t,
|
|
332
|
+
})
|
|
333
|
+
: false;
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<Button
|
|
337
|
+
key={`${action.key}-${user.id}`}
|
|
338
|
+
size="small"
|
|
339
|
+
variant="outlined"
|
|
340
|
+
onClick={() => runRowAction(action, user)}
|
|
341
|
+
disabled={isBusy || isDisabled}
|
|
342
|
+
sx={actionButtonSx}
|
|
343
|
+
>
|
|
344
|
+
{typeof action.label === 'function'
|
|
345
|
+
? action.label({ user, t, currentUser, canEdit: canEdit(user) })
|
|
346
|
+
: action.label}
|
|
347
|
+
</Button>
|
|
348
|
+
);
|
|
349
|
+
})}
|
|
350
|
+
|
|
351
|
+
<Tooltip title={t('Common.DELETE', 'Delete')}>
|
|
352
|
+
<span>
|
|
353
|
+
<Button
|
|
354
|
+
size="small"
|
|
355
|
+
variant="outlined"
|
|
356
|
+
color="error"
|
|
357
|
+
startIcon={<DeleteIcon />}
|
|
358
|
+
onClick={() => handleDelete(user.id)}
|
|
359
|
+
disabled={!canEdit(user)}
|
|
360
|
+
sx={actionButtonSx}
|
|
361
|
+
>
|
|
362
|
+
{t('Common.DELETE', 'Delete')}
|
|
363
|
+
</Button>
|
|
364
|
+
</span>
|
|
365
|
+
</Tooltip>
|
|
366
|
+
</Box>
|
|
367
|
+
);
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
372
|
+
}, [
|
|
373
|
+
t,
|
|
374
|
+
roles,
|
|
375
|
+
visibleExtraColumns,
|
|
376
|
+
visibleRowActions,
|
|
377
|
+
listContext,
|
|
378
|
+
rowActionLoading,
|
|
379
|
+
currentUser,
|
|
380
|
+
extraContext,
|
|
381
|
+
loadUsers,
|
|
382
|
+
canEdit,
|
|
383
|
+
]);
|
|
384
|
+
|
|
293
385
|
return (
|
|
294
386
|
<Box>
|
|
295
387
|
<Typography variant="h6" gutterBottom>{t('UserList.TITLE', 'All Users')}</Typography>
|
|
@@ -307,171 +399,49 @@ export function UserListComponent({
|
|
|
307
399
|
|
|
308
400
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{t(error)}</Alert>}
|
|
309
401
|
|
|
310
|
-
{loading
|
|
402
|
+
{loading && (
|
|
311
403
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
|
|
312
|
-
|
|
404
|
+
<CircularProgress />
|
|
405
|
+
</Box>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
{!loading && (
|
|
409
|
+
<Box sx={{ width: '100%', minHeight: 520 }}>
|
|
410
|
+
<DataGrid
|
|
411
|
+
rows={filteredUsers}
|
|
412
|
+
columns={columns}
|
|
413
|
+
disableRowSelectionOnClick
|
|
414
|
+
showToolbar
|
|
415
|
+
getRowHeight={() => 'auto'}
|
|
416
|
+
pageSizeOptions={[10, 25, 50, 100]}
|
|
417
|
+
initialState={{
|
|
418
|
+
sorting: { sortModel: [{ field: 'email', sort: 'asc' }] },
|
|
419
|
+
pagination: { paginationModel: { pageSize: 25, page: 0 } },
|
|
420
|
+
}}
|
|
421
|
+
localeText={{
|
|
422
|
+
noRowsLabel: t('UserList.NO_USERS', 'No users found.'),
|
|
423
|
+
toolbarQuickFilterPlaceholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'),
|
|
424
|
+
}}
|
|
425
|
+
slotProps={{
|
|
426
|
+
toolbar: {
|
|
427
|
+
showQuickFilter: true,
|
|
428
|
+
quickFilterProps: { debounceMs: 300 },
|
|
429
|
+
},
|
|
430
|
+
}}
|
|
431
|
+
sx={{
|
|
432
|
+
border: 1,
|
|
433
|
+
borderColor: 'divider',
|
|
434
|
+
'& .MuiDataGrid-cell': {
|
|
435
|
+
display: 'flex',
|
|
436
|
+
alignItems: 'flex-start',
|
|
437
|
+
py: 1,
|
|
438
|
+
},
|
|
439
|
+
'& .MuiDataGrid-columnHeaders': {
|
|
440
|
+
bgcolor: 'action.hover',
|
|
441
|
+
},
|
|
442
|
+
}}
|
|
443
|
+
/>
|
|
313
444
|
</Box>
|
|
314
|
-
) : (
|
|
315
|
-
<TableContainer component={Paper}>
|
|
316
|
-
<Table size="small">
|
|
317
|
-
<TableHead>
|
|
318
|
-
<TableRow>
|
|
319
|
-
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'email' ? sortConfig.direction : false}>
|
|
320
|
-
<TableSortLabel
|
|
321
|
-
active={sortConfig.key === 'email'}
|
|
322
|
-
direction={sortConfig.key === 'email' ? sortConfig.direction : 'asc'}
|
|
323
|
-
onClick={() => handleSort('email')}
|
|
324
|
-
>
|
|
325
|
-
{t('Auth.EMAIL_LABEL', 'Email')}
|
|
326
|
-
</TableSortLabel>
|
|
327
|
-
</TableCell>
|
|
328
|
-
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'name' ? sortConfig.direction : false}>
|
|
329
|
-
<TableSortLabel
|
|
330
|
-
active={sortConfig.key === 'name'}
|
|
331
|
-
direction={sortConfig.key === 'name' ? sortConfig.direction : 'asc'}
|
|
332
|
-
onClick={() => handleSort('name')}
|
|
333
|
-
>
|
|
334
|
-
{t('Profile.NAME_LABEL', 'Name')}
|
|
335
|
-
</TableSortLabel>
|
|
336
|
-
</TableCell>
|
|
337
|
-
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'role' ? sortConfig.direction : false}>
|
|
338
|
-
<TableSortLabel
|
|
339
|
-
active={sortConfig.key === 'role'}
|
|
340
|
-
direction={sortConfig.key === 'role' ? sortConfig.direction : 'asc'}
|
|
341
|
-
onClick={() => handleSort('role')}
|
|
342
|
-
>
|
|
343
|
-
{t('UserList.ROLE', 'Role')}
|
|
344
|
-
</TableSortLabel>
|
|
345
|
-
</TableCell>
|
|
346
|
-
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'is_support_agent' ? sortConfig.direction : false}>
|
|
347
|
-
<TableSortLabel
|
|
348
|
-
active={sortConfig.key === 'is_support_agent'}
|
|
349
|
-
direction={sortConfig.key === 'is_support_agent' ? sortConfig.direction : 'asc'}
|
|
350
|
-
onClick={() => handleSort('is_support_agent')}
|
|
351
|
-
>
|
|
352
|
-
{t('UserList.SUPPORTER', 'Support Agent')}
|
|
353
|
-
</TableSortLabel>
|
|
354
|
-
</TableCell>
|
|
355
|
-
{visibleExtraColumns.map((column) => (
|
|
356
|
-
<TableCell
|
|
357
|
-
key={column.key}
|
|
358
|
-
sx={cellSx}
|
|
359
|
-
align={column.align || 'left'}
|
|
360
|
-
sortDirection={sortConfig.key === `extra:${column.key}` ? sortConfig.direction : false}
|
|
361
|
-
>
|
|
362
|
-
<TableSortLabel
|
|
363
|
-
active={sortConfig.key === `extra:${column.key}`}
|
|
364
|
-
direction={sortConfig.key === `extra:${column.key}` ? sortConfig.direction : 'asc'}
|
|
365
|
-
onClick={() => handleSort(`extra:${column.key}`)}
|
|
366
|
-
>
|
|
367
|
-
{typeof column.label === 'function' ? column.label(listContext) : column.label}
|
|
368
|
-
</TableSortLabel>
|
|
369
|
-
</TableCell>
|
|
370
|
-
))}
|
|
371
|
-
<TableCell sx={cellSx}>{t('Common.ACTIONS', 'Actions')}</TableCell>
|
|
372
|
-
</TableRow>
|
|
373
|
-
</TableHead>
|
|
374
|
-
<TableBody>
|
|
375
|
-
{filteredAndSortedUsers.map((u) => (
|
|
376
|
-
<TableRow key={u.id}>
|
|
377
|
-
<TableCell sx={cellSx}>{u.email}</TableCell>
|
|
378
|
-
<TableCell sx={cellSx}>
|
|
379
|
-
{getUserDisplayName(u)}
|
|
380
|
-
</TableCell>
|
|
381
|
-
<TableCell sx={cellSx}>
|
|
382
|
-
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(u)}>
|
|
383
|
-
<Select
|
|
384
|
-
value={u.role || 'none'}
|
|
385
|
-
onChange={(e) => handleChangeRole(u.id, e.target.value)}
|
|
386
|
-
variant="outlined"
|
|
387
|
-
>
|
|
388
|
-
{roles.map(r => <MenuItem key={r} value={r}>{r}</MenuItem>)}
|
|
389
|
-
</Select>
|
|
390
|
-
</FormControl>
|
|
391
|
-
</TableCell>
|
|
392
|
-
<TableCell sx={cellSx}>
|
|
393
|
-
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(u)}>
|
|
394
|
-
<Select
|
|
395
|
-
value={u.is_support_agent ? 'yes' : 'no'}
|
|
396
|
-
onChange={(e) => handleToggleSupporter(u.id, e.target.value === 'yes')}
|
|
397
|
-
variant="outlined"
|
|
398
|
-
>
|
|
399
|
-
<MenuItem value="yes">{t('Common.YES', 'Yes')}</MenuItem>
|
|
400
|
-
<MenuItem value="no">{t('Common.NO', 'No')}</MenuItem>
|
|
401
|
-
</Select>
|
|
402
|
-
</FormControl>
|
|
403
|
-
</TableCell>
|
|
404
|
-
{visibleExtraColumns.map((column) => (
|
|
405
|
-
<TableCell key={`${column.key}-${u.id}`} sx={cellSx} align={column.align || 'left'}>
|
|
406
|
-
{column.renderCell({
|
|
407
|
-
user: u,
|
|
408
|
-
canEdit: canEdit(u),
|
|
409
|
-
currentUser,
|
|
410
|
-
extraContext,
|
|
411
|
-
t,
|
|
412
|
-
reloadUsers: loadUsers,
|
|
413
|
-
})}
|
|
414
|
-
</TableCell>
|
|
415
|
-
))}
|
|
416
|
-
<TableCell sx={cellSx}>
|
|
417
|
-
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>
|
|
418
|
-
{visibleRowActions.map((action) => {
|
|
419
|
-
const actionId = `${action.key}:${u.id}`;
|
|
420
|
-
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
421
|
-
const isDisabled = typeof action.disabled === 'function'
|
|
422
|
-
? action.disabled({
|
|
423
|
-
user: u,
|
|
424
|
-
canEdit: canEdit(u),
|
|
425
|
-
currentUser,
|
|
426
|
-
extraContext,
|
|
427
|
-
t,
|
|
428
|
-
})
|
|
429
|
-
: false;
|
|
430
|
-
|
|
431
|
-
return (
|
|
432
|
-
<Button
|
|
433
|
-
key={`${action.key}-${u.id}`}
|
|
434
|
-
size="small"
|
|
435
|
-
variant="outlined"
|
|
436
|
-
onClick={() => runRowAction(action, u)}
|
|
437
|
-
disabled={isBusy || isDisabled}
|
|
438
|
-
sx={actionButtonSx}
|
|
439
|
-
>
|
|
440
|
-
{typeof action.label === 'function'
|
|
441
|
-
? action.label({ user: u, t, currentUser, canEdit: canEdit(u) })
|
|
442
|
-
: action.label}
|
|
443
|
-
</Button>
|
|
444
|
-
);
|
|
445
|
-
})}
|
|
446
|
-
<Tooltip title={t('Common.DELETE')}>
|
|
447
|
-
<span>
|
|
448
|
-
<Button
|
|
449
|
-
size="small"
|
|
450
|
-
variant="outlined"
|
|
451
|
-
color="error"
|
|
452
|
-
startIcon={<DeleteIcon />}
|
|
453
|
-
onClick={() => handleDelete(u.id)}
|
|
454
|
-
disabled={!canEdit(u)}
|
|
455
|
-
sx={actionButtonSx}
|
|
456
|
-
>
|
|
457
|
-
{t('Common.DELETE', 'Delete')}
|
|
458
|
-
</Button>
|
|
459
|
-
</span>
|
|
460
|
-
</Tooltip>
|
|
461
|
-
</Box>
|
|
462
|
-
</TableCell>
|
|
463
|
-
</TableRow>
|
|
464
|
-
))}
|
|
465
|
-
{filteredAndSortedUsers.length === 0 && (
|
|
466
|
-
<TableRow>
|
|
467
|
-
<TableCell colSpan={5 + visibleExtraColumns.length} align="center">
|
|
468
|
-
{t('UserList.NO_USERS', 'No users found.')}
|
|
469
|
-
</TableCell>
|
|
470
|
-
</TableRow>
|
|
471
|
-
)}
|
|
472
|
-
</TableBody>
|
|
473
|
-
</Table>
|
|
474
|
-
</TableContainer>
|
|
475
445
|
)}
|
|
476
446
|
</Box>
|
|
477
447
|
);
|
package/src/index.js
CHANGED
|
@@ -28,6 +28,9 @@ export { AccessCodeManager } from './components/AccessCodeManager';
|
|
|
28
28
|
export { UserListComponent } from './components/UserListComponent';
|
|
29
29
|
export { UserInviteComponent } from './components/UserInviteComponent';
|
|
30
30
|
export { BulkInviteCsvTab } from './components/BulkInviteCsvTab';
|
|
31
|
+
export { RegistrationMethodsManager } from './components/RegistrationMethodsManager';
|
|
32
|
+
export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCard';
|
|
33
|
+
export { QrSignupManager } from './components/QrSignupManager';
|
|
31
34
|
|
|
32
35
|
// --- 6. Translations ---
|
|
33
36
|
export { authTranslations } from './i18n/authTranslations';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, useMemo } from 'react';
|
|
1
|
+
import React, { useContext, useMemo, useState } from 'react';
|
|
2
2
|
import { Helmet } from 'react-helmet';
|
|
3
3
|
import { useSearchParams } from 'react-router-dom';
|
|
4
4
|
import {
|
|
@@ -23,6 +23,9 @@ import { SecurityComponent } from '../components/SecurityComponent';
|
|
|
23
23
|
import { UserListComponent } from '../components/UserListComponent';
|
|
24
24
|
import { UserInviteComponent } from '../components/UserInviteComponent';
|
|
25
25
|
import { AccessCodeManager } from '../components/AccessCodeManager';
|
|
26
|
+
import { RegistrationMethodsManager } from '../components/RegistrationMethodsManager';
|
|
27
|
+
import { AuthFactorRequirementCard } from '../components/AuthFactorRequirementCard';
|
|
28
|
+
import { QrSignupManager } from '../components/QrSignupManager';
|
|
26
29
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
27
30
|
import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
|
|
28
31
|
import { updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
|
|
@@ -40,6 +43,7 @@ export function AccountPage({
|
|
|
40
43
|
const { t } = useTranslation();
|
|
41
44
|
const { user, login, loading } = useContext(AuthContext);
|
|
42
45
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
46
|
+
const [authPolicy, setAuthPolicy] = useState(null);
|
|
43
47
|
|
|
44
48
|
// 1. URL State Management
|
|
45
49
|
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
@@ -188,6 +192,18 @@ export function AccountPage({
|
|
|
188
192
|
{safeTab === 'invite' && (
|
|
189
193
|
<Box sx={{ mt: 2 }}>
|
|
190
194
|
<Stack spacing={2.5}>
|
|
195
|
+
{(isSuperUser || perms.can_invite) && (
|
|
196
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
197
|
+
<RegistrationMethodsManager onPolicyChange={setAuthPolicy} />
|
|
198
|
+
</Paper>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{(isSuperUser || perms.can_invite) && (
|
|
202
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
203
|
+
<AuthFactorRequirementCard />
|
|
204
|
+
</Paper>
|
|
205
|
+
)}
|
|
206
|
+
|
|
191
207
|
{(isSuperUser || perms.can_invite) && (
|
|
192
208
|
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
193
209
|
<UserInviteComponent />
|
|
@@ -208,6 +224,12 @@ export function AccountPage({
|
|
|
208
224
|
<BulkInviteCsvTab {...bulkInviteCsvProps} />
|
|
209
225
|
</Paper>
|
|
210
226
|
)}
|
|
227
|
+
|
|
228
|
+
{(isSuperUser || perms.can_invite) && (
|
|
229
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
230
|
+
<QrSignupManager enabled={Boolean(authPolicy?.allow_self_signup_qr)} />
|
|
231
|
+
</Paper>
|
|
232
|
+
)}
|
|
211
233
|
</Stack>
|
|
212
234
|
</Box>
|
|
213
235
|
)}
|