diff --git a/client/src/components/common/AdministrationModal/UserEditModal/ApiKeyPane.jsx b/client/src/components/common/AdministrationModal/UserEditModal/ApiKeyPane.jsx new file mode 100644 index 00000000..7206b048 --- /dev/null +++ b/client/src/components/common/AdministrationModal/UserEditModal/ApiKeyPane.jsx @@ -0,0 +1,148 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Button, Icon, Input, Message, Tab } from 'semantic-ui-react'; + +import selectors from '../../../../selectors'; +import entryActions from '../../../../entry-actions'; + +import styles from './ApiKeyPane.module.scss'; + +const ConfirmStates = { + REGENERATE: 'REGENERATE', + DELETE: 'DELETE', +}; + +const ApiKeyPane = React.memo(({ userId, onClose }) => { + const selectUserById = useMemo(() => selectors.makeSelectUserById(), []); + + const user = useSelector((state) => selectUserById(state, userId)); + + const dispatch = useDispatch(); + const [t] = useTranslation(); + const [confirmState, setConfirmState] = useState(null); + const [isCopied, setIsCopied] = useState(false); + + const handleGenerateClick = useCallback(() => { + if (user.apiKeyPrefix) { + setConfirmState(ConfirmStates.REGENERATE); + } else { + dispatch(entryActions.createUserApiKey(userId)); + } + }, [userId, user.apiKeyPrefix, dispatch]); + + const handleRegenerateConfirm = useCallback(() => { + dispatch(entryActions.createUserApiKey(userId)); + setConfirmState(null); + }, [userId, dispatch]); + + const handleDeleteConfirm = useCallback(() => { + dispatch(entryActions.deleteUserApiKey(userId)); + onClose(); + }, [userId, onClose, dispatch]); + + const handleCancelConfirm = useCallback(() => { + setConfirmState(null); + }, []); + + const handleDeleteClick = useCallback(() => { + setConfirmState(ConfirmStates.DELETE); + }, []); + + const handleCopyClick = useCallback(() => { + if (isCopied) return; + navigator.clipboard.writeText(user.apiKeyState.value); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 1000); + }, [user.apiKeyState.value, isCopied]); + + if (confirmState === ConfirmStates.REGENERATE) { + return ( + +

{t('common.areYouSureYouWantToRegenerateThisApiKey')}

+ + + + ) : ( + + ))} + + ) : ( + + )} + + + )} + {isEmailEditable && ( +
+
{t('common.email')}
+
+ + {isEmailUnlocked ? ( + + ) : ( + + )} +
+
+ )} +
+
{t('common.phone')}
+ +
+ +
+
+
{t('common.organization')}
+ +
+
+ + + + + {!isPasswordVisible ? ( +
+
+ ) : ( +
+
+
{t('common.newPassword')}
+ +
+
+
{t('common.confirmPassword')}
+ +
+ +
+ )} + + {withPasswordConfirmation && credentialsChanged && ( + <> +
{t('common.currentPassword')}
+ + + )} + + - - - ) : ( - - ))} - @@ -95,6 +103,7 @@ const Item = React.memo(({ id }) => { Item.propTypes = { id: PropTypes.string.isRequired, + onEdit: PropTypes.func.isRequired, }; export default Item; diff --git a/client/src/components/common/AdministrationModal/UsersPane/Item.module.scss b/client/src/components/common/AdministrationModal/UsersPane/Item.module.scss index b4140570..515ca193 100644 --- a/client/src/components/common/AdministrationModal/UsersPane/Item.module.scss +++ b/client/src/components/common/AdministrationModal/UsersPane/Item.module.scss @@ -51,4 +51,37 @@ .wrapperDeactivated { opacity: 0.64; } + + .nameLink { + color: inherit; + cursor: pointer; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + .mobileIdentity { + display: none; + } + + @container (max-width: 600px) { + .identityCell { + display: none !important; + } + + .mobileIdentity { + color: #6b6b6b; + display: block; + font-size: 13px; + margin-top: 4px; + } + } + + @container (max-width: 450px) { + .informationCell { + display: none !important; + } + } } diff --git a/client/src/components/common/AdministrationModal/UsersPane/ResetTotpStep.jsx b/client/src/components/common/AdministrationModal/UsersPane/ResetTotpStep.jsx deleted file mode 100644 index dcc69767..00000000 --- a/client/src/components/common/AdministrationModal/UsersPane/ResetTotpStep.jsx +++ /dev/null @@ -1,131 +0,0 @@ -/*! - * Copyright (c) 2024 PLANKA Software GmbH - * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md - */ - -import React, { useCallback, useMemo, useRef, useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { useDispatch, useSelector } from 'react-redux'; -import { useTranslation } from 'react-i18next'; -import { Button, Form, Message } from 'semantic-ui-react'; -import { Input, Popup } from '../../../../lib/custom-ui'; - -import selectors from '../../../../selectors'; -import entryActions from '../../../../entry-actions'; -import { useForm, useNestedRef } from '../../../../hooks'; - -import styles from './ResetTotpStep.module.scss'; - -const createMessage = (error) => { - if (!error) { - return error; - } - - switch (error.message) { - case 'Invalid current password': - return { - type: 'error', - content: 'common.invalidCurrentPassword', - }; - default: - return { - type: 'warning', - content: 'common.unknownError', - }; - } -}; - -const ResetTotpStep = React.memo(({ userId, onBack, onClose }) => { - const selectUserById = useMemo(() => selectors.makeSelectUserById(), []); - const user = useSelector((state) => selectUserById(state, userId)); - - const dispatch = useDispatch(); - const [t] = useTranslation(); - - const { isDisabling, error } = user.totpState || {}; - const wasDisablingRef = useRef(false); - - const [data, handleFieldChange] = useForm({ - currentPassword: '', - }); - - const [currentPasswordFieldRef, handleCurrentPasswordFieldRef] = useNestedRef('inputRef'); - - const message = useMemo(() => createMessage(error), [error]); - - // The admin confirms with their own password, so the only signal that the - // reset went through is the request finishing without an error. - useEffect(() => { - if (wasDisablingRef.current && !isDisabling && !error) { - onClose(); - } - - wasDisablingRef.current = isDisabling; - }, [isDisabling, error, onClose]); - - const handleSubmit = useCallback(() => { - if (!data.currentPassword) { - currentPasswordFieldRef.current.focus(); - return; - } - - dispatch( - entryActions.disableUserTotp(userId, { - currentPassword: data.currentPassword, - }), - ); - }, [dispatch, userId, data.currentPassword, currentPasswordFieldRef]); - - return ( - <> - - {t('common.reset2fa', { - context: 'title', - })} - - -

{t('common.reset2faWarning')}

- {message && ( - - )} -
-
{t('common.currentPassword')}
- - + {editingUserId && }
); }); diff --git a/client/src/components/common/AdministrationModal/UsersPane/UsersPane.module.scss b/client/src/components/common/AdministrationModal/UsersPane/UsersPane.module.scss index 166f2be7..d0870033 100644 --- a/client/src/components/common/AdministrationModal/UsersPane/UsersPane.module.scss +++ b/client/src/components/common/AdministrationModal/UsersPane/UsersPane.module.scss @@ -26,6 +26,7 @@ } .tableWrapper { + container-type: inline-size; margin-right: -2.5rem; max-height: calc(100vh - 338px); overflow: auto; diff --git a/client/src/components/users/EditUserInformationStep.jsx b/client/src/components/users/EditUserInformationStep.jsx deleted file mode 100644 index eaa8ee2d..00000000 --- a/client/src/components/users/EditUserInformationStep.jsx +++ /dev/null @@ -1,44 +0,0 @@ -/*! - * 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 PropTypes from 'prop-types'; -import { useTranslation } from 'react-i18next'; -import { Popup } from '../../lib/custom-ui'; - -import EditUserInformation from './EditUserInformation'; - -const EditUserInformationStep = React.memo(({ id, onBack, onClose }) => { - const [t] = useTranslation(); - - const handleUpdate = useCallback(() => { - onClose(); - }, [onClose]); - - return ( - <> - - {t('common.editInformation', { - context: 'title', - })} - - - - - - ); -}); - -EditUserInformationStep.propTypes = { - id: PropTypes.string.isRequired, - onBack: PropTypes.func, - onClose: PropTypes.func.isRequired, -}; - -EditUserInformationStep.defaultProps = { - onBack: undefined, -}; - -export default EditUserInformationStep; diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js index 550ad4cd..6cb4d1c4 100644 --- a/client/src/locales/de-DE/core.js +++ b/client/src/locales/de-DE/core.js @@ -140,6 +140,7 @@ export default { color: 'Farbe', comments: 'Kommentare', confirmCodesSaved: 'Ich habe diese Codes an einem sicheren Ort gespeichert.', + confirmPassword: 'Passwort bestätigen', contentExceedsLimit: 'Inhalt überschreitet {{limit}}', contentOfThisAttachmentIsTooBigToDisplay: 'Der Inhalt dieses Anhangs ist zu groß für die Anzeige.', @@ -468,6 +469,7 @@ export default { assignAsOwner: 'Als Eigentümer zuweisen', back: 'Zurück', cancel: 'Abbrechen', + changePassword: 'Passwort ändern', copy: 'Kopieren', copyAll: 'Alle kopieren', copyCard_title: 'Karte Kopieren', diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js index 821ff87f..250ba4ab 100644 --- a/client/src/locales/en-US/core.js +++ b/client/src/locales/en-US/core.js @@ -121,6 +121,7 @@ export default { color: 'Color', comments: 'Comments', confirmCodesSaved: 'I have saved these codes in a safe place.', + confirmPassword: 'Confirm password', contentExceedsLimit: 'Content exceeds {{limit}}', contentOfThisAttachmentIsTooBigToDisplay: 'Content of this attachment is too big to display.', copy_inline: 'copy', @@ -442,6 +443,7 @@ export default { assignAsOwner: 'Assign as owner', back: 'Back', cancel: 'Cancel', + changePassword: 'Change password', copy: 'Copy', copyAll: 'Copy all', copyCard_title: 'Copy Card',