F: Implement react-query for backend interactions

This commit is contained in:
Ivan 2025-01-27 15:03:48 +03:00
parent ec358cae2b
commit 711ed28a43
203 changed files with 4174 additions and 5050 deletions

View File

@ -46,6 +46,8 @@ This readme file is used mostly to document project dependencies and conventions
- html-to-image - html-to-image
- zustand - zustand
- @tanstack/react-table - @tanstack/react-table
- @tanstack/react-query
- @tanstack/react-query-devtools
- @uiw/react-codemirror - @uiw/react-codemirror
- @uiw/codemirror-themes - @uiw/codemirror-themes
- @lezer/lr - @lezer/lr

View File

@ -10,6 +10,8 @@
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.64.1",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"@uiw/codemirror-themes": "^4.23.7", "@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7", "@uiw/react-codemirror": "^4.23.7",
@ -2994,6 +2996,59 @@
"@sinonjs/commons": "^3.0.0" "@sinonjs/commons": "^3.0.0"
} }
}, },
"node_modules/@tanstack/query-core": {
"version": "5.64.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz",
"integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-devtools": {
"version": "5.62.16",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.62.16.tgz",
"integrity": "sha512-3ff6UBJr0H3nIhfLSl9911rvKqXf0u4B58jl0uYdDWLqPk9pCvYIbxC35cGxK2+8INl4IaFVUHb/IdgWrNkg3Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.64.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz",
"integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.64.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-query-devtools": {
"version": "5.64.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.64.1.tgz",
"integrity": "sha512-8ajcGE3wXYlb4KuJnkFYkJwJKc/qmPNTpQD7YTgLRMBPTGGp1xk7VMzxL87DoXuweO8luplUUblJJ3noVs/luQ==",
"license": "MIT",
"dependencies": {
"@tanstack/query-devtools": "5.62.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.64.1",
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-table": { "node_modules/@tanstack/react-table": {
"version": "8.20.6", "version": "8.20.6",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",

View File

@ -14,6 +14,8 @@
"dependencies": { "dependencies": {
"@dagrejs/dagre": "^1.1.4", "@dagrejs/dagre": "^1.1.4",
"@lezer/lr": "^1.4.2", "@lezer/lr": "^1.4.2",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.64.1",
"@tanstack/react-table": "^8.20.6", "@tanstack/react-table": "^8.20.6",
"@uiw/codemirror-themes": "^4.23.7", "@uiw/codemirror-themes": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7", "@uiw/react-codemirror": "^4.23.7",

View File

@ -5,7 +5,7 @@ import ConceptToaster from '@/app/ConceptToaster';
import Footer from '@/app/Footer'; import Footer from '@/app/Footer';
import Navigation from '@/app/Navigation'; import Navigation from '@/app/Navigation';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { NavigationState } from '@/context/NavigationContext'; import { NavigationState } from '@/app/Navigation/NavigationContext';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -19,6 +19,9 @@ function ApplicationLayout() {
const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation); const noNavigationAnimation = useAppLayoutStore(state => state.noNavigationAnimation);
const noNavigation = useAppLayoutStore(state => state.noNavigation); const noNavigation = useAppLayoutStore(state => state.noNavigation);
const noFooter = useAppLayoutStore(state => state.noFooter); const noFooter = useAppLayoutStore(state => state.noFooter);
// TODO: prefetch data
return ( return (
<NavigationState> <NavigationState>
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'> <div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema';
import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgChangeLocation from '@/pages/OssPage/DlgChangeLocation';
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem'; import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
import DlgCreateCst from '@/dialogs/DlgCreateCst'; import DlgCreateCst from '@/dialogs/DlgCreateCst';
import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation';

View File

@ -1,12 +1,11 @@
'use client'; 'use client';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { AuthState } from '@/context/AuthContext'; import { queryClient } from '@/backend/queryClient';
import { GlobalOssState } from '@/context/GlobalOssContext';
import { LibraryState } from '@/context/LibraryContext';
import { UsersState } from '@/context/UsersContext';
import ErrorFallback from './ErrorFallback'; import ErrorFallback from './ErrorFallback';
@ -30,18 +29,12 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
onError={logError} onError={logError}
> >
<IntlProvider locale='ru' defaultLocale='ru'> <IntlProvider locale='ru' defaultLocale='ru'>
<UsersState> <QueryClientProvider client={queryClient}>
<AuthState>
<LibraryState>
<GlobalOssState>
<ReactQueryDevtools initialIsOpen={false} />
{children} {children}
</GlobalOssState> </QueryClientProvider>
</LibraryState>
</AuthState>
</UsersState>
</IntlProvider> </IntlProvider>
</ErrorBoundary>); </ErrorBoundary>);
} }

View File

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons'; import { IconLibrary2, IconManuals, IconNewItem2 } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useAppLayoutStore } from '@/stores/appLayout'; import { useAppLayoutStore } from '@/stores/appLayout';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
@ -27,15 +27,16 @@ function Navigation() {
return ( return (
<nav <nav
className={clsx( className={clsx(
'z-navigation', // prettier: split lines 'z-navigation', //
'sticky top-0 left-0 right-0', 'sticky top-0 left-0 right-0',
'select-none' 'select-none',
'bg-prim-100'
)} )}
> >
<ToggleNavigation /> <ToggleNavigation />
<div <div
className={clsx( className={clsx(
'pl-2 pr-[1.5rem] sm:pr-[0.9rem] h-[3rem]', // prettier: split lines 'pl-2 pr-[1.5rem] sm:pr-[0.9rem] h-[3rem]', //
'flex', 'flex',
'cc-shadow-border' 'cc-shadow-border'
)} )}
@ -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>

View File

@ -29,7 +29,7 @@ function NavigationButton({
data-tooltip-hidden={hideTitle} data-tooltip-hidden={hideTitle}
onClick={onClick} onClick={onClick}
className={clsx( className={clsx(
'mr-1 h-full', // prettier: split lines 'mr-1 h-full', //
'flex items-center gap-1', 'flex items-center gap-1',
'clr-btn-nav cc-animate-color duration-500', 'clr-btn-nav cc-animate-color duration-500',
'rounded-xl', 'rounded-xl',

View File

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

View File

@ -1,3 +1,5 @@
import { useAuth } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout';
import { import {
IconAdmin, IconAdmin,
IconAdminOff, IconAdminOff,
@ -15,8 +17,7 @@ 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 '@/app/Navigation/NavigationContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls'; import { urls } from '../urls';
@ -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);

View File

@ -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 '@/app/Navigation/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>
); );
} }

View File

@ -7,7 +7,7 @@ import { toast } from 'react-toastify';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import { extractErrorMessage } from '@/utils/utils'; import { extractErrorMessage } from '@/utils/utils';
import { axiosInstance } from './apiConfiguration'; import { axiosInstance } from './axiosInstance';
// ================ Data transfer types ================ // ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void; export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
@ -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);
}); });
} }

View File

@ -0,0 +1,71 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { ICurrentUser } from '@/models/user';
/**
* 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 {
email: string;
}
/**
* Represents password reset data.
*/
export interface IResetPasswordDTO {
password: string;
token: string;
}
/**
* Represents password token data.
*/
export interface IPasswordTokenDTO {
token: string;
}
/**
* Authentication API.
*/
export const authApi = {
baseKey: 'auth',
getAuthQueryOptions: () => {
return queryOptions({
queryKey: [authApi.baseKey, 'user'],
staleTime: DELAYS.staleLong,
queryFn: meta =>
axiosInstance
.get<ICurrentUser>('/users/api/auth', {
signal: meta.signal
})
.then(response => (response.data.id === null ? null : response.data)),
placeholderData: null
});
},
logout: () => axiosInstance.post('/users/api/logout'),
login: (data: IUserLoginDTO) => axiosInstance.post('/users/api/login', data),
changePassword: (data: IChangePasswordDTO) => axiosInstance.post('/users/api/change-password', data),
requestPasswordReset: (data: IRequestPasswordDTO) => axiosInstance.post('/users/api/password-reset', data),
validatePasswordToken: (data: IPasswordTokenDTO) => axiosInstance.post('/users/api/password-reset/validate', data),
resetPassword: (data: IResetPasswordDTO) => axiosInstance.post('/users/api/password-reset/confirm', data)
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +0,0 @@
/**
* Endpoints: cctext.
*/
import { ILexemeData, ITextRequest, ITextResult, IWordFormPlain } from '@/models/language';
import { AxiosPost, FrontExchange } from './apiTransport';
export function postInflectText(request: FrontExchange<IWordFormPlain, ITextResult>) {
AxiosPost({
endpoint: `/api/cctext/inflect`,
request: request
});
}
export function postParseText(request: FrontExchange<ITextRequest, ITextResult>) {
AxiosPost({
endpoint: `/api/cctext/parse`,
request: request
});
}
export function postGenerateLexeme(request: FrontExchange<ITextRequest, ILexemeData>) {
AxiosPost({
endpoint: `/api/cctext/generate-lexeme`,
request: request
});
}

View File

@ -0,0 +1,26 @@
import { axiosInstance } from '@/backend/axiosInstance';
import { ILexemeData, IWordFormPlain } from '@/models/language';
/**
* Represents API result for text output.
*/
export interface ITextResult {
result: string;
}
export const cctextApi = {
baseKey: 'cctext',
inflectText: (data: IWordFormPlain) =>
axiosInstance //
.post<ITextResult>('/api/cctext/inflect', data)
.then(response => response.data),
parseText: (data: { text: string }) =>
axiosInstance //
.post<ITextResult>('/api/cctext/parse', data)
.then(response => response.data),
generateLexeme: (data: { text: string }) =>
axiosInstance //
.post<ILexemeData>('/api/cctext/generate-lexeme', data)
.then(response => response.data)
};

View File

@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query';
import { ILexemeData } from '@/models/language';
import { DataCallback } from '../apiTransport';
import { cctextApi } from './api';
export const useGenerateLexeme = () => {
const mutation = useMutation({
mutationKey: [cctextApi.baseKey, 'generate-lexeme'],
mutationFn: cctextApi.generateLexeme
});
return {
generateLexeme: (
data: { text: string }, //
onSuccess?: DataCallback<ILexemeData>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query';
import { IWordFormPlain } from '@/models/language';
import { DataCallback } from '../apiTransport';
import { cctextApi, ITextResult } from './api';
export const useInflectText = () => {
const mutation = useMutation({
mutationKey: [cctextApi.baseKey, 'inflect-text'],
mutationFn: cctextApi.inflectText
});
return {
inflectText: (
data: IWordFormPlain, //
onSuccess?: DataCallback<ITextResult>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,8 @@
import { useIsMutating } from '@tanstack/react-query';
import { cctextApi } from './api';
export const useIsProcessingCctext = () => {
const countMutations = useIsMutating({ mutationKey: [cctextApi.baseKey] });
return countMutations !== 0;
};

View File

@ -0,0 +1,17 @@
import { useMutation } from '@tanstack/react-query';
import { DataCallback } from '../apiTransport';
import { cctextApi, ITextResult } from './api';
export const useParseText = () => {
const mutation = useMutation({
mutationKey: [cctextApi.baseKey, 'parse-text'],
mutationFn: cctextApi.parseText
});
return {
parseText: (
data: { text: string }, //
onSuccess?: DataCallback<ITextResult>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,9 @@
/** Timing constants for API requests. */
export const DELAYS = {
garbageCollection: 1 * 60 * 60 * 1000,
staleDefault: 5 * 60 * 1000,
staleShort: 5 * 60 * 1000,
staleMedium: 1 * 60 * 60 * 1000,
staleLong: 24 * 60 * 60 * 1000
};

View File

@ -1,117 +0,0 @@
/**
* Endpoints: library.
*/
import {
ILibraryCreateData,
ILibraryItem,
ILibraryUpdateData,
IRenameLocationData,
ITargetAccessPolicy,
ITargetLocation,
IVersionCreateData
} from '@/models/library';
import { IRSFormCloneData, IRSFormData, IVersionCreatedResponse } from '@/models/rsform';
import { ITargetUser, ITargetUsers } from '@/models/user';
import {
AxiosDelete,
AxiosGet,
AxiosPatch,
AxiosPost,
FrontAction,
FrontExchange,
FrontPull,
FrontPush
} from './apiTransport';
export function getLibrary(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/active',
request: request
});
}
export function getAdminLibrary(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/all',
request: request
});
}
export function getTemplates(request: FrontPull<ILibraryItem[]>) {
AxiosGet({
endpoint: '/api/library/templates',
request: request
});
}
export function postCreateLibraryItem(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/library',
request: request
});
}
export function postCloneLibraryItem(target: string, request: FrontExchange<IRSFormCloneData, IRSFormData>) {
AxiosPost({
endpoint: `/api/library/${target}/clone`,
request: request
});
}
export function patchLibraryItem(target: string, request: FrontExchange<ILibraryUpdateData, ILibraryItem>) {
AxiosPatch({
endpoint: `/api/library/${target}`,
request: request
});
}
export function deleteLibraryItem(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/library/${target}`,
request: request
});
}
export function patchSetOwner(target: string, request: FrontPush<ITargetUser>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-owner`,
request: request
});
}
export function patchSetAccessPolicy(target: string, request: FrontPush<ITargetAccessPolicy>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-access-policy`,
request: request
});
}
export function patchSetLocation(target: string, request: FrontPush<ITargetLocation>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-location`,
request: request
});
}
export function patchRenameLocation(request: FrontPush<IRenameLocationData>) {
AxiosPatch({
endpoint: `/api/library/rename-location`,
request: request
});
}
export function patchSetEditors(target: string, request: FrontPush<ITargetUsers>) {
AxiosPatch({
endpoint: `/api/library/${target}/set-editors`,
request: request
});
}
export function postCreateVersion(target: string, request: FrontExchange<IVersionCreateData, IVersionCreatedResponse>) {
AxiosPost({
endpoint: `/api/library/${target}/create-version`,
request: request
});
}

View File

@ -0,0 +1,145 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
import { ConstituentaID, IRSFormData } from '@/models/rsform';
import { UserID } from '@/models/user';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
/**
* Represents update data for renaming Location.
*/
export interface IRenameLocationDTO {
target: string;
new_location: string;
}
/**
* Represents data, used for cloning {@link IRSForm}.
*/
export interface IRSFormCloneDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'owner'> {
items?: ConstituentaID[];
}
/**
* Represents data, used for creating {@link IRSForm}.
*/
export interface ILibraryCreateDTO extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'id' | 'owner'> {
file?: File;
fileName?: string;
}
/**
* Represents update data for editing {@link ILibraryItem}.
*/
export interface ILibraryUpdateDTO
extends Omit<ILibraryItem, 'time_create' | 'time_update' | 'access_policy' | 'location' | 'owner'> {}
/**
* Create version metadata in persistent storage.
*/
export interface IVersionCreateDTO {
version: string;
description: string;
items?: ConstituentaID[];
}
/**
* Represents data response when creating {@link IVersionInfo}.
*/
export interface IVersionCreatedResponse {
version: number;
schema: IRSFormData;
}
export const libraryApi = {
baseKey: 'library',
libraryListKey: ['library', 'list'],
getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) =>
queryOptions({
queryKey: libraryApi.libraryListKey,
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosInstance
.get<ILibraryItem[]>(isAdmin ? '/api/library/all' : '/api/library/active', {
signal: meta.signal
})
.then(response => response.data)
}),
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
return itemType === LibraryItemType.RSFORM
? rsformsApi.getRSFormQueryOptions({ itemID })
: ossApi.getOssQueryOptions({ itemID });
},
getTemplatesQueryOptions: () =>
queryOptions({
queryKey: [libraryApi.baseKey, 'templates'],
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosInstance
.get<ILibraryItem[]>('/api/library/templates', {
signal: meta.signal
})
.then(response => response.data)
}),
createItem: (data: ILibraryCreateDTO) =>
data.file
? axiosInstance
.post<ILibraryItem>('/api/rsforms/create-detailed', data, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => response.data)
: axiosInstance //
.post<ILibraryItem>('/api/library', data)
.then(response => response.data),
updateItem: (data: ILibraryUpdateDTO) =>
axiosInstance //
.patch<ILibraryItem>(`/api/library/${data.id}`, data)
.then(response => response.data),
setOwner: (data: { itemID: LibraryItemID; owner: UserID }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-owner`, { user: data.owner }),
setLocation: (data: { itemID: LibraryItemID; location: string }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-location`, { location: data.location }),
setAccessPolicy: (data: { itemID: LibraryItemID; policy: AccessPolicy }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-access-policy`, { access_policy: data.policy }),
setEditors: (data: { itemID: LibraryItemID; editors: UserID[] }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-editors`, { users: data.editors }),
deleteItem: (target: LibraryItemID) =>
axiosInstance //
.delete(`/api/library/${target}`),
cloneItem: (data: IRSFormCloneDTO) =>
axiosInstance //
.post<IRSFormData>(`/api/library/${data.id}/clone`, data)
.then(response => response.data),
renameLocation: (data: IRenameLocationDTO) =>
axiosInstance //
.patch('/api/library/rename-location', data),
versionCreate: (data: { itemID: LibraryItemID; data: IVersionData }) =>
axiosInstance //
.post<IVersionCreatedResponse>(`/api/library/${data.itemID}/versions`, data.data)
.then(response => response.data),
versionRestore: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
axiosInstance //
.patch<IRSFormData>(`/api/versions/${data.versionID}/restore`)
.then(response => response.data),
versionUpdate: (data: { itemID: LibraryItemID; versionID: VersionID; data: IVersionData }) =>
axiosInstance //
.patch(`/api/versions/${data.versionID}`, data.data),
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
axiosInstance //
.delete(`/api/versions/${data.versionID}`)
};

View File

@ -0,0 +1,47 @@
import { useAuth } from '@/backend/auth/useAuth';
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous';
import { useLibrary } from './useLibrary';
export function useApplyLibraryFilter(filter: ILibraryFilter) {
const { items } = useLibrary();
const { user } = useAuth();
let result = items;
if (!filter.folderMode && filter.head) {
result = result.filter(item => item.location.startsWith(filter.head!));
}
if (filter.folderMode && filter.location) {
if (filter.subfolders) {
result = result.filter(
item => item.location == filter.location || item.location.startsWith(filter.location! + '/')
);
} else {
result = result.filter(item => item.location == filter.location);
}
}
if (filter.type) {
result = result.filter(item => item.item_type === filter.type);
}
if (filter.isVisible !== undefined) {
result = result.filter(item => filter.isVisible === item.visible);
}
if (filter.isOwned !== undefined) {
result = result.filter(item => filter.isOwned === (item.owner === user?.id));
}
if (filter.isEditor !== undefined) {
result = result.filter(item => filter.isEditor == user?.editor.includes(item.id));
}
if (filter.filterUser !== undefined) {
result = result.filter(item => filter.filterUser === item.owner);
}
if (!filter.folderMode && filter.path) {
result = result.filter(item => matchLibraryItemLocation(item, filter.path!));
}
if (filter.query) {
result = result.filter(item => matchLibraryItem(item, filter.query!));
}
return { filtered: result };
}

View File

@ -0,0 +1,21 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { IRSFormData } from '@/models/rsform';
import { IRSFormCloneDTO, libraryApi } from './api';
export const useCloneItem = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'clone-item'],
mutationFn: libraryApi.cloneItem,
onSuccess: async () => await client.invalidateQueries({ queryKey: [libraryApi.baseKey] })
});
return {
cloneItem: (
data: IRSFormCloneDTO, //
onSuccess?: DataCallback<IRSFormData>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { ILibraryItem } from '@/models/library';
import { ILibraryCreateDTO, libraryApi } from './api';
export const useCreateItem = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'create-item'],
mutationFn: libraryApi.createItem,
onSuccess: () => client.invalidateQueries({ queryKey: [libraryApi.baseKey] })
});
return {
createItem: (
data: ILibraryCreateDTO, //
onSuccess?: DataCallback<ILibraryItem>
) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset
};
};

View File

@ -0,0 +1,26 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { libraryApi } from './api';
export const useDeleteItem = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'delete-item'],
mutationFn: libraryApi.deleteItem,
onSuccess: async (_, variables) => {
await client.cancelQueries({ queryKey: [libraryApi.libraryListKey] });
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.filter(item => item.id !== variables)
);
}
});
return {
deleteItem: (
target: LibraryItemID, //
onSuccess?: () => void
) => mutation.mutate(target, { onSuccess }),
isPending: mutation.isPending
};
};

View File

@ -0,0 +1,17 @@
import { FolderTree } from '@/models/FolderTree';
import { LocationHead } from '@/models/library';
import { useLibrary } from './useLibrary';
export function useFolders() {
const { items } = useLibrary();
const result = new FolderTree();
result.addPath(LocationHead.USER, 0);
result.addPath(LocationHead.COMMON, 0);
result.addPath(LocationHead.LIBRARY, 0);
result.addPath(LocationHead.PROJECTS, 0);
items.forEach(item => result.addPath(item.location));
return { folders: result };
}

View File

@ -0,0 +1,12 @@
import { useIsMutating } from '@tanstack/react-query';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
import { libraryApi } from './api';
export const useIsProcessingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
return countMutations + countOss + countRSForm !== 0;
};

View File

@ -0,0 +1,21 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { libraryApi } from './api';
export function useLibrarySuspense() {
const { user } = useAuthSuspense();
const { data: items } = useSuspenseQuery({
...libraryApi.getLibraryQueryOptions({ isAdmin: user?.is_staff ?? false })
});
return { items };
}
export function useLibrary() {
// NOTE: Using suspense here to avoid duplicated library data requests
const { user } = useAuthSuspense();
const { data: items, isLoading } = useQuery({
...libraryApi.getLibraryQueryOptions({ isAdmin: user?.is_staff ?? false })
});
return { items: items ?? [], isLoading };
}

View File

@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';
import { ILibraryItemVersioned, LibraryItemID, LibraryItemType } from '@/models/library';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
export function useLibraryItem({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) {
const { data: rsForm } = useQuery({
...rsformsApi.getRSFormQueryOptions({ itemID }),
enabled: itemType === LibraryItemType.RSFORM
});
const { data: oss } = useQuery({
...ossApi.getOssQueryOptions({ itemID }),
enabled: itemType === LibraryItemType.OSS
});
return {
item:
itemType === LibraryItemType.RSFORM
? (rsForm as ILibraryItemVersioned | undefined)
: (oss as ILibraryItemVersioned | undefined)
};
}

View File

@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IRenameLocationDTO, libraryApi } from './api';
export const useRenameLocation = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'rename-location'],
mutationFn: libraryApi.renameLocation,
onSuccess: () => client.invalidateQueries({ queryKey: [libraryApi.baseKey] })
});
return {
renameLocation: (
data: IRenameLocationDTO, //
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api';
import { AccessPolicy, ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { libraryApi } from './api';
export const useSetAccessPolicy = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setAccessPolicy,
onSuccess: (_, variables) => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item))
);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => {
if (!prev) {
return undefined;
}
if (prev.item_type === LibraryItemType.OSS) {
client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }).catch(console.error);
}
return {
...prev,
access_policy: variables.policy
};
});
}
});
return {
setAccessPolicy: (
data: {
itemID: LibraryItemID; //
policy: AccessPolicy;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,39 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { UserID } from '@/models/user';
import { libraryApi } from './api';
export const useSetEditors = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setEditors,
onSuccess: (_, variables) => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, editors: variables.editors } : item))
);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => {
if (!prev) {
return undefined;
}
return {
...prev,
editors: variables.editors
};
});
}
});
return {
setEditors: (
data: {
itemID: LibraryItemID; //
editors: UserID[];
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,41 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { libraryApi } from './api';
export const useSetLocation = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setLocation,
onSuccess: (_, variables) => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => {
if (!prev) {
return undefined;
}
if (prev.item_type === LibraryItemType.OSS) {
client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }).catch(console.error);
}
return {
...prev,
location: variables.location
};
});
}
});
return {
setLocation: (
data: {
itemID: LibraryItemID; //
location: string;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,42 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { UserID } from '@/models/user';
import { libraryApi } from './api';
export const useSetOwner = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'set-owner'],
mutationFn: libraryApi.setOwner,
onSuccess: (_, variables) => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item))
);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev => {
if (!prev) {
return undefined;
}
if (prev.item_type === LibraryItemType.OSS) {
client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }).catch(console.error);
}
return {
...prev,
owner: variables.owner
};
});
}
});
return {
setOwner: (
data: {
itemID: LibraryItemID; //
owner: UserID;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,17 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { libraryApi } from './api';
export function useTemplatesSuspense() {
const { data: templates } = useSuspenseQuery({
...libraryApi.getTemplatesQueryOptions()
});
return { templates };
}
export function useTemplates() {
const { data: templates } = useQuery({
...libraryApi.getTemplatesQueryOptions()
});
return { templates: templates ?? [] };
}

View File

@ -0,0 +1,34 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem } from '@/models/library';
import { ILibraryUpdateDTO, libraryApi } from './api';
export const useUpdateItem = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'update-item'],
mutationFn: libraryApi.updateItem,
onSuccess: (data: ILibraryItem) => {
client
.cancelQueries({ queryKey: [libraryApi.libraryListKey] })
.then(async () => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item))
);
await client.invalidateQueries({
queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey]
});
})
.catch(console.error);
}
});
return {
updateItem: (
data: ILibraryUpdateDTO, //
onSuccess?: DataCallback<ILibraryItem>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,17 @@
import { useQueryClient } from '@tanstack/react-query';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { libraryApi } from './api';
export function useUpdateTimestamp() {
const client = useQueryClient();
return {
updateTimestamp: (target: LibraryItemID) =>
client.setQueryData(
libraryApi.libraryListKey, //
(prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === target ? { ...item, time_update: Date() } : item))
)
};
}

View File

@ -0,0 +1,30 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { rsformsApi } from '@/backend/rsform/api';
import { IVersionData, LibraryItemID } from '@/models/library';
import { libraryApi } from './api';
export const useVersionCreate = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'create-version'],
mutationFn: libraryApi.versionCreate,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema);
updateTimestamp(data.schema.id);
}
});
return {
versionCreate: (
data: {
itemID: LibraryItemID; //
data: IVersionData;
},
onSuccess?: DataCallback<IVersionData>
) => mutation.mutate(data, { onSuccess: () => onSuccess?.(data.data) })
};
};

View File

@ -0,0 +1,33 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID, VersionID } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
import { libraryApi } from './api';
export const useVersionDelete = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'delete-version'],
mutationFn: libraryApi.versionDelete,
onSuccess: (_, variables) => {
client.setQueryData(
[rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey],
(prev: IRSFormData) => ({
...prev,
versions: prev.versions.filter(version => version.id !== variables.versionID)
})
);
}
});
return {
versionDelete: (
data: {
itemID: LibraryItemID; //
versionID: VersionID;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,29 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID, VersionID } from '@/models/library';
import { libraryApi } from './api';
export const useVersionRestore = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'restore-version'],
mutationFn: libraryApi.versionRestore,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
}
});
return {
versionRestore: (
data: {
itemID: LibraryItemID; //
versionID: VersionID;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,38 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api';
import { IVersionData, LibraryItemID, VersionID } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
import { libraryApi } from './api';
export const useVersionUpdate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'update-version'],
mutationFn: libraryApi.versionUpdate,
onSuccess: (_, variables) => {
client.setQueryData(
[rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey],
(prev: IRSFormData) => ({
...prev,
versions: prev.versions.map(version =>
version.id === variables.versionID
? { ...version, description: variables.data.description, version: variables.data.version }
: version
)
})
);
}
});
return {
versionUpdate: (
data: {
itemID: LibraryItemID; //
versionID: VersionID;
data: IVersionData;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -1,92 +0,0 @@
/**
* Endpoints: oss.
*/
import {
ICstRelocateData,
IInputCreatedResponse,
IOperationCreateData,
IOperationCreatedResponse,
IOperationDeleteData,
IOperationSchemaData,
IOperationSetInputData,
IOperationUpdateData,
IPositionsData,
ITargetOperation
} from '@/models/oss';
import { IConstituentaReference, ITargetCst } from '@/models/rsform';
import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull, FrontPush } from './apiTransport';
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
AxiosGet({
endpoint: `/api/oss/${target}/details`,
request: request
});
}
export function patchUpdatePositions(oss: string, request: FrontPush<IPositionsData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/update-positions`,
request: request
});
}
export function postCreateOperation(
oss: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) {
AxiosPost({
endpoint: `/api/oss/${oss}/create-operation`,
request: request
});
}
export function patchDeleteOperation(oss: string, request: FrontExchange<IOperationDeleteData, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/delete-operation`,
request: request
});
}
export function patchCreateInput(oss: string, request: FrontExchange<ITargetOperation, IInputCreatedResponse>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/create-input`,
request: request
});
}
export function patchSetInput(oss: string, request: FrontExchange<IOperationSetInputData, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/set-input`,
request: request
});
}
export function patchUpdateOperation(oss: string, request: FrontExchange<IOperationUpdateData, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/update-operation`,
request: request
});
}
export function postExecuteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPost({
endpoint: `/api/oss/${oss}/execute-operation`,
request: request
});
}
export function postRelocateConstituents(request: FrontPush<ICstRelocateData>) {
AxiosPost({
endpoint: `/api/oss/relocate-constituents`,
request: request
});
}
export function postFindPredecessor(request: FrontExchange<ITargetCst, IConstituentaReference>) {
AxiosPost({
endpoint: `/api/oss/get-predecessor`,
request: request
});
}

View File

@ -0,0 +1,148 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import {
ICstSubstitute,
IOperationData,
IOperationPosition,
IOperationSchemaData,
OperationID,
OperationType
} from '@/models/oss';
import { ConstituentaID, IConstituentaReference, ITargetCst } from '@/models/rsform';
/**
* Represents {@link IOperation} data, used in creation process.
*/
export interface IOperationCreateDTO {
positions: IOperationPosition[];
item_data: {
alias: string;
operation_type: OperationType;
title: string;
comment: string;
position_x: number;
position_y: number;
result: LibraryItemID | null;
};
arguments: OperationID[] | undefined;
create_schema: boolean;
}
/**
* Represents data response when creating {@link IOperation}.
*/
export interface IOperationCreatedResponse {
new_operation: IOperationData;
oss: IOperationSchemaData;
}
/**
* Represents target {@link IOperation}.
*/
export interface ITargetOperation {
positions: IOperationPosition[];
target: OperationID;
}
/**
* Represents {@link IOperation} data, used in destruction process.
*/
export interface IOperationDeleteDTO extends ITargetOperation {
keep_constituents: boolean;
delete_schema: boolean;
}
/**
* Represents data response when creating {@link IRSForm} for Input {@link IOperation}.
*/
export interface IInputCreatedResponse {
new_schema: ILibraryItem;
oss: IOperationSchemaData;
}
/**
* Represents {@link IOperation} data, used in setInput process.
*/
export interface IInputUpdateDTO extends ITargetOperation {
input: LibraryItemID | null;
}
/**
* Represents {@link IOperation} data, used in update process.
*/
export interface IOperationUpdateDTO extends ITargetOperation {
item_data: {
alias: string;
title: string;
comment: string;
};
arguments: OperationID[] | undefined;
substitutions: ICstSubstitute[] | undefined;
}
/**
* Represents data, used relocating {@link IConstituenta}s between {@link ILibraryItem}s.
*/
export interface ICstRelocateDTO {
destination: LibraryItemID;
items: ConstituentaID[];
}
export const ossApi = {
baseKey: 'oss',
getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => {
return queryOptions({
queryKey: [ossApi.baseKey, 'item', itemID],
staleTime: DELAYS.staleShort,
queryFn: meta =>
!itemID
? undefined
: axiosInstance
.get<IOperationSchemaData>(`/api/oss/${itemID}/details`, {
signal: meta.signal
})
.then(response => response.data)
});
},
updatePositions: (data: { itemID: LibraryItemID; positions: IOperationPosition[] }) =>
axiosInstance //
.patch(`/api/oss/${data.itemID}/update-positions`, { positions: data.positions }),
operationCreate: (data: { itemID: LibraryItemID; data: IOperationCreateDTO }) =>
axiosInstance //
.post<IOperationCreatedResponse>(`/api/oss/${data.itemID}/create-operation`, data.data)
.then(response => response.data),
operationDelete: (data: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
axiosInstance //
.patch<IOperationSchemaData>(`/api/oss/${data.itemID}/delete-operation`, data.data)
.then(response => response.data),
inputCreate: (data: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosInstance //
.patch<IInputCreatedResponse>(`/api/oss/${data.itemID}/create-input`, data.data)
.then(response => response.data),
inputUpdate: (data: { itemID: LibraryItemID; data: IInputUpdateDTO }) =>
axiosInstance //
.patch<IOperationSchemaData>(`/api/oss/${data.itemID}/set-input`, data.data)
.then(response => response.data),
operationUpdate: (data: { itemID: LibraryItemID; data: IOperationUpdateDTO }) =>
axiosInstance //
.patch<IOperationSchemaData>(`/api/oss/${data.itemID}/update-operation`, data.data)
.then(response => response.data),
operationExecute: (data: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosInstance //
.post<IOperationSchemaData>(`/api/oss/${data.itemID}/execute-operation`, data.data)
.then(response => response.data),
relocateConstituents: (data: { itemID: LibraryItemID; data: ICstRelocateDTO }) =>
axiosInstance //
.post<IOperationSchemaData>(`/api/oss/${data.itemID}/relocate-constituents`, data.data)
.then(response => response.data),
getPredecessor: (data: ITargetCst) =>
axiosInstance //
.post<IConstituentaReference>(`/api/oss/get-predecessor`, data)
.then(response => response.data)
};

View File

@ -0,0 +1,19 @@
import { useMutation } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { IConstituentaReference, ITargetCst } from '@/models/rsform';
import { ossApi } from './api';
export const useFindPredecessor = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'find-predecessor'],
mutationFn: ossApi.getPredecessor
});
return {
findPredecessor: (
data: ITargetCst, //
onSuccess?: DataCallback<IConstituentaReference>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,28 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { DataCallback } from '../apiTransport';
import { ITargetOperation, ossApi } from './api';
export const useInputCreate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'input-create'],
mutationFn: ossApi.inputCreate,
onSuccess: async data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey], data.oss);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] });
}
});
return {
inputCreate: (
data: {
itemID: LibraryItemID; //
data: ITargetOperation;
},
onSuccess?: DataCallback<ILibraryItem>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_schema) })
};
};

View File

@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { LibraryItemID } from '@/models/library';
import { IInputUpdateDTO, ossApi } from './api';
export const useInputUpdate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'input-update'],
mutationFn: ossApi.inputUpdate,
onSuccess: async data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] });
}
});
return {
inputUpdate: (
data: {
itemID: LibraryItemID; //
data: IInputUpdateDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,11 @@
import { useIsMutating } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { ossApi } from './api';
export const useIsProcessingOss = () => {
const countLibrary = useIsMutating({ mutationKey: [libraryApi.baseKey] });
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
return countLibrary + countOss !== 0;
};

View File

@ -0,0 +1,26 @@
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
import { LibraryItemID } from '@/models/library';
import { OssLoader } from '@/models/OssLoader';
import { ossApi } from './api';
export function useOss({ itemID }: { itemID?: LibraryItemID }) {
const { items: libraryItems, isLoading: libraryLoading } = useLibrary();
const { data, isLoading, error } = useQuery({
...ossApi.getOssQueryOptions({ itemID })
});
const schema = data && !libraryLoading ? new OssLoader(data, libraryItems).produceOSS() : undefined;
return { schema: schema, isLoading: isLoading || libraryLoading, error: error };
}
export function useOssSuspense({ itemID }: { itemID: LibraryItemID }) {
const { items: libraryItems } = useLibrarySuspense();
const { data } = useSuspenseQuery({
...ossApi.getOssQueryOptions({ itemID })
});
const schema = new OssLoader(data!, libraryItems).produceOSS();
return { schema };
}

View File

@ -0,0 +1,30 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IOperationData } from '@/models/oss';
import { IOperationCreateDTO, ossApi } from './api';
export const useOperationCreate = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-create'],
mutationFn: ossApi.operationCreate,
onSuccess: data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey], data.oss);
updateTimestamp(data.oss.id);
}
});
return {
operationCreate: (
data: {
itemID: LibraryItemID; //
data: IOperationCreateDTO;
},
onSuccess?: DataCallback<IOperationData>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_operation) })
};
};

View File

@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { LibraryItemID } from '@/models/library';
import { IOperationDeleteDTO, ossApi } from './api';
export const useOperationDelete = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-delete'],
mutationFn: ossApi.operationDelete,
onSuccess: async data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] });
}
});
return {
operationDelete: (
data: {
itemID: LibraryItemID; //
data: IOperationDeleteDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { LibraryItemID } from '@/models/library';
import { ITargetOperation, ossApi } from './api';
export const useOperationExecute = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-execute'],
mutationFn: ossApi.operationExecute,
onSuccess: async data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] });
}
});
return {
operationExecute: (
data: {
itemID: LibraryItemID; //
data: ITargetOperation;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { LibraryItemID } from '@/models/library';
import { IOperationUpdateDTO, ossApi } from './api';
export const useOperationUpdate = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-update'],
mutationFn: ossApi.operationUpdate,
onSuccess: async data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] });
}
});
return {
operationUpdate: (
data: {
itemID: LibraryItemID; //
data: IOperationUpdateDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { LibraryItemID } from '@/models/library';
import { ICstRelocateDTO, ossApi } from './api';
export const useRelocateConstituents = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'relocate-constituents'],
mutationFn: ossApi.relocateConstituents,
onSuccess: async data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.id }).queryKey], data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] });
}
});
return {
relocateConstituents: (
data: {
itemID: LibraryItemID; //
data: ICstRelocateDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,25 @@
import { useMutation } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IOperationPosition } from '@/models/oss';
import { ossApi } from './api';
export const useUpdatePositions = () => {
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'update-positions'],
mutationFn: ossApi.updatePositions,
onSuccess: (_, variables) => updateTimestamp(variables.itemID)
});
return {
updatePositions: (
data: {
itemID: LibraryItemID; //
positions: IOperationPosition[];
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,23 @@
import { QueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { DELAYS } from './configuration';
declare module '@tanstack/react-query' {
interface Register {
defaultError: AxiosError;
}
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: DELAYS.staleDefault,
gcTime: DELAYS.garbageCollection,
retry: 3,
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true
}
}
});

View File

@ -0,0 +1,191 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { LibraryItemID, VersionID } from '@/models/library';
import { ICstSubstitute, ICstSubstitutions } from '@/models/oss';
import {
ConstituentaID,
CstType,
IConstituentaList,
IConstituentaMeta,
IRSFormData,
ITargetCst,
TermForm
} from '@/models/rsform';
import { IExpressionParse } from '@/models/rslang';
/**
* Represents data, used for uploading {@link IRSForm} as file.
*/
export interface IRSFormUploadDTO {
itemID: LibraryItemID;
load_metadata: boolean;
file: File;
fileName: string;
}
/**
* Represents {@link IConstituenta} data, used in creation process.
*/
export interface ICstCreateDTO {
alias: string;
cst_type: CstType;
definition_raw: string;
term_raw: string;
convention: string;
definition_formal: string;
term_forms: TermForm[];
insert_after: ConstituentaID | null;
}
/**
* Represents data response when creating {@link IConstituenta}.
*/
export interface ICstCreatedResponse {
new_cst: IConstituentaMeta;
schema: IRSFormData;
}
/**
* Represents data, used in updating persistent attributes in {@link IConstituenta}.
*/
export interface ICstUpdateDTO {
target: ConstituentaID;
item_data: {
convention?: string;
definition_formal?: string;
definition_raw?: string;
term_raw?: string;
term_forms?: TermForm[];
};
}
/**
* Represents data, used in renaming {@link IConstituenta}.
*/
export interface ICstRenameDTO {
alias: string;
cst_type: CstType;
target: ConstituentaID;
}
/**
* Represents data, used in ordering a list of {@link IConstituenta}.
*/
export interface ICstMoveDTO {
items: ConstituentaID[];
move_to: number; // Note: 0-base index
}
/**
* Represents data response when creating producing structure of {@link IConstituenta}.
*/
export interface IProduceStructureResponse {
cst_list: ConstituentaID[];
schema: IRSFormData;
}
/**
* Represents input data for inline synthesis.
*/
export interface IInlineSynthesisDTO {
receiver: LibraryItemID;
source: LibraryItemID;
items: ConstituentaID[];
substitutions: ICstSubstitute[];
}
/**
* Represents {@link IConstituenta} data, used for checking expression.
*/
export interface ICheckConstituentaDTO {
alias: string;
cst_type: CstType;
definition_formal: string;
}
export const rsformsApi = {
baseKey: 'rsform',
getRSFormQueryOptions: ({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) => {
return queryOptions({
queryKey: [rsformsApi.baseKey, 'item', itemID, version ?? ''],
staleTime: DELAYS.staleShort,
queryFn: meta =>
!itemID
? undefined
: axiosInstance
.get<IRSFormData>(
version ? `/api/library/${itemID}/versions/${version}` : `/api/rsforms/${itemID}/details`,
{
signal: meta.signal
}
)
.then(response => response.data)
});
},
download: ({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) =>
axiosInstance //
.get<Blob>(version ? `/api/versions/${version}/export-file` : `/api/rsforms/${itemID}/export-trs`, {
responseType: 'blob'
})
.then(response => response.data),
upload: (data: IRSFormUploadDTO) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${data.itemID}/load-trs`, data, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => response.data),
cstCreate: ({ itemID, data }: { itemID: LibraryItemID; data: ICstCreateDTO }) =>
axiosInstance //
.post<ICstCreatedResponse>(`/api/rsforms/${itemID}/create-cst`, data)
.then(response => response.data),
cstUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: ICstUpdateDTO }) =>
axiosInstance //
.patch<IConstituentaMeta>(`/api/rsforms/${itemID}/update-cst`, data)
.then(response => response.data),
cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/delete-multiple-cst`, data)
.then(response => response.data),
cstRename: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRenameDTO }) =>
axiosInstance //
.patch<ICstCreatedResponse>(`/api/rsforms/${itemID}/rename-cst`, data)
.then(response => response.data),
cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/substitute`, data)
.then(response => response.data),
cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/move-cst`, data)
.then(response => response.data),
produceStructure: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetCst }) =>
axiosInstance //
.post<IProduceStructureResponse>(`/api/rsforms/${itemID}/produce-structure`, data)
.then(response => response.data),
inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) =>
axiosInstance //
.post<IRSFormData>(`/api/rsforms/${itemID}/inline-synthesis`, data)
.then(response => response.data),
restoreOrder: (itemID: LibraryItemID) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/restore-order`)
.then(response => response.data),
resetAliases: (itemID: LibraryItemID) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/reset-aliases`)
.then(response => response.data),
checkConstituenta: ({ itemID, data }: { itemID: LibraryItemID; data: ICheckConstituentaDTO }) =>
axiosInstance //
.post<IExpressionParse>(`/api/rsforms/${itemID}/check-constituenta`, data)
.then(response => response.data)
};

View File

@ -0,0 +1,25 @@
import { useMutation } from '@tanstack/react-query';
import { LibraryItemID } from '@/models/library';
import { IExpressionParse } from '@/models/rslang';
import { DataCallback } from '../apiTransport';
import { ICheckConstituentaDTO, rsformsApi } from './api';
export const useCheckConstituenta = () => {
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'check-constituenta'],
mutationFn: rsformsApi.checkConstituenta
});
return {
checkConstituenta: (
data: {
itemID: LibraryItemID; //
data: ICheckConstituentaDTO;
},
onSuccess?: DataCallback<IExpressionParse>
) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error
};
};

View File

@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform';
import { ICstCreateDTO, rsformsApi } from './api';
export const useCstCreate = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'create-cst'],
mutationFn: rsformsApi.cstCreate,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema);
updateTimestamp(data.schema.id);
// TODO: invalidate OSS?
}
});
return {
cstCreate: (
data: {
itemID: LibraryItemID; //
data: ICstCreateDTO;
},
onSuccess?: DataCallback<IConstituentaMeta>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_cst) })
};
};

View File

@ -0,0 +1,30 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IConstituentaList } from '@/models/rsform';
import { rsformsApi } from './api';
export const useCstDelete = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'delete-multiple-cst'],
mutationFn: rsformsApi.cstDelete,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}
});
return {
cstDelete: (
data: {
itemID: LibraryItemID; //
data: IConstituentaList;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,29 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { ICstMoveDTO, rsformsApi } from './api';
export const useCstMove = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'move-cst'],
mutationFn: rsformsApi.cstMove,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}
});
return {
cstMove: (
data: {
itemID: LibraryItemID; //
data: ICstMoveDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform';
import { DataCallback } from '../apiTransport';
import { ICstRenameDTO, rsformsApi } from './api';
export const useCstRename = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'rename-cst'],
mutationFn: rsformsApi.cstRename,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema);
updateTimestamp(data.schema.id);
// TODO: invalidate OSS?
}
});
return {
cstRename: (
data: {
itemID: LibraryItemID; //
data: ICstRenameDTO;
},
onSuccess?: DataCallback<IConstituentaMeta>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_cst) })
};
};

View File

@ -0,0 +1,30 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { ICstSubstitutions } from '@/models/oss';
import { rsformsApi } from './api';
export const useCstSubstitute = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'substitute-cst'],
mutationFn: rsformsApi.cstSubstitute,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}
});
return {
cstSubstitute: (
data: {
itemID: LibraryItemID; //
data: ICstSubstitutions;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,33 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform';
import { ICstUpdateDTO, rsformsApi } from './api';
export const useCstUpdate = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'update-cst'],
mutationFn: rsformsApi.cstUpdate,
onSuccess: async (_, variables) => {
updateTimestamp(variables.itemID);
await client.invalidateQueries({
queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey]
});
// TODO: invalidate OSS?
}
});
return {
cstUpdate: (
data: {
itemID: LibraryItemID; //
data: ICstUpdateDTO;
},
onSuccess?: DataCallback<IConstituentaMeta>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import { LibraryItemID, VersionID } from '@/models/library';
import { rsformsApi } from './api';
export const useDownloadRSForm = () => {
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'download'],
mutationFn: rsformsApi.download
});
return {
download: (
data: {
itemID: LibraryItemID; //
version?: VersionID;
},
onSuccess?: (data: Blob) => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
import { DataCallback } from '../apiTransport';
import { IInlineSynthesisDTO, rsformsApi } from './api';
export const useInlineSynthesis = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'inline-synthesis'],
mutationFn: rsformsApi.inlineSynthesis,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}
});
return {
inlineSynthesis: (
data: {
itemID: LibraryItemID; //
data: IInlineSynthesisDTO;
},
onSuccess?: DataCallback<IRSFormData>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -0,0 +1,11 @@
import { useIsMutating } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from './api';
export const useIsProcessingRSForm = () => {
const countLibrary = useIsMutating({ mutationKey: [libraryApi.baseKey] });
const countRsform = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
return countLibrary + countRsform !== 0;
};

View File

@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { ConstituentaID, ITargetCst } from '@/models/rsform';
import { rsformsApi } from './api';
export const useProduceStructure = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'produce-structure'],
mutationFn: rsformsApi.produceStructure,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey], data.schema);
updateTimestamp(data.schema.id);
// TODO: invalidate OSS?
}
});
return {
produceStructure: (
data: {
itemID: LibraryItemID; //
data: ITargetCst;
},
onSuccess?: DataCallback<ConstituentaID[]>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.cst_list) })
};
};

View File

@ -0,0 +1,34 @@
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { LibraryItemID, VersionID } from '@/models/library';
import { IRSForm, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader';
import { rsformsApi } from './api';
export function useRSForm({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) {
const { data, isLoading, error } = useQuery({
...rsformsApi.getRSFormQueryOptions({ itemID, version })
});
const schema = data ? new RSFormLoader(data).produceRSForm() : undefined;
return { schema, isLoading, error };
}
export function useRSFormSuspense({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) {
const { data } = useSuspenseQuery({
...rsformsApi.getRSFormQueryOptions({ itemID, version })
});
const schema = new RSFormLoader(data!).produceRSForm();
return { schema };
}
export function useRSFormUpdate({ itemID }: { itemID: LibraryItemID }) {
const client = useQueryClient();
const queryKey = [rsformsApi.getRSFormQueryOptions({ itemID }).queryKey];
return {
update: (data: IRSFormData) => client.setQueryData(queryKey, data),
partialUpdate: (data: Partial<IRSForm>) =>
client.setQueryData(queryKey, (prev: IRSForm) => (prev ? { ...prev, ...data } : prev))
};
}

View File

@ -0,0 +1,23 @@
import { useQueries } from '@tanstack/react-query';
import { LibraryItemID } from '@/models/library';
import { RSFormLoader } from '@/models/RSFormLoader';
import { DELAYS } from '../configuration';
import { rsformsApi } from './api';
export function useRSForms(itemIDs: LibraryItemID[]) {
const results = useQueries({
queries: itemIDs.map(itemID => ({
...rsformsApi.getRSFormQueryOptions({ itemID }),
enabled: itemIDs.length > 0,
staleTime: DELAYS.staleShort
}))
});
const schemas = results
.map(result => result.data)
.filter(data => data !== undefined)
.map(data => new RSFormLoader(data).produceRSForm());
return schemas;
}

View File

@ -0,0 +1,26 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { rsformsApi } from './api';
export const useResetAliases = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'reset-aliases'],
mutationFn: rsformsApi.resetAliases,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}
});
return {
resetAliases: (
itemID: LibraryItemID, //
onSuccess?: () => void
) => mutation.mutate(itemID, { onSuccess })
};
};

View File

@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { rsformsApi } from './api';
export const useRestoreOrder = () => {
const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'restore-order'],
mutationFn: rsformsApi.restoreOrder,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
updateTimestamp(data.id);
}
});
return {
restoreOrder: (
itemID: LibraryItemID, //
onSuccess?: () => void
) => mutation.mutate(itemID, { onSuccess })
};
};

View File

@ -0,0 +1,28 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { libraryApi } from '@/backend/library/api';
import { ILibraryItem } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
import { IRSFormUploadDTO, rsformsApi } from './api';
export const useUploadTRS = () => {
const client = useQueryClient();
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'load-trs'],
mutationFn: rsformsApi.upload,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData([libraryApi.libraryListKey], (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item))
);
}
});
return {
upload: (
data: IRSFormUploadDTO, //
onSuccess?: DataCallback<IRSFormData>
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -1,158 +0,0 @@
/**
* Endpoints: rsforms.
*/
import { ILibraryCreateData, ILibraryItem } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import {
ICheckConstituentaData,
IConstituentaList,
IConstituentaMeta,
ICstCreateData,
ICstCreatedResponse,
ICstMovetoData,
ICstRenameData,
ICstUpdateData,
IInlineSynthesisData,
IProduceStructureResponse,
IRSFormData,
IRSFormUploadData,
ITargetCst
} from '@/models/rsform';
import { IExpressionParse } from '@/models/rslang';
import { AxiosGet, AxiosPatch, AxiosPost, FrontExchange, FrontPull } from './apiTransport';
export function postRSFormFromFile(request: FrontExchange<ILibraryCreateData, ILibraryItem>) {
AxiosPost({
endpoint: '/api/rsforms/create-detailed',
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}
export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
if (!version) {
AxiosGet({
endpoint: `/api/rsforms/${target}/details`,
request: request
});
} else {
AxiosGet({
endpoint: `/api/library/${target}/versions/${version}`,
request: request
});
}
}
export function getTRSFile(target: string, version: string, request: FrontPull<Blob>) {
if (!version) {
AxiosGet({
endpoint: `/api/rsforms/${target}/export-trs`,
request: request,
options: { responseType: 'blob' }
});
} else {
AxiosGet({
endpoint: `/api/versions/${version}/export-file`,
request: request,
options: { responseType: 'blob' }
});
}
}
export function postCreateConstituenta(schema: string, request: FrontExchange<ICstCreateData, ICstCreatedResponse>) {
AxiosPost({
endpoint: `/api/rsforms/${schema}/create-cst`,
request: request
});
}
export function patchUpdateConstituenta(schema: string, request: FrontExchange<ICstUpdateData, IConstituentaMeta>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/update-cst`,
request: request
});
}
export function patchDeleteConstituenta(schema: string, request: FrontExchange<IConstituentaList, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/delete-multiple-cst`,
request: request
});
}
export function patchRenameConstituenta(schema: string, request: FrontExchange<ICstRenameData, ICstCreatedResponse>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/rename-cst`,
request: request
});
}
export function patchProduceStructure(schema: string, request: FrontExchange<ITargetCst, IProduceStructureResponse>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/produce-structure`,
request: request
});
}
export function patchSubstituteConstituents(schema: string, request: FrontExchange<ICstSubstituteData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/substitute`,
request: request
});
}
export function patchMoveConstituenta(schema: string, request: FrontExchange<ICstMovetoData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${schema}/move-cst`,
request: request
});
}
export function postCheckConstituenta(
schema: string,
request: FrontExchange<ICheckConstituentaData, IExpressionParse>
) {
AxiosPost({
endpoint: `/api/rsforms/${schema}/check-constituenta`,
request: request
});
}
export function patchResetAliases(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/reset-aliases`,
request: request
});
}
export function patchRestoreOrder(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/restore-order`,
request: request
});
}
export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUploadData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/${target}/load-trs`,
request: request,
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
}
});
}
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
AxiosPatch({
endpoint: `/api/rsforms/inline-synthesis`,
request: request
});
}

View File

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

View File

@ -0,0 +1,39 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
/**
* 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: () =>
queryOptions({
queryKey: [usersApi.baseKey, 'list'],
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosInstance
.get<IUserInfo[]>('/users/api/active-users', {
signal: meta.signal
})
.then(response => response.data)
}),
getProfileQueryOptions: () =>
queryOptions({
queryKey: [usersApi.baseKey, 'profile'],
staleTime: DELAYS.staleShort,
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)
};

View File

@ -0,0 +1,25 @@
import { useUsers } from './useUsers';
export function useLabelUser() {
const { users } = useUsers();
function getUserLabel(userID: number | null): string {
const user = users.find(({ id }) => id === userID);
if (!user || userID === null) {
return userID ? `Аноним ${userID.toString()}` : 'Отсутствует';
}
const hasFirstName = user.first_name !== '';
const hasLastName = user.last_name !== '';
if (hasFirstName || hasLastName) {
if (!hasLastName) {
return user.first_name;
}
if (!hasFirstName) {
return user.last_name;
}
return user.last_name + ' ' + user.first_name;
}
return `Аноним ${userID.toString()}`;
}
return getUserLabel;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +0,0 @@
/**
* Endpoints: versions.
*/
import { IVersionData } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
import { AxiosDelete, AxiosPatch, FrontAction, FrontPull, FrontPush } from './apiTransport';
export function patchVersion(target: string, request: FrontPush<IVersionData>) {
// title: `Version id=${target}`,
AxiosPatch({
endpoint: `/api/versions/${target}`,
request: request
});
}
export function patchRestoreVersion(target: string, request: FrontPull<IRSFormData>) {
AxiosPatch({
endpoint: `/api/versions/${target}/restore`,
request: request
});
}
export function deleteVersion(target: string, request: FrontAction) {
AxiosDelete({
endpoint: `/api/versions/${target}`,
request: request
});
}

View File

@ -47,7 +47,7 @@ interface RSInputProps
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>( const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
( (
{ {
id, // prettier: split lines id, //
label, label,
disabled, disabled,
noTooltip, noTooltip,

View File

@ -16,7 +16,7 @@ function BadgeGrammeme({ grammeme }: BadgeGrammemeProps) {
return ( return (
<div <div
className={clsx( className={clsx(
'min-w-[3rem]', // prettier: split lines 'min-w-[3rem]', //
'px-1', 'px-1',
'border rounded-md', 'border rounded-md',
'text-sm font-medium text-center whitespace-nowrap' 'text-sm font-medium text-center whitespace-nowrap'

View File

@ -19,7 +19,7 @@ function InfoCstStatus({ title }: InfoCstStatusProps) {
<p key={`${prefixes.cst_status_list}${index}`}> <p key={`${prefixes.cst_status_list}${index}`}>
<span <span
className={clsx( className={clsx(
'inline-block', // prettier: split lines 'inline-block', //
'min-w-[7rem]', 'min-w-[7rem]',
'px-1', 'px-1',
'border', 'border',

View File

@ -5,7 +5,7 @@ import { isResponseHtml } from '@/utils/utils';
import PrettyJson from '../ui/PrettyJSON'; import PrettyJson from '../ui/PrettyJSON';
export type ErrorData = string | Error | AxiosError | undefined; export type ErrorData = string | Error | AxiosError | undefined | null;
interface InfoErrorProps { interface InfoErrorProps {
error: ErrorData; error: ErrorData;

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useUsers } from '@/context/UsersContext'; import { useLabelUser } from '@/backend/users/useLabelUser';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { CProps } from '../props'; import { CProps } from '../props';
@ -12,8 +12,7 @@ interface InfoUsersProps extends CProps.Styling {
} }
function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) { function InfoUsers({ items, className, prefix, header, ...restProps }: InfoUsersProps) {
const { getUserLabel } = useUsers(); const getUserLabel = useLabelUser();
return ( return (
<div className={clsx('flex flex-col dense', className)} {...restProps}> <div className={clsx('flex flex-col dense', className)} {...restProps}>
{header ? <h2>{header}</h2> : null} {header ? <h2>{header}</h2> : null}

View File

@ -4,7 +4,6 @@ import { useIntl } from 'react-intl';
import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable'; import DataTable, { createColumnHelper, IConditionalStyle } from '@/components/ui/DataTable';
import SearchBar from '@/components/ui/SearchBar'; import SearchBar from '@/components/ui/SearchBar';
import { useLibrary } from '@/context/LibraryContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { matchLibraryItem } from '@/models/libraryAPI'; import { matchLibraryItem } from '@/models/libraryAPI';
@ -45,8 +44,6 @@ function PickSchema({
...restProps ...restProps
}: PickSchemaProps) { }: PickSchemaProps) {
const intl = useIntl(); const intl = useIntl();
const { folders } = useLibrary();
const [filterText, setFilterText] = useState(initialFilter); const [filterText, setFilterText] = useState(initialFilter);
const [filterLocation, setFilterLocation] = useState(''); const [filterLocation, setFilterLocation] = useState('');
const [filtered, setFiltered] = useState<ILibraryItem[]>([]); const [filtered, setFiltered] = useState<ILibraryItem[]>([]);
@ -128,7 +125,6 @@ function PickSchema({
/> />
<Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modalTooltip mt-0'> <Dropdown isOpen={locationMenu.isOpen} stretchLeft className='w-[20rem] h-[12.5rem] z-modalTooltip mt-0'>
<SelectLocation <SelectLocation
folderTree={folders}
value={filterLocation} value={filterLocation}
prefix={prefixes.folders_list} prefix={prefixes.folders_list}
dense dense

View File

@ -3,7 +3,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FolderNode, FolderTree } from '@/models/FolderTree'; import { useFolders } from '@/backend/library/useFolders';
import { FolderNode } from '@/models/FolderTree';
import { labelFolderNode } from '@/utils/labels'; import { labelFolderNode } from '@/utils/labels';
import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened } from '../Icons'; import { IconFolder, IconFolderClosed, IconFolderEmpty, IconFolderOpened } from '../Icons';
@ -12,15 +13,15 @@ import MiniButton from '../ui/MiniButton';
interface SelectLocationProps extends CProps.Styling { interface SelectLocationProps extends CProps.Styling {
value: string; value: string;
folderTree: FolderTree;
prefix: string; prefix: string;
dense?: boolean; dense?: boolean;
onClick: (event: CProps.EventMouse, target: FolderNode) => void; onClick: (event: CProps.EventMouse, target: FolderNode) => void;
} }
function SelectLocation({ value, folderTree, dense, prefix, onClick, className, style }: SelectLocationProps) { function SelectLocation({ value, dense, prefix, onClick, className, style }: SelectLocationProps) {
const activeNode = folderTree.at(value); const { folders } = useFolders();
const items = folderTree.getTree(); const activeNode = folders.at(value);
const items = folders.getTree();
const [folded, setFolded] = useState<FolderNode[]>(items); const [folded, setFolded] = useState<FolderNode[]>(items);
useEffect(() => { useEffect(() => {

View File

@ -4,7 +4,6 @@ import clsx from 'clsx';
import { useCallback } from 'react'; import { useCallback } from 'react';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { FolderTree } from '@/models/FolderTree';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import { IconFolderTree } from '../Icons'; import { IconFolderTree } from '../Icons';
@ -16,7 +15,6 @@ import SelectLocation from './SelectLocation';
interface SelectLocationContextProps extends CProps.Styling { interface SelectLocationContextProps extends CProps.Styling {
value: string; value: string;
title?: string; title?: string;
folderTree: FolderTree;
stretchTop?: boolean; stretchTop?: boolean;
onChange: (newValue: string) => void; onChange: (newValue: string) => void;
@ -25,7 +23,6 @@ interface SelectLocationContextProps extends CProps.Styling {
function SelectLocationContext({ function SelectLocationContext({
value, value,
title = 'Проводник...', title = 'Проводник...',
folderTree,
onChange, onChange,
className, className,
style style
@ -56,7 +53,6 @@ function SelectLocationContext({
style={style} style={style}
> >
<SelectLocation <SelectLocation
folderTree={folderTree}
value={value} value={value}
prefix={prefixes.folders_list} prefix={prefixes.folders_list}
dense dense

View File

@ -2,17 +2,18 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useUsers } from '@/context/UsersContext'; import { useLabelUser } from '@/backend/users/useLabelUser';
import { IUserInfo, UserID } from '@/models/user'; import { useUsers } from '@/backend/users/useUsers';
import { UserID } from '@/models/user';
import { matchUser } from '@/models/userAPI'; import { matchUser } from '@/models/userAPI';
import { CProps } from '../props'; import { CProps } from '../props';
import SelectSingle from '../ui/SelectSingle'; import SelectSingle from '../ui/SelectSingle';
interface SelectUserProps extends CProps.Styling { interface SelectUserProps extends CProps.Styling {
items?: IUserInfo[];
value?: UserID; value?: UserID;
onSelectValue: (newValue: UserID) => void; onSelectValue: (newValue: UserID) => void;
filter?: (userID: UserID) => boolean;
placeholder?: string; placeholder?: string;
noBorder?: boolean; noBorder?: boolean;
@ -20,20 +21,23 @@ interface SelectUserProps extends CProps.Styling {
function SelectUser({ function SelectUser({
className, className,
items, filter,
value, value,
onSelectValue, onSelectValue,
placeholder = 'Выберите пользователя', placeholder = 'Выберите пользователя',
...restProps ...restProps
}: SelectUserProps) { }: SelectUserProps) {
const { getUserLabel } = useUsers(); const { users } = useUsers();
const getUserLabel = useLabelUser();
const items = filter ? users.filter(user => filter(user.id)) : users;
const options = const options =
items?.map(user => ({ items?.map(user => ({
value: user.id, value: user.id,
label: getUserLabel(user.id) label: getUserLabel(user.id)
})) ?? []; })) ?? [];
function filter(option: { value: UserID | undefined; label: string }, inputValue: string) { function filterLabel(option: { value: UserID | undefined; label: string }, inputValue: string) {
const user = items?.find(item => item.id === option.value); const user = items?.find(item => item.id === option.value);
return !user ? false : matchUser(user, inputValue); return !user ? false : matchUser(user, inputValue);
} }
@ -47,7 +51,7 @@ function SelectUser({
if (data?.value !== undefined) onSelectValue(data.value); if (data?.value !== undefined) onSelectValue(data.value);
}} }}
// @ts-expect-error: TODO: use type definitions from react-select in filter object // @ts-expect-error: TODO: use type definitions from react-select in filter object
filterOption={filter} filterOption={filterLabel}
placeholder={placeholder} placeholder={placeholder}
{...restProps} {...restProps}
/> />

View File

@ -48,7 +48,7 @@ function Checkbox({
<button <button
type='button' type='button'
className={clsx( className={clsx(
'flex items-center gap-2', // prettier: split lines 'flex items-center gap-2', //
'outline-none', 'outline-none',
'focus-frame', 'focus-frame',
cursor, cursor,
@ -64,7 +64,7 @@ function Checkbox({
> >
<div <div
className={clsx( className={clsx(
'max-w-[1rem] min-w-[1rem] h-4', // prettier: split lines 'max-w-[1rem] min-w-[1rem] h-4', //
'pt-[0.1rem] pl-[0.1rem]', 'pt-[0.1rem] pl-[0.1rem]',
'border rounded-sm', 'border rounded-sm',
'cc-animate-color', 'cc-animate-color',

Some files were not shown because too many files have changed in this diff Show More