@micha.bigler/ui-core-micha 3.8.0 → 3.9.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/CHANGELOG.md +22 -0
- package/WORK_ORDERS.md +2 -0
- package/dist/auth/apiClient.js +8 -0
- package/dist/components/AccessCodeManager.js +5 -5
- package/dist/components/AccessCodeSingleUseToggle.js +3 -2
- package/dist/components/AllowedEmailDomainsManager.js +3 -2
- package/dist/components/AuthFactorRequirementCard.js +3 -2
- package/dist/components/BulkInviteCsvTab.js +3 -2
- package/dist/components/MFAComponent.js +9 -8
- package/dist/components/MfaLoginComponent.js +7 -6
- package/dist/components/PasskeysComponent.js +7 -6
- package/dist/components/ProfileComponent.js +4 -3
- package/dist/components/QrSignupManager.js +4 -3
- package/dist/components/QrSignupValidityManager.js +3 -2
- package/dist/components/RegistrationMethodsManager.js +3 -2
- package/dist/components/SecurityComponent.js +5 -4
- package/dist/components/SupportRecoveryRequestsTab.js +8 -7
- package/dist/components/UserInviteComponent.js +3 -2
- package/dist/components/UserListComponent.js +14 -7
- package/dist/i18n/authTranslations.js +420 -0
- package/dist/pages/AccountPage.js +3 -2
- package/dist/pages/LoginPage.js +10 -9
- package/dist/pages/PasswordChangePage.js +4 -3
- package/dist/pages/PasswordInvitePage.js +7 -6
- package/dist/pages/PasswordResetRequestPage.js +5 -4
- package/dist/pages/SignUpPage.js +7 -6
- package/dist/pages/SignupConfirmPage.js +7 -6
- package/dist/utils/auth-errors.js +45 -8
- package/dist/utils/resolveErrorText.js +24 -0
- package/package.json +1 -1
- package/src/auth/apiClient.jsx +9 -0
- package/src/components/AccessCodeManager.jsx +5 -5
- package/src/components/AccessCodeSingleUseToggle.jsx +3 -2
- package/src/components/AllowedEmailDomainsManager.jsx +3 -2
- package/src/components/AuthFactorRequirementCard.jsx +3 -2
- package/src/components/BulkInviteCsvTab.jsx +3 -2
- package/src/components/MFAComponent.jsx +9 -8
- package/src/components/MfaLoginComponent.jsx +7 -6
- package/src/components/PasskeysComponent.jsx +7 -6
- package/src/components/ProfileComponent.jsx +4 -3
- package/src/components/QrSignupManager.jsx +4 -3
- package/src/components/QrSignupValidityManager.jsx +3 -2
- package/src/components/RegistrationMethodsManager.jsx +3 -2
- package/src/components/SecurityComponent.jsx +5 -4
- package/src/components/SupportRecoveryRequestsTab.jsx +9 -8
- package/src/components/UserInviteComponent.jsx +3 -2
- package/src/components/UserListComponent.jsx +14 -7
- package/src/i18n/authTranslations.ts +421 -0
- package/src/pages/AccountPage.jsx +3 -4
- package/src/pages/LoginPage.jsx +10 -9
- package/src/pages/PasswordChangePage.jsx +4 -3
- package/src/pages/PasswordInvitePage.jsx +7 -6
- package/src/pages/PasswordResetRequestPage.jsx +5 -4
- package/src/pages/SignUpPage.jsx +7 -6
- package/src/pages/SignupConfirmPage.jsx +7 -6
- package/src/utils/auth-errors.js +44 -7
- package/src/utils/resolveErrorText.js +28 -0
- package/tests/AccountPage.test.jsx +27 -20
- package/tests/PasswordInvitePage.test.jsx +28 -21
- package/tests/QrSignupManager.registrationContext.test.jsx +5 -1
- package/tests/SignUpPage.test.jsx +14 -3
- package/tests/UserListComponent.test.jsx +100 -16
- package/tests/apiClientLanguage.test.js +33 -0
- package/tests/authErrors.test.js +31 -0
- package/tests/errorCallSiteGuard.test.js +32 -0
- package/tests/loginPageErrorResolution.test.jsx +102 -0
- package/tests/resolveErrorText.test.js +72 -0
- package/work-orders/UCM-AUTH-10.md +490 -0
- package/work-orders/UCM-AUTH-11.md +185 -0
|
@@ -10,10 +10,11 @@ import { MFAComponent } from './MFAComponent';
|
|
|
10
10
|
import { changePassword } from '../auth/authApi';
|
|
11
11
|
import { startSocialLogin } from '../utils/authService';
|
|
12
12
|
import { AuthContext } from '../auth/AuthContext';
|
|
13
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
13
14
|
export const SECURITY_ALERT_SX = { mb: 2 };
|
|
14
15
|
export const SECURITY_DIVIDER_SX = { my: 3 };
|
|
15
16
|
export function SecurityComponent({ fromRecovery = false, fromWeakLogin = false, }) {
|
|
16
|
-
const { t } = useTranslation();
|
|
17
|
+
const { t, i18n } = useTranslation();
|
|
17
18
|
const { authMethods } = useContext(AuthContext);
|
|
18
19
|
const [messageKey, setMessageKey] = useState(null);
|
|
19
20
|
const [errorKey, setErrorKey] = useState(null);
|
|
@@ -44,7 +45,7 @@ export function SecurityComponent({ fromRecovery = false, fromWeakLogin = false,
|
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
catch (err) {
|
|
47
|
-
setErrorKey(err
|
|
48
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.SOCIAL_LOGIN_FAILED'));
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
51
|
const handlePasswordChange = async (currentPassword, newPassword) => {
|
|
@@ -55,9 +56,9 @@ export function SecurityComponent({ fromRecovery = false, fromWeakLogin = false,
|
|
|
55
56
|
setMessageKey('Auth.RESET_PASSWORD_SUCCESS');
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
|
-
setErrorKey(err
|
|
59
|
+
setErrorKey(resolveErrorText(i18n, err, 'Auth.PASSWORD_CHANGE_FAILED'));
|
|
59
60
|
}
|
|
60
61
|
};
|
|
61
|
-
return (_jsxs(Box, { children: [fromRecovery && (_jsx(Alert, { severity: "warning", sx: SECURITY_ALERT_SX, children: t('Security.RECOVERY_LOGIN_WARNING') })), fromWeakLogin && (_jsx(Alert, { severity: "warning", sx: SECURITY_ALERT_SX, children: t('Security.WEAK_LOGIN_WARNING') })), messageKey && (_jsx(Alert, { severity: "success", sx: SECURITY_ALERT_SX, children: t(messageKey) })), errorKey && (_jsx(Alert, { severity: "error", sx: SECURITY_ALERT_SX, children:
|
|
62
|
+
return (_jsxs(Box, { children: [fromRecovery && (_jsx(Alert, { severity: "warning", sx: SECURITY_ALERT_SX, children: t('Security.RECOVERY_LOGIN_WARNING') })), fromWeakLogin && (_jsx(Alert, { severity: "warning", sx: SECURITY_ALERT_SX, children: t('Security.WEAK_LOGIN_WARNING') })), messageKey && (_jsx(Alert, { severity: "success", sx: SECURITY_ALERT_SX, children: t(messageKey) })), errorKey && (_jsx(Alert, { severity: "error", sx: SECURITY_ALERT_SX, children: errorKey })), canChangePassword && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.LOGIN_PASSWORD_LABEL') }), _jsx(PasswordChangeForm, { onSubmit: handlePasswordChange }), needsDividerAfter('password') && _jsx(Divider, { sx: SECURITY_DIVIDER_SX })] })), socialLoginEnabled && (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Security.SOCIAL_SECTION_TITLE') }), _jsx(Typography, { variant: "body2", sx: { mb: 1 }, children: t('Security.SOCIAL_SECTION_DESCRIPTION') }), _jsx(SocialLoginButtons, { onProviderClick: handleSocialClick, providers: socialProviders }), needsDividerAfter('social') && _jsx(Divider, { sx: SECURITY_DIVIDER_SX })] })), passkeysEnabled && (_jsxs(_Fragment, { children: [_jsx(PasskeysComponent, {}), needsDividerAfter('passkeys') && _jsx(Divider, { sx: SECURITY_DIVIDER_SX })] })), mfaEnabled && _jsx(MFAComponent, {})] }));
|
|
62
63
|
}
|
|
63
64
|
;
|
|
@@ -4,6 +4,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
|
|
4
4
|
import { Alert, Autocomplete, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from '@mui/material';
|
|
5
5
|
import CloseIcon from '@mui/icons-material/Close';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
7
8
|
import { AuthContext } from '../auth/AuthContext';
|
|
8
9
|
import { approveRecoveryRequest, fetchRecoveryRequests, fetchUsersList, rejectRecoveryRequest, updateUserSupportStatus, } from '../auth/authApi';
|
|
9
10
|
export const SUPPORT_RECOVERY_AGENT_PAPER_SX = { p: 2, mb: 3 };
|
|
@@ -33,7 +34,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
33
34
|
setRequests(Array.isArray(data) ? data : []);
|
|
34
35
|
}
|
|
35
36
|
catch (err) {
|
|
36
|
-
setErrorKey(err
|
|
37
|
+
setErrorKey(resolveErrorText(i18n, err, 'Support.RECOVERY_REQUESTS_LOAD_FAILED'));
|
|
37
38
|
}
|
|
38
39
|
finally {
|
|
39
40
|
setLoading(false);
|
|
@@ -58,7 +59,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
58
59
|
setAllUsers(list);
|
|
59
60
|
}
|
|
60
61
|
catch (err) {
|
|
61
|
-
setAgentErrorKey(err
|
|
62
|
+
setAgentErrorKey(resolveErrorText(i18n, err, 'Auth.USER_LIST_FAILED'));
|
|
62
63
|
}
|
|
63
64
|
finally {
|
|
64
65
|
setAgentLoading(false);
|
|
@@ -92,7 +93,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
92
93
|
closeDialog();
|
|
93
94
|
}
|
|
94
95
|
catch (err) {
|
|
95
|
-
setErrorKey(err
|
|
96
|
+
setErrorKey(resolveErrorText(i18n, err, 'Support.RECOVERY_REQUEST_APPROVE_FAILED'));
|
|
96
97
|
}
|
|
97
98
|
};
|
|
98
99
|
const handleReject = async () => {
|
|
@@ -105,7 +106,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
105
106
|
closeDialog();
|
|
106
107
|
}
|
|
107
108
|
catch (err) {
|
|
108
|
-
setErrorKey(err
|
|
109
|
+
setErrorKey(resolveErrorText(i18n, err, 'Support.RECOVERY_REQUEST_REJECT_FAILED'));
|
|
109
110
|
}
|
|
110
111
|
};
|
|
111
112
|
const getUserDisplayName = (entry) => {
|
|
@@ -135,7 +136,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
135
136
|
await loadUsers();
|
|
136
137
|
}
|
|
137
138
|
catch (err) {
|
|
138
|
-
setAgentErrorKey(err
|
|
139
|
+
setAgentErrorKey(resolveErrorText(i18n, err, 'Auth.USER_SUPPORT_UPDATE_FAILED'));
|
|
139
140
|
}
|
|
140
141
|
finally {
|
|
141
142
|
setAgentActionUserId(null);
|
|
@@ -154,7 +155,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
154
155
|
await loadUsers();
|
|
155
156
|
}
|
|
156
157
|
catch (err) {
|
|
157
|
-
setAgentErrorKey(err
|
|
158
|
+
setAgentErrorKey(resolveErrorText(i18n, err, 'Auth.USER_SUPPORT_UPDATE_FAILED'));
|
|
158
159
|
}
|
|
159
160
|
finally {
|
|
160
161
|
setAgentActionUserId(null);
|
|
@@ -163,7 +164,7 @@ export function SupportRecoveryRequestsTab() {
|
|
|
163
164
|
if (loading) {
|
|
164
165
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 4 }, children: _jsx(CircularProgress, {}) }));
|
|
165
166
|
}
|
|
166
|
-
return (_jsxs(Box, { children: [canManageAgents && (_jsxs(Paper, { variant: "outlined", sx: SUPPORT_RECOVERY_AGENT_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.AGENTS_TITLE', 'Support agents') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.AGENTS_DESCRIPTION', 'Assign which users are allowed to handle support requests.') }), agentErrorKey && (_jsx(Alert, { severity: "error", sx: SUPPORT_RECOVERY_ALERT_SX, children:
|
|
167
|
+
return (_jsxs(Box, { children: [canManageAgents && (_jsxs(Paper, { variant: "outlined", sx: SUPPORT_RECOVERY_AGENT_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.AGENTS_TITLE', 'Support agents') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.AGENTS_DESCRIPTION', 'Assign which users are allowed to handle support requests.') }), agentErrorKey && (_jsx(Alert, { severity: "error", sx: SUPPORT_RECOVERY_ALERT_SX, children: agentErrorKey })), _jsxs(Stack, { direction: { xs: 'column', md: 'row' }, spacing: 2, sx: { mb: 2, alignItems: { md: 'flex-start' } }, children: [_jsx(Autocomplete, { fullWidth: true, options: availableAgentCandidates, value: selectedAgentCandidate, loading: agentLoading, onChange: (_event, value) => setSelectedAgentCandidate(value), isOptionEqualToValue: (option, value) => option.id === value.id, getOptionLabel: getUserOptionLabel, renderInput: (params) => (_jsx(TextField, Object.assign({}, params, { label: t('Support.AGENTS_SELECT_LABEL', 'Select user'), placeholder: t('Support.AGENTS_SELECT_PLACEHOLDER', 'Choose a user') }))) }), _jsx(Button, { variant: "contained", onClick: handleAddAgent, disabled: !selectedAgentCandidate || agentLoading || Boolean(agentActionUserId), sx: SUPPORT_RECOVERY_AGENT_BUTTON_SX, children: t('Support.AGENTS_ADD', 'Add as agent') })] }), agentLoading ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', py: 3 }, children: _jsx(CircularProgress, { size: 24 }) })) : supportAgents.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.AGENTS_EMPTY', 'No support agents assigned yet.') })) : (_jsx(TableContainer, { component: Paper, variant: "outlined", children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Profile.NAME_LABEL', 'Name') }), _jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { align: "right", children: t('Common.ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: supportAgents.map((agentUser) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: getUserDisplayName(agentUser) || '-' }), _jsx(TableCell, { children: agentUser.email || '-' }), _jsx(TableCell, { align: "right", children: _jsx(IconButton, { size: "small", color: "error", onClick: () => handleRemoveAgent(agentUser), disabled: agentActionUserId === agentUser.id, "aria-label": t('Support.AGENTS_REMOVE', 'Remove support agent'), children: _jsx(CloseIcon, { fontSize: "small" }) }) })] }, agentUser.id))) })] }) }))] })), _jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Support.RECOVERY_REQUESTS_TITLE', 'Account recovery requests') }), _jsx(Typography, { variant: "body2", sx: { mb: 2 }, children: t('Support.RECOVERY_REQUESTS_DESCRIPTION', 'Users who cannot complete MFA can request support. You can review their request and approve or reject it.') }), errorKey && (_jsx(Alert, { severity: "error", sx: SUPPORT_RECOVERY_ALERT_SX, children: errorKey })), _jsxs(Stack, { direction: "row", spacing: 2, sx: { mb: 2 }, children: [_jsx(Button, { variant: statusFilter === 'pending' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('pending'), children: t('Support.RECOVERY_FILTER_PENDING', 'Open') }), _jsx(Button, { variant: statusFilter === 'approved' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('approved'), children: t('Support.RECOVERY_FILTER_APPROVED', 'Approved') }), _jsx(Button, { variant: statusFilter === 'rejected' ? 'contained' : 'outlined', size: "small", onClick: () => setStatusFilter('rejected'), children: t('Support.RECOVERY_FILTER_REJECTED', 'Rejected') })] }), requests.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Support.RECOVERY_REQUESTS_EMPTY', 'No recovery requests for this filter.') })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Support.RECOVERY_COL_CREATED', 'Created') }), _jsx(TableCell, { children: t('Support.RECOVERY_USER', 'User') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_STATUS', 'Status') }), _jsx(TableCell, { children: t('Support.RECOVERY_COL_ACTIONS', 'Actions') })] }) }), _jsx(TableBody, { children: requests.map((req) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: req.created_at
|
|
167
168
|
? new Date(req.created_at).toLocaleString(i18n === null || i18n === void 0 ? void 0 : i18n.language)
|
|
168
169
|
: '-' }), _jsx(TableCell, { children: req.user_email || req.user }), _jsx(TableCell, { children: t(`Support.RECOVERY_STATUS_${req.status}`, req.status) }), _jsx(TableCell, { children: _jsx(Button, { variant: "contained", size: "small", onClick: () => openDialog(req), disabled: req.status !== 'pending', children: t('Support.RECOVERY_ACTION_REVIEW', 'Review') }) })] }, req.id))) })] }) })), _jsxs(Dialog, { open: dialogOpen, onClose: closeDialog, fullWidth: true, maxWidth: "sm", children: [_jsx(DialogTitle, { children: t('Support.RECOVERY_REVIEW_DIALOG_TITLE', 'Review recovery request') }), _jsxs(DialogContent, { dividers: true, children: [selectedRequest && (_jsxs(Box, { sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_USER', 'User'), ":"] }), ' ', selectedRequest.user_email || selectedRequest.user] }), _jsxs(Typography, { variant: "body2", sx: { mb: 1 }, children: [_jsxs("strong", { children: [t('Support.RECOVERY_REVIEW_CREATED', 'Requested at'), ":"] }), ' ', selectedRequest.created_at
|
|
169
170
|
? new Date(selectedRequest.created_at).toLocaleString(i18n === null || i18n === void 0 ? void 0 : i18n.language)
|
|
@@ -3,6 +3,7 @@ import React, { useState } from 'react';
|
|
|
3
3
|
import { Box, TextField, Button, Typography, Alert, CircularProgress } from '@mui/material';
|
|
4
4
|
import { sendAdminInvite } from '../auth/authApi';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
6
7
|
export const USER_INVITE_ACTION_SX = {
|
|
7
8
|
minWidth: 120,
|
|
8
9
|
height: 40,
|
|
@@ -10,7 +11,7 @@ export const USER_INVITE_ACTION_SX = {
|
|
|
10
11
|
};
|
|
11
12
|
export const USER_INVITE_ALERT_SX = { mb: 2 };
|
|
12
13
|
export function UserInviteComponent() {
|
|
13
|
-
const { t } = useTranslation();
|
|
14
|
+
const { t, i18n } = useTranslation();
|
|
14
15
|
const [inviteEmail, setInviteEmail] = useState('');
|
|
15
16
|
const [message, setMessage] = useState('');
|
|
16
17
|
const [error, setError] = useState('');
|
|
@@ -30,7 +31,7 @@ export function UserInviteComponent() {
|
|
|
30
31
|
// err.message contains normalized text or code from authApi
|
|
31
32
|
// eslint-disable-next-line no-console
|
|
32
33
|
console.error('Error inviting user:', err);
|
|
33
|
-
setError(
|
|
34
|
+
setError(resolveErrorText(i18n, err, 'Auth.INVITE_FAILED'));
|
|
34
35
|
}
|
|
35
36
|
finally {
|
|
36
37
|
setLoading(false);
|
|
@@ -6,6 +6,7 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
|
|
6
6
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { fetchUsersList, deleteUser, updateUserRole } from '../auth/authApi';
|
|
9
|
+
import { resolveErrorText } from '../utils/resolveErrorText';
|
|
9
10
|
const DEFAULT_ROLES = ['none', 'student', 'teacher', 'admin'];
|
|
10
11
|
export const USER_LIST_ACTION_SX = { minWidth: 90 };
|
|
11
12
|
export const USER_LIST_ALERT_SX = { mb: 2 };
|
|
@@ -13,7 +14,7 @@ export const USER_LIST_HEADER_CELL_SX = { fontWeight: 600, whiteSpace: 'nowrap'
|
|
|
13
14
|
export const USER_LIST_EMPTY_CELL_SX = { py: 4, color: 'text.secondary' };
|
|
14
15
|
export const USER_LIST_BODY_CELL_SX = { verticalAlign: 'top', py: 1 };
|
|
15
16
|
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, }) {
|
|
16
|
-
const { t } = useTranslation();
|
|
17
|
+
const { t, i18n } = useTranslation();
|
|
17
18
|
const [users, setUsers] = useState([]);
|
|
18
19
|
const [loading, setLoading] = useState(true);
|
|
19
20
|
const [error, setError] = useState(null);
|
|
@@ -40,7 +41,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
40
41
|
setUsers(list);
|
|
41
42
|
}
|
|
42
43
|
catch (err) {
|
|
43
|
-
setError(err
|
|
44
|
+
setError(resolveErrorText(i18n, err, 'Auth.USER_LIST_FAILED'));
|
|
44
45
|
}
|
|
45
46
|
finally {
|
|
46
47
|
setLoading(false);
|
|
@@ -65,7 +66,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
65
66
|
setUsers(prev => prev.filter(u => u.id !== userId));
|
|
66
67
|
}
|
|
67
68
|
catch (err) {
|
|
68
|
-
alert(
|
|
69
|
+
alert(resolveErrorText(i18n, err, 'Auth.USER_DELETE_FAILED'));
|
|
69
70
|
}
|
|
70
71
|
};
|
|
71
72
|
const handleChangeRole = async (userId, newRole) => {
|
|
@@ -87,7 +88,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
87
88
|
alert(t('UserList.ROLE_UPDATE_SUCCESS', 'Role updated.'));
|
|
88
89
|
}
|
|
89
90
|
catch (err) {
|
|
90
|
-
alert(
|
|
91
|
+
alert(resolveErrorText(i18n, err, 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
91
92
|
}
|
|
92
93
|
};
|
|
93
94
|
const defaultCanEdit = (targetUser) => {
|
|
@@ -120,6 +121,11 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
120
121
|
}
|
|
121
122
|
return canEdit(targetUser);
|
|
122
123
|
};
|
|
124
|
+
const isOwnRow = (targetUser) => {
|
|
125
|
+
if ((currentUser === null || currentUser === void 0 ? void 0 : currentUser.id) == null || (targetUser === null || targetUser === void 0 ? void 0 : targetUser.id) == null)
|
|
126
|
+
return false;
|
|
127
|
+
return String(targetUser.id) === String(currentUser.id);
|
|
128
|
+
};
|
|
123
129
|
const listContext = useMemo(() => ({
|
|
124
130
|
currentUser,
|
|
125
131
|
extraContext,
|
|
@@ -210,7 +216,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
210
216
|
}
|
|
211
217
|
catch (err) {
|
|
212
218
|
// eslint-disable-next-line no-alert
|
|
213
|
-
alert((
|
|
219
|
+
alert(resolveErrorText(i18n, err, 'Common.OPERATION_FAILED'));
|
|
214
220
|
}
|
|
215
221
|
finally {
|
|
216
222
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: false })));
|
|
@@ -332,7 +338,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
332
338
|
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, row), disabled: isBusy || isDisabled, sx: USER_LIST_ACTION_SX, children: typeof action.label === 'function'
|
|
333
339
|
? action.label({ user: row, t, currentUser, canEdit: canEdit(row) })
|
|
334
340
|
: action.label }, `${action.key}-${row.id}`));
|
|
335
|
-
}), 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: USER_LIST_ACTION_SX, children: t('Common.DELETE', 'Delete') }) }) }))] })),
|
|
341
|
+
}), showDeleteAction && !isOwnRow(row) && (_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: USER_LIST_ACTION_SX, children: t('Common.DELETE', 'Delete') }) }) }))] })),
|
|
336
342
|
};
|
|
337
343
|
return [...baseColumns, ...mappedExtraColumns, actionColumn];
|
|
338
344
|
}, [
|
|
@@ -351,6 +357,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
351
357
|
loadUsers,
|
|
352
358
|
canEdit,
|
|
353
359
|
canDelete,
|
|
360
|
+
isOwnRow,
|
|
354
361
|
]);
|
|
355
362
|
const sortedUsers = useMemo(() => {
|
|
356
363
|
const col = columns.find((c) => c.field === sortField);
|
|
@@ -373,7 +380,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
373
380
|
}
|
|
374
381
|
setPage(0);
|
|
375
382
|
};
|
|
376
|
-
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: USER_LIST_ALERT_SX, children:
|
|
383
|
+
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: USER_LIST_ALERT_SX, children: 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: [USER_LIST_HEADER_CELL_SX, { minWidth: col.minWidth }], 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: USER_LIST_EMPTY_CELL_SX, 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: USER_LIST_BODY_CELL_SX, 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) => {
|
|
377
384
|
setPageSize(Number(event.target.value));
|
|
378
385
|
setPage(0);
|
|
379
386
|
}, labelRowsPerPage: t('UserList.ROWS_PER_PAGE', 'Rows per page:'), rowsPerPageOptions: [10, 25, 50, 100] })] }))] }));
|