@micha.bigler/ui-core-micha 3.7.0 → 3.9.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/DESIGN.md +8 -0
  3. package/WORK_ORDERS.md +2 -0
  4. package/dist/auth/apiClient.js +8 -0
  5. package/dist/components/AccessCodeManager.js +5 -5
  6. package/dist/components/AccessCodeSingleUseToggle.js +3 -2
  7. package/dist/components/AllowedEmailDomainsManager.js +3 -2
  8. package/dist/components/AuthFactorRequirementCard.js +3 -2
  9. package/dist/components/BulkInviteCsvTab.js +3 -2
  10. package/dist/components/MFAComponent.js +9 -8
  11. package/dist/components/MfaLoginComponent.js +7 -6
  12. package/dist/components/PasskeysComponent.js +7 -6
  13. package/dist/components/ProfileComponent.js +4 -3
  14. package/dist/components/QrSignupManager.js +4 -3
  15. package/dist/components/QrSignupValidityManager.js +3 -2
  16. package/dist/components/RegistrationMethodsManager.js +3 -2
  17. package/dist/components/SecurityComponent.js +5 -4
  18. package/dist/components/SupportRecoveryRequestsTab.js +8 -7
  19. package/dist/components/UserInviteComponent.js +3 -2
  20. package/dist/components/UserListComponent.js +7 -6
  21. package/dist/i18n/authTranslations.js +420 -0
  22. package/dist/pages/AccountPage.js +3 -2
  23. package/dist/pages/LoginPage.js +10 -9
  24. package/dist/pages/PasswordChangePage.js +4 -3
  25. package/dist/pages/PasswordInvitePage.js +7 -6
  26. package/dist/pages/PasswordResetRequestPage.js +5 -4
  27. package/dist/pages/SignUpPage.js +7 -6
  28. package/dist/pages/SignupConfirmPage.js +7 -6
  29. package/dist/theme/createAppTheme.js +62 -9
  30. package/dist/theme/tokens.js +1 -1
  31. package/dist/utils/auth-errors.js +45 -8
  32. package/dist/utils/resolveErrorText.js +24 -0
  33. package/package.json +1 -1
  34. package/src/auth/apiClient.jsx +9 -0
  35. package/src/components/AccessCodeManager.jsx +5 -5
  36. package/src/components/AccessCodeSingleUseToggle.jsx +3 -2
  37. package/src/components/AllowedEmailDomainsManager.jsx +3 -2
  38. package/src/components/AuthFactorRequirementCard.jsx +3 -2
  39. package/src/components/BulkInviteCsvTab.jsx +3 -2
  40. package/src/components/MFAComponent.jsx +9 -8
  41. package/src/components/MfaLoginComponent.jsx +7 -6
  42. package/src/components/PasskeysComponent.jsx +7 -6
  43. package/src/components/ProfileComponent.jsx +4 -3
  44. package/src/components/QrSignupManager.jsx +4 -3
  45. package/src/components/QrSignupValidityManager.jsx +3 -2
  46. package/src/components/RegistrationMethodsManager.jsx +3 -2
  47. package/src/components/SecurityComponent.jsx +5 -4
  48. package/src/components/SupportRecoveryRequestsTab.jsx +9 -8
  49. package/src/components/UserInviteComponent.jsx +3 -2
  50. package/src/components/UserListComponent.jsx +7 -6
  51. package/src/i18n/authTranslations.ts +421 -0
  52. package/src/pages/AccountPage.jsx +3 -4
  53. package/src/pages/LoginPage.jsx +10 -9
  54. package/src/pages/PasswordChangePage.jsx +4 -3
  55. package/src/pages/PasswordInvitePage.jsx +7 -6
  56. package/src/pages/PasswordResetRequestPage.jsx +5 -4
  57. package/src/pages/SignUpPage.jsx +7 -6
  58. package/src/pages/SignupConfirmPage.jsx +7 -6
  59. package/src/theme/createAppTheme.js +68 -4
  60. package/src/theme/tokens.js +1 -1
  61. package/src/utils/auth-errors.js +44 -7
  62. package/src/utils/resolveErrorText.js +28 -0
  63. package/tests/AccountPage.test.jsx +27 -20
  64. package/tests/PasswordInvitePage.test.jsx +28 -21
  65. package/tests/QrSignupManager.registrationContext.test.jsx +5 -1
  66. package/tests/SignUpPage.test.jsx +14 -3
  67. package/tests/UserListComponent.test.jsx +22 -15
  68. package/tests/apiClientLanguage.test.js +33 -0
  69. package/tests/authErrors.test.js +31 -0
  70. package/tests/createAppThemePageContrast.test.js +108 -0
  71. package/tests/errorCallSiteGuard.test.js +32 -0
  72. package/tests/loginPageErrorResolution.test.jsx +102 -0
  73. package/tests/resolveErrorText.test.js +72 -0
  74. package/work-orders/UCM-AUTH-10.md +490 -0
  75. package/work-orders/UCM-THEME-14.md +3 -1
  76. package/work-orders/UCM-THEME-15.md +356 -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.code || 'Auth.SOCIAL_LOGIN_FAILED');
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.code || 'Auth.PASSWORD_CHANGE_FAILED');
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: t(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
+ 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.code || 'Support.RECOVERY_REQUESTS_LOAD_FAILED');
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.code || 'Auth.USER_LIST_FAILED');
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.code || 'Support.RECOVERY_REQUEST_APPROVE_FAILED');
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.code || 'Support.RECOVERY_REQUEST_REJECT_FAILED');
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.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
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.code || 'Auth.USER_SUPPORT_UPDATE_FAILED');
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: t(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: t(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
+ 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(t(err.code) || err.message || t('Auth.INVITE_FAILED'));
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.code || 'Auth.USER_LIST_FAILED');
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(t(err.code || 'Auth.USER_DELETE_FAILED'));
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(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
91
+ alert(resolveErrorText(i18n, err, 'Auth.USER_ROLE_UPDATE_FAILED'));
91
92
  }
92
93
  };
93
94
  const defaultCanEdit = (targetUser) => {
@@ -210,7 +211,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
210
211
  }
211
212
  catch (err) {
212
213
  // eslint-disable-next-line no-alert
213
- alert((err === null || err === void 0 ? void 0 : err.message) || t('Common.OPERATION_FAILED', 'Operation failed.'));
214
+ alert(resolveErrorText(i18n, err, 'Common.OPERATION_FAILED'));
214
215
  }
215
216
  finally {
216
217
  setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: false })));
@@ -373,7 +374,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
373
374
  }
374
375
  setPage(0);
375
376
  };
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: 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: [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
+ 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
378
  setPageSize(Number(event.target.value));
378
379
  setPage(0);
379
380
  }, labelRowsPerPage: t('UserList.ROWS_PER_PAGE', 'Rows per page:'), rowsPerPageOptions: [10, 25, 50, 100] })] }))] }));