@micha.bigler/ui-core-micha 2.2.7 → 2.2.9

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.
@@ -2,11 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useMemo, useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Typography, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TextField, } from '@mui/material';
4
4
  import { DataGrid } from '@mui/x-data-grid';
5
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
6
+ import CancelIcon from '@mui/icons-material/Cancel';
5
7
  import DeleteIcon from '@mui/icons-material/Delete';
6
8
  import { useTranslation } from 'react-i18next';
7
9
  import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
8
10
  const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
9
- export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, }) {
11
+ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraColumns = [], extraRowActions = [], extraContext = null, refreshTrigger = 0, canEditUser, showNewColumn = true, showSuccessfulLoginColumn = true, showRoleColumn = true, onChangeRole = null, showDeleteAction = true, canDeleteUser = null, onDeleteUser = null, }) {
10
12
  const { t } = useTranslation();
11
13
  const [users, setUsers] = useState([]);
12
14
  const [loading, setLoading] = useState(true);
@@ -44,7 +46,12 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
44
46
  if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?')))
45
47
  return;
46
48
  try {
47
- await deleteUser(userId);
49
+ if (typeof onDeleteUser === 'function') {
50
+ await onDeleteUser({ userId, currentUser, extraContext, t, reloadUsers: loadUsers });
51
+ }
52
+ else {
53
+ await deleteUser(userId);
54
+ }
48
55
  setUsers(prev => prev.filter(u => u.id !== userId));
49
56
  }
50
57
  catch (err) {
@@ -53,7 +60,19 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
53
60
  };
54
61
  const handleChangeRole = async (userId, newRole) => {
55
62
  try {
56
- await updateUserRole(userId, newRole);
63
+ if (typeof onChangeRole === 'function') {
64
+ await onChangeRole({
65
+ userId,
66
+ newRole,
67
+ currentUser,
68
+ extraContext,
69
+ t,
70
+ reloadUsers: loadUsers,
71
+ });
72
+ }
73
+ else {
74
+ await updateUserRole(userId, newRole);
75
+ }
57
76
  await loadUsers();
58
77
  }
59
78
  catch (err) {
@@ -84,6 +103,12 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
84
103
  }
85
104
  return defaultCanEdit(targetUser);
86
105
  };
106
+ const canDelete = (targetUser) => {
107
+ if (typeof canDeleteUser === 'function') {
108
+ return Boolean(canDeleteUser({ targetUser, currentUser, extraContext }));
109
+ }
110
+ return canEdit(targetUser);
111
+ };
87
112
  const listContext = useMemo(() => ({
88
113
  currentUser,
89
114
  extraContext,
@@ -98,6 +123,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
98
123
  }
99
124
  return (user === null || user === void 0 ? void 0 : user.username) || '';
100
125
  };
126
+ const renderBooleanStatusIcon = (value, positiveLabel, negativeLabel) => (_jsx(Tooltip, { title: value ? positiveLabel : negativeLabel, children: _jsx("span", { children: value ? (_jsx(CheckCircleIcon, { color: "success", fontSize: "small" })) : (_jsx(CancelIcon, { color: "error", fontSize: "small" })) }) }));
101
127
  const getExtraColumnSortValue = (column, user) => {
102
128
  var _a;
103
129
  const valueContext = { user, currentUser, extraContext, t };
@@ -184,45 +210,70 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
184
210
  {
185
211
  field: 'email',
186
212
  headerName: t('Auth.EMAIL_LABEL', 'Email'),
187
- minWidth: 240,
188
- flex: 1.2,
213
+ minWidth: 220,
214
+ maxWidth: 340,
215
+ flex: 1,
189
216
  },
190
217
  {
191
218
  field: 'name',
192
219
  headerName: t('Profile.NAME_LABEL', 'Name'),
193
- minWidth: 220,
194
- flex: 1,
220
+ minWidth: 180,
221
+ maxWidth: 260,
222
+ flex: 0.9,
195
223
  valueGetter: (_value, row) => getUserDisplayName(row),
196
224
  },
197
- {
225
+ ];
226
+ if (showNewColumn) {
227
+ baseColumns.push({
228
+ field: 'is_new',
229
+ headerName: t('UserList.NEW', 'New'),
230
+ minWidth: 90,
231
+ maxWidth: 110,
232
+ flex: 0.35,
233
+ align: 'center',
234
+ headerAlign: 'center',
235
+ valueGetter: (_value, 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)); },
236
+ renderCell: (params) => {
237
+ var _a, _b, _c;
238
+ return renderBooleanStatusIcon(Boolean(((_b = (_a = params.row) === null || _a === void 0 ? void 0 : _a.profile) === null || _b === void 0 ? void 0 : _b.is_new) || ((_c = params.row) === null || _c === void 0 ? void 0 : _c.is_new)), t('Common.YES', 'Yes'), t('Common.NO', 'No'));
239
+ },
240
+ });
241
+ }
242
+ if (showSuccessfulLoginColumn) {
243
+ baseColumns.push({
198
244
  field: 'successful_login',
199
245
  headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
200
- minWidth: 180,
201
- flex: 0.8,
246
+ minWidth: 120,
247
+ maxWidth: 150,
248
+ flex: 0.4,
249
+ align: 'center',
250
+ headerAlign: 'center',
202
251
  valueGetter: (_value, 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); },
203
252
  renderCell: (params) => {
204
253
  var _a, _b, _c;
205
- return (_jsx(Typography, { variant: "body2", children: Boolean((_b = (_a = params.row) === null || _a === void 0 ? void 0 : _a.successful_login) !== null && _b !== void 0 ? _b : (_c = params.row) === null || _c === void 0 ? void 0 : _c.last_login)
206
- ? t('Common.YES', 'Yes')
207
- : t('Common.NO', 'No') }));
254
+ return renderBooleanStatusIcon(Boolean((_b = (_a = params.row) === null || _a === void 0 ? void 0 : _a.successful_login) !== null && _b !== void 0 ? _b : (_c = params.row) === null || _c === void 0 ? void 0 : _c.last_login), t('Common.YES', 'Yes'), t('Common.NO', 'No'));
208
255
  },
209
- },
210
- {
256
+ });
257
+ }
258
+ if (showRoleColumn) {
259
+ baseColumns.push({
211
260
  field: 'role',
212
261
  headerName: t('UserList.ROLE', 'Role'),
213
262
  minWidth: 180,
214
- flex: 0.8,
263
+ maxWidth: 240,
264
+ flex: 0.7,
215
265
  valueGetter: (_value, row) => (row === null || row === void 0 ? void 0 : row.role) || 'none',
216
266
  renderCell: (params) => {
217
267
  const user = params.row;
218
268
  return (_jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(user), children: _jsx(Select, { value: user.role || 'none', onChange: (event) => handleChangeRole(user.id, event.target.value), variant: "outlined", children: roles.map((role) => _jsx(MenuItem, { value: role, children: role }, role)) }) }));
219
269
  },
220
- },
221
- ];
270
+ });
271
+ }
222
272
  const mappedExtraColumns = visibleExtraColumns.map((column) => ({
223
273
  field: `extra:${column.key}`,
224
274
  headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
225
275
  minWidth: Number(column.minWidth) || 180,
276
+ maxWidth: Number(column.maxWidth) || 320,
226
277
  flex: Number(column.flex) || 0.9,
227
278
  sortable: column.sortable !== false,
228
279
  align: column.align || 'left',
@@ -237,11 +288,16 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
237
288
  reloadUsers: loadUsers,
238
289
  }),
239
290
  }));
291
+ const hasActionColumn = showDeleteAction || visibleRowActions.length > 0;
292
+ if (!hasActionColumn) {
293
+ return [...baseColumns, ...mappedExtraColumns];
294
+ }
240
295
  const actionColumn = {
241
296
  field: 'actions',
242
297
  headerName: t('Common.ACTIONS', 'Actions'),
243
- minWidth: Math.max(220, 110 + visibleRowActions.length * 110),
244
- flex: 1.4,
298
+ minWidth: Math.max(220, 110 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 110),
299
+ maxWidth: Math.max(360, 120 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 120),
300
+ flex: 1.1,
245
301
  sortable: false,
246
302
  filterable: false,
247
303
  disableColumnMenu: true,
@@ -262,13 +318,17 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
262
318
  return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, user), disabled: isBusy || isDisabled, sx: actionButtonSx, children: typeof action.label === 'function'
263
319
  ? action.label({ user, t, currentUser, canEdit: canEdit(user) })
264
320
  : action.label }, `${action.key}-${user.id}`));
265
- }), _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: !canEdit(user), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) })] }));
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') }) }) }))] }));
266
322
  },
267
323
  };
268
324
  return [...baseColumns, ...mappedExtraColumns, actionColumn];
269
325
  }, [
270
326
  t,
271
327
  roles,
328
+ showNewColumn,
329
+ showSuccessfulLoginColumn,
330
+ showRoleColumn,
331
+ showDeleteAction,
272
332
  visibleExtraColumns,
273
333
  visibleRowActions,
274
334
  listContext,
@@ -277,6 +337,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
277
337
  extraContext,
278
338
  loadUsers,
279
339
  canEdit,
340
+ canDelete,
280
341
  ]);
281
342
  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 && (_jsx(Box, { sx: { width: '100%', minHeight: 520 }, children: _jsx(DataGrid, { rows: filteredUsers, columns: columns, disableRowSelectionOnClick: true, showToolbar: true, getRowHeight: () => 'auto', pageSizeOptions: [10, 25, 50, 100], initialState: {
282
343
  sorting: { sortModel: [{ field: 'email', sort: 'asc' }] },
@@ -21,7 +21,7 @@ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
21
21
  import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
22
22
  import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
23
23
  import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
24
- export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
24
+ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowNewColumn = true, userListShowSuccessfulLoginColumn = true, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
25
25
  var _a;
26
26
  const { t } = useTranslation();
27
27
  const { user, login, loading } = useContext(AuthContext);
@@ -129,5 +129,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
129
129
  const activeExtraTab = builtInTabValues.has(safeTab)
130
130
  ? null
131
131
  : extraTabs.find((tab) => tab.value === safeTab);
132
- return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
132
+ return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
133
133
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,
@@ -12,6 +12,8 @@ import {
12
12
  TextField,
13
13
  } from '@mui/material';
14
14
  import { DataGrid } from '@mui/x-data-grid';
15
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
16
+ import CancelIcon from '@mui/icons-material/Cancel';
15
17
  import DeleteIcon from '@mui/icons-material/Delete';
16
18
  import { useTranslation } from 'react-i18next';
17
19
  import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
@@ -26,6 +28,13 @@ export function UserListComponent({
26
28
  extraContext = null,
27
29
  refreshTrigger = 0,
28
30
  canEditUser,
31
+ showNewColumn = true,
32
+ showSuccessfulLoginColumn = true,
33
+ showRoleColumn = true,
34
+ onChangeRole = null,
35
+ showDeleteAction = true,
36
+ canDeleteUser = null,
37
+ onDeleteUser = null,
29
38
  }) {
30
39
  const { t } = useTranslation();
31
40
  const [users, setUsers] = useState([]);
@@ -63,7 +72,11 @@ export function UserListComponent({
63
72
  const handleDelete = async (userId) => {
64
73
  if (!window.confirm(t('UserList.DELETE_CONFIRM', 'Are you sure you want to delete this user?'))) return;
65
74
  try {
66
- await deleteUser(userId);
75
+ if (typeof onDeleteUser === 'function') {
76
+ await onDeleteUser({ userId, currentUser, extraContext, t, reloadUsers: loadUsers });
77
+ } else {
78
+ await deleteUser(userId);
79
+ }
67
80
  setUsers(prev => prev.filter(u => u.id !== userId));
68
81
  } catch (err) {
69
82
  alert(t(err.code || 'Auth.USER_DELETE_FAILED'));
@@ -72,7 +85,18 @@ export function UserListComponent({
72
85
 
73
86
  const handleChangeRole = async (userId, newRole) => {
74
87
  try {
75
- await updateUserRole(userId, newRole);
88
+ if (typeof onChangeRole === 'function') {
89
+ await onChangeRole({
90
+ userId,
91
+ newRole,
92
+ currentUser,
93
+ extraContext,
94
+ t,
95
+ reloadUsers: loadUsers,
96
+ });
97
+ } else {
98
+ await updateUserRole(userId, newRole);
99
+ }
76
100
  await loadUsers();
77
101
  } catch (err) {
78
102
  alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
@@ -102,6 +126,13 @@ export function UserListComponent({
102
126
  return defaultCanEdit(targetUser);
103
127
  };
104
128
 
129
+ const canDelete = (targetUser) => {
130
+ if (typeof canDeleteUser === 'function') {
131
+ return Boolean(canDeleteUser({ targetUser, currentUser, extraContext }));
132
+ }
133
+ return canEdit(targetUser);
134
+ };
135
+
105
136
  const listContext = useMemo(() => ({
106
137
  currentUser,
107
138
  extraContext,
@@ -132,6 +163,18 @@ export function UserListComponent({
132
163
  return user?.username || '';
133
164
  };
134
165
 
166
+ const renderBooleanStatusIcon = (value, positiveLabel, negativeLabel) => (
167
+ <Tooltip title={value ? positiveLabel : negativeLabel}>
168
+ <span>
169
+ {value ? (
170
+ <CheckCircleIcon color="success" fontSize="small" />
171
+ ) : (
172
+ <CancelIcon color="error" fontSize="small" />
173
+ )}
174
+ </span>
175
+ </Tooltip>
176
+ );
177
+
135
178
  const getExtraColumnSortValue = (column, user) => {
136
179
  const valueContext = { user, currentUser, extraContext, t };
137
180
 
@@ -223,35 +266,63 @@ export function UserListComponent({
223
266
  {
224
267
  field: 'email',
225
268
  headerName: t('Auth.EMAIL_LABEL', 'Email'),
226
- minWidth: 240,
227
- flex: 1.2,
269
+ minWidth: 220,
270
+ maxWidth: 340,
271
+ flex: 1,
228
272
  },
229
273
  {
230
274
  field: 'name',
231
275
  headerName: t('Profile.NAME_LABEL', 'Name'),
232
- minWidth: 220,
233
- flex: 1,
276
+ minWidth: 180,
277
+ maxWidth: 260,
278
+ flex: 0.9,
234
279
  valueGetter: (_value, row) => getUserDisplayName(row),
235
280
  },
236
- {
281
+ ];
282
+
283
+ if (showNewColumn) {
284
+ baseColumns.push({
285
+ field: 'is_new',
286
+ headerName: t('UserList.NEW', 'New'),
287
+ minWidth: 90,
288
+ maxWidth: 110,
289
+ flex: 0.35,
290
+ align: 'center',
291
+ headerAlign: 'center',
292
+ valueGetter: (_value, row) => Boolean(row?.profile?.is_new || row?.is_new),
293
+ renderCell: (params) => renderBooleanStatusIcon(
294
+ Boolean(params.row?.profile?.is_new || params.row?.is_new),
295
+ t('Common.YES', 'Yes'),
296
+ t('Common.NO', 'No'),
297
+ ),
298
+ });
299
+ }
300
+
301
+ if (showSuccessfulLoginColumn) {
302
+ baseColumns.push({
237
303
  field: 'successful_login',
238
304
  headerName: t('UserList.SUCCESSFUL_LOGIN', 'Successful Login'),
239
- minWidth: 180,
240
- flex: 0.8,
305
+ minWidth: 120,
306
+ maxWidth: 150,
307
+ flex: 0.4,
308
+ align: 'center',
309
+ headerAlign: 'center',
241
310
  valueGetter: (_value, row) => Boolean(row?.successful_login ?? row?.last_login),
242
- renderCell: (params) => (
243
- <Typography variant="body2">
244
- {Boolean(params.row?.successful_login ?? params.row?.last_login)
245
- ? t('Common.YES', 'Yes')
246
- : t('Common.NO', 'No')}
247
- </Typography>
311
+ renderCell: (params) => renderBooleanStatusIcon(
312
+ Boolean(params.row?.successful_login ?? params.row?.last_login),
313
+ t('Common.YES', 'Yes'),
314
+ t('Common.NO', 'No'),
248
315
  ),
249
- },
250
- {
316
+ });
317
+ }
318
+
319
+ if (showRoleColumn) {
320
+ baseColumns.push({
251
321
  field: 'role',
252
322
  headerName: t('UserList.ROLE', 'Role'),
253
323
  minWidth: 180,
254
- flex: 0.8,
324
+ maxWidth: 240,
325
+ flex: 0.7,
255
326
  valueGetter: (_value, row) => row?.role || 'none',
256
327
  renderCell: (params) => {
257
328
  const user = params.row;
@@ -267,13 +338,14 @@ export function UserListComponent({
267
338
  </FormControl>
268
339
  );
269
340
  },
270
- },
271
- ];
341
+ });
342
+ }
272
343
 
273
344
  const mappedExtraColumns = visibleExtraColumns.map((column) => ({
274
345
  field: `extra:${column.key}`,
275
346
  headerName: typeof column.label === 'function' ? column.label(listContext) : column.label,
276
347
  minWidth: Number(column.minWidth) || 180,
348
+ maxWidth: Number(column.maxWidth) || 320,
277
349
  flex: Number(column.flex) || 0.9,
278
350
  sortable: column.sortable !== false,
279
351
  align: column.align || 'left',
@@ -290,11 +362,18 @@ export function UserListComponent({
290
362
  }),
291
363
  }));
292
364
 
365
+ const hasActionColumn = showDeleteAction || visibleRowActions.length > 0;
366
+
367
+ if (!hasActionColumn) {
368
+ return [...baseColumns, ...mappedExtraColumns];
369
+ }
370
+
293
371
  const actionColumn = {
294
372
  field: 'actions',
295
373
  headerName: t('Common.ACTIONS', 'Actions'),
296
- minWidth: Math.max(220, 110 + visibleRowActions.length * 110),
297
- flex: 1.4,
374
+ minWidth: Math.max(220, 110 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 110),
375
+ maxWidth: Math.max(360, 120 + (visibleRowActions.length + (showDeleteAction ? 1 : 0)) * 120),
376
+ flex: 1.1,
298
377
  sortable: false,
299
378
  filterable: false,
300
379
  disableColumnMenu: true,
@@ -332,21 +411,23 @@ export function UserListComponent({
332
411
  );
333
412
  })}
334
413
 
335
- <Tooltip title={t('Common.DELETE', 'Delete')}>
336
- <span>
337
- <Button
338
- size="small"
339
- variant="outlined"
340
- color="error"
341
- startIcon={<DeleteIcon />}
342
- onClick={() => handleDelete(user.id)}
343
- disabled={!canEdit(user)}
344
- sx={actionButtonSx}
345
- >
346
- {t('Common.DELETE', 'Delete')}
347
- </Button>
348
- </span>
349
- </Tooltip>
414
+ {showDeleteAction && (
415
+ <Tooltip title={t('Common.DELETE', 'Delete')}>
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
+ )}
350
431
  </Box>
351
432
  );
352
433
  },
@@ -356,6 +437,10 @@ export function UserListComponent({
356
437
  }, [
357
438
  t,
358
439
  roles,
440
+ showNewColumn,
441
+ showSuccessfulLoginColumn,
442
+ showRoleColumn,
443
+ showDeleteAction,
359
444
  visibleExtraColumns,
360
445
  visibleRowActions,
361
446
  listContext,
@@ -364,6 +449,7 @@ export function UserListComponent({
364
449
  extraContext,
365
450
  loadUsers,
366
451
  canEdit,
452
+ canDelete,
367
453
  ]);
368
454
 
369
455
  return (
@@ -38,6 +38,13 @@ export function AccountPage({
38
38
  userListExtraContext = null,
39
39
  userListRefreshTrigger = 0,
40
40
  userListCanEditUser = null,
41
+ userListShowNewColumn = true,
42
+ userListShowSuccessfulLoginColumn = true,
43
+ userListShowRoleColumn = true,
44
+ userListOnChangeRole = null,
45
+ userListShowDeleteAction = true,
46
+ userListCanDeleteUser = null,
47
+ userListOnDeleteUser = null,
41
48
  showBulkInviteCsvTab = false,
42
49
  bulkInviteCsvProps = {},
43
50
  extraTabs = [],
@@ -227,6 +234,13 @@ export function AccountPage({
227
234
  extraContext={userListExtraContext}
228
235
  refreshTrigger={userListRefreshTrigger}
229
236
  canEditUser={userListCanEditUser}
237
+ showNewColumn={userListShowNewColumn}
238
+ showSuccessfulLoginColumn={userListShowSuccessfulLoginColumn}
239
+ showRoleColumn={userListShowRoleColumn}
240
+ onChangeRole={userListOnChangeRole}
241
+ showDeleteAction={userListShowDeleteAction}
242
+ canDeleteUser={userListCanDeleteUser}
243
+ onDeleteUser={userListOnDeleteUser}
230
244
  />
231
245
  </Box>
232
246
  )}