F: Rework user profile editor
This commit is contained in:
parent
f5419472f5
commit
4cf24d0200
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue
Block a user