F: Rework user profile editor
This commit is contained in:
parent
f5419472f5
commit
4cf24d0200
|
@ -103,6 +103,7 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
]
|
]
|
||||||
|
read_only_fields = ('id', 'username')
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
attrs = super().validate(attrs)
|
attrs = super().validate(attrs)
|
||||||
|
|
|
@ -101,6 +101,10 @@ class TestUserUserProfileAPIView(EndpointTester):
|
||||||
data = {'email': self.user2.email}
|
data = {'email': self.user2.email}
|
||||||
self.executeBadData(data=data)
|
self.executeBadData(data=data)
|
||||||
|
|
||||||
|
data = {'username': 'new_username'}
|
||||||
|
response = self.executeOK(data=data)
|
||||||
|
self.assertNotEqual(response.data['username'], data['username'])
|
||||||
|
|
||||||
self.logout()
|
self.logout()
|
||||||
self.executeForbidden()
|
self.executeForbidden()
|
||||||
|
|
||||||
|
|
|
@ -30,12 +30,16 @@ export type IUserSignupDTO = z.infer<typeof UserSignupSchema>;
|
||||||
/**
|
/**
|
||||||
* Represents user data, intended to update user profile in persistent storage.
|
* Represents user data, intended to update user profile in persistent storage.
|
||||||
*/
|
*/
|
||||||
export interface IUpdateProfileDTO {
|
export const UpdateProfileSchema = z.object({
|
||||||
username: string;
|
email: z.string().email(errors.emailField),
|
||||||
email: string;
|
first_name: z.string(),
|
||||||
first_name: string;
|
last_name: z.string()
|
||||||
last_name: string;
|
});
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Represents user data, intended to update user profile in persistent storage.
|
||||||
|
*/
|
||||||
|
export type IUpdateProfileDTO = z.infer<typeof UpdateProfileSchema>;
|
||||||
|
|
||||||
export const usersApi = {
|
export const usersApi = {
|
||||||
baseKey: 'users',
|
baseKey: 'users',
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { IUserProfile } from '@/models/user';
|
||||||
|
|
||||||
|
import { DataCallback } from '../apiTransport';
|
||||||
import { IUpdateProfileDTO, usersApi } from './api';
|
import { IUpdateProfileDTO, usersApi } from './api';
|
||||||
|
|
||||||
export const useUpdateProfile = () => {
|
export const useUpdateProfile = () => {
|
||||||
|
@ -13,7 +16,8 @@ export const useUpdateProfile = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
updateProfile: (data: IUpdateProfileDTO) => mutation.mutate(data),
|
updateProfile: (data: IUpdateProfileDTO, onSuccess?: DataCallback<IUserProfile>) =>
|
||||||
|
mutation.mutate(data, { onSuccess }),
|
||||||
isPending: mutation.isPending,
|
isPending: mutation.isPending,
|
||||||
error: mutation.error,
|
error: mutation.error,
|
||||||
reset: mutation.reset
|
reset: mutation.reset
|
||||||
|
|
|
@ -35,7 +35,7 @@ function LoginPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isAnonymous } = useAuthSuspense();
|
const { isAnonymous } = useAuthSuspense();
|
||||||
const { login, isPending, error: serverError, reset } = useLogin();
|
const { login, isPending, error: serverError, reset: clearServerError } = useLogin();
|
||||||
|
|
||||||
function onSubmit(data: IUserLoginDTO) {
|
function onSubmit(data: IUserLoginDTO) {
|
||||||
login(data, () => {
|
login(data, () => {
|
||||||
|
@ -49,7 +49,7 @@ function LoginPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetErrors() {
|
function resetErrors() {
|
||||||
reset();
|
clearServerError();
|
||||||
clearErrors();
|
clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
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 { urls } from '@/app/urls';
|
||||||
import { IUserSignupDTO, UserSignupSchema } from '@/backend/users/api';
|
import { IUserSignupDTO, UserSignupSchema } from '@/backend/users/api';
|
||||||
import { useSignup } from '@/backend/users/useSignup';
|
import { useSignup } from '@/backend/users/useSignup';
|
||||||
|
@ -26,7 +26,7 @@ import { globals, patterns } from '@/utils/constants';
|
||||||
|
|
||||||
function FormSignup() {
|
function FormSignup() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { signup, isPending, error: serverError, reset } = useSignup();
|
const { signup, isPending, error: serverError, reset: clearServerError } = useSignup();
|
||||||
const [acceptPrivacy, setAcceptPrivacy] = useState(false);
|
const [acceptPrivacy, setAcceptPrivacy] = useState(false);
|
||||||
const [acceptRules, setAcceptRules] = useState(false);
|
const [acceptRules, setAcceptRules] = useState(false);
|
||||||
|
|
||||||
|
@ -34,13 +34,15 @@ function FormSignup() {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
formState: { errors }
|
formState: { errors, isDirty }
|
||||||
} = useForm<IUserSignupDTO>({
|
} = useForm<IUserSignupDTO>({
|
||||||
resolver: zodResolver(UserSignupSchema)
|
resolver: zodResolver(UserSignupSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useBlockNavigation(isDirty);
|
||||||
|
|
||||||
function resetErrors() {
|
function resetErrors() {
|
||||||
reset();
|
clearServerError();
|
||||||
clearErrors();
|
clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import TextInput from '@/components/ui/TextInput';
|
||||||
|
|
||||||
function EditorPassword() {
|
function EditorPassword() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { changePassword, isPending, error: serverError, reset } = useChangePassword();
|
const { changePassword, isPending, error: serverError, reset: clearServerError } = useChangePassword();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
@ -27,7 +27,7 @@ function EditorPassword() {
|
||||||
});
|
});
|
||||||
|
|
||||||
function resetErrors() {
|
function resetErrors() {
|
||||||
reset();
|
clearServerError();
|
||||||
clearErrors();
|
clearErrors();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useEffect, useState } from 'react';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { useBlockNavigation } from '@/app/Navigation/NavigationContext';
|
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 { useProfileSuspense } from '@/backend/users/useProfile';
|
||||||
import { useUpdateProfile } from '@/backend/users/useUpdateProfile';
|
import { useUpdateProfile } from '@/backend/users/useUpdateProfile';
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
import { ErrorData } from '@/components/info/InfoError';
|
||||||
|
@ -13,76 +14,67 @@ import TextInput from '@/components/ui/TextInput';
|
||||||
|
|
||||||
function EditorProfile() {
|
function EditorProfile() {
|
||||||
const { profile } = useProfileSuspense();
|
const { profile } = useProfileSuspense();
|
||||||
const { updateProfile, isPending, error } = useUpdateProfile();
|
const { updateProfile, isPending, error: serverError, reset: clearServerError } = useUpdateProfile();
|
||||||
|
|
||||||
const [username, setUsername] = useState(profile.username);
|
const {
|
||||||
const [email, setEmail] = useState(profile.email);
|
register,
|
||||||
const [first_name, setFirstName] = useState(profile.first_name);
|
handleSubmit,
|
||||||
const [last_name, setLastName] = useState(profile.last_name);
|
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(() => {
|
function onSubmit(data: IUpdateProfileDTO) {
|
||||||
setUsername(profile.username);
|
updateProfile(data, () => resetForm({ ...data }));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className='cc-column w-[18rem] px-6 py-2'>
|
<form
|
||||||
<TextInput
|
className='cc-column w-[18rem] px-6 py-2'
|
||||||
id='username'
|
onSubmit={event => void handleSubmit(onSubmit)(event)}
|
||||||
autoComplete='username'
|
onChange={resetErrors}
|
||||||
disabled
|
>
|
||||||
label='Логин'
|
<TextInput id='username' disabled label='Логин' title='Логин изменить нельзя' value={profile.username} />
|
||||||
title='Логин изменить нельзя'
|
|
||||||
value={username}
|
|
||||||
/>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
id='first_name'
|
id='first_name'
|
||||||
|
{...register('first_name')}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
allowEnter
|
allowEnter
|
||||||
label='Имя'
|
label='Имя'
|
||||||
value={first_name}
|
error={errors.first_name}
|
||||||
onChange={event => setFirstName(event.target.value)}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='last_name'
|
id='last_name'
|
||||||
|
{...register('last_name')}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
allowEnter
|
allowEnter
|
||||||
label='Фамилия'
|
label='Фамилия'
|
||||||
value={last_name}
|
error={errors.last_name}
|
||||||
onChange={event => setLastName(event.target.value)}
|
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='email'
|
id='email'
|
||||||
|
{...register('email')}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
allowEnter
|
allowEnter
|
||||||
label='Электронная почта'
|
label='Электронная почта'
|
||||||
value={email}
|
error={errors.email}
|
||||||
onChange={event => setEmail(event.target.value)}
|
|
||||||
/>
|
|
||||||
{error ? <ProcessError error={error} /> : null}
|
|
||||||
<SubmitButton
|
|
||||||
className='self-center mt-6'
|
|
||||||
text='Сохранить данные'
|
|
||||||
loading={isPending}
|
|
||||||
disabled={!isModified || email == ''}
|
|
||||||
/>
|
/>
|
||||||
|
{serverError ? <ServerError error={serverError} /> : null}
|
||||||
|
<SubmitButton className='self-center mt-6' text='Сохранить данные' loading={isPending} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -90,7 +82,7 @@ function EditorProfile() {
|
||||||
export default EditorProfile;
|
export default EditorProfile;
|
||||||
|
|
||||||
// ====== Internals =========
|
// ====== 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 (axios.isAxiosError(error) && error.response && error.response.status === 400) {
|
||||||
if ('email' in error.response.data) {
|
if ('email' in error.response.data) {
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue
Block a user