From d3ff3e2db81b11c668965494ab7eb22ea0786400 Mon Sep 17 00:00:00 2001 From: Sebastian Dichler Date: Wed, 25 Mar 2026 11:24:27 +0100 Subject: [PATCH] feat(client): Add ability for admins to edit user avatars (#1604) --- .../UsersPane/ActionsStep.jsx | 14 +++++++ .../EditUserAvatarStep.jsx} | 40 +++++++++++-------- .../EditUserAvatarStep.module.scss} | 0 .../index.js | 4 +- .../{AccountPane => }/AccountPane.jsx | 26 ++++++------ .../{AccountPane => }/AccountPane.module.scss | 0 client/src/constants/EntryActionTypes.js | 1 + client/src/entry-actions/users.js | 9 +++++ client/src/locales/ar-YE/core.js | 1 + client/src/locales/bg-BG/core.js | 1 + client/src/locales/ca-ES/core.js | 1 + client/src/locales/cs-CZ/core.js | 1 + client/src/locales/da-DK/core.js | 1 + client/src/locales/de-DE/core.js | 1 + client/src/locales/el-GR/core.js | 1 + client/src/locales/en-GB/core.js | 1 + client/src/locales/en-US/core.js | 1 + client/src/locales/es-ES/core.js | 1 + client/src/locales/et-EE/core.js | 1 + client/src/locales/fa-IR/core.js | 1 + client/src/locales/fi-FI/core.js | 1 + client/src/locales/fr-FR/core.js | 1 + client/src/locales/hu-HU/core.js | 1 + client/src/locales/id-ID/core.js | 1 + client/src/locales/it-IT/core.js | 1 + client/src/locales/ja-JP/core.js | 1 + client/src/locales/ko-KR/core.js | 1 + client/src/locales/nl-NL/core.js | 1 + client/src/locales/pl-PL/core.js | 1 + client/src/locales/pt-BR/core.js | 1 + client/src/locales/pt-PT/core.js | 1 + client/src/locales/ro-RO/core.js | 1 + client/src/locales/ru-RU/core.js | 1 + client/src/locales/sk-SK/core.js | 1 + client/src/locales/sr-Cyrl-RS/core.js | 1 + client/src/locales/sr-Latn-RS/core.js | 1 + client/src/locales/sv-SE/core.js | 1 + client/src/locales/tr-TR/core.js | 1 + client/src/locales/uk-UA/core.js | 1 + client/src/locales/uz-UZ/core.js | 1 + client/src/locales/vi-VN/core.js | 1 + client/src/locales/zh-CN/core.js | 1 + client/src/locales/zh-TW/core.js | 1 + client/src/sagas/core/watchers/users.js | 3 ++ 44 files changed, 101 insertions(+), 31 deletions(-) rename client/src/components/users/{UserSettingsModal/AccountPane/EditAvatarStep.jsx => EditUserAvatarStep/EditUserAvatarStep.jsx} (59%) rename client/src/components/users/{UserSettingsModal/AccountPane/EditAvatarStep.module.scss => EditUserAvatarStep/EditUserAvatarStep.module.scss} (100%) rename client/src/components/users/{UserSettingsModal/AccountPane => EditUserAvatarStep}/index.js (62%) rename client/src/components/users/UserSettingsModal/{AccountPane => }/AccountPane.jsx (83%) rename client/src/components/users/UserSettingsModal/{AccountPane => }/AccountPane.module.scss (100%) diff --git a/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx b/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx index b5f224a2..9fbad0ed 100644 --- a/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx +++ b/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx @@ -17,6 +17,7 @@ import SelectRoleStep from './SelectRoleStep'; import ApiKeyStep from './ApiKeyStep'; import ConfirmationStep from '../../ConfirmationStep'; import EditUserInformationStep from '../../../users/EditUserInformationStep'; +import EditUserAvatarStep from '../../../users/EditUserAvatarStep'; import EditUserUsernameStep from '../../../users/EditUserUsernameStep'; import EditUserEmailStep from '../../../users/EditUserEmailStep'; import EditUserPasswordStep from '../../../users/EditUserPasswordStep'; @@ -25,6 +26,7 @@ import styles from './ActionsStep.module.scss'; const StepTypes = { EDIT_INFORMATION: 'EDIT_INFORMATION', + EDIT_AVATAR: 'EDIT_AVATAR', EDIT_USERNAME: 'EDIT_USERNAME', EDIT_EMAIL: 'EDIT_EMAIL', EDIT_PASSWORD: 'EDIT_PASSWORD', @@ -97,6 +99,10 @@ const ActionsStep = React.memo(({ userId, onClose }) => { openStep(StepTypes.EDIT_INFORMATION); }, [openStep]); + const handleEditAvatarClick = useCallback(() => { + openStep(StepTypes.EDIT_AVATAR); + }, [openStep]); + const handleEditUsernameClick = useCallback(() => { openStep(StepTypes.EDIT_USERNAME); }, [openStep]); @@ -137,6 +143,8 @@ const ActionsStep = React.memo(({ userId, onClose }) => { switch (step.type) { case StepTypes.EDIT_INFORMATION: return ; + case StepTypes.EDIT_AVATAR: + return ; case StepTypes.EDIT_USERNAME: return ; case StepTypes.EDIT_EMAIL: @@ -219,6 +227,12 @@ const ActionsStep = React.memo(({ userId, onClose }) => { context: 'title', })} + + + {t('action.editAvatar', { + context: 'title', + })} + {!user.lockedFieldNames.includes('username') && ( diff --git a/client/src/components/users/UserSettingsModal/AccountPane/EditAvatarStep.jsx b/client/src/components/users/EditUserAvatarStep/EditUserAvatarStep.jsx similarity index 59% rename from client/src/components/users/UserSettingsModal/AccountPane/EditAvatarStep.jsx rename to client/src/components/users/EditUserAvatarStep/EditUserAvatarStep.jsx index 2b89c2c4..3f816be7 100644 --- a/client/src/components/users/UserSettingsModal/AccountPane/EditAvatarStep.jsx +++ b/client/src/components/users/EditUserAvatarStep/EditUserAvatarStep.jsx @@ -3,20 +3,22 @@ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md */ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, 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 { FilePicker, Popup } from '../../../lib/custom-ui'; -import selectors from '../../../../selectors'; -import entryActions from '../../../../entry-actions'; +import selectors from '../../../selectors'; +import entryActions from '../../../entry-actions'; -import styles from './EditAvatarStep.module.scss'; +import styles from './EditUserAvatarStep.module.scss'; -const EditAvatarStep = React.memo(({ onClose }) => { - const defaultValue = useSelector((state) => selectors.selectCurrentUser(state).avatar); +const EditUserAvatarStep = React.memo(({ id, onBack, onClose }) => { + const selectUserById = useMemo(() => selectors.makeSelectUserById(), []); + + const avatar = useSelector((state) => selectUserById(state, id).avatar); const dispatch = useDispatch(); const [t] = useTranslation(); @@ -26,33 +28,33 @@ const EditAvatarStep = React.memo(({ onClose }) => { const handleFileSelect = useCallback( (file) => { dispatch( - entryActions.updateCurrentUserAvatar({ + entryActions.updateUserAvatar(id, { file, }), ); onClose(); }, - [onClose, dispatch], + [dispatch, id, onClose], ); const handleDeleteClick = useCallback(() => { dispatch( - entryActions.updateCurrentUser({ + entryActions.updateUser(id, { avatar: null, }), ); onClose(); - }, [onClose, dispatch]); + }, [dispatch, id, onClose]); useEffect(() => { - fieldRef.current.focus(); + fieldRef.current?.focus?.(); }, []); return ( <> - + {t('common.editAvatar', { context: 'title', })} @@ -67,7 +69,7 @@ const EditAvatarStep = React.memo(({ onClose }) => { /> - {defaultValue && ( + {avatar && (