Add admin interface to change user avatars (#12405)

This commit is contained in:
Tom Moor
2026-06-03 07:32:11 -04:00
committed by GitHub
parent 8464d99589
commit c419c3ab63
4 changed files with 91 additions and 7 deletions
+46
View File
@@ -1,13 +1,17 @@
import { observer } from "mobx-react";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
import { UserRole } from "@shared/types";
import { UserValidation } from "@shared/validations";
import type User from "~/models/User";
import Button from "~/components/Button";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import ImageInput from "~/scenes/Settings/components/ImageInput";
import { client } from "~/utils/ApiClient";
import Text from "./Text";
@@ -142,6 +146,48 @@ export function UserChangeNameDialog({ user, onSubmit }: Props) {
);
}
export const UserChangeAvatarDialog = observer(function UserChangeAvatarDialog({
user,
onSubmit,
}: Props) {
const { t } = useTranslation();
const handleAvatarChange = async (avatarUrl: string | null) => {
try {
await user.save({ avatarUrl });
toast.success(t("Profile picture updated"));
} catch (err) {
toast.error(err.message);
}
};
const handleAvatarError = (error: string | null | undefined) => {
toast.error(error || t("Unable to upload new profile picture"));
};
return (
<Flex column gap={16}>
<Flex justify="center">
<ImageInput
alt={t("Profile picture")}
onSuccess={handleAvatarChange}
onError={handleAvatarError}
model={user}
showRemoveOption={false}
/>
</Flex>
<Flex justify="flex-end" gap={8}>
{user.avatarUrl && (
<Button onClick={() => handleAvatarChange(null)} neutral>
{t("Remove")}
</Button>
)}
<Button onClick={onSubmit}>{t("Done")}</Button>
</Flex>
</Flex>
);
});
export function UserChangeEmailDialog({ user, onSubmit }: Props) {
const { t } = useTranslation();
const actor = useCurrentUser();
+23
View File
@@ -20,6 +20,7 @@ import {
UserSuspendDialog,
UserChangeNameDialog,
UserChangeEmailDialog,
UserChangeAvatarDialog,
} from "~/components/UserDialogs";
/**
@@ -33,6 +34,21 @@ export function useUserMenuActions(targetUser: User | null) {
const { t } = useTranslation();
const can = usePolicy(targetUser ?? ({} as User));
const openAvatarDialog = React.useCallback(() => {
if (!targetUser) {
return;
}
dialogs.openModal({
title: t("Change profile picture"),
content: (
<UserChangeAvatarDialog
user={targetUser}
onSubmit={dialogs.closeAllModals}
/>
),
});
}, [dialogs, t, targetUser]);
const openNameDialog = React.useCallback(() => {
if (!targetUser) {
return;
@@ -127,6 +143,12 @@ export function useUserMenuActions(targetUser: User | null) {
visible: can.demote || can.promote,
children: roleChangeActions,
}),
createAction({
name: `${t("Change profile picture")}`,
section: UserSection,
visible: can.update,
perform: openAvatarDialog,
}),
createAction({
name: `${t("Change name")}`,
section: UserSection,
@@ -177,6 +199,7 @@ export function useUserMenuActions(targetUser: User | null) {
can.update,
can.resendInvite,
roleChangeActions,
openAvatarDialog,
openNameDialog,
openEmailDialog,
resendInvitation,
+17 -4
View File
@@ -11,11 +11,24 @@ import type { Props as ImageUploadProps } from "./ImageUpload";
import ImageUpload from "./ImageUpload";
type Props = ImageUploadProps & {
/** The model whose avatar is displayed and updated by this input. */
model: IAvatar;
/** Alt text for the avatar image. */
alt: string;
/**
* Whether to render the inline "Remove" button when the model has an
* existing avatar. Defaults to true.
*/
showRemoveOption?: boolean;
};
export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
export default function ImageInput({
model,
onSuccess,
alt,
showRemoveOption = true,
...rest
}: Props) {
const { t } = useTranslation();
return (
@@ -29,7 +42,7 @@ export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
<Avatar
model={model}
size={AvatarSize.Upload}
variant={AvatarVariant.Square}
variant={AvatarVariant.Round}
alt={alt}
/>
<Flex auto align="center" justify="center" className="upload">
@@ -37,7 +50,7 @@ export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
</Flex>
</ImageUpload>
</ImageBox>
{model.avatarUrl && (
{model.avatarUrl && showRemoveOption && (
<Button onClick={() => onSuccess(null)} neutral>
{t("Remove")}
</Button>
@@ -55,7 +68,7 @@ const ImageBox = styled(Flex)`
${avatarStyles};
position: relative;
font-size: 14px;
border-radius: 8px;
border-radius: 50%;
box-shadow: 0 0 0 1px ${s("backgroundSecondary")};
background: ${s("background")};
overflow: hidden;
+5 -3
View File
@@ -546,6 +546,10 @@
"Are you sure you want to suspend {{ userName }}? Suspended users will be prevented from logging in.": "Are you sure you want to suspend {{ userName }}? Suspended users will be prevented from logging in.",
"New name": "New name",
"Name can't be empty": "Name can't be empty",
"Profile picture updated": "Profile picture updated",
"Unable to upload new profile picture": "Unable to upload new profile picture",
"Profile picture": "Profile picture",
"Done": "Done",
"Check your email to verify the new address.": "Check your email to verify the new address.",
"The email will be changed once verified.": "The email will be changed once verified.",
"You will receive an email to verify your new address. It must be unique in the workspace.": "You will receive an email to verify your new address. It must be unique in the workspace.",
@@ -575,7 +579,6 @@
"Paste a link": "Paste a link",
"Delete embed": "Delete embed",
"Delete image": "Delete image",
"Profile picture": "Profile picture",
"Create a new doc": "Create a new doc",
"Create a nested doc": "Create a nested doc",
"{{ userName }} won't be notified, as they do not have access to this document": "{{ userName }} won't be notified, as they do not have access to this document",
@@ -680,6 +683,7 @@
"Embeds": "Embeds",
"Configure which embed providers are available in the editor.": "Configure which embed providers are available in the editor.",
"Install": "Install",
"Change profile picture": "Change profile picture",
"Change name": "Change name",
"Change email": "Change email",
"Suspend user": "Suspend user",
@@ -1361,8 +1365,6 @@
"Choose how unread notifications are indicated on the app icon.": "Choose how unread notifications are indicated on the app icon.",
"You may delete your account at any time, note that this is unrecoverable": "You may delete your account at any time, note that this is unrecoverable",
"Profile saved": "Profile saved",
"Profile picture updated": "Profile picture updated",
"Unable to upload new profile picture": "Unable to upload new profile picture",
"Manage how you appear to other members of the workspace.": "Manage how you appear to other members of the workspace.",
"Photo": "Photo",
"Choose a photo or image to represent yourself.": "Choose a photo or image to represent yourself.",