@micha.bigler/ui-core-micha 3.6.0 → 3.7.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.
- package/DESIGN.md +1 -1
- package/README.md +2 -0
- package/WORK_ORDERS.md +7 -0
- package/dist/auth/UserMenu.js +23 -1
- package/dist/components/EmptyState.js +17 -0
- package/dist/components/UserListComponent.js +2 -1
- package/dist/i18n/authTranslations.js +196 -0
- package/dist/i18n/uiCoreTranslations.js +19 -0
- package/dist/index.js +2 -1
- package/dist/messaging/Composer.js +4 -2
- package/dist/messaging/MessageBubble.js +2 -1
- package/dist/messaging/ReadTicks.js +5 -3
- package/dist/messaging/linkifyText.js +39 -0
- package/dist/pages/PasswordInvitePage.js +23 -8
- package/dist/pages/SignUpPage.js +4 -4
- package/dist/theme/kitSxRegistry.js +5 -1
- package/package.json +1 -1
- package/src/auth/UserMenu.jsx +26 -1
- package/src/components/EmptyState.jsx +38 -0
- package/src/components/UserListComponent.jsx +2 -0
- package/src/i18n/authTranslations.ts +197 -0
- package/src/i18n/uiCoreTranslations.js +10 -0
- package/src/index.js +2 -1
- package/src/messaging/Composer.jsx +7 -3
- package/src/messaging/MessageBubble.jsx +2 -1
- package/src/messaging/ReadTicks.jsx +4 -3
- package/src/messaging/linkifyText.jsx +37 -0
- package/src/pages/PasswordInvitePage.jsx +36 -7
- package/src/pages/SignUpPage.jsx +79 -68
- package/src/theme/kitSxRegistry.js +9 -1
- package/tests/EmptyState.test.jsx +62 -0
- package/tests/PasswordInvitePage.test.jsx +135 -0
- package/tests/SignUpPage.test.jsx +15 -2
- package/tests/UserListComponent.test.jsx +85 -0
- package/tests/UserMenu.test.jsx +40 -4
- package/tests/i18nGermanVariant.test.js +65 -0
- package/tests/messagingComposer.test.jsx +25 -0
- package/tests/messagingLinkify.test.jsx +51 -0
- package/tests/messagingReadTicksTooltip.test.jsx +36 -0
- package/tests/userScreenTranslations.test.jsx +165 -0
- package/work-orders/UCM-AUTH-9.md +207 -0
- package/work-orders/UCM-I18N-4.md +226 -0
- package/work-orders/UCM-MSG-20.md +192 -0
- package/work-orders/UCM-PRIM-2.md +114 -0
- package/work-orders/UCM-SHELL-7.md +134 -0
- package/work-orders/UCM-THEME-13.md +69 -0
- package/work-orders/UCM-THEME-14.md +140 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Link } from '@mui/material';
|
|
4
|
+
const HTTP_URL_PATTERN = /(https?:\/\/[^\s<>"']+)/g;
|
|
5
|
+
function splitTrailingPunctuation(rawUrl) {
|
|
6
|
+
let url = rawUrl;
|
|
7
|
+
let trailing = '';
|
|
8
|
+
while (/[.,!?]$/.test(url)) {
|
|
9
|
+
trailing = `${url.slice(-1)}${trailing}`;
|
|
10
|
+
url = url.slice(0, -1);
|
|
11
|
+
}
|
|
12
|
+
const openingParentheses = (url.match(/\(/g) || []).length;
|
|
13
|
+
const closingParentheses = (url.match(/\)/g) || []).length;
|
|
14
|
+
if (closingParentheses > openingParentheses) {
|
|
15
|
+
trailing = `)${trailing}`;
|
|
16
|
+
url = url.slice(0, -1);
|
|
17
|
+
}
|
|
18
|
+
return { url, trailing };
|
|
19
|
+
}
|
|
20
|
+
export function linkifyText(text) {
|
|
21
|
+
var _a;
|
|
22
|
+
const value = String(text !== null && text !== void 0 ? text : '');
|
|
23
|
+
const parts = [];
|
|
24
|
+
let lastIndex = 0;
|
|
25
|
+
for (const match of value.matchAll(HTTP_URL_PATTERN)) {
|
|
26
|
+
const rawUrl = match[0];
|
|
27
|
+
const start = (_a = match.index) !== null && _a !== void 0 ? _a : 0;
|
|
28
|
+
if (start > lastIndex)
|
|
29
|
+
parts.push(value.slice(lastIndex, start));
|
|
30
|
+
const { url, trailing } = splitTrailingPunctuation(rawUrl);
|
|
31
|
+
parts.push(_jsx(Link, { href: url, target: "_blank", rel: "noopener noreferrer", children: url }, `url-${start}`));
|
|
32
|
+
if (trailing)
|
|
33
|
+
parts.push(trailing);
|
|
34
|
+
lastIndex = start + rawUrl.length;
|
|
35
|
+
}
|
|
36
|
+
if (lastIndex < value.length)
|
|
37
|
+
parts.push(value.slice(lastIndex));
|
|
38
|
+
return parts.length ? parts : [value];
|
|
39
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// src/auth/pages/PasswordInvitePage.jsx
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
4
|
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
|
5
5
|
import { Helmet } from 'react-helmet';
|
|
6
|
-
import { Typography } from '@mui/material';
|
|
6
|
+
import { Button, Typography } from '@mui/material';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
|
8
8
|
import { NarrowPage } from '../layout/PageLayout';
|
|
9
9
|
import { PasswordSetForm } from '../components/PasswordSetForm';
|
|
@@ -17,7 +17,9 @@ export function PasswordInvitePage() {
|
|
|
17
17
|
const nextPath = searchParams.get('next');
|
|
18
18
|
const [submitting, setSubmitting] = useState(false);
|
|
19
19
|
const [errorKey, setErrorKey] = useState(null);
|
|
20
|
+
const [submitErrorKey, setSubmitErrorKey] = useState(null);
|
|
20
21
|
const [successKey, setSuccessKey] = useState(null);
|
|
22
|
+
const [continueTarget, setContinueTarget] = useState(null);
|
|
21
23
|
const [checked, setChecked] = useState(false);
|
|
22
24
|
// Unterscheidung Invite-Link vs klassischer Reset
|
|
23
25
|
const isInvite = location.pathname.startsWith('/invite/');
|
|
@@ -47,12 +49,12 @@ export function PasswordInvitePage() {
|
|
|
47
49
|
check();
|
|
48
50
|
}, [uid, token]);
|
|
49
51
|
const handleSubmit = async (newPassword) => {
|
|
52
|
+
var _a, _b, _c;
|
|
50
53
|
if (!uid || !token) {
|
|
51
|
-
setErrorKey('Auth.RESET_LINK_INVALID');
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
56
|
setSubmitting(true);
|
|
55
|
-
|
|
57
|
+
setSubmitErrorKey(null);
|
|
56
58
|
setSuccessKey(null);
|
|
57
59
|
try {
|
|
58
60
|
await setNewPassword(uid, token, newPassword);
|
|
@@ -80,17 +82,30 @@ export function PasswordInvitePage() {
|
|
|
80
82
|
safeNextPath = parsed.pathname + parsed.search + parsed.hash;
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
|
-
catch (
|
|
85
|
+
catch (_d) {
|
|
84
86
|
// fall through to default redirect
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
const target = safeNextPath
|
|
88
90
|
? `/login?next=${encodeURIComponent(safeNextPath)}`
|
|
89
91
|
: '/login';
|
|
90
|
-
|
|
92
|
+
setContinueTarget(target);
|
|
91
93
|
}
|
|
92
94
|
catch (err) {
|
|
93
|
-
|
|
95
|
+
// Told apart by CODE, not by the presence of `messages` (a submission
|
|
96
|
+
// can fail with a link problem too, e.g. the token expires in the gap
|
|
97
|
+
// between the page's initial verifyResetToken check and this submit) —
|
|
98
|
+
// only a password rejection keeps the form; a link problem must still
|
|
99
|
+
// end in the same invalid-link state the initial check produces.
|
|
100
|
+
if (err.code === 'Auth.RESET_LINK_INVALID') {
|
|
101
|
+
setErrorKey(err.code);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const responseMessages = ((_a = err.raw) === null || _a === void 0 ? void 0 : _a.messages) || ((_c = (_b = err.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.messages);
|
|
105
|
+
setSubmitErrorKey(Array.isArray(responseMessages) && responseMessages.length > 0
|
|
106
|
+
? responseMessages.join(' ')
|
|
107
|
+
: t(err.code || 'Auth.RESET_PASSWORD_FAILED'));
|
|
108
|
+
}
|
|
94
109
|
}
|
|
95
110
|
finally {
|
|
96
111
|
setSubmitting(false);
|
|
@@ -100,5 +115,5 @@ export function PasswordInvitePage() {
|
|
|
100
115
|
if (!checked && !errorKey) {
|
|
101
116
|
return (_jsx(NarrowPage, { title: t('Auth.PAGE_CHECKING_LINK_TITLE'), children: _jsx(Typography, { children: t('Auth.PAGE_CHECKING_LINK_TEXT') }) }));
|
|
102
117
|
}
|
|
103
|
-
return (_jsxs(NarrowPage, { title: t(pageTitleKey), subtitle: t(pageSubtitleKey), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t(pageTitleKey)] }) }), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: t(errorKey) })), successKey && (_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) })), !successKey && !errorKey && (_jsx(PasswordSetForm, { onSubmit: handleSubmit, submitting: submitting }))] }));
|
|
118
|
+
return (_jsxs(NarrowPage, { title: t(pageTitleKey), subtitle: t(pageSubtitleKey), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t(pageTitleKey)] }) }), errorKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: t(errorKey) })), successKey && (_jsxs(_Fragment, { children: [_jsx(Typography, { color: "primary", gutterBottom: true, children: t(successKey) }), _jsx(Button, { variant: "contained", onClick: () => navigate(continueTarget), children: t('Auth.SIGNUP_GO_TO_LOGIN') })] })), submitErrorKey && !successKey && (_jsx(Typography, { color: "error", gutterBottom: true, children: submitErrorKey })), !successKey && !errorKey && (_jsx(PasswordSetForm, { onSubmit: handleSubmit, submitting: submitting }))] }));
|
|
104
119
|
}
|
package/dist/pages/SignUpPage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
4
4
|
import { Alert, Box, Button, Stack, TextField, Typography, } from '@mui/material';
|
|
@@ -117,7 +117,7 @@ export function SignUpPage() {
|
|
|
117
117
|
const handleGoToLogin = () => {
|
|
118
118
|
navigate('/login');
|
|
119
119
|
};
|
|
120
|
-
return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: pageSubtitle, children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), successKey && (_jsx(Alert, { severity: "success", sx: SIGN_UP_PAGE_ALERT_SX, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: SIGN_UP_PAGE_ALERT_SX, children: t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.')) })), signupModes.length > 1 && (_jsx(Stack, { spacing: 1, sx: { mb: 2 }, children: _jsx(Stack, { spacing: 1, children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, fullWidth: true, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) }) })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), mode === 'self_signup_access_code' && (_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting })), turnstileRequired && (_jsx(TurnstileWidget, { ref: turnstileWidgetRef, siteKey: authMethods.turnstile_site_key, onToken: setTurnstileToken })), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting ||
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
return (_jsxs(NarrowPage, { title: t('Auth.LOGIN_SIGNUP_BUTTON'), subtitle: pageSubtitle, children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('App.NAME'), " \u2013 ", t('Auth.LOGIN_SIGNUP_BUTTON')] }) }), signupModes.length === 0 ? (_jsx(Alert, { severity: "info", sx: SIGN_UP_PAGE_ALERT_SX, children: t('Auth.SELF_SIGNUP_DISABLED', 'Self sign-up is currently disabled.') })) : (_jsxs(_Fragment, { children: [successKey && (_jsx(Alert, { severity: "success", sx: SIGN_UP_PAGE_ALERT_SX, children: t(successKey, { email }) })), errorKey && (_jsx(Alert, { severity: "error", sx: SIGN_UP_PAGE_ALERT_SX, children: t(errorKey, t('Auth.INVITE_FAILED', 'Could not complete signup.')) })), signupModes.length > 1 && (_jsx(Stack, { spacing: 1, sx: { mb: 2 }, children: _jsx(Stack, { spacing: 1, children: signupModes.map((entry) => (_jsx(Button, { variant: mode === entry ? 'contained' : 'outlined', onClick: () => setMode(entry), disabled: submitting, fullWidth: true, children: t(MODE_LABELS[entry] || entry, entry) }, entry))) }) })), _jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { display: 'flex', flexDirection: 'column', gap: 2 }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", required: true, fullWidth: true, value: email, onChange: (e) => setEmail(e.target.value), disabled: submitting }), mode === 'self_signup_access_code' && (_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), type: "text", required: true, fullWidth: true, value: accessCode, onChange: (e) => setAccessCode(e.target.value), disabled: submitting })), turnstileRequired && (_jsx(TurnstileWidget, { ref: turnstileWidgetRef, siteKey: authMethods.turnstile_site_key, onToken: setTurnstileToken })), _jsx(Button, { type: "submit", variant: "contained", disabled: submitting || (turnstileRequired && !turnstileToken), children: submitting
|
|
121
|
+
? t('Auth.SIGNUP_SUBMITTING')
|
|
122
|
+
: t('Auth.SIGNUP_SUBMIT') })] })] })), _jsx(Box, { sx: { mt: 3 }, children: _jsxs(Typography, { variant: "body2", children: [t('Auth.SIGNUP_ALREADY_HAVE_ACCOUNT'), ' ', _jsx(Button, { onClick: handleGoToLogin, variant: "text", size: "small", children: t('Auth.SIGNUP_GO_TO_LOGIN') })] }) })] }));
|
|
123
123
|
}
|
|
@@ -20,7 +20,7 @@ import { CHART_FRAME_ALERT_SX, CHART_FRAME_ROOT_SX } from '../components/charts/
|
|
|
20
20
|
import { PAGE_LAYOUT_CONTAINER_SX } from '../layout/PageLayout';
|
|
21
21
|
import { SECTION_NAV_DRAWER_PAPER_SX, SECTION_NAV_GROUP_PAPER_SX, } from '../layout/SectionNav';
|
|
22
22
|
import { ATTACHMENT_LIST_ALERT_SX, ATTACHMENT_LIST_LIGHTBOX_ACTION_SX, } from '../messaging/AttachmentList';
|
|
23
|
-
import { COMPOSER_EMOJI_BUTTON_SX } from '../messaging/Composer';
|
|
23
|
+
import { COMPOSER_EMOJI_BUTTON_SX, COMPOSER_MESSAGE_FIELD_SX, COMPOSER_SEND_BUTTON_SX, } from '../messaging/Composer';
|
|
24
24
|
import { DIRECT_MESSAGE_LAUNCHER_ALERT_SX } from '../messaging/DirectMessageLauncher';
|
|
25
25
|
import { MESSAGE_BUBBLE_ACTION_SX, MESSAGE_BUBBLE_ROOT_SX } from '../messaging/MessageBubble';
|
|
26
26
|
import { POLL_CARD_CHECKBOX_SX, POLL_CARD_FORM_CONTROL_SX, POLL_CARD_ROOT_SX, } from '../messaging/PollCard';
|
|
@@ -51,6 +51,8 @@ const SX_EXPORTS = {
|
|
|
51
51
|
CHART_FRAME_ALERT_SX,
|
|
52
52
|
CHART_FRAME_ROOT_SX,
|
|
53
53
|
COMPOSER_EMOJI_BUTTON_SX,
|
|
54
|
+
COMPOSER_MESSAGE_FIELD_SX,
|
|
55
|
+
COMPOSER_SEND_BUTTON_SX,
|
|
54
56
|
DIRECT_MESSAGE_LAUNCHER_ALERT_SX,
|
|
55
57
|
LOGIN_FORM_DIVIDER_SX,
|
|
56
58
|
LOGIN_PAGE_ALERT_SX,
|
|
@@ -177,6 +179,8 @@ export const KIT_COMPONENT_SX_REGISTRY = [
|
|
|
177
179
|
entry('AttachmentList.lightboxAction', 'MuiIconButton', ATTACHMENT_LIST_LIGHTBOX_ACTION_SX),
|
|
178
180
|
entry('AttachmentList.alert', 'MuiAlert', ATTACHMENT_LIST_ALERT_SX),
|
|
179
181
|
entry('Composer.emojiAction', 'MuiIconButton', COMPOSER_EMOJI_BUTTON_SX),
|
|
182
|
+
entry('Composer.messageField', 'MuiTextField', COMPOSER_MESSAGE_FIELD_SX),
|
|
183
|
+
entry('Composer.sendAction', 'MuiButton', COMPOSER_SEND_BUTTON_SX),
|
|
180
184
|
entry('DirectMessageLauncher.alert', 'MuiAlert', DIRECT_MESSAGE_LAUNCHER_ALERT_SX),
|
|
181
185
|
entry('MessageBubble.root', 'MuiPaper', MESSAGE_BUBBLE_ROOT_SX),
|
|
182
186
|
entry('MessageBubble.action', 'MuiIconButton', MESSAGE_BUBBLE_ACTION_SX),
|
package/package.json
CHANGED
package/src/auth/UserMenu.jsx
CHANGED
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
MenuItem,
|
|
10
10
|
Typography,
|
|
11
11
|
} from '@mui/material';
|
|
12
|
+
import { useTheme } from '@mui/material/styles';
|
|
12
13
|
import { useTranslation } from 'react-i18next';
|
|
13
14
|
|
|
14
15
|
import { AuthContext } from './AuthContext';
|
|
16
|
+
import { calculateContrastRatio } from '../theme';
|
|
15
17
|
|
|
16
18
|
function getInitials(user) {
|
|
17
19
|
if (!user) return '';
|
|
@@ -30,8 +32,22 @@ function getDisplayName(user) {
|
|
|
30
32
|
export function UserMenu({ resolveLink, items = [], profileLink = '/account', avatarSrc }) {
|
|
31
33
|
const { t } = useTranslation();
|
|
32
34
|
const { user, logout } = useContext(AuthContext);
|
|
35
|
+
const theme = useTheme();
|
|
33
36
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
34
37
|
const open = Boolean(anchorEl);
|
|
38
|
+
const avatarBackground = theme.palette.primary.main;
|
|
39
|
+
// True white/black (theme.palette.common.*, always present on any MUI theme,
|
|
40
|
+
// never a source-level literal), not background.paper/ink.primary: contrast
|
|
41
|
+
// against true white and true black multiplies to a constant 21 regardless
|
|
42
|
+
// of the background's luminance, so max(contrastWhite, contrastBlack) is
|
|
43
|
+
// mathematically guaranteed to be >= sqrt(21) =~ 4.58 for ANY primary.main a
|
|
44
|
+
// consumer sets -- a near-black token like ink.primary (#212529, luminance
|
|
45
|
+
// ~0.018, not 0) does not carry that guarantee and left a real gap for a
|
|
46
|
+
// medium-luminance primary (caught by ui_reviewer, neither required test
|
|
47
|
+
// exercised the fallback branch that gap lived in).
|
|
48
|
+
const avatarText = calculateContrastRatio(theme.palette.common.white, avatarBackground) >= 4.5
|
|
49
|
+
? theme.palette.common.white
|
|
50
|
+
: theme.palette.common.black;
|
|
35
51
|
|
|
36
52
|
const handleClose = () => setAnchorEl(null);
|
|
37
53
|
|
|
@@ -57,7 +73,16 @@ export function UserMenu({ resolveLink, items = [], profileLink = '/account', av
|
|
|
57
73
|
color="inherit"
|
|
58
74
|
onClick={(event) => setAnchorEl(event.currentTarget)}
|
|
59
75
|
>
|
|
60
|
-
<Avatar
|
|
76
|
+
<Avatar
|
|
77
|
+
src={avatarSrc}
|
|
78
|
+
sx={{
|
|
79
|
+
width: 32,
|
|
80
|
+
height: 32,
|
|
81
|
+
fontSize: '0.8rem',
|
|
82
|
+
bgcolor: avatarBackground,
|
|
83
|
+
color: avatarText,
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
61
86
|
{avatarSrc ? null : getInitials(user)}
|
|
62
87
|
</Avatar>
|
|
63
88
|
</IconButton>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Box, Button, Typography } from '@mui/material';
|
|
2
|
+
import { cloneElement } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A shared, compact presentation for an empty section: optional decorative icon,
|
|
6
|
+
* title, supporting description, and an optional outlined action.
|
|
7
|
+
*/
|
|
8
|
+
export default function EmptyState({ icon, title, description, action, sx }) {
|
|
9
|
+
return (
|
|
10
|
+
<Box
|
|
11
|
+
sx={[{
|
|
12
|
+
display: 'flex',
|
|
13
|
+
flexDirection: 'column',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
gap: 1,
|
|
16
|
+
p: 2,
|
|
17
|
+
textAlign: 'center',
|
|
18
|
+
}, sx]}
|
|
19
|
+
>
|
|
20
|
+
{icon && (
|
|
21
|
+
<Box sx={{ color: 'text.secondary', display: 'flex' }}>
|
|
22
|
+
{cloneElement(icon, { 'aria-hidden': true, focusable: false })}
|
|
23
|
+
</Box>
|
|
24
|
+
)}
|
|
25
|
+
<Typography component="p">{title}</Typography>
|
|
26
|
+
{description && (
|
|
27
|
+
<Typography component="p" variant="body2" color="text.secondary">
|
|
28
|
+
{description}
|
|
29
|
+
</Typography>
|
|
30
|
+
)}
|
|
31
|
+
{action && (
|
|
32
|
+
<Button type="button" variant="outlined" onClick={action.onClick}>
|
|
33
|
+
{action.label}
|
|
34
|
+
</Button>
|
|
35
|
+
)}
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -118,6 +118,7 @@ export function UserListComponent({
|
|
|
118
118
|
await updateUserRole(userId, newRole);
|
|
119
119
|
}
|
|
120
120
|
await loadUsers();
|
|
121
|
+
alert(t('UserList.ROLE_UPDATE_SUCCESS', 'Role updated.'));
|
|
121
122
|
} catch (err) {
|
|
122
123
|
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
123
124
|
}
|
|
@@ -588,6 +589,7 @@ export function UserListComponent({
|
|
|
588
589
|
setPageSize(Number(event.target.value));
|
|
589
590
|
setPage(0);
|
|
590
591
|
}}
|
|
592
|
+
labelRowsPerPage={t('UserList.ROWS_PER_PAGE', 'Rows per page:')}
|
|
591
593
|
rowsPerPageOptions={[10, 25, 50, 100]}
|
|
592
594
|
/>
|
|
593
595
|
</Box>
|