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 { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { queryClient } from '@/backend/queryClient';
|
import { queryClient } from '@/backend/queryClient';
|
||||||
import { AuthState } from '@/context/AuthContext';
|
|
||||||
import { GlobalOssState } from '@/context/GlobalOssContext';
|
import { GlobalOssState } from '@/context/GlobalOssContext';
|
||||||
import { LibraryState } from '@/context/LibraryContext';
|
import { LibraryState } from '@/context/LibraryContext';
|
||||||
|
|
||||||
|
@ -33,7 +32,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
|
||||||
>
|
>
|
||||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthState>
|
|
||||||
<LibraryState>
|
<LibraryState>
|
||||||
<GlobalOssState>
|
<GlobalOssState>
|
||||||
|
|
||||||
|
@ -42,7 +40,6 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
|
||||||
|
|
||||||
</GlobalOssState>
|
</GlobalOssState>
|
||||||
</LibraryState>
|
</LibraryState>
|
||||||
</AuthState>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ErrorBoundary>);
|
</ErrorBoundary>);
|
||||||
|
|
|
@ -29,7 +29,8 @@ function Navigation() {
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'z-navigation', // prettier: split lines
|
'z-navigation', // prettier: split lines
|
||||||
'sticky top-0 left-0 right-0',
|
'sticky top-0 left-0 right-0',
|
||||||
'select-none'
|
'select-none',
|
||||||
|
'bg-prim-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ToggleNavigation />
|
<ToggleNavigation />
|
||||||
|
@ -57,6 +58,7 @@ function Navigation() {
|
||||||
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
<NavigationButton text='Новая схема' icon={<IconNewItem2 size='1.5rem' />} onClick={navigateCreateNew} />
|
||||||
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
<NavigationButton text='Библиотека' icon={<IconLibrary2 size='1.5rem' />} onClick={navigateLibrary} />
|
||||||
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
<NavigationButton text='Справка' icon={<IconManuals size='1.5rem' />} onClick={navigateHelp} />
|
||||||
|
|
||||||
<UserMenu />
|
<UserMenu />
|
||||||
</div>
|
</div>
|
||||||
</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 {
|
import {
|
||||||
IconAdmin,
|
IconAdmin,
|
||||||
IconAdminOff,
|
IconAdminOff,
|
||||||
|
@ -15,7 +17,6 @@ import {
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
import DropdownButton from '@/components/ui/DropdownButton';
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
|
|
||||||
|
@ -28,7 +29,8 @@ interface UserDropdownProps {
|
||||||
|
|
||||||
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user, logout } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { logout } = useLogout();
|
||||||
|
|
||||||
const darkMode = usePreferencesStore(state => state.darkMode);
|
const darkMode = usePreferencesStore(state => state.darkMode);
|
||||||
const toggleDarkMode = usePreferencesStore(state => state.toggleDarkMode);
|
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 Loader from '@/components/ui/Loader';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
|
||||||
|
|
||||||
import { urls } from '../urls';
|
import { urls } from '../urls';
|
||||||
import NavigationButton from './NavigationButton';
|
import UserButton from './UserButton';
|
||||||
import UserDropdown from './UserDropdown';
|
import UserDropdown from './UserDropdown';
|
||||||
|
|
||||||
function UserMenu() {
|
function UserMenu() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user, loading } = useAuth();
|
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
|
||||||
const menu = useDropdown();
|
const menu = useDropdown();
|
||||||
|
|
||||||
const navigateLogin = () => router.push(urls.login);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
||||||
{loading ? <Loader circular scale={1.5} /> : null}
|
<Suspense fallback={<Loader circular scale={1.5} />}>
|
||||||
{!user && !loading ? (
|
<UserButton onLogin={() => router.push(urls.login)} onClickUser={menu.toggle} />
|
||||||
<NavigationButton
|
</Suspense>
|
||||||
className='cc-fade-in'
|
<UserDropdown isOpen={menu.isOpen} hideDropdown={() => menu.hide()} />
|
||||||
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()} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,17 +42,17 @@ export interface IAxiosRequest<RequestData, ResponseData> {
|
||||||
|
|
||||||
// ================ Transport API calls ================
|
// ================ Transport API calls ================
|
||||||
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
|
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
|
||||||
if (request.setLoading) request.setLoading(true);
|
request.setLoading?.(true);
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.get<ResponseData>(endpoint, options)
|
.get<ResponseData>(endpoint, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.onSuccess) request.onSuccess(response.data);
|
request.onSuccess?.(response.data);
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.showError) toast.error(extractErrorMessage(error));
|
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,
|
request,
|
||||||
options
|
options
|
||||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||||
if (request.setLoading) request.setLoading(true);
|
request.setLoading?.(true);
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.post<ResponseData>(endpoint, request.data, options)
|
.post<ResponseData>(endpoint, request.data, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.onSuccess) request.onSuccess(response.data);
|
request.onSuccess?.(response.data);
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.showError) toast.error(extractErrorMessage(error));
|
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,
|
request,
|
||||||
options
|
options
|
||||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||||
if (request.setLoading) request.setLoading(true);
|
request.setLoading?.(true);
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.delete<ResponseData>(endpoint, options)
|
.delete<ResponseData>(endpoint, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.onSuccess) request.onSuccess(response.data);
|
request.onSuccess?.(response.data);
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.showError) toast.error(extractErrorMessage(error));
|
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,
|
request,
|
||||||
options
|
options
|
||||||
}: IAxiosRequest<RequestData, ResponseData>) {
|
}: IAxiosRequest<RequestData, ResponseData>) {
|
||||||
if (request.setLoading) request.setLoading(true);
|
request.setLoading?.(true);
|
||||||
axiosInstance
|
axiosInstance
|
||||||
.patch<ResponseData>(endpoint, request.data, options)
|
.patch<ResponseData>(endpoint, request.data, options)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.onSuccess) request.onSuccess(response.data);
|
request.onSuccess?.(response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
.catch((error: Error | AxiosError) => {
|
.catch((error: Error | AxiosError) => {
|
||||||
if (request.setLoading) request.setLoading(false);
|
request.setLoading?.(false);
|
||||||
if (request.showError) toast.error(extractErrorMessage(error));
|
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 { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { IUserInfo } from '@/models/user';
|
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
|
||||||
|
|
||||||
import { axiosInstance } from '../apiConfiguration';
|
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 = {
|
export const usersApi = {
|
||||||
baseKey: 'users',
|
baseKey: 'users',
|
||||||
getUsersQueryOptions: () => {
|
getUsersQueryOptions: () =>
|
||||||
return queryOptions({
|
queryOptions({
|
||||||
queryKey: [usersApi.baseKey, 'list'],
|
queryKey: [usersApi.baseKey, 'list'],
|
||||||
queryFn: meta =>
|
queryFn: meta =>
|
||||||
axiosInstance.get<IUserInfo[]>(`/users/api/active-users`, {
|
axiosInstance
|
||||||
|
.get<IUserInfo[]>('/users/api/active-users', {
|
||||||
signal: meta.signal
|
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';
|
import { usersApi } from './api';
|
||||||
|
|
||||||
export function useUsersSuspense() {
|
export function useUsersSuspense() {
|
||||||
const { data: users, refetch } = useSuspenseQuery({
|
const { data: users } = useSuspenseQuery({
|
||||||
...usersApi.getUsersQueryOptions()
|
...usersApi.getUsersQueryOptions(),
|
||||||
|
initialData: []
|
||||||
});
|
});
|
||||||
|
return { users };
|
||||||
return { users: users?.data ?? [], refetch };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUsers() {
|
export function useUsers() {
|
||||||
const { data: users, refetch } = useQuery({
|
const { data: users } = useQuery({
|
||||||
...usersApi.getUsersQueryOptions()
|
...usersApi.getUsersQueryOptions()
|
||||||
});
|
});
|
||||||
|
return { users: users ?? [] };
|
||||||
return { users: users?.data ?? [], refetch };
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading, className, ...re
|
||||||
loading && 'cursor-progress',
|
loading && 'cursor-progress',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
disabled={disabled ?? loading}
|
disabled={disabled || loading}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
{icon ? <span>{icon}</span> : null}
|
{icon ? <span>{icon}</span> : null}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { urls } from '@/app/urls';
|
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 { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
|
|
||||||
import TextURL from '../ui/TextURL';
|
import TextURL from '../ui/TextURL';
|
||||||
|
|
||||||
function ExpectedAnonymous() {
|
function ExpectedAnonymous() {
|
||||||
const { user, logout } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { logout } = useLogout();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
|
|
||||||
function logoutAndRedirect() {
|
function logoutAndRedirect() {
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
|
|
||||||
import Loader from '../ui/Loader';
|
import Loader from '../ui/Loader';
|
||||||
import TextURL from '../ui/TextURL';
|
import TextURL from '../ui/TextURL';
|
||||||
|
|
||||||
function RequireAuth({ children }: React.PropsWithChildren) {
|
function RequireAuth({ children }: React.PropsWithChildren) {
|
||||||
const { user, loading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
if (loading) {
|
if (isLoading) {
|
||||||
return <Loader key='auth-loader' />;
|
return <Loader key='auth-loader' />;
|
||||||
}
|
}
|
||||||
if (user) {
|
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 { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import {
|
import {
|
||||||
deleteLibraryItem,
|
deleteLibraryItem,
|
||||||
getAdminLibrary,
|
getAdminLibrary,
|
||||||
|
@ -24,8 +25,6 @@ import { RSFormLoader } from '@/models/RSFormLoader';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
import { useAuth } from './AuthContext';
|
|
||||||
|
|
||||||
interface ILibraryContext {
|
interface ILibraryContext {
|
||||||
items: ILibraryItem[];
|
items: ILibraryItem[];
|
||||||
templates: ILibraryItem[];
|
templates: ILibraryItem[];
|
||||||
|
@ -62,7 +61,7 @@ export const useLibrary = (): ILibraryContext => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LibraryState = ({ children }: React.PropsWithChildren) => {
|
export const LibraryState = ({ children }: React.PropsWithChildren) => {
|
||||||
const { user, loading: userLoading } = useAuth();
|
const { user, isLoading: userLoading } = useAuth();
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
|
|
||||||
const [items, setItems] = useState<ILibraryItem[]>([]);
|
const [items, setItems] = useState<ILibraryItem[]>([]);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import {
|
import {
|
||||||
patchLibraryItem,
|
patchLibraryItem,
|
||||||
patchSetAccessPolicy,
|
patchSetAccessPolicy,
|
||||||
|
@ -38,7 +39,6 @@ import {
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
import { useAuth } from './AuthContext';
|
|
||||||
import { useGlobalOss } from './GlobalOssContext';
|
import { useGlobalOss } from './GlobalOssContext';
|
||||||
import { useLibrary } from './LibraryContext';
|
import { useLibrary } from './LibraryContext';
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DataCallback } from '@/backend/apiTransport';
|
import { DataCallback } from '@/backend/apiTransport';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import {
|
import {
|
||||||
patchLibraryItem,
|
patchLibraryItem,
|
||||||
patchSetAccessPolicy,
|
patchSetAccessPolicy,
|
||||||
|
@ -50,7 +51,6 @@ import {
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
import { contextOutsideScope } from '@/utils/labels';
|
||||||
|
|
||||||
import { useAuth } from './AuthContext';
|
|
||||||
import { useGlobalOss } from './GlobalOssContext';
|
import { useGlobalOss } from './GlobalOssContext';
|
||||||
import { useLibrary } from './LibraryContext';
|
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 clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import SelectLocationContext from '@/components/select/SelectLocationContext';
|
import SelectLocationContext from '@/components/select/SelectLocationContext';
|
||||||
import SelectLocationHead from '@/components/select/SelectLocationHead';
|
import SelectLocationHead from '@/components/select/SelectLocationHead';
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { LocationHead } from '@/models/library';
|
import { LocationHead } from '@/models/library';
|
||||||
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
import { combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
|
||||||
import SelectLocationContext from '@/components/select/SelectLocationContext';
|
import SelectLocationContext from '@/components/select/SelectLocationContext';
|
||||||
|
@ -15,7 +16,6 @@ import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
||||||
|
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { getOssDetails } from '@/backend/oss';
|
import { getOssDetails } from '@/backend/oss';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
||||||
import { OssLoader } from '@/models/OssLoader';
|
import { OssLoader } from '@/models/OssLoader';
|
||||||
|
|
||||||
function useOssDetails({ target }: { target?: string }) {
|
function useOssDetails({ target }: { target?: string }) {
|
||||||
const { loading: userLoading } = useAuth();
|
const { isLoading: userLoading } = useAuth();
|
||||||
const library = useLibrary();
|
const library = useLibrary();
|
||||||
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
|
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(target != undefined);
|
const [loading, setLoading] = useState(target != undefined);
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { getRSFormDetails } from '@/backend/rsforms';
|
import { getRSFormDetails } from '@/backend/rsforms';
|
||||||
import { type ErrorData } from '@/components/info/InfoError';
|
import { type ErrorData } from '@/components/info/InfoError';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { IRSForm, IRSFormData } from '@/models/rsform';
|
import { IRSForm, IRSFormData } from '@/models/rsform';
|
||||||
import { RSFormLoader } from '@/models/RSFormLoader';
|
import { RSFormLoader } from '@/models/RSFormLoader';
|
||||||
|
|
||||||
function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
|
function useRSFormDetails({ target, version }: { target?: string; version?: string }) {
|
||||||
const { loading: userLoading } = useAuth();
|
const { isLoading: userLoading } = useAuth();
|
||||||
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
|
const [schema, setInnerSchema] = useState<IRSForm | undefined>(undefined);
|
||||||
const [loading, setLoading] = useState(target != undefined);
|
const [loading, setLoading] = useState(target != undefined);
|
||||||
const [error, setError] = useState<ErrorData>(undefined);
|
const [error, setError] = useState<ErrorData>(undefined);
|
||||||
|
|
|
@ -26,35 +26,9 @@ export interface IUser {
|
||||||
* Represents CurrentUser information.
|
* Represents CurrentUser information.
|
||||||
*/
|
*/
|
||||||
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
|
||||||
subscriptions: LibraryItemID[];
|
|
||||||
editor: 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.
|
* Represents signup data, used to create new users.
|
||||||
*/
|
*/
|
||||||
|
@ -63,11 +37,6 @@ export interface IUserSignupData extends Omit<IUser, 'is_staff' | 'id'> {
|
||||||
password2: string;
|
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}.
|
* 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'> {}
|
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}.
|
* Represents target {@link User}.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
import { IconDownload } from '@/components/Icons';
|
import { IconDownload } from '@/components/Icons';
|
||||||
import InfoError from '@/components/info/InfoError';
|
import InfoError from '@/components/info/InfoError';
|
||||||
|
@ -19,7 +20,6 @@ import Overlay from '@/components/ui/Overlay';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import Loader from '@/components/ui/Loader';
|
import Loader from '@/components/ui/Loader';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user, loading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading) {
|
if (!isLoading) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.replace(urls.manuals);
|
router.replace(urls.manuals);
|
||||||
|
@ -22,7 +22,7 @@ function HomePage() {
|
||||||
}, PARAMETER.refreshTimeout);
|
}, PARAMETER.refreshTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [router, user, loading]);
|
}, [router, user, isLoading]);
|
||||||
|
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { SubfoldersIcon } from '@/components/DomainIcons';
|
import { SubfoldersIcon } from '@/components/DomainIcons';
|
||||||
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
import { IconFolderEdit, IconFolderTree } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import SelectLocation from '@/components/select/SelectLocation';
|
import SelectLocation from '@/components/select/SelectLocation';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
import { FolderNode, FolderTree } from '@/models/FolderTree';
|
||||||
|
|
|
@ -5,15 +5,15 @@ import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
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 InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
|
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
import { IUserLoginData } from '@/models/user';
|
|
||||||
import { resources } from '@/utils/constants';
|
import { resources } from '@/utils/constants';
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
|
@ -21,23 +21,20 @@ function LoginPage() {
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const userQuery = query.get('username');
|
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 [username, setUsername] = useState(userQuery || '');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined);
|
reset();
|
||||||
}, [username, password, setError]);
|
}, [username, password, reset]);
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!loading) {
|
if (!isPending) {
|
||||||
const data: IUserLoginData = {
|
login(username, password, () => {
|
||||||
username: username,
|
|
||||||
password: password
|
|
||||||
};
|
|
||||||
login(data, () => {
|
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
router.back();
|
router.back();
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,7 +75,7 @@ function LoginPage() {
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Войти'
|
text='Войти'
|
||||||
className='self-center w-[12rem] mt-3'
|
className='self-center w-[12rem] mt-3'
|
||||||
loading={loading}
|
loading={isPending}
|
||||||
disabled={!username || !password}
|
disabled={!username || !password}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-col text-sm'>
|
<div className='flex flex-col text-sm'>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import {
|
import {
|
||||||
IconAdmin,
|
IconAdmin,
|
||||||
IconAlert,
|
IconAlert,
|
||||||
|
@ -19,7 +20,6 @@ import Button from '@/components/ui/Button';
|
||||||
import Divider from '@/components/ui/Divider';
|
import Divider from '@/components/ui/Divider';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
import DropdownButton from '@/components/ui/DropdownButton';
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useOSS } from '@/context/OssContext';
|
import { useOSS } from '@/context/OssContext';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useOSS } from '@/context/OssContext';
|
import { useOSS } from '@/context/OssContext';
|
||||||
|
|
|
@ -7,12 +7,12 @@ import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import Loader from '@/components/ui/Loader';
|
import Loader from '@/components/ui/Loader';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useLibrary } from '@/context/LibraryContext';
|
import { useLibrary } from '@/context/LibraryContext';
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useOSS } from '@/context/OssContext';
|
import { useOSS } from '@/context/OssContext';
|
||||||
|
@ -46,7 +46,7 @@ function OssTabs() {
|
||||||
useBlockNavigation(
|
useBlockNavigation(
|
||||||
isModified &&
|
isModified &&
|
||||||
schema !== undefined &&
|
schema !== undefined &&
|
||||||
user !== undefined &&
|
!!user &&
|
||||||
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
|
(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 { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
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 InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import DataLoader from '@/components/wrap/DataLoader';
|
import DataLoader from '@/components/wrap/DataLoader';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
import { IPasswordTokenData, IResetPasswordData } from '@/models/user';
|
|
||||||
|
|
||||||
function PasswordChangePage() {
|
function PasswordChangePage() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const token = useQueryStrings().get('token');
|
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 [isTokenValid, setIsTokenValid] = useState(false);
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
@ -31,8 +31,8 @@ function PasswordChangePage() {
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!loading) {
|
if (!isPending) {
|
||||||
const data: IResetPasswordData = {
|
const data: IResetPasswordDTO = {
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
token: token!
|
token: token!
|
||||||
};
|
};
|
||||||
|
@ -44,21 +44,18 @@ function PasswordChangePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined);
|
reset();
|
||||||
}, [newPassword, newPasswordRepeat, setError]);
|
}, [newPassword, newPasswordRepeat, reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data: IPasswordTokenData = {
|
const data: IPasswordTokenDTO = {
|
||||||
token: token ?? ''
|
token: token ?? ''
|
||||||
};
|
};
|
||||||
validateToken(data, () => setIsTokenValid(true));
|
validateToken(data, () => setIsTokenValid(true));
|
||||||
}, [token, validateToken]);
|
}, [token, validateToken]);
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <ProcessError error={error} />;
|
|
||||||
}
|
|
||||||
return (
|
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}>
|
<form className={clsx('cc-fade-in cc-column', 'w-[24rem] mx-auto', 'px-6 mt-3')} onSubmit={handleSubmit}>
|
||||||
<TextInput
|
<TextInput
|
||||||
id='new_password'
|
id='new_password'
|
||||||
|
@ -88,7 +85,7 @@ function PasswordChangePage() {
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Установить пароль'
|
text='Установить пароль'
|
||||||
className='self-center w-[12rem] mt-3'
|
className='self-center w-[12rem] mt-3'
|
||||||
loading={loading}
|
loading={isPending}
|
||||||
disabled={!canSubmit}
|
disabled={!canSubmit}
|
||||||
/>
|
/>
|
||||||
{error ? <ProcessError error={error} /> : null}
|
{error ? <ProcessError error={error} /> : null}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import {
|
import {
|
||||||
IconAdmin,
|
IconAdmin,
|
||||||
IconAlert,
|
IconAlert,
|
||||||
|
@ -31,7 +32,6 @@ import Button from '@/components/ui/Button';
|
||||||
import Divider from '@/components/ui/Divider';
|
import Divider from '@/components/ui/Divider';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
import DropdownButton from '@/components/ui/DropdownButton';
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useGlobalOss } from '@/context/GlobalOssContext';
|
import { useGlobalOss } from '@/context/GlobalOssContext';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useState }
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useSignup } from '@/backend/users/useSignup';
|
||||||
import { IconHelp } from '@/components/Icons';
|
import { IconHelp } from '@/components/Icons';
|
||||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import Button from '@/components/ui/Button';
|
import Button from '@/components/ui/Button';
|
||||||
|
@ -17,15 +18,15 @@ import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import Tooltip from '@/components/ui/Tooltip';
|
import Tooltip from '@/components/ui/Tooltip';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IUserSignupData } from '@/models/user';
|
import { IUserSignupData } from '@/models/user';
|
||||||
import { globals, patterns } from '@/utils/constants';
|
import { globals, patterns } from '@/utils/constants';
|
||||||
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
function FormSignup() {
|
function FormSignup() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { signup, loading, error, setError } = useAuth();
|
const { signup, isPending, error, reset } = useSignup();
|
||||||
|
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
@ -40,8 +41,8 @@ function FormSignup() {
|
||||||
const isValid = acceptPrivacy && acceptRules && !!email && !!username;
|
const isValid = acceptPrivacy && acceptRules && !!email && !!username;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined);
|
reset();
|
||||||
}, [username, email, password, password2, setError]);
|
}, [username, email, password, password2, reset]);
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
if (router.canBack()) {
|
if (router.canBack()) {
|
||||||
|
@ -53,7 +54,7 @@ function FormSignup() {
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!loading) {
|
if (!isPending) {
|
||||||
const data: IUserSignupData = {
|
const data: IUserSignupData = {
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
|
@ -64,7 +65,7 @@ function FormSignup() {
|
||||||
};
|
};
|
||||||
signup(data, createdUser => {
|
signup(data, createdUser => {
|
||||||
router.push(urls.login_hint(createdUser.username));
|
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>
|
||||||
|
|
||||||
<div className='flex justify-around my-3'>
|
<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()} />
|
<Button text='Назад' className='min-w-[10rem]' onClick={() => handleCancel()} />
|
||||||
</div>
|
</div>
|
||||||
{error ? <ProcessError error={error} /> : null}
|
{error ? <ProcessError error={error} /> : null}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import Loader from '@/components/ui/Loader';
|
import Loader from '@/components/ui/Loader';
|
||||||
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
|
import ExpectedAnonymous from '@/components/wrap/ExpectedAnonymous';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
|
|
||||||
import FormSignup from './FormSignup';
|
import FormSignup from './FormSignup';
|
||||||
|
|
||||||
function RegisterPage() {
|
function RegisterPage() {
|
||||||
const { user, loading } = useAuth();
|
const { user, isLoading } = useAuth();
|
||||||
|
|
||||||
if (loading) {
|
if (isLoading) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
|
@ -4,32 +4,28 @@ import axios from 'axios';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useRequestPasswordReset } from '@/backend/auth/useRequestPasswordReset';
|
||||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { IRequestPasswordData } from '@/models/user';
|
|
||||||
|
|
||||||
function RestorePasswordPage() {
|
function RestorePasswordPage() {
|
||||||
const { requestPasswordReset, loading, error, setError } = useAuth();
|
const { requestPasswordReset, isPending, error, reset } = useRequestPasswordReset();
|
||||||
|
|
||||||
const [isCompleted, setIsCompleted] = useState(false);
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!loading) {
|
if (!isPending) {
|
||||||
const data: IRequestPasswordData = {
|
requestPasswordReset({ email: email }, () => setIsCompleted(true));
|
||||||
email: email
|
|
||||||
};
|
|
||||||
requestPasswordReset(data, () => setIsCompleted(true));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined);
|
reset();
|
||||||
}, [email, setError]);
|
}, [email, reset]);
|
||||||
|
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
return (
|
return (
|
||||||
|
@ -55,7 +51,7 @@ function RestorePasswordPage() {
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Запросить пароль'
|
text='Запросить пароль'
|
||||||
className='self-center w-[12rem] mt-3'
|
className='self-center w-[12rem] mt-3'
|
||||||
loading={loading}
|
loading={isPending}
|
||||||
disabled={!email}
|
disabled={!email}
|
||||||
/>
|
/>
|
||||||
{error ? <ProcessError error={error} /> : null}
|
{error ? <ProcessError error={error} /> : null}
|
||||||
|
|
|
@ -6,18 +6,18 @@ import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { urls } from '@/app/urls';
|
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 InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import FlexColumn from '@/components/ui/FlexColumn';
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useAuth } from '@/context/AuthContext';
|
|
||||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||||
import { IUserUpdatePassword } from '@/models/user';
|
|
||||||
import { errors, information } from '@/utils/labels';
|
import { errors, information } from '@/utils/labels';
|
||||||
|
|
||||||
function EditorPassword() {
|
function EditorPassword() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { updatePassword, error, setError, loading } = useAuth();
|
const { changePassword, isPending, error, reset } = useChangePassword();
|
||||||
|
|
||||||
const [oldPassword, setOldPassword] = useState('');
|
const [oldPassword, setOldPassword] = useState('');
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
@ -34,19 +34,19 @@ function EditorPassword() {
|
||||||
toast.error(errors.passwordsMismatch);
|
toast.error(errors.passwordsMismatch);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: IUserUpdatePassword = {
|
const data: IChangePasswordDTO = {
|
||||||
old_password: oldPassword,
|
old_password: oldPassword,
|
||||||
new_password: newPassword
|
new_password: newPassword
|
||||||
};
|
};
|
||||||
updatePassword(data, () => {
|
changePassword(data, () => {
|
||||||
toast.success(information.changesSaved);
|
toast.success(information.changesSaved);
|
||||||
router.push(urls.login);
|
router.push(urls.login);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError(undefined);
|
reset();
|
||||||
}, [newPassword, oldPassword, newPasswordRepeat, setError]);
|
}, [newPassword, oldPassword, newPasswordRepeat, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
@ -89,7 +89,7 @@ function EditorPassword() {
|
||||||
/>
|
/>
|
||||||
{error ? <ProcessError error={error} /> : null}
|
{error ? <ProcessError error={error} /> : null}
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
<SubmitButton text='Сменить пароль' className='self-center' disabled={!canSubmit} loading={loading} />
|
<SubmitButton text='Сменить пароль' className='self-center' disabled={!canSubmit} loading={isPending} />
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,45 +4,44 @@ import axios from 'axios';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
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 InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useBlockNavigation } from '@/context/NavigationContext';
|
import { useBlockNavigation } from '@/context/NavigationContext';
|
||||||
import { useUserProfile } from '@/context/UserProfileContext';
|
|
||||||
import { IUserUpdateData } from '@/models/user';
|
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
function EditorProfile() {
|
function EditorProfile() {
|
||||||
const { updateUser, user, errorProcessing, processing } = useUserProfile();
|
const { profile } = useProfileSuspense();
|
||||||
|
const { updateProfile, isPending, error } = useUpdateProfile();
|
||||||
|
|
||||||
const [username, setUsername] = useState(user?.username ?? '');
|
const [username, setUsername] = useState(profile.username);
|
||||||
const [email, setEmail] = useState(user?.email ?? '');
|
const [email, setEmail] = useState(profile.email);
|
||||||
const [first_name, setFirstName] = useState(user?.first_name ?? '');
|
const [first_name, setFirstName] = useState(profile.first_name);
|
||||||
const [last_name, setLastName] = useState(user?.last_name ?? '');
|
const [last_name, setLastName] = useState(profile.last_name);
|
||||||
|
|
||||||
const isModified =
|
const isModified = profile.email !== email || profile.first_name !== first_name || profile.last_name !== last_name;
|
||||||
user != undefined && (user.email !== email || user.first_name !== first_name || user.last_name !== last_name);
|
|
||||||
|
|
||||||
useBlockNavigation(isModified);
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
setUsername(profile.username);
|
||||||
setUsername(user.username);
|
setEmail(profile.email);
|
||||||
setEmail(user.email);
|
setFirstName(profile.first_name);
|
||||||
setFirstName(user.first_name);
|
setLastName(profile.last_name);
|
||||||
setLastName(user.last_name);
|
}, [profile]);
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const data: IUserUpdateData = {
|
const data: IUpdateProfileDTO = {
|
||||||
username: username,
|
username: username,
|
||||||
email: email,
|
email: email,
|
||||||
first_name: first_name,
|
first_name: first_name,
|
||||||
last_name: last_name
|
last_name: last_name
|
||||||
};
|
};
|
||||||
updateUser(data, () => toast.success(information.changesSaved));
|
updateProfile(data, () => toast.success(information.changesSaved));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -79,11 +78,11 @@ function EditorProfile() {
|
||||||
value={email}
|
value={email}
|
||||||
onChange={event => setEmail(event.target.value)}
|
onChange={event => setEmail(event.target.value)}
|
||||||
/>
|
/>
|
||||||
{errorProcessing ? <ProcessError error={errorProcessing} /> : null}
|
{error ? <ProcessError error={error} /> : null}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
className='self-center mt-6'
|
className='self-center mt-6'
|
||||||
text='Сохранить данные'
|
text='Сохранить данные'
|
||||||
loading={processing}
|
loading={isPending}
|
||||||
disabled={!isModified || email == ''}
|
disabled={!isModified || email == ''}
|
||||||
/>
|
/>
|
||||||
</form>
|
</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 { Suspense } from 'react';
|
||||||
import { UserProfileState } from '@/context/UserProfileContext';
|
|
||||||
|
|
||||||
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() {
|
function UserProfilePage() {
|
||||||
return (
|
return (
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<UserProfileState>
|
<Suspense fallback={<Loader />}>
|
||||||
<UserContents />
|
<div className='cc-fade-in flex flex-col py-2 mx-auto w-fit'>
|
||||||
</UserProfileState>
|
<h1 className='mb-2 select-none'>Учетные данные пользователя</h1>
|
||||||
|
<div className='flex py-2'>
|
||||||
|
<EditorProfile />
|
||||||
|
<EditorPassword />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -958,8 +958,9 @@ export const information = {
|
||||||
noDataToExport: 'Нет данных для экспорта',
|
noDataToExport: 'Нет данных для экспорта',
|
||||||
substitutionsCorrect: 'Таблица отождествлений прошла проверку',
|
substitutionsCorrect: 'Таблица отождествлений прошла проверку',
|
||||||
|
|
||||||
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
|
||||||
newLibraryItem: 'Схема успешно создана',
|
newLibraryItem: 'Схема успешно создана',
|
||||||
|
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
|
||||||
|
newUser: (username: string) => `Пользователь успешно создан: ${username}`,
|
||||||
newVersion: (version: string) => `Версия создана: ${version}`,
|
newVersion: (version: string) => `Версия создана: ${version}`,
|
||||||
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
|
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
|
||||||
newOperation: (alias: string) => `Операция добавлена: ${alias}`,
|
newOperation: (alias: string) => `Операция добавлена: ${alias}`,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user