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 * as React from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { UserRole } from "@shared/types";
|
import { UserRole } from "@shared/types";
|
||||||
import { UserValidation } from "@shared/validations";
|
import { UserValidation } from "@shared/validations";
|
||||||
import type User from "~/models/User";
|
import type User from "~/models/User";
|
||||||
|
import Button from "~/components/Button";
|
||||||
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
||||||
|
import Flex from "~/components/Flex";
|
||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
|
import ImageInput from "~/scenes/Settings/components/ImageInput";
|
||||||
import { client } from "~/utils/ApiClient";
|
import { client } from "~/utils/ApiClient";
|
||||||
import Text from "./Text";
|
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) {
|
export function UserChangeEmailDialog({ user, onSubmit }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const actor = useCurrentUser();
|
const actor = useCurrentUser();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
UserSuspendDialog,
|
UserSuspendDialog,
|
||||||
UserChangeNameDialog,
|
UserChangeNameDialog,
|
||||||
UserChangeEmailDialog,
|
UserChangeEmailDialog,
|
||||||
|
UserChangeAvatarDialog,
|
||||||
} from "~/components/UserDialogs";
|
} from "~/components/UserDialogs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +34,21 @@ export function useUserMenuActions(targetUser: User | null) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const can = usePolicy(targetUser ?? ({} as User));
|
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(() => {
|
const openNameDialog = React.useCallback(() => {
|
||||||
if (!targetUser) {
|
if (!targetUser) {
|
||||||
return;
|
return;
|
||||||
@@ -127,6 +143,12 @@ export function useUserMenuActions(targetUser: User | null) {
|
|||||||
visible: can.demote || can.promote,
|
visible: can.demote || can.promote,
|
||||||
children: roleChangeActions,
|
children: roleChangeActions,
|
||||||
}),
|
}),
|
||||||
|
createAction({
|
||||||
|
name: `${t("Change profile picture")}…`,
|
||||||
|
section: UserSection,
|
||||||
|
visible: can.update,
|
||||||
|
perform: openAvatarDialog,
|
||||||
|
}),
|
||||||
createAction({
|
createAction({
|
||||||
name: `${t("Change name")}…`,
|
name: `${t("Change name")}…`,
|
||||||
section: UserSection,
|
section: UserSection,
|
||||||
@@ -177,6 +199,7 @@ export function useUserMenuActions(targetUser: User | null) {
|
|||||||
can.update,
|
can.update,
|
||||||
can.resendInvite,
|
can.resendInvite,
|
||||||
roleChangeActions,
|
roleChangeActions,
|
||||||
|
openAvatarDialog,
|
||||||
openNameDialog,
|
openNameDialog,
|
||||||
openEmailDialog,
|
openEmailDialog,
|
||||||
resendInvitation,
|
resendInvitation,
|
||||||
|
|||||||
@@ -11,11 +11,24 @@ import type { Props as ImageUploadProps } from "./ImageUpload";
|
|||||||
import ImageUpload from "./ImageUpload";
|
import ImageUpload from "./ImageUpload";
|
||||||
|
|
||||||
type Props = ImageUploadProps & {
|
type Props = ImageUploadProps & {
|
||||||
|
/** The model whose avatar is displayed and updated by this input. */
|
||||||
model: IAvatar;
|
model: IAvatar;
|
||||||
|
/** Alt text for the avatar image. */
|
||||||
alt: string;
|
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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +42,7 @@ export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
|
|||||||
<Avatar
|
<Avatar
|
||||||
model={model}
|
model={model}
|
||||||
size={AvatarSize.Upload}
|
size={AvatarSize.Upload}
|
||||||
variant={AvatarVariant.Square}
|
variant={AvatarVariant.Round}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
/>
|
/>
|
||||||
<Flex auto align="center" justify="center" className="upload">
|
<Flex auto align="center" justify="center" className="upload">
|
||||||
@@ -37,7 +50,7 @@ export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</ImageUpload>
|
</ImageUpload>
|
||||||
</ImageBox>
|
</ImageBox>
|
||||||
{model.avatarUrl && (
|
{model.avatarUrl && showRemoveOption && (
|
||||||
<Button onClick={() => onSuccess(null)} neutral>
|
<Button onClick={() => onSuccess(null)} neutral>
|
||||||
{t("Remove")}
|
{t("Remove")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -55,7 +68,7 @@ const ImageBox = styled(Flex)`
|
|||||||
${avatarStyles};
|
${avatarStyles};
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-radius: 8px;
|
border-radius: 50%;
|
||||||
box-shadow: 0 0 0 1px ${s("backgroundSecondary")};
|
box-shadow: 0 0 0 1px ${s("backgroundSecondary")};
|
||||||
background: ${s("background")};
|
background: ${s("background")};
|
||||||
overflow: hidden;
|
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.",
|
"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",
|
"New name": "New name",
|
||||||
"Name can't be empty": "Name can't be empty",
|
"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.",
|
"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.",
|
"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.",
|
"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",
|
"Paste a link": "Paste a link",
|
||||||
"Delete embed": "Delete embed",
|
"Delete embed": "Delete embed",
|
||||||
"Delete image": "Delete image",
|
"Delete image": "Delete image",
|
||||||
"Profile picture": "Profile picture",
|
|
||||||
"Create a new doc": "Create a new doc",
|
"Create a new doc": "Create a new doc",
|
||||||
"Create a nested doc": "Create a nested 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",
|
"{{ 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",
|
"Embeds": "Embeds",
|
||||||
"Configure which embed providers are available in the editor.": "Configure which embed providers are available in the editor.",
|
"Configure which embed providers are available in the editor.": "Configure which embed providers are available in the editor.",
|
||||||
"Install": "Install",
|
"Install": "Install",
|
||||||
|
"Change profile picture": "Change profile picture",
|
||||||
"Change name": "Change name",
|
"Change name": "Change name",
|
||||||
"Change email": "Change email",
|
"Change email": "Change email",
|
||||||
"Suspend user": "Suspend user",
|
"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.",
|
"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",
|
"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 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.",
|
"Manage how you appear to other members of the workspace.": "Manage how you appear to other members of the workspace.",
|
||||||
"Photo": "Photo",
|
"Photo": "Photo",
|
||||||
"Choose a photo or image to represent yourself.": "Choose a photo or image to represent yourself.",
|
"Choose a photo or image to represent yourself.": "Choose a photo or image to represent yourself.",
|
||||||
|
|||||||
Reference in New Issue
Block a user