F: Rework user profile editor
Some checks failed
Frontend CI / build (22.x) (push) Waiting to run
Backend CI / build (3.12) (push) Has been cancelled

This commit is contained in:
Ivan 2025-02-03 18:49:01 +03:00
parent e3d8c9110a
commit b6f1681f49
8 changed files with 70 additions and 63 deletions

View File

@ -103,6 +103,7 @@ class UserSerializer(serializers.ModelSerializer):
'first_name',
'last_name',
]
read_only_fields = ('id', 'username')
def validate(self, attrs):
attrs = super().validate(attrs)

View File

@ -101,6 +101,10 @@ class TestUserUserProfileAPIView(EndpointTester):
data = {'email': self.user2.email}
self.executeBadData(data=data)
data = {'username': 'new_username'}
response = self.executeOK(data=data)
self.assertNotEqual(response.data['username'], data['username'])
self.logout()
self.executeForbidden()

View File

@ -30,12 +30,16 @@ export type IUserSignupDTO = z.infer<typeof UserSignupSchema>;
/**
* Represents user data, intended to update user profile in persistent storage.
*/
export interface IUpdateProfileDTO {
username: string;
email: string;
first_name: string;
last_name: string;
}
export const UpdateProfileSchema = z.object({
email: z.string().email(errors.emailField),
first_name: z.string(),
last_name: z.string()
});
/**
* Represents user data, intended to update user profile in persistent storage.
*/
export type IUpdateProfileDTO = z.infer<typeof UpdateProfileSchema>;
export const usersApi = {
baseKey: 'users',

View File

@ -1,5 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IUserProfile } from '@/models/user';
import { DataCallback } from '../apiTransport';
import { IUpdateProfileDTO, usersApi } from './api';
export const useUpdateProfile = () => {
@ -13,7 +16,8 @@ export const useUpdateProfile = () => {
}
});
return {
updateProfile: (data: IUpdateProfileDTO) => mutation.mutate(data),
updateProfile: (data: IUpdateProfileDTO, onSuccess?: DataCallback<IUserProfile>) =>
mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset

View File

@ -35,7 +35,7 @@ function LoginPage() {
});
const { isAnonymous } = useAuthSuspense();
const { login, isPending, error: serverError, reset } = useLogin();
const { login, isPending, error: serverError, reset: clearServerError } = useLogin();
function onSubmit(data: IUserLoginDTO) {
login(data, () => {
@ -49,7 +49,7 @@ function LoginPage() {
}
function resetErrors() {
reset();
clearServerError();
clearErrors();
}

View File

@ -6,7 +6,7 @@ import clsx from 'clsx';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { IUserSignupDTO, UserSignupSchema } from '@/backend/users/api';
import { useSignup } from '@/backend/users/useSignup';
@ -26,7 +26,7 @@ import { globals, patterns } from '@/utils/constants';
function FormSignup() {
const router = useConceptNavigation();
const { signup, isPending, error: serverError, reset } = useSignup();
const { signup, isPending, error: serverError, reset: clearServerError } = useSignup();
const [acceptPrivacy, setAcceptPrivacy] = useState(false);
const [acceptRules, setAcceptRules] = useState(false);
@ -34,13 +34,15 @@ function FormSignup() {
register,
handleSubmit,
clearErrors,
formState: { errors }
formState: { errors, isDirty }
} = useForm<IUserSignupDTO>({
resolver: zodResolver(UserSignupSchema)
});
useBlockNavigation(isDirty);
function resetErrors() {
reset();
clearServerError();
clearErrors();
}

View File

@ -16,7 +16,7 @@ import TextInput from '@/components/ui/TextInput';
function EditorPassword() {
const router = useConceptNavigation();
const { changePassword, isPending, error: serverError, reset } = useChangePassword();
const { changePassword, isPending, error: serverError, reset: clearServerError } = useChangePassword();
const {
register,
handleSubmit,
@ -27,7 +27,7 @@ function EditorPassword() {
});
function resetErrors() {
reset();
clearServerError();
clearErrors();
}

View File

@ -1,10 +1,11 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useBlockNavigation } from '@/app/Navigation/NavigationContext';
import { IUpdateProfileDTO } from '@/backend/users/api';
import { IUpdateProfileDTO, UpdateProfileSchema } from '@/backend/users/api';
import { useProfileSuspense } from '@/backend/users/useProfile';
import { useUpdateProfile } from '@/backend/users/useUpdateProfile';
import { ErrorData } from '@/components/info/InfoError';
@ -13,76 +14,67 @@ import TextInput from '@/components/ui/TextInput';
function EditorProfile() {
const { profile } = useProfileSuspense();
const { updateProfile, isPending, error } = useUpdateProfile();
const { updateProfile, isPending, error: serverError, reset: clearServerError } = useUpdateProfile();
const [username, setUsername] = useState(profile.username);
const [email, setEmail] = useState(profile.email);
const [first_name, setFirstName] = useState(profile.first_name);
const [last_name, setLastName] = useState(profile.last_name);
const {
register,
handleSubmit,
clearErrors,
reset: resetForm,
formState: { errors, isDirty }
} = useForm<IUpdateProfileDTO>({
resolver: zodResolver(UpdateProfileSchema),
defaultValues: {
first_name: profile.first_name,
last_name: profile.last_name,
email: profile.email
}
});
const isModified = profile.email !== email || profile.first_name !== first_name || profile.last_name !== last_name;
useBlockNavigation(isDirty);
useBlockNavigation(isModified);
function resetErrors() {
clearServerError();
clearErrors();
}
useEffect(() => {
setUsername(profile.username);
setEmail(profile.email);
setFirstName(profile.first_name);
setLastName(profile.last_name);
}, [profile]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const data: IUpdateProfileDTO = {
username: username,
email: email,
first_name: first_name,
last_name: last_name
};
updateProfile(data);
function onSubmit(data: IUpdateProfileDTO) {
updateProfile(data, () => resetForm({ ...data }));
}
return (
<form onSubmit={handleSubmit} className='cc-column w-[18rem] px-6 py-2'>
<TextInput
id='username'
autoComplete='username'
disabled
label='Логин'
title='Логин изменить нельзя'
value={username}
/>
<form
className='cc-column w-[18rem] px-6 py-2'
onSubmit={event => void handleSubmit(onSubmit)(event)}
onChange={resetErrors}
>
<TextInput id='username' disabled label='Логин' title='Логин изменить нельзя' value={profile.username} />
<TextInput
id='first_name'
{...register('first_name')}
autoComplete='off'
allowEnter
label='Имя'
value={first_name}
onChange={event => setFirstName(event.target.value)}
error={errors.first_name}
/>
<TextInput
id='last_name'
{...register('last_name')}
autoComplete='off'
allowEnter
label='Фамилия'
value={last_name}
onChange={event => setLastName(event.target.value)}
error={errors.last_name}
/>
<TextInput
id='email'
{...register('email')}
autoComplete='off'
allowEnter
label='Электронная почта'
value={email}
onChange={event => setEmail(event.target.value)}
/>
{error ? <ProcessError error={error} /> : null}
<SubmitButton
className='self-center mt-6'
text='Сохранить данные'
loading={isPending}
disabled={!isModified || email == ''}
error={errors.email}
/>
{serverError ? <ServerError error={serverError} /> : null}
<SubmitButton className='self-center mt-6' text='Сохранить данные' loading={isPending} />
</form>
);
}
@ -90,7 +82,7 @@ function EditorProfile() {
export default EditorProfile;
// ====== Internals =========
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
function ServerError({ error }: { error: ErrorData }): React.ReactElement {
if (axios.isAxiosError(error) && error.response && error.response.status === 400) {
if ('email' in error.response.data) {
return (