mirror of
https://github.com/plankanban/planka.git
synced 2025-12-27 17:25:02 +03:00
22
client/src/components/users/UserSettingsModal/AboutPane.jsx
Normal file
22
client/src/components/users/UserSettingsModal/AboutPane.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Image, Tab } from 'semantic-ui-react';
|
||||
|
||||
import version from '../../../version';
|
||||
|
||||
import logo from '../../../assets/images/logo.png';
|
||||
|
||||
import styles from './AboutPane.module.scss';
|
||||
|
||||
const AboutPane = React.memo(() => (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<Image centered src={logo} size="large" />
|
||||
<div className={styles.version}>{version}</div>
|
||||
</Tab.Pane>
|
||||
));
|
||||
|
||||
export default AboutPane;
|
||||
@@ -0,0 +1,16 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.version {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Divider, Dropdown, Header, Tab } from 'semantic-ui-react';
|
||||
|
||||
import { usePopupInClosableContext } from '../../../../hooks';
|
||||
import locales from '../../../../locales';
|
||||
import EditAvatarStep from './EditAvatarStep';
|
||||
import EditUserInformation from '../../EditUserInformation';
|
||||
import EditUserUsernameStep from '../../EditUserUsernameStep';
|
||||
import EditUserEmailStep from '../../EditUserEmailStep';
|
||||
import EditUserPasswordStep from '../../EditUserPasswordStep';
|
||||
import UserAvatar from '../../UserAvatar';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
|
||||
import styles from './AccountPane.module.scss';
|
||||
|
||||
const AccountPane = React.memo(() => {
|
||||
const user = useSelector(selectors.selectCurrentUser);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleLanguageChange = useCallback(
|
||||
(_, { value }) => {
|
||||
// FIXME: hack
|
||||
dispatch(entryActions.updateCurrentUserLanguage(value === 'auto' ? null : value));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const EditAvatarPopup = usePopupInClosableContext(EditAvatarStep);
|
||||
const EditUserUsernamePopup = usePopupInClosableContext(EditUserUsernameStep);
|
||||
const EditUserEmailPopup = usePopupInClosableContext(EditUserEmailStep);
|
||||
const EditUserPasswordPopup = usePopupInClosableContext(EditUserPasswordStep);
|
||||
|
||||
const isUsernameEditable = !user.lockedFieldNames.includes('username');
|
||||
const isEmailEditable = !user.lockedFieldNames.includes('email');
|
||||
const isPasswordEditable = !user.lockedFieldNames.includes('password');
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<EditAvatarPopup>
|
||||
<UserAvatar id={user.id} size="massive" isDisabled={user.isAvatarUpdating} />
|
||||
</EditAvatarPopup>
|
||||
<br />
|
||||
<br />
|
||||
<EditUserInformation id={user.id} />
|
||||
<Divider horizontal section>
|
||||
<Header as="h4">
|
||||
{t('common.language', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Header>
|
||||
</Divider>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
options={[
|
||||
{
|
||||
value: 'auto',
|
||||
text: t('common.detectAutomatically'),
|
||||
},
|
||||
...locales.map((locale) => ({
|
||||
value: locale.language,
|
||||
flag: locale.country,
|
||||
text: locale.name,
|
||||
})),
|
||||
]}
|
||||
value={user.language || 'auto'}
|
||||
onChange={handleLanguageChange}
|
||||
/>
|
||||
{(isUsernameEditable || isEmailEditable || isPasswordEditable) && (
|
||||
<>
|
||||
<Divider horizontal section>
|
||||
<Header as="h4">
|
||||
{t('common.authentication', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Header>
|
||||
</Divider>
|
||||
{isUsernameEditable && (
|
||||
<div className={styles.action}>
|
||||
<EditUserUsernamePopup id={user.id} withPasswordConfirmation={!user.isSsoUser}>
|
||||
<Button className={styles.actionButton}>
|
||||
{t('action.editUsername', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Button>
|
||||
</EditUserUsernamePopup>
|
||||
</div>
|
||||
)}
|
||||
{isEmailEditable && (
|
||||
<div className={styles.action}>
|
||||
<EditUserEmailPopup id={user.id} withPasswordConfirmation={!user.isSsoUser}>
|
||||
<Button className={styles.actionButton}>
|
||||
{t('action.editEmail', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Button>
|
||||
</EditUserEmailPopup>
|
||||
</div>
|
||||
)}
|
||||
{isPasswordEditable && (
|
||||
<div className={styles.action}>
|
||||
<EditUserPasswordPopup id={user.id} withPasswordConfirmation={!user.isSsoUser}>
|
||||
<Button className={styles.actionButton}>
|
||||
{t('action.editPassword', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Button>
|
||||
</EditUserPasswordPopup>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default AccountPane;
|
||||
@@ -0,0 +1,38 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.action {
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: background 0.3s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
background: transparent;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
height: 36px;
|
||||
line-height: 24px;
|
||||
padding: 6px 12px;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from 'semantic-ui-react';
|
||||
import { FilePicker, Popup } from '../../../../lib/custom-ui';
|
||||
|
||||
import selectors from '../../../../selectors';
|
||||
import entryActions from '../../../../entry-actions';
|
||||
|
||||
import styles from './EditAvatarStep.module.scss';
|
||||
|
||||
const EditAvatarStep = React.memo(({ onClose }) => {
|
||||
const defaultValue = useSelector((state) => selectors.selectCurrentUser(state).avatar);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const fieldRef = useRef(null);
|
||||
|
||||
const handleFileSelect = useCallback(
|
||||
(file) => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentUserAvatar({
|
||||
file,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
},
|
||||
[onClose, dispatch],
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentUser({
|
||||
avatar: null,
|
||||
}),
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [onClose, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
fieldRef.current.focus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popup.Header>
|
||||
{t('common.editAvatar', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Popup.Header>
|
||||
<Popup.Content>
|
||||
<div className={styles.action}>
|
||||
<FilePicker accept="image/*" onSelect={handleFileSelect}>
|
||||
<Button
|
||||
ref={fieldRef}
|
||||
content={t('action.uploadNewAvatar')}
|
||||
className={styles.actionButton}
|
||||
/>
|
||||
</FilePicker>
|
||||
</div>
|
||||
{defaultValue && (
|
||||
<Button negative content={t('action.deleteAvatar')} onClick={handleDeleteClick} />
|
||||
)}
|
||||
</Popup.Content>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
EditAvatarStep.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EditAvatarStep;
|
||||
@@ -0,0 +1,33 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.action {
|
||||
border: none;
|
||||
border-radius: 0.28571429rem;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: background 0.3s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: #e9e9e9;
|
||||
}
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
background: transparent;
|
||||
color: #6b808c;
|
||||
font-weight: normal;
|
||||
height: 36px;
|
||||
line-height: 24px;
|
||||
padding: 6px 12px;
|
||||
text-align: left;
|
||||
text-decoration: underline;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import AccountPane from './AccountPane';
|
||||
|
||||
export default AccountPane;
|
||||
@@ -0,0 +1,35 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
import NotificationServices from '../../notification-services/NotificationServices';
|
||||
|
||||
import styles from './NotificationsPane.module.scss';
|
||||
|
||||
const NotificationsPane = React.memo(() => {
|
||||
const notificationServiceIds = useSelector(selectors.selectNotificationServiceIdsForCurrentUser);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(data) => {
|
||||
dispatch(entryActions.createNotificationServiceInCurrentUser(data));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<NotificationServices ids={notificationServiceIds} onCreate={handleCreate} />
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default NotificationsPane;
|
||||
@@ -0,0 +1,11 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Radio, Tab } from 'semantic-ui-react';
|
||||
|
||||
import selectors from '../../../selectors';
|
||||
import entryActions from '../../../entry-actions';
|
||||
|
||||
import styles from './PreferencesPane.module.scss';
|
||||
|
||||
const PreferencesPane = React.memo(() => {
|
||||
const user = useSelector(selectors.selectCurrentUser);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(_, { name: fieldName, checked }) => {
|
||||
dispatch(
|
||||
entryActions.updateCurrentUser({
|
||||
[fieldName]: checked,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||
<Radio
|
||||
toggle
|
||||
name="subscribeToOwnCards"
|
||||
checked={user.subscribeToOwnCards}
|
||||
label={t('common.subscribeToMyOwnCardsByDefault')}
|
||||
className={styles.radio}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Radio
|
||||
toggle
|
||||
name="subscribeToCardWhenCommenting"
|
||||
checked={user.subscribeToCardWhenCommenting}
|
||||
label={t('common.subscribeToCardWhenCommenting')}
|
||||
className={styles.radio}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Radio
|
||||
toggle
|
||||
name="turnOffRecentCardHighlighting"
|
||||
checked={user.turnOffRecentCardHighlighting}
|
||||
label={t('common.turnOffRecentCardHighlighting')}
|
||||
className={styles.radio}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
);
|
||||
});
|
||||
|
||||
export default PreferencesPane;
|
||||
@@ -0,0 +1,20 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
:global(#app) {
|
||||
.radio {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Tab } from 'semantic-ui-react';
|
||||
|
||||
import entryActions from '../../../entry-actions';
|
||||
import { useClosableModal } from '../../../hooks';
|
||||
import AccountPane from './AccountPane';
|
||||
import PreferencesPane from './PreferencesPane';
|
||||
import NotificationsPane from './NotificationsPane';
|
||||
import AboutPane from './AboutPane';
|
||||
|
||||
const UserSettingsModal = React.memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(entryActions.closeModal());
|
||||
}, [dispatch]);
|
||||
|
||||
const [ClosableModal] = useClosableModal();
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: t('common.account', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <AccountPane />,
|
||||
},
|
||||
{
|
||||
menuItem: t('common.preferences', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <PreferencesPane />,
|
||||
},
|
||||
{
|
||||
menuItem: t('common.notifications', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <NotificationsPane />,
|
||||
},
|
||||
{
|
||||
menuItem: t('common.aboutPlanka', {
|
||||
context: 'title',
|
||||
}),
|
||||
render: () => <AboutPane />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ClosableModal open closeIcon size="small" centered={false} onClose={handleClose}>
|
||||
<ClosableModal.Content>
|
||||
<Tab
|
||||
menu={{
|
||||
secondary: true,
|
||||
pointing: true,
|
||||
}}
|
||||
panes={panes}
|
||||
/>
|
||||
</ClosableModal.Content>
|
||||
</ClosableModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default UserSettingsModal;
|
||||
8
client/src/components/users/UserSettingsModal/index.js
Normal file
8
client/src/components/users/UserSettingsModal/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import UserSettingsModal from './UserSettingsModal';
|
||||
|
||||
export default UserSettingsModal;
|
||||
Reference in New Issue
Block a user