F: Implement react-query pt2
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled

This commit is contained in:
Ivan 2025-01-21 20:33:05 +03:00
parent d899e17fcd
commit 76aee5bea7
50 changed files with 460 additions and 620 deletions

View File

@ -6,7 +6,6 @@ import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl';
import { queryClient } from '@/backend/queryClient';
import { AuthState } from '@/context/AuthContext';
import { GlobalOssState } from '@/context/GlobalOssContext';
import { LibraryState } from '@/context/LibraryContext';
@ -33,7 +32,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
>
<IntlProvider locale='ru' defaultLocale='ru'>
<QueryClientProvider client={queryClient}>
<AuthState>
<LibraryState>
<GlobalOssState>
@ -42,7 +40,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
</GlobalOssState>
</LibraryState>
</AuthState>
</QueryClientProvider>
</IntlProvider>
</ErrorBoundary>);

View File

@ -29,7 +29,8 @@ function Navigation() {
className={clsx(
'z-navigation', // prettier: split lines
'sticky top-0 left-0 right-0',
'select-none'
'select-none',
'bg-prim-100'
)}
>
<ToggleNavigation />
@ -57,6 +58,7 @@ function Navigation() {
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
<UserMenu />
</div>
</div>

View File

@ -0,0 +1,35 @@
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { IconLogin, IconUser2 } from '@/components/Icons';
import { usePreferencesStore } from '@/stores/preferences';
import NavigationButton from './NavigationButton';
interface UserButtonProps {
onLogin: () => void;
onClickUser: () => void;
}
function UserButton({ onLogin, onClickUser }: UserButtonProps) {
const { user } = useAuthSuspense();
const adminMode = usePreferencesStore(state => state.adminMode);
if (!user) {
return (
<NavigationButton
className='cc-fade-in'
title='Перейти на страницу логина'
icon={<IconLogin size='1.5rem' className='icon-primary' />}
onClick={onLogin}
/>
);
} else {
return (
<NavigationButton
className='cc-fade-in'
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
onClick={onClickUser}
/>
);
}
}
export default UserButton;

View File

@ -1,3 +1,5 @@
import { useAuth } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout';
import {
IconAdmin,
IconAdminOff,
@ -15,7 +17,6 @@ import {
import { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { usePreferencesStore } from '@/stores/preferences';
@ -28,7 +29,8 @@ interface UserDropdownProps {
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const router = useConceptNavigation();
const { user, logout } = useAuth();
const { user } = useAuth();
const { logout } = useLogout();
const darkMode = usePreferencesStore(state => state.darkMode);
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);

View File

@ -1,41 +1,22 @@
import { IconLogin, IconUser2 } from '@/components/Icons';
import { Suspense } from 'react';
import Loader from '@/components/ui/Loader';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import useDropdown from '@/hooks/useDropdown';
import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls';
import NavigationButton from './NavigationButton';
import UserButton from './UserButton';
import UserDropdown from './UserDropdown';
function UserMenu() {
const router = useConceptNavigation();
const { user, loading } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode);
const menu = useDropdown();
const navigateLogin = () => router.push(urls.login);
return (
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
{loading ? <Loader circular scale={1.5} /> : null}
{!user && !loading ? (
<NavigationButton
className='cc-fade-in'
title='Перейти на страницу логина'
icon={<IconLogin size='1.5rem' className='icon-primary' />}
onClick={navigateLogin}
/>
) : null}
{user && !loading ? (
<NavigationButton
className='cc-fade-in'
icon={<IconUser2 size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
onClick={menu.toggle}
/>
) : null}
<UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
<Suspense fallback={<Loader circular scale={1.5} />}>
<UserButton onLogin={() => router.push(urls.login)} onClickUser={menu.toggle} />
</Suspense>
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
</div>
);
}

View File

@ -42,17 +42,17 @@ export interface IAxiosRequest<RequestData, ResponseData> {
// ================ Transport API calls ================
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
if (request.setLoading) request.setLoading(true);
request.setLoading?.(true);
axiosInstance
.get<ResponseData>(endpoint, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
request.setLoading?.(false);
request.onSuccess?.(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error);
request.onError?.(error);
});
}
@ -61,17 +61,17 @@ export function AxiosPost<RequestData, ResponseData>({
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
request.setLoading?.(true);
axiosInstance
.post<ResponseData>(endpoint, request.data, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
request.setLoading?.(false);
request.onSuccess?.(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error);
request.onError?.(error);
});
}
@ -80,17 +80,17 @@ export function AxiosDelete<RequestData, ResponseData>({
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
request.setLoading?.(true);
axiosInstance
.delete<ResponseData>(endpoint, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
request.setLoading?.(false);
request.onSuccess?.(response.data);
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error);
request.onError?.(error);
});
}
@ -99,17 +99,17 @@ export function AxiosPatch<RequestData, ResponseData>({
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true);
request.setLoading?.(true);
axiosInstance
.patch<ResponseData>(endpoint, request.data, options)
.then(response => {
if (request.setLoading) request.setLoading(false);
if (request.onSuccess) request.onSuccess(response.data);
request.setLoading?.(false);
request.onSuccess?.(response.data);
return response.data;
})
.catch((error: Error | AxiosError) => {
if (request.setLoading) request.setLoading(false);
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
if (request.onError) request.onError(error);
request.onError?.(error);
});
}

View File

@ -0,0 +1,67 @@
import { queryOptions } from '@tanstack/react-query';
import { ICurrentUser, IUser } from '@/models/user';
import { axiosInstance } from '../apiConfiguration';
/**
* Represents login data, used to authenticate users.
*/
export interface IUserLoginDTO {
username: string;
password: string;
}
/**
* Represents data needed to update password for current user.
*/
export interface IChangePasswordDTO {
old_password: string;
new_password: string;
}
/**
* Represents password reset request data.
*/
export interface IRequestPasswordDTO extends Pick<IUser, 'email'> {}
/**
* Represents password reset data.
*/
export interface IResetPasswordDTO {
password: string;
token: string;
}
/**
* Represents password token data.
*/
export interface IPasswordTokenDTO extends Pick<IResetPasswordDTO, 'token'> {}
/**
* Authentication API.
*/
export const authApi = {
baseKey: 'auth',
getAuthQueryOptions: () => {
return queryOptions({
queryKey: [authApi.baseKey, 'user'],
queryFn: meta =>
axiosInstance
.get<ICurrentUser>('/users/api/auth', {
signal: meta.signal
})
.then(response => (response.data.id === null ? null : response.data)),
placeholderData: null,
staleTime: 24 * 60 * 60 * 1000
});
},
logout: () => axiosInstance.post('/users/api/logout'),
login: (data: IUserLoginDTO) => axiosInstance.post('/users/api/login', data),
changePassword: (data: IChangePasswordDTO) => axiosInstance.post('/users/api/change-password', data),
requestPasswordReset: (data: IRequestPasswordDTO) => axiosInstance.post('/users/api/password-reset', data),
validatePasswordToken: (data: IPasswordTokenDTO) => axiosInstance.post('/users/api/password-reset/validate', data),
resetPassword: (data: IResetPasswordDTO) => axiosInstance.post('/users/api/password-reset/confirm', data)
};

View File

@ -0,0 +1,21 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { authApi } from './api';
export function useAuth() {
const {
data: user,
isLoading,
error
} = useQuery({
...authApi.getAuthQueryOptions()
});
return { user, isLoading, error };
}
export function useAuthSuspense() {
const { data: user } = useSuspenseQuery({
...authApi.getAuthQueryOptions()
});
return { user };
}

View File

@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi, IChangePasswordDTO } from './api';
export const useChangePassword = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationKey: ['change-password'],
mutationFn: authApi.changePassword,
onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
});
return {
changePassword: (data: IChangePasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset
};
};

View File

@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi } from './api';
export const useLogin = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationKey: ['login'],
mutationFn: authApi.login,
onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
});
return {
login: (username: string, password: string, onSuccess?: () => void) =>
mutation.mutate({ username, password }, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset
};
};

View File

@ -0,0 +1,13 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi } from './api';
export const useLogout = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationKey: ['logout'],
mutationFn: authApi.logout,
onSettled: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
});
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
};

View File

@ -0,0 +1,16 @@
import { useMutation } from '@tanstack/react-query';
import { authApi, IRequestPasswordDTO } from './api';
export const useRequestPasswordReset = () => {
const mutation = useMutation({
mutationKey: ['request-password-reset'],
mutationFn: authApi.requestPasswordReset
});
return {
requestPasswordReset: (data: IRequestPasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset
};
};

View File

@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api';
export const useResetPassword = () => {
const queryClient = useQueryClient();
const validateMutation = useMutation({
mutationKey: ['reset-password'],
mutationFn: authApi.validatePasswordToken,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
});
const resetMutation = useMutation({
mutationKey: ['reset-password'],
mutationFn: authApi.resetPassword,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] })
});
return {
validateToken: (data: IPasswordTokenDTO, onSuccess?: () => void) => validateMutation.mutate(data, { onSuccess }),
resetPassword: (data: IResetPasswordDTO, onSuccess?: () => void) => resetMutation.mutate(data, { onSuccess }),
isPending: resetMutation.isPending,
error: resetMutation.error,
reset: resetMutation.reset
};
};

View File

@ -1,99 +0,0 @@
/**
* Endpoints: users.
*/
import {
ICurrentUser,
IPasswordTokenData,
IRequestPasswordData,
IResetPasswordData,
IUserInfo,
IUserLoginData,
IUserProfile,
IUserSignupData,
IUserUpdateData,
IUserUpdatePassword
} from '@/models/user';
import { AxiosGet, AxiosPatch, AxiosPost, FrontAction, FrontExchange, FrontPull, FrontPush } from './apiTransport';
export function getAuth(request: FrontPull<ICurrentUser>) {
AxiosGet({
endpoint: `/users/api/auth`,
request: request
});
}
export function postLogin(request: FrontPush<IUserLoginData>) {
AxiosPost({
endpoint: '/users/api/login',
request: request
});
}
export function postLogout(request: FrontAction) {
AxiosPost({
endpoint: '/users/api/logout',
request: request
});
}
export function postSignup(request: FrontExchange<IUserSignupData, IUserProfile>) {
AxiosPost({
endpoint: '/users/api/signup',
request: request
});
}
export function getProfile(request: FrontPull<IUserProfile>) {
AxiosGet({
endpoint: '/users/api/profile',
request: request
});
}
export function patchProfile(request: FrontExchange<IUserUpdateData, IUserProfile>) {
AxiosPatch({
endpoint: '/users/api/profile',
request: request
});
}
export function patchPassword(request: FrontPush<IUserUpdatePassword>) {
AxiosPatch({
endpoint: '/users/api/change-password',
request: request
});
}
export function postRequestPasswordReset(request: FrontPush<IRequestPasswordData>) {
// title: 'Request password reset',
AxiosPost({
endpoint: '/users/api/password-reset',
request: request
});
}
export function postValidatePasswordToken(request: FrontPush<IPasswordTokenData>) {
// title: 'Validate password token',
AxiosPost({
endpoint: '/users/api/password-reset/validate',
request: request
});
}
export function postResetPassword(request: FrontPush<IResetPasswordData>) {
// title: 'Reset password',
AxiosPost({
endpoint: '/users/api/password-reset/confirm',
request: request
});
}
export function getActiveUsers(request: FrontPull<IUserInfo[]>) {
// title: 'Active users list',
AxiosGet({
endpoint: '/users/api/active-users',
request: request
});
}

View File

@ -1,18 +1,40 @@
import { queryOptions } from '@tanstack/react-query';
import { IUserInfo } from '@/models/user';
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
import { axiosInstance } from '../apiConfiguration';
/**
* Represents user data, intended to update user profile in persistent storage.
*/
export interface IUpdateProfileDTO extends Omit<IUser, 'is_staff' | 'id'> {}
export const usersApi = {
baseKey: 'users',
getUsersQueryOptions: () => {
return queryOptions({
getUsersQueryOptions: () =>
queryOptions({
queryKey: [usersApi.baseKey, 'list'],
queryFn: meta =>
axiosInstance.get<IUserInfo[]>(`/users/api/active-users`, {
signal: meta.signal
})
});
}
axiosInstance
.get<IUserInfo[]>('/users/api/active-users', {
signal: meta.signal
})
.then(response => response.data),
placeholderData: []
}),
getProfileQueryOptions: () =>
queryOptions({
queryKey: [usersApi.baseKey, 'profile'],
queryFn: meta =>
axiosInstance
.get<IUserProfile>('/users/api/profile', {
signal: meta.signal
})
.then(response => response.data)
}),
signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data),
updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data)
};
//DataCallback<IUserProfile>

View File

@ -0,0 +1,21 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { usersApi } from './api';
export function useProfile() {
const {
data: profile,
isLoading,
error
} = useQuery({
...usersApi.getProfileQueryOptions()
});
return { profile, isLoading, error };
}
export function useProfileSuspense() {
const { data: profile } = useSuspenseQuery({
...usersApi.getProfileQueryOptions()
});
return { profile };
}

View File

@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { usersApi } from '@/backend/users/api';
import { IUserProfile, IUserSignupData } from '@/models/user';
import { DataCallback } from '../apiTransport';
export const useSignup = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationKey: ['signup'],
mutationFn: usersApi.signup,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [usersApi.baseKey] })
});
return {
signup: (data: IUserSignupData, onSuccess?: DataCallback<IUserProfile>) =>
mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset
};
};

View File

@ -0,0 +1,23 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IUserProfile } from '@/models/user';
import { IUpdateProfileDTO, usersApi } from './api';
// TODO: reload users / optimistic update
export const useUpdateProfile = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationKey: ['update-profile'],
mutationFn: usersApi.updateProfile,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [usersApi.baseKey] })
});
return {
updateProfile: (data: IUpdateProfileDTO, onSuccess?: (newUser: IUserProfile) => void) =>
mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset
};
};

View File

@ -3,17 +3,16 @@ import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { usersApi } from './api';
export function useUsersSuspense() {
const { data: users, refetch } = useSuspenseQuery({
...usersApi.getUsersQueryOptions()
const { data: users } = useSuspenseQuery({
...usersApi.getUsersQueryOptions(),
initialData: []
});
return { users: users?.data ?? [], refetch };
return { users };
}
export function useUsers() {
const { data: users, refetch } = useQuery({
const { data: users } = useQuery({
...usersApi.getUsersQueryOptions()
});
return { users: users?.data ?? [], refetch };
return { users: users ?? [] };
}

View File

@ -29,7 +29,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...re
loading && 'cursor-progress',
className
)}
disabled={disabled ?? loading}
disabled={disabled || loading}
{...restProps}
>
{icon ? <span>{icon}</span> : null}

View File

@ -1,11 +1,13 @@
import { urls } from '@/app/urls';
import { useAuth } from '@/context/AuthContext';
import { useAuth } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout';
import { useConceptNavigation } from '@/context/NavigationContext';
import TextURL from '../ui/TextURL';
function ExpectedAnonymous() {
const { user, logout } = useAuth();
const { user } = useAuth();
const { logout } = useLogout();
const router = useConceptNavigation();
function logoutAndRedirect() {

View File

@ -1,14 +1,14 @@
'use client';
import { useAuth } from '@/context/AuthContext';
import { useAuth } from '@/backend/auth/useAuth';
import Loader from '../ui/Loader';
import TextURL from '../ui/TextURL';
function RequireAuth({ children }: React.PropsWithChildren) {
const { user, loading } = useAuth();
const { user, isLoading } = useAuth();
if (loading) {
if (isLoading) {
return <Loader key='auth-loader' />;
}
if (user) {

View File

@ -1,204 +0,0 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import {
getAuth,
patchPassword,
postLogin,
postLogout,
postRequestPasswordReset,
postResetPassword,
postSignup,
postValidatePasswordToken
} from '@/backend/users';
import { type ErrorData } from '@/components/info/InfoError';
import {
ICurrentUser,
IPasswordTokenData,
IRequestPasswordData,
IResetPasswordData,
IUserLoginData,
IUserProfile,
IUserSignupData,
IUserUpdatePassword
} from '@/models/user';
import { contextOutsideScope } from '@/utils/labels';
interface IAuthContext {
user: ICurrentUser | undefined;
login: (data: IUserLoginData, callback?: DataCallback) => void;
logout: (callback?: DataCallback) => void;
signup: (data: IUserSignupData, callback?: DataCallback<IUserProfile>) => void;
updatePassword: (data: IUserUpdatePassword, callback?: () => void) => void;
requestPasswordReset: (data: IRequestPasswordData, callback?: () => void) => void;
validateToken: (data: IPasswordTokenData, callback?: () => void) => void;
resetPassword: (data: IResetPasswordData, callback?: () => void) => void;
loading: boolean;
error: ErrorData;
setError: (error: ErrorData) => void;
}
const AuthContext = createContext<IAuthContext | null>(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error(contextOutsideScope('useAuth', 'AuthState'));
}
return context;
};
export const AuthState = ({ children }: React.PropsWithChildren) => {
const [user, setUser] = useState<ICurrentUser | undefined>(undefined);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<ErrorData>(undefined);
const reload = useCallback(
(callback?: () => void) => {
getAuth({
onError: () => setUser(undefined),
setLoading: setLoading,
onSuccess: currentUser => {
if (currentUser.id) {
setUser(currentUser);
} else {
setUser(undefined);
}
callback?.();
}
});
},
[setUser]
);
function login(data: IUserLoginData, callback?: DataCallback) {
setError(undefined);
postLogin({
data: data,
showError: false,
setLoading: setLoading,
onError: setError,
onSuccess: newData =>
reload(() => {
callback?.(newData);
})
});
}
function logout(callback?: DataCallback) {
setError(undefined);
postLogout({
showError: true,
onSuccess: newData =>
reload(() => {
callback?.(newData);
})
});
}
function signup(data: IUserSignupData, callback?: DataCallback<IUserProfile>) {
setError(undefined);
postSignup({
data: data,
showError: true,
setLoading: setLoading,
onError: setError,
onSuccess: newData =>
reload(() => {
// TODO: reload users / optimistic update
// users.push(newData as IUserInfo);
callback?.(newData);
})
});
}
const updatePassword = useCallback(
(data: IUserUpdatePassword, callback?: () => void) => {
setError(undefined);
patchPassword({
data: data,
showError: true,
setLoading: setLoading,
onError: setError,
onSuccess: () => reload(callback)
});
},
[reload]
);
const requestPasswordReset = useCallback(
(data: IRequestPasswordData, callback?: () => void) => {
setError(undefined);
postRequestPasswordReset({
data: data,
showError: false,
setLoading: setLoading,
onError: setError,
onSuccess: () =>
reload(() => {
callback?.();
})
});
},
[reload]
);
const validateToken = useCallback(
(data: IPasswordTokenData, callback?: () => void) => {
setError(undefined);
postValidatePasswordToken({
data: data,
showError: false,
setLoading: setLoading,
onError: setError,
onSuccess: () =>
reload(() => {
callback?.();
})
});
},
[reload]
);
const resetPassword = useCallback(
(data: IResetPasswordData, callback?: () => void) => {
setError(undefined);
postResetPassword({
data: data,
showError: false,
setLoading: setLoading,
onError: setError,
onSuccess: () =>
reload(() => {
callback?.();
})
});
},
[reload]
);
useEffect(() => {
reload();
}, [reload]);
return (
<AuthContext
value={{
user,
login,
logout,
signup,
loading,
error,
setError,
updatePassword,
requestPasswordReset,
validateToken,
resetPassword
}}
>
{children}
</AuthContext>
);
};

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import {
deleteLibraryItem,
getAdminLibrary,
@ -24,8 +25,6 @@ import { RSFormLoader } from '@/models/RSFormLoader';
import { usePreferencesStore } from '@/stores/preferences';
import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
interface ILibraryContext {
items: ILibraryItem[];
templates: ILibraryItem[];
@ -62,7 +61,7 @@ export const useLibrary = (): ILibraryContext => {
};
export const LibraryState = ({ children }: React.PropsWithChildren) => {
const { user, loading: userLoading } = useAuth();
const { user, isLoading: userLoading } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode);
const [items, setItems] = useState<ILibraryItem[]>([]);

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import {
patchLibraryItem,
patchSetAccessPolicy,
@ -38,7 +39,6 @@ import {
import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext';

View File

@ -3,6 +3,7 @@
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import {
patchLibraryItem,
patchSetAccessPolicy,
@ -50,7 +51,6 @@ import {
import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels';
import { useAuth } from './AuthContext';
import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext';

View File

@ -1,76 +0,0 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { getProfile, patchProfile } from '@/backend/users';
import { ErrorData } from '@/components/info/InfoError';
import { IUserProfile, IUserUpdateData } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels';
interface IUserProfileContext {
user: IUserProfile | undefined;
loading: boolean;
processing: boolean;
error: ErrorData;
errorProcessing: ErrorData;
setError: (error: ErrorData) => void;
updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void;
}
const ProfileContext = createContext<IUserProfileContext | null>(null);
export const useUserProfile = () => {
const context = useContext(ProfileContext);
if (!context) {
throw new Error(contextOutsideScope('useUserProfile', 'UserProfileState'));
}
return context;
};
export const UserProfileState = ({ children }: React.PropsWithChildren) => {
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
const [loading, setLoading] = useState(true);
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<ErrorData>(undefined);
const [errorProcessing, setErrorProcessing] = useState<ErrorData>(undefined);
const reload = useCallback(() => {
setError(undefined);
setUser(undefined);
getProfile({
showError: true,
setLoading: setLoading,
onError: setError,
onSuccess: newData => setUser(newData)
});
}, [setUser]);
const updateUser = useCallback(
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
setErrorProcessing(undefined);
patchProfile({
data: data,
showError: true,
setLoading: setProcessing,
onError: setErrorProcessing,
onSuccess: newData => {
setUser(newData);
// TODO: reload users / optimistic update
callback?.(newData);
}
});
},
[setUser]
);
useEffect(() => {
reload();
}, [reload]);
return (
<ProfileContext value={{ user, updateUser, error, loading, setError, processing, errorProcessing }}>
{children}
</ProfileContext>
);
};

View File

@ -3,12 +3,12 @@
import clsx from 'clsx';
import { useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import SelectLocationContext from '@/components/select/SelectLocationContext';
import SelectLocationHead from '@/components/select/SelectLocationHead';
import Label from '@/components/ui/Label';
import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { LocationHead } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI';

View File

@ -5,6 +5,7 @@ import { useState } from 'react';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { VisibilityIcon } from '@/components/DomainIcons';
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
import SelectLocationContext from '@/components/select/SelectLocationContext';
@ -15,7 +16,6 @@ import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';

View File

@ -2,15 +2,15 @@
import { useCallback, useEffect, useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import { getOssDetails } from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader';
function useOssDetails({ target }: { target?: string }) {
const { loading: userLoading } = useAuth();
const { isLoading: userLoading } = useAuth();
const library = useLibrary();
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
const [loading, setLoading] = useState(target != undefined);

View File

@ -2,14 +2,14 @@
import { useCallback, useEffect, useState } from 'react';
import { useAuth } from '@/backend/auth/useAuth';
import { getRSFormDetails } from '@/backend/rsforms';
import { type ErrorData } from '@/components/info/InfoError';
import { useAuth } from '@/context/AuthContext';
import { IRSForm, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader';
function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
const { loading: userLoading } = useAuth();
const { isLoading: userLoading } = useAuth();
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
const [loading, setLoading] = useState(target != undefined);
const [error, setError] = useState<ErrorData>(undefined);

View File

@ -26,35 +26,9 @@ export interface IUser {
* Represents CurrentUser information.
*/
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
subscriptions: LibraryItemID[];
editor: LibraryItemID[];
}
/**
* Represents login data, used to authenticate users.
*/
export interface IUserLoginData extends Pick<IUser, 'username'> {
password: string;
}
/**
* Represents password reset data.
*/
export interface IResetPasswordData {
password: string;
token: string;
}
/**
* Represents password token data.
*/
export interface IPasswordTokenData extends Pick<IResetPasswordData, 'token'> {}
/**
* Represents password reset request data.
*/
export interface IRequestPasswordData extends Pick<IUser, 'email'> {}
/**
* Represents signup data, used to create new users.
*/
@ -63,11 +37,6 @@ export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
password2: string;
}
/**
* Represents user data, intended to update user profile in persistent storage.
*/
export interface IUserUpdateData extends Omit<IUser, 'is_staff' | 'id'> {}
/**
* Represents user profile for viewing and editing {@link IUser}.
*/
@ -78,14 +47,6 @@ export interface IUserProfile extends Omit<IUser, 'is_staff'> {}
*/
export interface IUserInfo extends Omit<IUserProfile, 'email' | 'username'> {}
/**
* Represents data needed to update password for current user.
*/
export interface IUserUpdatePassword {
old_password: string;
new_password: string;
}
/**
* Represents target {@link User}.
*/

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { VisibilityIcon } from '@/components/DomainIcons';
import { IconDownload } from '@/components/Icons';
import InfoError from '@/components/info/InfoError';
@ -19,7 +20,6 @@ import Overlay from '@/components/ui/Overlay';
import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';

View File

@ -1,17 +1,17 @@
import { useEffect } from 'react';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { PARAMETER } from '@/utils/constants';
function HomePage() {
const router = useConceptNavigation();
const { user, loading } = useAuth();
const { user, isLoading } = useAuth();
useEffect(() => {
if (!loading) {
if (!isLoading) {
if (!user) {
setTimeout(() => {
router.replace(urls.manuals);
@ -22,7 +22,7 @@ function HomePage() {
}, PARAMETER.refreshTimeout);
}
}
}, [router, user, loading]);
}, [router, user, isLoading]);
return <Loader />;
}

View File

@ -1,13 +1,13 @@
import clsx from 'clsx';
import { toast } from 'react-toastify';
import { useAuth } from '@/backend/auth/useAuth';
import { SubfoldersIcon } from '@/components/DomainIcons';
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp';
import { CProps } from '@/components/props';
import SelectLocation from '@/components/select/SelectLocation';
import MiniButton from '@/components/ui/MiniButton';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import useWindowSize from '@/hooks/useWindowSize';
import { FolderNode, FolderTree } from '@/models/FolderTree';

View File

@ -5,15 +5,15 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useLogin } from '@/backend/auth/useLogin';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import { IUserLoginData } from '@/models/user';
import { resources } from '@/utils/constants';
function LoginPage() {
@ -21,23 +21,20 @@ function LoginPage() {
const query = useQueryStrings();
const userQuery = query.get('username');
const { user, login, loading, error, setError } = useAuth();
const { user } = useAuth();
const { login, isPending, error, reset } = useLogin();
const [username, setUsername] = useState(userQuery || '');
const [password, setPassword] = useState('');
useEffect(() => {
setError(undefined);
}, [username, password, setError]);
reset();
}, [username, password, reset]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!loading) {
const data: IUserLoginData = {
username: username,
password: password
};
login(data, () => {
if (!isPending) {
login(username, password, () => {
if (router.canBack()) {
router.back();
} else {
@ -78,7 +75,7 @@ function LoginPage() {
<SubmitButton
text='Войти'
className='self-center w-[12rem] mt-3'
loading={loading}
loading={isPending}
disabled={!username || !password}
/>
<div className='flex flex-col text-sm'>

View File

@ -1,6 +1,7 @@
'use client';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import {
IconAdmin,
IconAlert,
@ -19,7 +20,6 @@ import Button from '@/components/ui/Button';
import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext';
import useDropdown from '@/hooks/useDropdown';

View File

@ -4,7 +4,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAuth } from '@/context/AuthContext';
import { useAuth } from '@/backend/auth/useAuth';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext';

View File

@ -7,12 +7,12 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Loader from '@/components/ui/Loader';
import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL';
import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
import { useOSS } from '@/context/OssContext';
@ -46,7 +46,7 @@ function OssTabs() {
useBlockNavigation(
isModified &&
schema !== undefined &&
user !== undefined &&
!!user &&
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
);

View File

@ -5,20 +5,20 @@ import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { urls } from '@/app/urls';
import { IPasswordTokenDTO, IResetPasswordDTO } from '@/backend/auth/api';
import { useResetPassword } from '@/backend/auth/useResetPassword';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import DataLoader from '@/components/wrap/DataLoader';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import useQueryStrings from '@/hooks/useQueryStrings';
import { IPasswordTokenData, IResetPasswordData } from '@/models/user';
function PasswordChangePage() {
const router = useConceptNavigation();
const token = useQueryStrings().get('token');
const { validateToken, resetPassword, loading, error, setError } = useAuth();
const { validateToken, resetPassword, isPending, error, reset } = useResetPassword();
const [isTokenValid, setIsTokenValid] = useState(false);
const [newPassword, setNewPassword] = useState('');
@ -31,8 +31,8 @@ function PasswordChangePage() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!loading) {
const data: IResetPasswordData = {
if (!isPending) {
const data: IResetPasswordDTO = {
password: newPassword,
token: token!
};
@ -44,21 +44,18 @@ function PasswordChangePage() {
}
useEffect(() => {
setError(undefined);
}, [newPassword, newPasswordRepeat, setError]);
reset();
}, [newPassword, newPasswordRepeat, reset]);
useEffect(() => {
const data: IPasswordTokenData = {
const data: IPasswordTokenDTO = {
token: token ?? ''
};
validateToken(data, () => setIsTokenValid(true));
}, [token, validateToken]);
if (error) {
return <ProcessError error={error} />;
}
return (
<DataLoader isLoading={loading} hasNoData={!isTokenValid}>
<DataLoader isLoading={isPending} hasNoData={!isTokenValid}>
<form className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}>
<TextInput
id='new_password'
@ -88,7 +85,7 @@ function PasswordChangePage() {
<SubmitButton
text='Установить пароль'
className='self-center w-[12rem] mt-3'
loading={loading}
loading={isPending}
disabled={!canSubmit}
/>
{error ? <ProcessError error={error} /> : null}

View File

@ -1,6 +1,7 @@
'use client';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import {
IconAdmin,
IconAlert,
@ -31,7 +32,6 @@ import Button from '@/components/ui/Button';
import Divider from '@/components/ui/Divider';
import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton';
import { useAuth } from '@/context/AuthContext';
import { useGlobalOss } from '@/context/GlobalOssContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';

View File

@ -5,7 +5,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState }
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useAuth } from '@/context/AuthContext';
import { useAuth } from '@/backend/auth/useAuth';
import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext';
import {

View File

@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { useSignup } from '@/backend/users/useSignup';
import { IconHelp } from '@/components/Icons';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import Button from '@/components/ui/Button';
@ -17,15 +18,15 @@ import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL';
import Tooltip from '@/components/ui/Tooltip';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { HelpTopic } from '@/models/miscellaneous';
import { IUserSignupData } from '@/models/user';
import { globals, patterns } from '@/utils/constants';
import { information } from '@/utils/labels';
function FormSignup() {
const router = useConceptNavigation();
const { signup, loading, error, setError } = useAuth();
const { signup, isPending, error, reset } = useSignup();
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
@ -40,8 +41,8 @@ function FormSignup() {
const isValid = acceptPrivacy && acceptRules && !!email && !!username;
useEffect(() => {
setError(undefined);
}, [username, email, password, password2, setError]);
reset();
}, [username, email, password, password2, reset]);
function handleCancel() {
if (router.canBack()) {
@ -53,7 +54,7 @@ function FormSignup() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!loading) {
if (!isPending) {
const data: IUserSignupData = {
username,
email,
@ -64,7 +65,7 @@ function FormSignup() {
};
signup(data, createdUser => {
router.push(urls.login_hint(createdUser.username));
toast.success(`Пользователь успешно создан: ${createdUser.username}`);
toast.success(information.newUser(createdUser.username));
});
}
}
@ -159,7 +160,7 @@ function FormSignup() {
</div>
<div className='flex justify-around my-3'>
<SubmitButton text='Регистрировать' className='min-w-[10rem]' loading={loading} disabled={!isValid} />
<SubmitButton text='Регистрировать' className='min-w-[10rem]' loading={isPending} disabled={!isValid} />
<Button text='Назад' className='min-w-[10rem]' onClick={() => handleCancel()} />
</div>
{error ? <ProcessError error={error} /> : null}

View File

@ -1,13 +1,13 @@
import { useAuth } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader';
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
import { useAuth } from '@/context/AuthContext';
import FormSignup from './FormSignup';
function RegisterPage() {
const { user, loading } = useAuth();
const { user, isLoading } = useAuth();
if (loading) {
if (isLoading) {
return <Loader />;
}
if (user) {

View File

@ -4,32 +4,28 @@ import axios from 'axios';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useRequestPasswordReset } from '@/backend/auth/useRequestPasswordReset';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL';
import { useAuth } from '@/context/AuthContext';
import { IRequestPasswordData } from '@/models/user';
function RestorePasswordPage() {
const { requestPasswordReset, loading, error, setError } = useAuth();
const { requestPasswordReset, isPending, error, reset } = useRequestPasswordReset();
const [isCompleted, setIsCompleted] = useState(false);
const [email, setEmail] = useState('');
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!loading) {
const data: IRequestPasswordData = {
email: email
};
requestPasswordReset(data, () => setIsCompleted(true));
if (!isPending) {
requestPasswordReset({ email: email }, () => setIsCompleted(true));
}
}
useEffect(() => {
setError(undefined);
}, [email, setError]);
reset();
}, [email, reset]);
if (isCompleted) {
return (
@ -55,7 +51,7 @@ function RestorePasswordPage() {
<SubmitButton
text='Запросить пароль'
className='self-center w-[12rem] mt-3'
loading={loading}
loading={isPending}
disabled={!email}
/>
{error ? <ProcessError error={error} /> : null}

View File

@ -6,18 +6,18 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { urls } from '@/app/urls';
import { IChangePasswordDTO } from '@/backend/auth/api';
import { useChangePassword } from '@/backend/auth/useChangePassword';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import FlexColumn from '@/components/ui/FlexColumn';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { IUserUpdatePassword } from '@/models/user';
import { errors, information } from '@/utils/labels';
function EditorPassword() {
const router = useConceptNavigation();
const { updatePassword, error, setError, loading } = useAuth();
const { changePassword, isPending, error, reset } = useChangePassword();
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
@ -34,19 +34,19 @@ function EditorPassword() {
toast.error(errors.passwordsMismatch);
return;
}
const data: IUserUpdatePassword = {
const data: IChangePasswordDTO = {
old_password: oldPassword,
new_password: newPassword
};
updatePassword(data, () => {
changePassword(data, () => {
toast.success(information.changesSaved);
router.push(urls.login);
});
}
useEffect(() => {
setError(undefined);
}, [newPassword, oldPassword, newPasswordRepeat, setError]);
reset();
}, [newPassword, oldPassword, newPasswordRepeat, reset]);
return (
<form
@ -89,7 +89,7 @@ function EditorPassword() {
/>
{error ? <ProcessError error={error} /> : null}
</FlexColumn>
<SubmitButton text='Сменить пароль' className='self-center' disabled={!canSubmit} loading={loading} />
<SubmitButton text='Сменить пароль' className='self-center' disabled={!canSubmit} loading={isPending} />
</form>
);
}

View File

@ -4,45 +4,44 @@ import axios from 'axios';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { IUpdateProfileDTO } from '@/backend/users/api';
import { useProfileSuspense } from '@/backend/users/useProfile';
import { useUpdateProfile } from '@/backend/users/useUpdateProfile';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import { useBlockNavigation } from '@/context/NavigationContext';
import { useUserProfile } from '@/context/UserProfileContext';
import { IUserUpdateData } from '@/models/user';
import { information } from '@/utils/labels';
function EditorProfile() {
const { updateUser, user, errorProcessing, processing } = useUserProfile();
const { profile } = useProfileSuspense();
const { updateProfile, isPending, error } = useUpdateProfile();
const [username, setUsername] = useState(user?.username ?? '');
const [email, setEmail] = useState(user?.email ?? '');
const [first_name, setFirstName] = useState(user?.first_name ?? '');
const [last_name, setLastName] = useState(user?.last_name ?? '');
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 isModified =
user != undefined && (user.email !== email || user.first_name !== first_name || user.last_name !== last_name);
const isModified = profile.email !== email || profile.first_name !== first_name || profile.last_name !== last_name;
useBlockNavigation(isModified);
useEffect(() => {
if (user) {
setUsername(user.username);
setEmail(user.email);
setFirstName(user.first_name);
setLastName(user.last_name);
}
}, [user]);
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: IUserUpdateData = {
const data: IUpdateProfileDTO = {
username: username,
email: email,
first_name: first_name,
last_name: last_name
};
updateUser(data, () => toast.success(information.changesSaved));
updateProfile(data, () => toast.success(information.changesSaved));
}
return (
@ -79,11 +78,11 @@ function EditorProfile() {
value={email}
onChange={event => setEmail(event.target.value)}
/>
{errorProcessing ? <ProcessError error={errorProcessing} /> : null}
{error ? <ProcessError error={error} /> : null}
<SubmitButton
className='self-center mt-6'
text='Сохранить данные'
loading={processing}
loading={isPending}
disabled={!isModified || email == ''}
/>
</form>

View File

@ -1,25 +0,0 @@
'use client';
import DataLoader from '@/components/wrap/DataLoader';
import { useUserProfile } from '@/context/UserProfileContext';
import EditorPassword from './EditorPassword';
import EditorProfile from './EditorProfile';
function UserContents() {
const { user, error, loading } = useUserProfile();
return (
<DataLoader isLoading={loading} error={error} hasNoData={!user}>
<div className='cc-fade-in flex flex-col py-2 mx-auto w-fit'>
<h1 className='mb-2 select-none'>Учетные данные пользователя</h1>
<div className='flex py-2'>
<EditorProfile />
<EditorPassword />
</div>
</div>
</DataLoader>
);
}
export default UserContents;

View File

@ -1,14 +1,23 @@
import RequireAuth from '@/components/wrap/RequireAuth';
import { UserProfileState } from '@/context/UserProfileContext';
import { Suspense } from 'react';
import UserContents from './UserContents';
import Loader from '@/components/ui/Loader';
import RequireAuth from '@/components/wrap/RequireAuth';
import EditorPassword from './EditorPassword';
import EditorProfile from './EditorProfile';
function UserProfilePage() {
return (
<RequireAuth>
<UserProfileState>
<UserContents />
</UserProfileState>
<Suspense fallback={<Loader />}>
<div className='cc-fade-in flex flex-col py-2 mx-auto w-fit'>
<h1 className='mb-2 select-none'>Учетные данные пользователя</h1>
<div className='flex py-2'>
<EditorProfile />
<EditorPassword />
</div>
</div>
</Suspense>
</RequireAuth>
);
}

View File

@ -958,8 +958,9 @@ export const information = {
noDataToExport: 'Нет данных для экспорта',
substitutionsCorrect: 'Таблица отождествлений прошла проверку',
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
newLibraryItem: 'Схема успешно создана',
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
newUser: (username: string) => `Пользователь успешно создан: ${username}`,
newVersion: (version: string) => `Версия создана: ${version}`,
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
newOperation: (alias: string) => `Операция добавлена: ${alias}`,