feat(client): Add ability for admins to edit user avatars (#1604)

This commit is contained in:
Sebastian Dichler
2026-03-25 11:24:27 +01:00
committed by GitHub
parent 952a078aa1
commit d3ff3e2db8
44 changed files with 101 additions and 31 deletions
@@ -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 <EditUserInformationStep id={userId} onBack={handleBack} onClose={onClose} />;
case StepTypes.EDIT_AVATAR:
return <EditUserAvatarStep id={userId} onBack={handleBack} onClose={onClose} />;
case StepTypes.EDIT_USERNAME:
return <EditUserUsernameStep id={userId} onBack={handleBack} onClose={onClose} />;
case StepTypes.EDIT_EMAIL:
@@ -219,6 +227,12 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
context: 'title',
})}
</Menu.Item>
<Menu.Item className={styles.menuItem} onClick={handleEditAvatarClick}>
<Icon name="image outline" className={styles.menuItemIcon} />
{t('action.editAvatar', {
context: 'title',
})}
</Menu.Item>
{!user.lockedFieldNames.includes('username') && (
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
<Icon name="at" className={styles.menuItemIcon} />
@@ -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 (
<>
<Popup.Header>
<Popup.Header onBack={onBack}>
{t('common.editAvatar', {
context: 'title',
})}
@@ -67,7 +69,7 @@ const EditAvatarStep = React.memo(({ onClose }) => {
/>
</FilePicker>
</div>
{defaultValue && (
{avatar && (
<Button negative content={t('action.deleteAvatar')} onClick={handleDeleteClick} />
)}
</Popup.Content>
@@ -75,8 +77,14 @@ const EditAvatarStep = React.memo(({ onClose }) => {
);
});
EditAvatarStep.propTypes = {
EditUserAvatarStep.propTypes = {
id: PropTypes.string.isRequired,
onBack: PropTypes.func,
onClose: PropTypes.func.isRequired,
};
export default EditAvatarStep;
EditUserAvatarStep.defaultProps = {
onBack: undefined,
};
export default EditUserAvatarStep;
@@ -3,6 +3,6 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import AccountPane from './AccountPane';
import EditUserAvatarStep from './EditUserAvatarStep';
export default AccountPane;
export default EditUserAvatarStep;
@@ -8,16 +8,16 @@ import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Dropdown, Header, Tab } from 'semantic-ui-react';
import selectors from '../../../../selectors';
import entryActions from '../../../../entry-actions';
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 { usePopupInClosableContext } from '../../../hooks';
import locales from '../../../locales';
import EditUserInformation from '../EditUserInformation';
import EditUserAvatarStep from '../EditUserAvatarStep';
import EditUserUsernameStep from '../EditUserUsernameStep';
import EditUserEmailStep from '../EditUserEmailStep';
import EditUserPasswordStep from '../EditUserPasswordStep';
import UserAvatar from '../UserAvatar';
import styles from './AccountPane.module.scss';
@@ -34,7 +34,7 @@ const AccountPane = React.memo(() => {
[dispatch],
);
const EditAvatarPopup = usePopupInClosableContext(EditAvatarStep);
const EditUserAvatarPopup = usePopupInClosableContext(EditUserAvatarStep);
const EditUserUsernamePopup = usePopupInClosableContext(EditUserUsernameStep);
const EditUserEmailPopup = usePopupInClosableContext(EditUserEmailStep);
const EditUserPasswordPopup = usePopupInClosableContext(EditUserPasswordStep);
@@ -45,9 +45,9 @@ const AccountPane = React.memo(() => {
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<EditAvatarPopup>
<EditUserAvatarPopup id={user.id}>
<UserAvatar id={user.id} size="massive" isDisabled={user.isAvatarUpdating} />
</EditAvatarPopup>
</EditUserAvatarPopup>
<br />
<br />
<EditUserInformation id={user.id} />
+1
View File
@@ -75,6 +75,7 @@ export default {
CURRENT_USER_USERNAME_UPDATE: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE`,
USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_USERNAME_UPDATE_ERROR_CLEAR`,
CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR: `${PREFIX}/CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR`,
USER_AVATAR_UPDATE: `${PREFIX}/USER_AVATAR_UPDATE`,
CURRENT_USER_AVATAR_UPDATE: `${PREFIX}/CURRENT_USER_AVATAR_UPDATE`,
USER_API_KEY_CREATE: `${PREFIX}/USER_API_KEY_CREATE`,
USER_API_KEY_DELETE: `${PREFIX}/USER_API_KEY_DELETE`,
+9
View File
@@ -139,6 +139,14 @@ const clearCurrentUserUsernameUpdateError = () => ({
payload: {},
});
const updateUserAvatar = (id, data) => ({
type: EntryActionTypes.USER_AVATAR_UPDATE,
payload: {
id,
data,
},
});
const updateCurrentUserAvatar = (data) => ({
type: EntryActionTypes.CURRENT_USER_AVATAR_UPDATE,
payload: {
@@ -271,6 +279,7 @@ export default {
updateCurrentUserUsername,
clearUserUsernameUpdateError,
clearCurrentUserUsernameUpdateError,
updateUserAvatar,
updateCurrentUserAvatar,
createUserApiKey,
deleteUserApiKey,
+1
View File
@@ -436,6 +436,7 @@ export default {
download: 'تحميل',
duplicateCard_title: 'تكرار البطاقة',
edit: 'تعديل',
editAvatar_title: 'تعديل الصورة الرمزية',
editColor_title: 'تعديل اللون',
editDescription_title: 'تعديل الوصف',
editDueDate_title: 'تعديل تاريخ الاستحقاق',
+1
View File
@@ -451,6 +451,7 @@ export default {
download: 'Изтегляне',
duplicateCard_title: 'Дублирана карта',
edit: 'Редактиране',
editAvatar_title: 'Редактиране на аватар',
editColor_title: 'Редактиране на цвят',
editDescription_title: 'Редактиране на описание',
editDueDate_title: 'Редактиране на краен срок',
+1
View File
@@ -453,6 +453,7 @@ export default {
download: 'Descarregar',
duplicateCard_title: 'Duplicar targeta',
edit: 'Editar',
editAvatar_title: 'Editar avatar',
editColor_title: 'Editar color',
editDescription_title: 'Editar descripció',
editDueDate_title: 'Editar data de venciment',
+1
View File
@@ -441,6 +441,7 @@ export default {
download: 'Stáhnout',
duplicateCard_title: 'Duplikovat kartu',
edit: 'Upravit',
editAvatar_title: 'Upravit avatar',
editColor_title: 'Upravit barvu',
editDescription_title: 'Upravit popis',
editDueDate_title: 'Upravit termín',
+1
View File
@@ -447,6 +447,7 @@ export default {
download: 'Download',
duplicateCard_title: 'Duplikér kort',
edit: 'Rediger',
editAvatar_title: 'Rediger profilbillede',
editColor_title: 'Rediger farve',
editDescription_title: 'Rediger beskrivelse',
editDueDate_title: 'Rediger frist',
+1
View File
@@ -463,6 +463,7 @@ export default {
download: 'Herunterladen',
duplicateCard_title: 'Karte duplizieren',
edit: 'Bearbeiten',
editAvatar_title: 'Profilbild bearbeiten',
editColor_title: 'Farbe bearbeiten',
editDescription_title: 'Beschreibung ändern',
editDueDate_title: 'Fälligkeitsdatum bearbeiten',
+1
View File
@@ -467,6 +467,7 @@ export default {
download: 'Λήψη',
duplicateCard_title: 'Διπλασιασμός κάρτας',
edit: 'Επεξεργασία',
editAvatar_title: 'Επεξεργασία avatar',
editColor_title: 'Επεξεργασία χρώματος',
editDescription_title: 'Επεξεργασία περιγραφής',
editDueDate_title: 'Επεξεργασία ημερομηνίας λήξης',
+1
View File
@@ -443,6 +443,7 @@ export default {
download: 'Download',
duplicateCard_title: 'Duplicate Card',
edit: 'Edit',
editAvatar_title: 'Edit Avatar',
editColor_title: 'Edit Color',
editDescription_title: 'Edit Description',
editDueDate_title: 'Edit Due Date',
+1
View File
@@ -438,6 +438,7 @@ export default {
download: 'Download',
duplicateCard_title: 'Duplicate Card',
edit: 'Edit',
editAvatar_title: 'Edit Avatar',
editColor_title: 'Edit Color',
editDescription_title: 'Edit Description',
editDueDate_title: 'Edit Due Date',
+1
View File
@@ -453,6 +453,7 @@ export default {
download: 'Descargar',
duplicateCard_title: 'Duplicar tarjeta',
edit: 'Editar',
editAvatar_title: 'Editar avatar',
editColor_title: 'Editar color',
editDescription_title: 'Editar descripción',
editDueDate_title: 'Editar fecha de vencimiento',
+1
View File
@@ -445,6 +445,7 @@ export default {
download: 'Laadi alla',
duplicateCard_title: 'Duplikaardi loomine',
edit: 'Muuda',
editAvatar_title: 'Muuda avatar',
editColor_title: 'Muuda värvi',
editDescription_title: 'Muuda kirjeldust',
editDueDate_title: 'Muuda tähtaega',
+1
View File
@@ -447,6 +447,7 @@ export default {
download: 'دانلود',
duplicateCard_title: 'تکرار کارت',
edit: 'ویرایش',
editAvatar_title: 'ویرایش آواتار',
editColor_title: 'ویرایش رنگ',
editDescription_title: 'ویرایش توضیحات',
editDueDate_title: 'ویرایش تاریخ سررسید',
+1
View File
@@ -446,6 +446,7 @@ export default {
download: 'Lataa',
duplicateCard_title: 'Monista kortti',
edit: 'Muokkaa',
editAvatar_title: 'Muokkaa avatar',
editColor_title: 'Muokkaa väriä',
editDescription_title: 'Muokkaa kuvausta',
editDueDate_title: 'Muokkaa määräpäivää',
+1
View File
@@ -451,6 +451,7 @@ export default {
download: 'Télécharger',
duplicateCard_title: 'Dupliquer la carte',
edit: 'Modifier',
editAvatar_title: "Modifier l'avatar",
editColor_title: 'Modifier la couleur',
editDescription_title: 'Modifier la description',
editDueDate_title: "Modifier la date d'échéance",
+1
View File
@@ -445,6 +445,7 @@ export default {
download: 'Letöltés',
duplicateCard_title: 'Kártya másolása',
edit: 'Szerkesztés',
editAvatar_title: 'Avatar szerkesztése',
editColor_title: 'Szín szerkesztése',
editDescription_title: 'Leírás szerkesztése',
editDueDate_title: 'Esedékesség dátumának szerkesztése',
+1
View File
@@ -446,6 +446,7 @@ export default {
download: 'Unduh',
duplicateCard_title: 'Duplikat kartu',
edit: 'Ubah',
editAvatar_title: 'Ubah avatar',
editColor_title: 'Ubah warna',
editDescription_title: 'Ubah deskripsi',
editDueDate_title: 'Ubah tenggat waktu',
+1
View File
@@ -449,6 +449,7 @@ export default {
download: 'Scarica',
duplicateCard_title: 'Duplica scheda',
edit: 'Modifica',
editAvatar_title: 'Modifica avatar',
editColor_title: 'Modifica colore',
editDescription_title: 'Modifica descrizione',
editDueDate_title: 'Modifica data di scadenza',
+1
View File
@@ -443,6 +443,7 @@ export default {
download: 'ダウンロード',
duplicateCard_title: 'カードを複製',
edit: '編集',
editAvatar_title: 'アバターを編集',
editColor_title: '色を編集',
editDescription_title: '説明を編集',
editDueDate_title: '期限を編集',
+1
View File
@@ -440,6 +440,7 @@ export default {
download: '다운로드',
duplicateCard_title: '카드 복제',
edit: '편집',
editAvatar_title: '아바타 편집',
editColor_title: '색상 편집',
editDescription_title: '설명 편집',
editDueDate_title: '마감일 편집',
+1
View File
@@ -449,6 +449,7 @@ export default {
download: 'Downloaden',
duplicateCard_title: 'Kaart dupliceren',
edit: 'Bewerken',
editAvatar_title: 'Avatar bewerken',
editColor_title: 'Kleur bewerken',
editDescription_title: 'Beschrijving bewerken',
editDueDate_title: 'Vervaldatum bewerken',
+1
View File
@@ -446,6 +446,7 @@ export default {
download: 'Pobierz',
duplicateCard_title: 'Duplikuj kartę',
edit: 'Edytuj',
editAvatar_title: 'Edytuj awatar',
editColor_title: 'Edytuj kolor',
editDescription_title: 'Edytuj opis',
editDueDate_title: 'Edytuj termin',
+1
View File
@@ -449,6 +449,7 @@ export default {
download: 'Baixar',
duplicateCard_title: 'Duplicar cartão',
edit: 'Editar',
editAvatar_title: 'Editar avatar',
editColor_title: 'Editar cor',
editDescription_title: 'Editar descrição',
editDueDate_title: 'Editar data de vencimento',
+1
View File
@@ -452,6 +452,7 @@ export default {
download: 'Transferir',
duplicateCard_title: 'Duplicar cartão',
edit: 'Editar',
editAvatar_title: 'Editar avatar',
editColor_title: 'Editar cor',
editDescription_title: 'Editar descrição',
editDueDate_title: 'Editar data de vencimento',
+1
View File
@@ -446,6 +446,7 @@ export default {
download: 'Descărcați',
duplicateCard_title: 'Duplicați cardul',
edit: 'Editați',
editAvatar_title: 'Editați avatarul',
editColor_title: 'Editați culoarea',
editDescription_title: 'Editați descrierea',
editDueDate_title: 'Editați data scadentă',
+1
View File
@@ -448,6 +448,7 @@ export default {
download: 'Скачать',
duplicateCard_title: 'Дублировать карточку',
edit: 'Изменить',
editAvatar_title: 'Изменить аватар',
editColor_title: 'Изменить цвет',
editDescription_title: 'Изменить описание',
editDueDate_title: 'Изменить срок',
+1
View File
@@ -439,6 +439,7 @@ export default {
download: 'Stiahnuť',
duplicateCard_title: 'Duplikovať kartu',
edit: 'Upraviť',
editAvatar_title: 'Upraviť avatar',
editColor_title: 'Upraviť farbu',
editDescription_title: 'Upraviť popis',
editDueDate_title: 'Upraviť termín do',
+1
View File
@@ -442,6 +442,7 @@ export default {
download: 'Преузми',
duplicateCard_title: 'Клонирај картицу',
edit: 'Измени',
editAvatar_title: 'Уреди аватара',
editColor_title: 'Уреди боју',
editDescription_title: 'Измени опис',
editDueDate_title: 'Измени рок',
+1
View File
@@ -444,6 +444,7 @@ export default {
download: 'Preuzmi',
duplicateCard_title: 'Kloniraj karticu',
edit: 'Izmeni',
editAvatar_title: 'Uredi avatara',
editColor_title: 'Uredi boju',
editDescription_title: 'Izmeni opis',
editDueDate_title: 'Izmeni rok',
+1
View File
@@ -453,6 +453,7 @@ export default {
download: 'Ladda ner',
duplicateCard_title: 'Duplicera kort',
edit: 'Redigera',
editAvatar_title: 'Redigera avatar',
editColor_title: 'Redigera färg',
editDescription_title: 'Redigera beskrivning',
editDueDate_title: 'Redigera förfallodatum',
+1
View File
@@ -449,6 +449,7 @@ export default {
download: 'İndir',
duplicateCard_title: 'Kartı kopyala',
edit: 'Düzenle',
editAvatar_title: 'Avatarı düzenle',
editColor_title: 'Rengi düzenle',
editDescription_title: 'Açıklamayı düzenle',
editDueDate_title: 'Son tarihi düzenle',
+1
View File
@@ -444,6 +444,7 @@ export default {
download: 'Завантажити',
duplicateCard_title: 'Дублювати картку',
edit: 'Редагувати',
editAvatar_title: 'Редагувати аватар',
editColor_title: 'Редагувати колір',
editDescription_title: 'Редагувати опис',
editDueDate_title: 'Редагувати термін виконання',
+1
View File
@@ -442,6 +442,7 @@ export default {
download: 'Yuklab olish',
duplicateCard_title: 'Kartani nusxalash',
edit: 'Tahrirlash',
editAvatar_title: 'Avatarni tahrirlash',
editColor_title: 'Rangni tahrirlash',
editDescription_title: 'Izohni tahrirlash',
editDueDate_title: 'Muddatni tahrirlash',
+1
View File
@@ -444,6 +444,7 @@ export default {
download: 'Tải xuống',
duplicateCard_title: 'Nhân bản Thẻ',
edit: 'Chỉnh sửa',
editAvatar_title: 'Chỉnh sửa ảnh đại diện',
editColor_title: 'Chỉnh sửa Màu sắc',
editDescription_title: 'Chỉnh sửa Mô tả',
editDueDate_title: 'Chỉnh sửa Hạn chót',
+1
View File
@@ -419,6 +419,7 @@ export default {
download: '下载',
duplicateCard_title: '复制卡片',
edit: '编辑',
editAvatar_title: '编辑头像',
editColor_title: '编辑颜色',
editDescription_title: '编辑描述',
editDueDate_title: '编辑到期时间',
+1
View File
@@ -419,6 +419,7 @@ export default {
download: '下載',
duplicateCard_title: '複製卡片',
edit: '編輯',
editAvatar_title: '編輯頭像',
editColor_title: '編輯顏色',
editDescription_title: '編輯描述',
editDueDate_title: '編輯截止時間',
+3
View File
@@ -64,6 +64,9 @@ export default function* usersWatchers() {
takeEvery(EntryActionTypes.CURRENT_USER_USERNAME_UPDATE_ERROR_CLEAR, () =>
services.clearCurrentUserUsernameUpdateError(),
),
takeEvery(EntryActionTypes.USER_AVATAR_UPDATE, ({ payload: { id, data } }) =>
services.updateUserAvatar(id, data),
),
takeEvery(EntryActionTypes.CURRENT_USER_AVATAR_UPDATE, ({ payload: { data } }) =>
services.updateCurrentUserAvatar(data),
),