feat: add AvatarInput to update own avatar

This commit is contained in:
Mohamed Marrouchi 2025-01-06 19:06:11 +01:00
parent 3721f4365e
commit 355c6ebe26
7 changed files with 140 additions and 42 deletions

View File

@ -532,6 +532,7 @@
"invite": "Invite",
"send": "Send",
"fields": "Fields",
"upload": "Upload",
"import": "Import",
"export": "Export",
"manage": "Manage",

View File

@ -533,6 +533,7 @@
"invite": "Inviter",
"send": "Envoyer",
"fields": "Champs",
"upload": "Télécharger",
"import": "Import",
"export": "Export",
"manage": "Gérer",

View 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;

View File

@ -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:
* 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 CheckIcon from "@mui/icons-material/Check";
import DeleteIcon from "@mui/icons-material/Delete";
import EmailIcon from "@mui/icons-material/Email";
import KeyIcon from "@mui/icons-material/Key";
import LanguageIcon from "@mui/icons-material/Language";
@ -16,10 +16,10 @@ import { FC } from "react";
import { Controller, useForm } from "react-hook-form";
import { useQueryClient } from "react-query";
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
import { ContentItem } from "@/app-components/dialogs";
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
import { Adornment } from "@/app-components/inputs/Adornment";
import AvatarInput from "@/app-components/inputs/AvatarInput";
import { Input } from "@/app-components/inputs/Input";
import { PasswordInput } from "@/app-components/inputs/PasswordInput";
import { useUpdateProfile } from "@/hooks/entities/auth-hooks";
@ -27,11 +27,9 @@ import { CURRENT_USER_KEY } from "@/hooks/useAuth";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
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";
type TUserProfileExtendedPayload = IUserAttributes & { password2: string };
type ProfileFormProps = { user: IUser };
export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
@ -55,14 +53,12 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
formState: { errors },
register,
setValue,
getValues,
} = useForm<TUserProfileExtendedPayload>({
} = useForm<IProfileAttributes>({
defaultValues: {
first_name: user.first_name,
last_name: user.last_name,
email: user.email,
language: user.language,
avatar: user.avatar,
},
});
const rules = useValidationRules();
@ -89,7 +85,7 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
password,
password2: _password2,
...rest
}: TUserProfileExtendedPayload) => {
}: IProfileAttributes) => {
await updateProfile({
...rest,
password: password || undefined,
@ -106,32 +102,13 @@ export const ProfileForm: FC<ProfileFormProps> = ({ user }) => {
render={({ field }) => (
<>
<Box sx={{ position: "relative" }}>
<AttachmentInput
<AvatarInput
label={t("label.avatar")}
format="small"
accept={MIME_TYPES["images"].join(",")}
enableMediaLibrary={false}
size={256}
{...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>
<Typography
variant="body2"

View File

@ -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:
* 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 { useEffect } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { EntityType, TMutationOptions } from "@/services/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 { useFind } from "../crud/useFind";
@ -156,7 +162,7 @@ export const useLoadSettings = () => {
export const useUpdateProfile = (
options?: Omit<
TMutationOptions<IUserStub, Error, Partial<IUserAttributes>>,
TMutationOptions<IUserStub, Error, Partial<IProfileAttributes>>,
"mutationFn"
>,
) => {

View File

@ -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:
* 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 { AxiosInstance, AxiosResponse } from "axios";
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 { INlpDatasetSampleAttributes } from "@/types/nlp-sample.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";
@ -100,15 +106,27 @@ export class ApiClient {
return data;
}
async updateProfile(id: string, payload: Partial<IUserAttributes>) {
async updateProfile(id: string, payload: Partial<IProfileAttributes>) {
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<
IUserStub,
AxiosResponse<IUserStub>,
Partial<IUserAttributes> & ICsrf
>(`${ROUTES.PROFILE}/${id}`, {
...payload,
_csrf,
Partial<IProfileAttributes>
>(`${ROUTES.PROFILE}/${id}?_csrf=${_csrf}`, payload, {
headers: {
"Content-Type": "multipart/form-data",
},
});
return data;

View File

@ -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:
* 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 { EntityType, Format } from "@/services/types";
import { IAttachment } from "./attachment.types";
@ -27,6 +28,11 @@ export interface IUserStub
extends IBaseSchema,
OmitPopulate<IUserAttributes, EntityType.USER> {}
export interface IProfileAttributes extends Partial<IUserStub> {
password2?: string;
avatar?: File | null;
}
export interface IUser extends IUserStub, IFormat<Format.BASIC> {
roles: string[]; //populated by default
avatar: string | null;