F: Implement react-query pt2
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
This commit is contained in:
parent
d899e17fcd
commit
76aee5bea7
|
@ -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>);
|
||||
|
|
|
@ -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>
|
||||
|
|
35
rsconcept/frontend/src/app/Navigation/UserButton.tsx
Normal file
35
rsconcept/frontend/src/app/Navigation/UserButton.tsx
Normal 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;
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
67
rsconcept/frontend/src/backend/auth/api.ts
Normal file
67
rsconcept/frontend/src/backend/auth/api.ts
Normal 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)
|
||||
};
|
21
rsconcept/frontend/src/backend/auth/useAuth.tsx
Normal file
21
rsconcept/frontend/src/backend/auth/useAuth.tsx
Normal 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 };
|
||||
}
|
18
rsconcept/frontend/src/backend/auth/useChangePassword.tsx
Normal file
18
rsconcept/frontend/src/backend/auth/useChangePassword.tsx
Normal 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
|
||||
};
|
||||
};
|
19
rsconcept/frontend/src/backend/auth/useLogin.tsx
Normal file
19
rsconcept/frontend/src/backend/auth/useLogin.tsx
Normal 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
|
||||
};
|
||||
};
|
13
rsconcept/frontend/src/backend/auth/useLogout.tsx
Normal file
13
rsconcept/frontend/src/backend/auth/useLogout.tsx
Normal 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 }) };
|
||||
};
|
|
@ -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
|
||||
};
|
||||
};
|
24
rsconcept/frontend/src/backend/auth/useResetPassword.tsx
Normal file
24
rsconcept/frontend/src/backend/auth/useResetPassword.tsx
Normal 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
|
||||
};
|
||||
};
|
|
@ -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
|
||||
});
|
||||
}
|
|
@ -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>
|
||||
|
|
21
rsconcept/frontend/src/backend/users/useProfile.tsx
Normal file
21
rsconcept/frontend/src/backend/users/useProfile.tsx
Normal 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 };
|
||||
}
|
22
rsconcept/frontend/src/backend/users/useSignup.tsx
Normal file
22
rsconcept/frontend/src/backend/users/useSignup.tsx
Normal 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
|
||||
};
|
||||
};
|
23
rsconcept/frontend/src/backend/users/useUpdateProfile.tsx
Normal file
23
rsconcept/frontend/src/backend/users/useUpdateProfile.tsx
Normal 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
|
||||
};
|
||||
};
|
|
@ -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 ?? [] };
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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[]>([]);
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}.
|
||||
*/
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}`,
|
||||
|
|
Loading…
Reference in New Issue
Block a user