mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
feat: add AvatarInput to update own avatar
This commit is contained in:
parent
3721f4365e
commit
355c6ebe26
@ -532,6 +532,7 @@
|
|||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"fields": "Fields",
|
"fields": "Fields",
|
||||||
|
"upload": "Upload",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"manage": "Manage",
|
"manage": "Manage",
|
||||||
|
@ -533,6 +533,7 @@
|
|||||||
"invite": "Inviter",
|
"invite": "Inviter",
|
||||||
"send": "Envoyer",
|
"send": "Envoyer",
|
||||||
"fields": "Champs",
|
"fields": "Champs",
|
||||||
|
"upload": "Télécharger",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"manage": "Gérer",
|
"manage": "Gérer",
|
||||||
|
89
frontend/src/app-components/inputs/AvatarInput.tsx
Normal file
89
frontend/src/app-components/inputs/AvatarInput.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { Avatar, Box, FormHelperText, FormLabel } from "@mui/material";
|
||||||
|
import { forwardRef, useState } from "react";
|
||||||
|
|
||||||
|
import { getAvatarSrc } from "@/components/inbox/helpers/mapMessages";
|
||||||
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
|
import { useConfig } from "@/hooks/useConfig";
|
||||||
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
|
import { theme } from "@/layout/themes/theme";
|
||||||
|
import { EntityType } from "@/services/types";
|
||||||
|
|
||||||
|
import FileUploadButton from "./FileInput";
|
||||||
|
|
||||||
|
type AvatarInputProps = {
|
||||||
|
label: string;
|
||||||
|
value: File | undefined | null;
|
||||||
|
accept: string;
|
||||||
|
size: number;
|
||||||
|
onChange: (file: File) => void;
|
||||||
|
error?: boolean;
|
||||||
|
helperText?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AvatarInput = forwardRef<HTMLDivElement, AvatarInputProps>(
|
||||||
|
({ label, accept, size, onChange, error, helperText }, ref) => {
|
||||||
|
const { apiUrl } = useConfig();
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [avatarSrc, setAvatarSrc] = useState(
|
||||||
|
getAvatarSrc(apiUrl, EntityType.USER, user?.id),
|
||||||
|
);
|
||||||
|
const { t } = useTranslate();
|
||||||
|
const handleChange = (file: File) => {
|
||||||
|
onChange(file);
|
||||||
|
setAvatarSrc(URL.createObjectURL(file));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={ref}
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormLabel
|
||||||
|
component="h2"
|
||||||
|
style={{ display: "inline-block", marginBottom: 1 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</FormLabel>
|
||||||
|
<Avatar
|
||||||
|
src={avatarSrc}
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
sx={{ width: size, height: size, margin: "auto" }}
|
||||||
|
variant="rounded"
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "50%",
|
||||||
|
bottom: "1rem",
|
||||||
|
transform: "translateX(50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileUploadButton
|
||||||
|
accept={accept}
|
||||||
|
label={t("button.upload")}
|
||||||
|
onChange={handleChange}
|
||||||
|
isLoading={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{helperText ? (
|
||||||
|
<FormHelperText error={error}>{helperText}</FormHelperText>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
AvatarInput.displayName = "AttachmentInput";
|
||||||
|
|
||||||
|
export default AvatarInput;
|
@ -1,13 +1,13 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Hexastack. All rights reserved.
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import CheckIcon from "@mui/icons-material/Check";
|
import CheckIcon from "@mui/icons-material/Check";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import EmailIcon from "@mui/icons-material/Email";
|
import EmailIcon from "@mui/icons-material/Email";
|
||||||
import KeyIcon from "@mui/icons-material/Key";
|
import KeyIcon from "@mui/icons-material/Key";
|
||||||
import LanguageIcon from "@mui/icons-material/Language";
|
import LanguageIcon from "@mui/icons-material/Language";
|
||||||
@ -16,10 +16,10 @@ import { FC } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useQueryClient } from "react-query";
|
import { useQueryClient } from "react-query";
|
||||||
|
|
||||||
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
|
|
||||||
import { ContentItem } from "@/app-components/dialogs";
|
import { ContentItem } from "@/app-components/dialogs";
|
||||||
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
|
||||||
import { Adornment } from "@/app-components/inputs/Adornment";
|
import { Adornment } from "@/app-components/inputs/Adornment";
|
||||||
|
import AvatarInput from "@/app-components/inputs/AvatarInput";
|
||||||
import { Input } from "@/app-components/inputs/Input";
|
import { Input } from "@/app-components/inputs/Input";
|
||||||
import { PasswordInput } from "@/app-components/inputs/PasswordInput";
|
import { PasswordInput } from "@/app-components/inputs/PasswordInput";
|
||||||
import { useUpdateProfile } from "@/hooks/entities/auth-hooks";
|
import { useUpdateProfile } from "@/hooks/entities/auth-hooks";
|
||||||
@ -27,11 +27,9 @@ import { CURRENT_USER_KEY } from "@/hooks/useAuth";
|
|||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { useValidationRules } from "@/hooks/useValidationRules";
|
import { useValidationRules } from "@/hooks/useValidationRules";
|
||||||
import { IUser, IUserAttributes } from "@/types/user.types";
|
import { IProfileAttributes, IUser } from "@/types/user.types";
|
||||||
import { MIME_TYPES } from "@/utils/attachment";
|
import { MIME_TYPES } from "@/utils/attachment";
|
||||||
|
|
||||||
type TUserProfileExtendedPayload = IUserAttributes & { password2: string };
|
|
||||||
|
|
||||||
type ProfileFormProps = { user: IUser };
|
type ProfileFormProps = { user: IUser };
|
||||||
|
|
||||||
export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
||||||
@ -55,14 +53,12 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
|||||||
formState: { errors },
|
formState: { errors },
|
||||||
register,
|
register,
|
||||||
setValue,
|
setValue,
|
||||||
getValues,
|
} = useForm<IProfileAttributes>({
|
||||||
} = useForm<TUserProfileExtendedPayload>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
language: user.language,
|
language: user.language,
|
||||||
avatar: user.avatar,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const rules = useValidationRules();
|
const rules = useValidationRules();
|
||||||
@ -89,7 +85,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
|||||||
password,
|
password,
|
||||||
password2: _password2,
|
password2: _password2,
|
||||||
...rest
|
...rest
|
||||||
}: TUserProfileExtendedPayload) => {
|
}: IProfileAttributes) => {
|
||||||
await updateProfile({
|
await updateProfile({
|
||||||
...rest,
|
...rest,
|
||||||
password: password || undefined,
|
password: password || undefined,
|
||||||
@ -106,32 +102,13 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ position: "relative" }}>
|
<Box sx={{ position: "relative" }}>
|
||||||
<AttachmentInput
|
<AvatarInput
|
||||||
label={t("label.avatar")}
|
label={t("label.avatar")}
|
||||||
format="small"
|
|
||||||
accept={MIME_TYPES["images"].join(",")}
|
accept={MIME_TYPES["images"].join(",")}
|
||||||
enableMediaLibrary={false}
|
|
||||||
size={256}
|
size={256}
|
||||||
{...field}
|
{...field}
|
||||||
onChange={(attachment) => setValue("avatar", attachment)}
|
onChange={(file) => setValue("avatar", file)}
|
||||||
/>
|
/>
|
||||||
{getValues("avatar") ? (
|
|
||||||
<Button
|
|
||||||
startIcon={<DeleteIcon />}
|
|
||||||
onClick={() => setValue("avatar", null)}
|
|
||||||
color="error"
|
|
||||||
variant="contained"
|
|
||||||
size="small"
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
right: "50%",
|
|
||||||
bottom: "1rem",
|
|
||||||
transform: "translateX(50%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("button.remove")}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
</Box>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Hexastack. All rights reserved.
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||||
|
|
||||||
import { EntityType, TMutationOptions } from "@/services/types";
|
import { EntityType, TMutationOptions } from "@/services/types";
|
||||||
import { ILoginAttributes } from "@/types/auth/login.types";
|
import { ILoginAttributes } from "@/types/auth/login.types";
|
||||||
import { IUser, IUserAttributes, IUserStub } from "@/types/user.types";
|
import {
|
||||||
|
IProfileAttributes,
|
||||||
|
IUser,
|
||||||
|
IUserAttributes,
|
||||||
|
IUserStub,
|
||||||
|
} from "@/types/user.types";
|
||||||
import { useSocket } from "@/websocket/socket-hooks";
|
import { useSocket } from "@/websocket/socket-hooks";
|
||||||
|
|
||||||
import { useFind } from "../crud/useFind";
|
import { useFind } from "../crud/useFind";
|
||||||
@ -156,7 +162,7 @@ export const useLoadSettings = () => {
|
|||||||
|
|
||||||
export const useUpdateProfile = (
|
export const useUpdateProfile = (
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
TMutationOptions<IUserStub, Error, Partial<IUserAttributes>>,
|
TMutationOptions<IUserStub, Error, Partial<IProfileAttributes>>,
|
||||||
"mutationFn"
|
"mutationFn"
|
||||||
>,
|
>,
|
||||||
) => {
|
) => {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Hexastack. All rights reserved.
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { AxiosInstance, AxiosResponse } from "axios";
|
import { AxiosInstance, AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { ILoginAttributes } from "@/types/auth/login.types";
|
import { ILoginAttributes } from "@/types/auth/login.types";
|
||||||
@ -15,7 +16,12 @@ import { ICsrf } from "@/types/csrf.types";
|
|||||||
import { IInvitation, IInvitationAttributes } from "@/types/invitation.types";
|
import { IInvitation, IInvitationAttributes } from "@/types/invitation.types";
|
||||||
import { INlpDatasetSampleAttributes } from "@/types/nlp-sample.types";
|
import { INlpDatasetSampleAttributes } from "@/types/nlp-sample.types";
|
||||||
import { IResetPayload, IResetRequest } from "@/types/reset.types";
|
import { IResetPayload, IResetRequest } from "@/types/reset.types";
|
||||||
import { IUser, IUserAttributes, IUserStub } from "@/types/user.types";
|
import {
|
||||||
|
IProfileAttributes,
|
||||||
|
IUser,
|
||||||
|
IUserAttributes,
|
||||||
|
IUserStub,
|
||||||
|
} from "@/types/user.types";
|
||||||
|
|
||||||
import { EntityType, Format, TCount, TypeByFormat } from "./types";
|
import { EntityType, Format, TCount, TypeByFormat } from "./types";
|
||||||
|
|
||||||
@ -100,15 +106,27 @@ export class ApiClient {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateProfile(id: string, payload: Partial<IUserAttributes>) {
|
async updateProfile(id: string, payload: Partial<IProfileAttributes>) {
|
||||||
const { _csrf } = await this.getCsrf();
|
const { _csrf } = await this.getCsrf();
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(payload)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
formData.append(key, value as string | Blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the CSRF token
|
||||||
|
formData.append("_csrf", _csrf);
|
||||||
|
|
||||||
const { data } = await this.request.patch<
|
const { data } = await this.request.patch<
|
||||||
IUserStub,
|
IUserStub,
|
||||||
AxiosResponse<IUserStub>,
|
AxiosResponse<IUserStub>,
|
||||||
Partial<IUserAttributes> & ICsrf
|
Partial<IProfileAttributes>
|
||||||
>(`${ROUTES.PROFILE}/${id}`, {
|
>(`${ROUTES.PROFILE}/${id}?_csrf=${_csrf}`, payload, {
|
||||||
...payload,
|
headers: {
|
||||||
_csrf,
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright © 2024 Hexastack. All rights reserved.
|
* Copyright © 2025 Hexastack. All rights reserved.
|
||||||
*
|
*
|
||||||
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
|
||||||
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
|
||||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { EntityType, Format } from "@/services/types";
|
import { EntityType, Format } from "@/services/types";
|
||||||
|
|
||||||
import { IAttachment } from "./attachment.types";
|
import { IAttachment } from "./attachment.types";
|
||||||
@ -27,6 +28,11 @@ export interface IUserStub
|
|||||||
extends IBaseSchema,
|
extends IBaseSchema,
|
||||||
OmitPopulate<IUserAttributes, EntityType.USER> {}
|
OmitPopulate<IUserAttributes, EntityType.USER> {}
|
||||||
|
|
||||||
|
export interface IProfileAttributes extends Partial<IUserStub> {
|
||||||
|
password2?: string;
|
||||||
|
avatar?: File | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUser extends IUserStub, IFormat<Format.BASIC> {
|
export interface IUser extends IUserStub, IFormat<Format.BASIC> {
|
||||||
roles: string[]; //populated by default
|
roles: string[]; //populated by default
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
|
Loading…
Reference in New Issue
Block a user