mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
Add admin interface to change user avatars (#12405)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user