F: Implement react-query pt3

This commit is contained in:
Ivan 2025-01-23 19:41:31 +03:00
parent 76aee5bea7
commit 6543d88cbe
162 changed files with 2667 additions and 3113 deletions

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';

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

@ -2,24 +2,22 @@
import { createContext, useCallback, useContext, useState } from 'react'; import { createContext, useCallback, useContext, useState } from 'react';
import { useOss, useOssInvalidate, useOssUpdate } from '@/backend/oss/useOSS';
import { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import useOssDetails from '@/hooks/useOssDetails';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { contextOutsideScope } from '@/utils/labels'; import { contextOutsideScope } from '@/utils/labels';
interface IGlobalOssContext { interface IGlobalOssContext {
schema: IOperationSchema | undefined; schema: IOperationSchema | undefined;
setID: (id: string | undefined) => void; setID: (id: LibraryItemID | undefined) => void;
setData: (data: IOperationSchemaData) => void; setData: (data: IOperationSchemaData) => void;
loading: boolean; loading: boolean;
loadingError: ErrorData; loadingError: ErrorData;
isValid: boolean;
invalidate: () => void; invalidate: () => Promise<void>;
invalidateItem: (target: LibraryItemID) => void; invalidateItem: (target: LibraryItemID) => void;
partialUpdate: (data: Partial<IOperationSchema>) => void; partialUpdate: (data: Partial<IOperationSchema>) => void;
reload: (callback?: () => void) => void;
} }
const GlobalOssContext = createContext<IGlobalOssContext | null>(null); const GlobalOssContext = createContext<IGlobalOssContext | null>(null);
@ -32,58 +30,18 @@ export const useGlobalOss = (): IGlobalOssContext => {
}; };
export const GlobalOssState = ({ children }: React.PropsWithChildren) => { export const GlobalOssState = ({ children }: React.PropsWithChildren) => {
const [isValid, setIsValid] = useState(true); const [ossID, setID] = useState<LibraryItemID | undefined>(undefined);
const [ossID, setIdInternal] = useState<string | undefined>(undefined); const { schema: schema, error: loadingError, isLoading: loading } = useOss({ itemID: ossID });
const { const { update, partialUpdate } = useOssUpdate({ itemID: ossID });
schema: schema, // prettier: split lines const { invalidate } = useOssInvalidate({ itemID: ossID });
error: loadingError,
setSchema: setDataInternal,
loading: loading,
reload: reloadInternal,
partialUpdate
} = useOssDetails({ target: ossID });
const reload = useCallback(
(callback?: () => void) => {
reloadInternal(undefined, () => {
setIsValid(true);
callback?.();
});
},
[reloadInternal]
);
const setData = useCallback(
(data: IOperationSchemaData) => {
setDataInternal(data);
setIsValid(true);
},
[setDataInternal]
);
const setID = useCallback(
(id: string | undefined) => {
setIdInternal(prev => {
if (prev === id && !isValid) {
reload();
}
return id;
});
},
[setIdInternal, isValid, reload]
);
const invalidate = useCallback(() => {
setIsValid(false);
}, []);
const invalidateItem = useCallback( const invalidateItem = useCallback(
(target: LibraryItemID) => { (target: LibraryItemID) => {
if (schema?.schemas.includes(target)) { if (schema?.schemas.includes(target)) {
setIsValid(false); invalidate().catch(console.error);
} }
}, },
[schema] [invalidate, schema]
); );
return ( return (
@ -91,12 +49,10 @@ export const GlobalOssState = ({ children }: React.PropsWithChildren) => {
value={{ value={{
schema, schema,
setID, setID,
setData, setData: update,
loading, loading,
loadingError, loadingError,
reload,
partialUpdate, partialUpdate,
isValid,
invalidateItem, invalidateItem,
invalidate invalidate
}} }}

View File

@ -6,8 +6,7 @@ import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl'; import { IntlProvider } from 'react-intl';
import { queryClient } from '@/backend/queryClient'; import { queryClient } from '@/backend/queryClient';
import { GlobalOssState } from '@/context/GlobalOssContext'; import { GlobalOssState } from '@/app/GlobalOssContext';
import { LibraryState } from '@/context/LibraryContext';
import ErrorFallback from './ErrorFallback'; import ErrorFallback from './ErrorFallback';
@ -32,14 +31,12 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
> >
<IntlProvider locale='ru' defaultLocale='ru'> <IntlProvider locale='ru' defaultLocale='ru'>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<LibraryState>
<GlobalOssState> <GlobalOssState>
<ReactQueryDevtools initialIsOpen={false} /> <ReactQueryDevtools initialIsOpen={false} />
{children} {children}
</GlobalOssState> </GlobalOssState>
</LibraryState>
</QueryClientProvider> </QueryClientProvider>
</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,7 +27,7 @@ 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' 'bg-prim-100'
@ -36,7 +36,7 @@ function Navigation() {
<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'
)} )}

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

@ -17,7 +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 { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { urls } from '../urls'; import { urls } from '../urls';

View File

@ -1,7 +1,7 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { urls } from '../urls'; import { urls } from '../urls';

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;

View File

@ -1,8 +1,8 @@
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { ICurrentUser, IUser } from '@/models/user'; import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { axiosInstance } from '../apiConfiguration'; import { ICurrentUser } from '@/models/user';
/** /**
* Represents login data, used to authenticate users. * Represents login data, used to authenticate users.
@ -23,7 +23,9 @@ export interface IChangePasswordDTO {
/** /**
* Represents password reset request data. * Represents password reset request data.
*/ */
export interface IRequestPasswordDTO extends Pick<IUser, 'email'> {} export interface IRequestPasswordDTO {
email: string;
}
/** /**
* Represents password reset data. * Represents password reset data.
@ -36,7 +38,9 @@ export interface IResetPasswordDTO {
/** /**
* Represents password token data. * Represents password token data.
*/ */
export interface IPasswordTokenDTO extends Pick<IResetPasswordDTO, 'token'> {} export interface IPasswordTokenDTO {
token: string;
}
/** /**
* Authentication API. * Authentication API.
@ -47,14 +51,14 @@ export const authApi = {
getAuthQueryOptions: () => { getAuthQueryOptions: () => {
return queryOptions({ return queryOptions({
queryKey: [authApi.baseKey, 'user'], queryKey: [authApi.baseKey, 'user'],
staleTime: DELAYS.staleLong,
queryFn: meta => queryFn: meta =>
axiosInstance axiosInstance
.get<ICurrentUser>('/users/api/auth', { .get<ICurrentUser>('/users/api/auth', {
signal: meta.signal signal: meta.signal
}) })
.then(response => (response.data.id === null ? null : response.data)), .then(response => (response.data.id === null ? null : response.data)),
placeholderData: null, placeholderData: null
staleTime: 24 * 60 * 60 * 1000
}); });
}, },

View File

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

View File

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

View File

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

View File

@ -8,7 +8,10 @@ export const useRequestPasswordReset = () => {
mutationFn: authApi.requestPasswordReset mutationFn: authApi.requestPasswordReset
}); });
return { return {
requestPasswordReset: (data: IRequestPasswordDTO, onSuccess?: () => void) => mutation.mutate(data, { onSuccess }), requestPasswordReset: (
data: IRequestPasswordDTO, //
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending, isPending: mutation.isPending,
error: mutation.error, error: mutation.error,
reset: mutation.reset reset: mutation.reset

View File

@ -3,20 +3,26 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api'; import { authApi, IPasswordTokenDTO, IResetPasswordDTO } from './api';
export const useResetPassword = () => { export const useResetPassword = () => {
const queryClient = useQueryClient(); const client = useQueryClient();
const validateMutation = useMutation({ const validateMutation = useMutation({
mutationKey: ['reset-password'], mutationKey: ['reset-password'],
mutationFn: authApi.validatePasswordToken, mutationFn: authApi.validatePasswordToken,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) onSuccess: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
const resetMutation = useMutation({ const resetMutation = useMutation({
mutationKey: ['reset-password'], mutationKey: ['reset-password'],
mutationFn: authApi.resetPassword, mutationFn: authApi.resetPassword,
onSuccess: () => queryClient.invalidateQueries({ queryKey: [authApi.baseKey] }) onSuccess: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
return { return {
validateToken: (data: IPasswordTokenDTO, onSuccess?: () => void) => validateMutation.mutate(data, { onSuccess }), validateToken: (
resetPassword: (data: IResetPasswordDTO, onSuccess?: () => void) => resetMutation.mutate(data, { onSuccess }), data: IPasswordTokenDTO, //
onSuccess?: () => void
) => validateMutation.mutate(data, { onSuccess }),
resetPassword: (
data: IResetPasswordDTO, //
onSuccess?: () => void
) => resetMutation.mutate(data, { onSuccess }),
isPending: resetMutation.isPending, isPending: resetMutation.isPending,
error: resetMutation.error, error: resetMutation.error,
reset: resetMutation.reset 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,137 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, VersionID } from '@/models/library';
import { ConstituentaID, IRSFormData } from '@/models/rsform';
import { UserID } from '@/models/user';
/**
* 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)
}),
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,8 @@
import { useIsMutating } from '@tanstack/react-query';
import { libraryApi } from './api';
export const useIsProcessingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
return countMutations !== 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,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: 'library',
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,47 @@
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { LibraryItemID } from '@/models/library';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader';
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
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 = data ? new OssLoader(data, libraryItems).produceOSS() : undefined;
return { schema };
}
export function useOssUpdate({ itemID }: { itemID?: LibraryItemID }) {
const { items: libraryItems } = useLibrary();
const client = useQueryClient();
const queryKey = [ossApi.getOssQueryOptions({ itemID }).queryKey];
return {
update: (data: IOperationSchemaData) =>
client.setQueryData(queryKey, new OssLoader(data, libraryItems).produceOSS()),
partialUpdate: (data: Partial<IOperationSchema>) =>
client.setQueryData(queryKey, (prev: IOperationSchema) => (prev ? { ...prev, ...data } : prev))
};
}
export function useOssInvalidate({ itemID }: { itemID?: LibraryItemID }) {
const client = useQueryClient();
const queryKey = [ossApi.getOssQueryOptions({ itemID }).queryKey];
return {
invalidate: () => client.invalidateQueries({ queryKey })
};
}

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 {
cstDelete: (
data: {
itemID: LibraryItemID; //
positions: IOperationPosition[];
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
};
};

View File

@ -1,6 +1,8 @@
import { QueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { DELAYS } from './configuration';
declare module '@tanstack/react-query' { declare module '@tanstack/react-query' {
interface Register { interface Register {
defaultError: AxiosError; defaultError: AxiosError;
@ -10,8 +12,8 @@ declare module '@tanstack/react-query' {
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
staleTime: 5 * 60 * 1000, staleTime: DELAYS.staleDefault,
gcTime: 24 * 60 * 60 * 1000, gcTime: DELAYS.garbageCollection,
retry: 3, retry: 3,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchOnMount: true, refetchOnMount: 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: 'library',
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,26 @@
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,
reset: mutation.reset
};
};

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 = data ? new RSFormLoader(data).produceRSForm() : undefined;
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,9 +1,9 @@
import { queryOptions } from '@tanstack/react-query'; import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user'; import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
import { axiosInstance } from '../apiConfiguration';
/** /**
* Represents user data, intended to update user profile in persistent storage. * Represents user data, intended to update user profile in persistent storage.
*/ */
@ -14,17 +14,18 @@ export const usersApi = {
getUsersQueryOptions: () => getUsersQueryOptions: () =>
queryOptions({ queryOptions({
queryKey: [usersApi.baseKey, 'list'], queryKey: [usersApi.baseKey, 'list'],
staleTime: DELAYS.staleMedium,
queryFn: meta => queryFn: meta =>
axiosInstance axiosInstance
.get<IUserInfo[]>('/users/api/active-users', { .get<IUserInfo[]>('/users/api/active-users', {
signal: meta.signal signal: meta.signal
}) })
.then(response => response.data), .then(response => response.data)
placeholderData: []
}), }),
getProfileQueryOptions: () => getProfileQueryOptions: () =>
queryOptions({ queryOptions({
queryKey: [usersApi.baseKey, 'profile'], queryKey: [usersApi.baseKey, 'profile'],
staleTime: DELAYS.staleShort,
queryFn: meta => queryFn: meta =>
axiosInstance axiosInstance
.get<IUserProfile>('/users/api/profile', { .get<IUserProfile>('/users/api/profile', {
@ -36,5 +37,3 @@ export const usersApi = {
signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data), signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data),
updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data) updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data)
}; };
//DataCallback<IUserProfile>

View File

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

View File

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

View File

@ -4,8 +4,7 @@ import { usersApi } from './api';
export function useUsersSuspense() { export function useUsersSuspense() {
const { data: users } = useSuspenseQuery({ const { data: users } = useSuspenseQuery({
...usersApi.getUsersQueryOptions(), ...usersApi.getUsersQueryOptions()
initialData: []
}); });
return { users }; return { 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

@ -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

@ -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',

View File

@ -49,7 +49,7 @@ function CheckboxTristate({
<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,
@ -65,7 +65,7 @@ function CheckboxTristate({
> >
<div <div
className={clsx( className={clsx(
'w-4 h-4', // prettier: split lines 'w-4 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',

View File

@ -6,7 +6,8 @@ interface PrettyJsonProps {
* Displays JSON data in a formatted string. * Displays JSON data in a formatted string.
*/ */
function PrettyJson({ data }: PrettyJsonProps) { function PrettyJson({ data }: PrettyJsonProps) {
return <pre>{JSON.stringify(data, null, 2)}</pre>; const text = JSON.stringify(data, null, 2);
return <pre>{text === '{}' ? '' : text}</pre>;
} }
export default PrettyJson; export default PrettyJson;

View File

@ -1,7 +1,7 @@
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth'; import { useAuth } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout'; import { useLogout } from '@/backend/auth/useLogout';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import TextURL from '../ui/TextURL'; import TextURL from '../ui/TextURL';

View File

@ -1,322 +0,0 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import {
deleteLibraryItem,
getAdminLibrary,
getLibrary,
getTemplates,
patchRenameLocation,
postCloneLibraryItem,
postCreateLibraryItem
} from '@/backend/library';
import { getRSFormDetails, postRSFormFromFile } from '@/backend/rsforms';
import { ErrorData } from '@/components/info/InfoError';
import { FolderTree } from '@/models/FolderTree';
import { ILibraryItem, IRenameLocationData, LibraryItemID, LocationHead } from '@/models/library';
import { ILibraryCreateData } from '@/models/library';
import { matchLibraryItem, matchLibraryItemLocation } from '@/models/libraryAPI';
import { ILibraryFilter } from '@/models/miscellaneous';
import { IRSForm, IRSFormCloneData, IRSFormData } from '@/models/rsform';
import { RSFormLoader } from '@/models/RSFormLoader';
import { usePreferencesStore } from '@/stores/preferences';
import { contextOutsideScope } from '@/utils/labels';
interface ILibraryContext {
items: ILibraryItem[];
templates: ILibraryItem[];
folders: FolderTree;
loading: boolean;
loadingError: ErrorData;
setLoadingError: (error: ErrorData) => void;
processing: boolean;
processingError: ErrorData;
setProcessingError: (error: ErrorData) => void;
reloadItems: (callback?: () => void) => void;
applyFilter: (params: ILibraryFilter) => ILibraryItem[];
retrieveTemplate: (templateID: LibraryItemID, callback: (schema: IRSForm) => void) => void;
createItem: (data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => void;
cloneItem: (target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => void;
destroyItem: (target: LibraryItemID, callback?: () => void) => void;
renameLocation: (data: IRenameLocationData, callback?: () => void) => void;
localUpdateItem: (data: Partial<ILibraryItem>) => void;
localUpdateTimestamp: (target: LibraryItemID) => void;
}
const LibraryContext = createContext<ILibraryContext | null>(null);
export const useLibrary = (): ILibraryContext => {
const context = useContext(LibraryContext);
if (context === null) {
throw new Error(contextOutsideScope('useLibrary', 'LibraryState'));
}
return context;
};
export const LibraryState = ({ children }: React.PropsWithChildren) => {
const { user, isLoading: userLoading } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode);
const [items, setItems] = useState<ILibraryItem[]>([]);
const [templates, setTemplates] = useState<ILibraryItem[]>([]);
const [loading, setLoading] = useState(true);
const [processing, setProcessing] = useState(false);
const [loadingError, setLoadingError] = useState<ErrorData>(undefined);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const [cachedTemplates, setCachedTemplates] = useState<IRSForm[]>([]);
const folders = useMemo(() => {
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 result;
}, [items]);
const applyFilter = useCallback(
(filter: ILibraryFilter) => {
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 result;
},
[items, user]
);
const retrieveTemplate = useCallback(
(templateID: LibraryItemID, callback: (schema: IRSForm) => void) => {
const cached = cachedTemplates.find(schema => schema.id == templateID);
if (cached) {
callback(cached);
return;
}
setProcessingError(undefined);
getRSFormDetails(String(templateID), '', {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: data => {
const schema = new RSFormLoader(data).produceRSForm();
setCachedTemplates(prev => [...prev, schema]);
callback(schema);
}
});
},
[cachedTemplates]
);
const reloadItems = useCallback(
(callback?: () => void) => {
setItems([]);
setLoadingError(undefined);
if (user?.is_staff && adminMode) {
getAdminLibrary({
setLoading: setLoading,
showError: true,
onError: setLoadingError,
onSuccess: newData => {
setItems(newData);
callback?.();
}
});
} else {
getLibrary({
setLoading: setLoading,
showError: true,
onError: setLoadingError,
onSuccess: newData => {
setItems(newData);
callback?.();
}
});
}
},
[user, adminMode]
);
const reloadTemplates = useCallback(() => {
setTemplates([]);
getTemplates({
setLoading: setProcessing,
onError: setProcessingError,
showError: true,
onSuccess: newData => setTemplates(newData)
});
}, []);
useEffect(() => {
if (!userLoading) {
reloadItems();
}
}, [reloadItems, userLoading]);
useEffect(() => {
reloadTemplates();
}, [reloadTemplates]);
const localUpdateItem = useCallback(
(data: Partial<ILibraryItem>) => {
setItems(prev => prev.map(item => (item.id === data.id ? { ...item, ...data } : item)));
},
[setItems]
);
const localUpdateTimestamp = useCallback(
(target: LibraryItemID) => {
setItems(prev => prev.map(item => (item.id === target ? { ...item, time_update: Date() } : item)));
},
[setItems]
);
const createItem = useCallback(
(data: ILibraryCreateData, callback?: DataCallback<ILibraryItem>) => {
const onSuccess = (newSchema: ILibraryItem) =>
reloadItems(() => {
callback?.(newSchema);
});
setProcessingError(undefined);
if (data.file) {
postRSFormFromFile({
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: onSuccess
});
} else {
postCreateLibraryItem({
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: onSuccess
});
}
},
[reloadItems]
);
const destroyItem = useCallback(
(target: LibraryItemID, callback?: () => void) => {
setProcessingError(undefined);
deleteLibraryItem(String(target), {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () =>
reloadItems(() => {
callback?.();
})
});
},
[reloadItems]
);
const cloneItem = useCallback(
(target: LibraryItemID, data: IRSFormCloneData, callback: DataCallback<IRSFormData>) => {
if (!user) {
return;
}
setProcessingError(undefined);
postCloneLibraryItem(String(target), {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newSchema =>
reloadItems(() => {
callback?.(newSchema);
})
});
},
[reloadItems, user]
);
const renameLocation = useCallback(
(data: IRenameLocationData, callback?: () => void) => {
setProcessingError(undefined);
patchRenameLocation({
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () =>
reloadItems(() => {
callback?.();
})
});
},
[reloadItems]
);
return (
<LibraryContext
value={{
items,
folders,
templates,
loading,
loadingError,
setLoadingError,
reloadItems,
processing,
processingError,
setProcessingError,
applyFilter,
createItem,
cloneItem,
destroyItem,
renameLocation,
retrieveTemplate,
localUpdateItem,
localUpdateTimestamp
}}
>
{children}
</LibraryContext>
);
};

View File

@ -1,390 +0,0 @@
'use client';
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import {
patchLibraryItem,
patchSetAccessPolicy,
patchSetEditors,
patchSetLocation,
patchSetOwner
} from '@/backend/library';
import {
patchCreateInput,
patchDeleteOperation,
patchSetInput,
patchUpdateOperation,
patchUpdatePositions,
postCreateOperation,
postExecuteOperation,
postRelocateConstituents
} from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError';
import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library';
import {
ICstRelocateData,
IOperationCreateData,
IOperationData,
IOperationDeleteData,
IOperationSchema,
IOperationSchemaData,
IOperationSetInputData,
IOperationUpdateData,
IPositionsData,
ITargetOperation
} from '@/models/oss';
import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels';
import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext';
interface IOssContext {
schema?: IOperationSchema;
itemID: string;
loading: boolean;
loadingError: ErrorData;
processing: boolean;
processingError: ErrorData;
isOwned: boolean;
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
setOwner: (newOwner: UserID, callback?: () => void) => void;
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void;
setEditors: (newEditors: UserID[], callback?: () => void) => void;
savePositions: (data: IPositionsData, callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperationData>) => void;
deleteOperation: (data: IOperationDeleteData, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
setInput: (data: IOperationSetInputData, callback?: () => void) => void;
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
executeOperation: (data: ITargetOperation, callback?: () => void) => void;
relocateConstituents: (data: ICstRelocateData, callback?: () => void) => void;
}
const OssContext = createContext<IOssContext | null>(null);
export const useOSS = () => {
const context = useContext(OssContext);
if (context === null) {
throw new Error(contextOutsideScope('useOSS', 'OssState'));
}
return context;
};
interface OssStateProps {
itemID: string;
}
export const OssState = ({ itemID, children }: React.PropsWithChildren<OssStateProps>) => {
const library = useLibrary();
const ossData = useGlobalOss();
const { user } = useAuth();
const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const isOwned = useMemo(() => {
return user?.id === ossData.schema?.owner || false;
}, [user, ossData.schema?.owner]);
useEffect(() => {
ossData.setID(itemID);
}, [itemID, ossData]);
const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchLibraryItem(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
const fullData: IOperationSchemaData = Object.assign(ossData.schema!, newData);
ossData.setData(fullData);
library.localUpdateItem(newData);
callback?.(newData);
}
});
},
[itemID, library, ossData]
);
const setOwner = useCallback(
(newOwner: UserID, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchSetOwner(itemID, {
data: { user: newOwner },
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
ossData.partialUpdate({ owner: newOwner });
library.reloadItems(callback);
}
});
},
[itemID, ossData, library]
);
const setAccessPolicy = useCallback(
(newPolicy: AccessPolicy, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchSetAccessPolicy(itemID, {
data: {
access_policy: newPolicy
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
ossData.partialUpdate({ access_policy: newPolicy });
library.reloadItems(callback);
}
});
},
[itemID, ossData, library]
);
const setLocation = useCallback(
(newLocation: string, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchSetLocation(itemID, {
data: {
location: newLocation
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
ossData.partialUpdate({ location: newLocation });
library.reloadItems(callback);
}
});
},
[itemID, ossData, library]
);
const setEditors = useCallback(
(newEditors: UserID[], callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchSetEditors(itemID, {
data: {
users: newEditors
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
ossData.partialUpdate({ editors: newEditors });
library.reloadItems(callback);
}
});
},
[itemID, ossData, library]
);
const savePositions = useCallback(
(data: IPositionsData, callback?: () => void) => {
setProcessingError(undefined);
patchUpdatePositions(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
library.localUpdateTimestamp(Number(itemID));
callback?.();
}
});
},
[itemID, library]
);
const createOperation = useCallback(
(data: IOperationCreateData, callback?: DataCallback<IOperationData>) => {
setProcessingError(undefined);
postCreateOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
ossData.setData(newData.oss);
library.localUpdateTimestamp(newData.oss.id);
callback?.(newData.new_operation);
}
});
},
[itemID, library, ossData]
);
const deleteOperation = useCallback(
(data: IOperationDeleteData, callback?: () => void) => {
setProcessingError(undefined);
patchDeleteOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
ossData.setData(newData);
library.reloadItems(callback);
}
});
},
[itemID, library, ossData]
);
const createInput = useCallback(
(data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => {
setProcessingError(undefined);
patchCreateInput(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
ossData.setData(newData.oss);
library.reloadItems(() => {
callback?.(newData.new_schema);
});
}
});
},
[itemID, library, ossData]
);
const setInput = useCallback(
(data: IOperationSetInputData, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchSetInput(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
ossData.setData(newData);
library.reloadItems(callback);
}
});
},
[itemID, ossData, library]
);
const updateOperation = useCallback(
(data: IOperationUpdateData, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
patchUpdateOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
ossData.setData(newData);
library.reloadItems(callback);
}
});
},
[itemID, library, ossData]
);
const executeOperation = useCallback(
(data: ITargetOperation, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
postExecuteOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
ossData.setData(newData);
library.reloadItems(callback);
}
});
},
[itemID, library, ossData]
);
const relocateConstituents = useCallback(
(data: ICstRelocateData, callback?: () => void) => {
if (!ossData.schema) {
return;
}
setProcessingError(undefined);
postRelocateConstituents({
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
ossData.reload();
library.reloadItems(callback);
}
});
},
[library, ossData]
);
return (
<OssContext
value={{
schema: ossData.schema,
itemID,
loading: ossData.loading,
loadingError: ossData.loadingError,
processing,
processingError,
isOwned,
update,
setOwner,
setEditors,
setAccessPolicy,
setLocation,
savePositions,
createOperation,
deleteOperation,
createInput,
setInput,
updateOperation,
executeOperation,
relocateConstituents
}}
>
{children}
</OssContext>
);
};

View File

@ -1,592 +0,0 @@
'use client';
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
import { DataCallback } from '@/backend/apiTransport';
import { useAuth } from '@/backend/auth/useAuth';
import {
patchLibraryItem,
patchSetAccessPolicy,
patchSetEditors,
patchSetLocation,
patchSetOwner,
postCreateVersion
} from '@/backend/library';
import { postFindPredecessor } from '@/backend/oss';
import {
getTRSFile,
patchDeleteConstituenta,
patchInlineSynthesis,
patchMoveConstituenta,
patchProduceStructure,
patchRenameConstituenta,
patchResetAliases,
patchRestoreOrder,
patchSubstituteConstituents,
patchUpdateConstituenta,
patchUploadTRS,
postCreateConstituenta
} from '@/backend/rsforms';
import { deleteVersion, patchRestoreVersion, patchVersion } from '@/backend/versions';
import { type ErrorData } from '@/components/info/InfoError';
import useRSFormDetails from '@/hooks/useRSFormDetails';
import { AccessPolicy, ILibraryItem, IVersionData, VersionID } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library';
import { ICstSubstituteData } from '@/models/oss';
import {
ConstituentaID,
IConstituentaList,
IConstituentaMeta,
IConstituentaReference,
ICstCreateData,
ICstMovetoData,
ICstRenameData,
ICstUpdateData,
IInlineSynthesisData,
IRSForm,
IRSFormData,
IRSFormUploadData,
ITargetCst
} from '@/models/rsform';
import { UserID } from '@/models/user';
import { contextOutsideScope } from '@/utils/labels';
import { useGlobalOss } from './GlobalOssContext';
import { useLibrary } from './LibraryContext';
interface IRSFormContext {
schema?: IRSForm;
itemID: string;
versionID?: string;
loading: boolean;
errorLoading: ErrorData;
processing: boolean;
processingError: ErrorData;
isArchive: boolean;
isOwned: boolean;
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
download: (callback: DataCallback<Blob>) => void;
upload: (data: IRSFormUploadData, callback: () => void) => void;
setOwner: (newOwner: UserID, callback?: () => void) => void;
setAccessPolicy: (newPolicy: AccessPolicy, callback?: () => void) => void;
setLocation: (newLocation: string, callback?: () => void) => void;
setEditors: (newEditors: UserID[], callback?: () => void) => void;
resetAliases: (callback: () => void) => void;
restoreOrder: (callback: () => void) => void;
produceStructure: (data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => void;
inlineSynthesis: (data: IInlineSynthesisData, callback?: DataCallback<IRSFormData>) => void;
cstCreate: (data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => void;
cstRename: (data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => void;
cstSubstitute: (data: ICstSubstituteData, callback?: () => void) => void;
cstUpdate: (data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => void;
cstDelete: (data: IConstituentaList, callback?: () => void) => void;
cstMoveTo: (data: ICstMovetoData, callback?: () => void) => void;
findPredecessor: (data: ITargetCst, callback: (reference: IConstituentaReference) => void) => void;
versionCreate: (data: IVersionData, callback?: (version: VersionID) => void) => void;
versionUpdate: (target: VersionID, data: IVersionData, callback?: () => void) => void;
versionDelete: (target: VersionID, callback?: () => void) => void;
versionRestore: (target: string, callback?: () => void) => void;
}
const RSFormContext = createContext<IRSFormContext | null>(null);
export const useRSForm = () => {
const context = useContext(RSFormContext);
if (context === null) {
throw new Error(contextOutsideScope('useRSForm', 'RSFormState'));
}
return context;
};
interface RSFormStateProps {
itemID: string;
versionID?: string;
}
export const RSFormState = ({ itemID, versionID, children }: React.PropsWithChildren<RSFormStateProps>) => {
const library = useLibrary();
const oss = useGlobalOss();
const { user } = useAuth();
const rsData = useRSFormDetails({ target: itemID, version: versionID });
const [processing, setProcessing] = useState(false);
const [processingError, setProcessingError] = useState<ErrorData>(undefined);
const isOwned = useMemo(() => {
return user?.id === rsData.schema?.owner || false;
}, [user, rsData.schema?.owner]);
const isArchive = useMemo(() => !!versionID, [versionID]);
const update = useCallback(
(data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => {
if (!rsData.schema) {
return;
}
setProcessingError(undefined);
patchLibraryItem(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(Object.assign(rsData.schema!, newData));
library.localUpdateItem(newData);
oss.invalidateItem(newData.id);
callback?.(newData);
}
});
},
[itemID, rsData, library, oss]
);
const upload = useCallback(
(data: IRSFormUploadData, callback?: () => void) => {
if (!rsData.schema) {
return;
}
setProcessingError(undefined);
patchUploadTRS(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateItem(newData);
callback?.();
}
});
},
[itemID, rsData, library]
);
const setOwner = useCallback(
(newOwner: UserID, callback?: () => void) => {
setProcessingError(undefined);
patchSetOwner(itemID, {
data: {
user: newOwner
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
rsData.partialUpdate({ owner: newOwner });
library.localUpdateItem({ id: Number(itemID), owner: newOwner });
callback?.();
}
});
},
[itemID, rsData, library]
);
const setAccessPolicy = useCallback(
(newPolicy: AccessPolicy, callback?: () => void) => {
if (!rsData.schema) {
return;
}
setProcessingError(undefined);
patchSetAccessPolicy(itemID, {
data: {
access_policy: newPolicy
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
rsData.partialUpdate({ access_policy: newPolicy });
library.localUpdateItem({ id: Number(itemID), access_policy: newPolicy });
callback?.();
}
});
},
[itemID, rsData, library]
);
const setLocation = useCallback(
(newLocation: string, callback?: () => void) => {
if (!rsData.schema) {
return;
}
setProcessingError(undefined);
patchSetLocation(itemID, {
data: {
location: newLocation
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
rsData.partialUpdate({ location: newLocation });
library.localUpdateItem({ id: Number(itemID), location: newLocation });
callback?.();
}
});
},
[itemID, rsData, library]
);
const setEditors = useCallback(
(newEditors: UserID[], callback?: () => void) => {
if (!rsData.schema) {
return;
}
setProcessingError(undefined);
patchSetEditors(itemID, {
data: {
users: newEditors
},
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
rsData.partialUpdate({ editors: newEditors });
callback?.();
}
});
},
[itemID, rsData]
);
const resetAliases = useCallback(
(callback?: () => void) => {
if (!rsData.schema || !user) {
return;
}
setProcessingError(undefined);
patchResetAliases(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
callback?.();
}
});
},
[itemID, rsData, user, library, oss]
);
const restoreOrder = useCallback(
(callback?: () => void) => {
if (!rsData.schema || !user) {
return;
}
setProcessingError(undefined);
patchRestoreOrder(itemID, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateTimestamp(newData.id);
callback?.();
}
});
},
[itemID, rsData, user, library]
);
const produceStructure = useCallback(
(data: ITargetCst, callback?: DataCallback<ConstituentaID[]>) => {
setProcessingError(undefined);
patchProduceStructure(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id);
callback?.(newData.cst_list);
}
});
},
[rsData, itemID, library, oss]
);
const download = useCallback(
(callback: DataCallback<Blob>) => {
setProcessingError(undefined);
getTRSFile(itemID, String(rsData.schema?.version ?? ''), {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: callback
});
},
[itemID, rsData]
);
const cstCreate = useCallback(
(data: ICstCreateData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined);
postCreateConstituenta(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id);
callback?.(newData.new_cst);
}
});
},
[itemID, rsData, library, oss]
);
const cstDelete = useCallback(
(data: IConstituentaList, callback?: () => void) => {
setProcessingError(undefined);
patchDeleteConstituenta(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
callback?.();
}
});
},
[itemID, rsData, library, oss]
);
const cstUpdate = useCallback(
(data: ICstUpdateData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined);
patchUpdateConstituenta(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData =>
rsData.reload(setProcessing, () => {
library.localUpdateTimestamp(Number(itemID));
oss.invalidateItem(Number(itemID));
callback?.(newData);
})
});
},
[itemID, rsData, library, oss]
);
const cstRename = useCallback(
(data: ICstRenameData, callback?: DataCallback<IConstituentaMeta>) => {
setProcessingError(undefined);
patchRenameConstituenta(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData.schema);
library.localUpdateTimestamp(newData.schema.id);
oss.invalidateItem(newData.schema.id);
callback?.(newData.new_cst);
}
});
},
[rsData, itemID, library, oss]
);
const cstSubstitute = useCallback(
(data: ICstSubstituteData, callback?: () => void) => {
setProcessingError(undefined);
patchSubstituteConstituents(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
callback?.();
}
});
},
[rsData, itemID, library, oss]
);
const cstMoveTo = useCallback(
(data: ICstMovetoData, callback?: () => void) => {
setProcessingError(undefined);
patchMoveConstituenta(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateTimestamp(Number(itemID));
callback?.();
}
});
},
[itemID, rsData, library]
);
const versionCreate = useCallback(
(data: IVersionData, callback?: (version: number) => void) => {
setProcessingError(undefined);
postCreateVersion(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData.schema);
library.localUpdateTimestamp(Number(itemID));
callback?.(newData.version);
}
});
},
[itemID, rsData, library]
);
const findPredecessor = useCallback((data: ITargetCst, callback: (reference: IConstituentaReference) => void) => {
setProcessingError(undefined);
postFindPredecessor({
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: callback
});
}, []);
const versionUpdate = useCallback(
(target: number, data: IVersionData, callback?: () => void) => {
setProcessingError(undefined);
patchVersion(String(target), {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
const newVersions = rsData.schema!.versions.map(prev => {
if (prev.id === target) {
prev.description = data.description;
prev.version = data.version;
return prev;
} else {
return prev;
}
});
rsData.partialUpdate({ versions: newVersions });
callback?.();
}
});
},
[rsData]
);
const versionDelete = useCallback(
(target: number, callback?: () => void) => {
setProcessingError(undefined);
deleteVersion(String(target), {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: () => {
const newVersions = rsData.schema!.versions.filter(prev => prev.id !== target);
rsData.partialUpdate({ versions: newVersions });
callback?.();
}
});
},
[rsData]
);
const versionRestore = useCallback(
(target: string, callback?: () => void) => {
setProcessingError(undefined);
patchRestoreVersion(target, {
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateItem(newData);
callback?.();
}
});
},
[rsData, library]
);
const inlineSynthesis = useCallback(
(data: IInlineSynthesisData, callback?: DataCallback<IRSFormData>) => {
setProcessingError(undefined);
patchInlineSynthesis({
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
rsData.setSchema(newData);
library.localUpdateTimestamp(newData.id);
oss.invalidateItem(newData.id);
callback?.(newData);
}
});
},
[rsData, library, oss]
);
return (
<RSFormContext
value={{
schema: rsData.schema,
itemID,
versionID,
loading: rsData.loading,
errorLoading: rsData.error,
processing,
processingError,
isOwned,
isArchive,
update,
download,
upload,
restoreOrder,
resetAliases,
produceStructure,
inlineSynthesis,
setOwner,
setEditors,
setAccessPolicy,
setLocation,
cstUpdate,
cstCreate,
cstRename,
cstSubstitute,
cstDelete,
cstMoveTo,
findPredecessor,
versionCreate,
versionUpdate,
versionDelete,
versionRestore
}}
>
{children}
</RSFormContext>
);
};

View File

@ -3,12 +3,12 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { useLibrary } from '@/backend/library/useLibrary';
import { IconReset } from '@/components/Icons'; import { IconReset } from '@/components/Icons';
import PickSchema from '@/components/select/PickSchema'; import PickSchema from '@/components/select/PickSchema';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import { useLibrary } from '@/context/LibraryContext';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library';
import { IOperation, IOperationSchema, OperationID } from '@/models/oss'; import { IOperation, IOperationSchema, OperationID } from '@/models/oss';
import { sortItemsForOSS } from '@/models/ossAPI'; import { sortItemsForOSS } from '@/models/ossAPI';
@ -23,8 +23,8 @@ export interface DlgChangeInputSchemaProps {
function DlgChangeInputSchema() { function DlgChangeInputSchema() {
const { oss, target, onSubmit } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps); const { oss, target, onSubmit } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps);
const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined); const [selected, setSelected] = useState<LibraryItemID | undefined>(target.result ?? undefined);
const library = useLibrary(); const { items } = useLibrary();
const sortedItems = sortItemsForOSS(oss, library.items); const sortedItems = sortItemsForOSS(oss, items);
const isValid = target.result !== selected; const isValid = target.result !== selected;
function baseFilter(item: ILibraryItem) { function baseFilter(item: ILibraryItem) {

View File

@ -6,6 +6,8 @@ import { toast } from 'react-toastify';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth'; import { useAuth } from '@/backend/auth/useAuth';
import { IRSFormCloneDTO } from '@/backend/library/api';
import { useCloneItem } from '@/backend/library/useCloneItem';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import SelectAccessPolicy from '@/components/select/SelectAccessPolicy'; import SelectAccessPolicy from '@/components/select/SelectAccessPolicy';
import SelectLocationContext from '@/components/select/SelectLocationContext'; import SelectLocationContext from '@/components/select/SelectLocationContext';
@ -16,11 +18,10 @@ import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { useLibrary } from '@/context/LibraryContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { useConceptNavigation } from '@/context/NavigationContext';
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library'; import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI'; import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
import { ConstituentaID, IRSFormCloneData } from '@/models/rsform'; import { ConstituentaID } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
@ -50,7 +51,7 @@ function DlgCloneLibraryItem() {
const [body, setBody] = useState(initialLocation.substring(3)); const [body, setBody] = useState(initialLocation.substring(3));
const location = combineLocation(head, body); const location = combineLocation(head, body);
const { cloneItem, folders } = useLibrary(); const { cloneItem } = useCloneItem();
const canSubmit = title !== '' && alias !== '' && validateLocation(location); const canSubmit = title !== '' && alias !== '' && validateLocation(location);
@ -60,7 +61,8 @@ function DlgCloneLibraryItem() {
} }
function handleSubmit() { function handleSubmit() {
const data: IRSFormCloneData = { const data: IRSFormCloneDTO = {
id: base.id,
item_type: base.item_type, item_type: base.item_type,
title: title, title: title,
alias: alias, alias: alias,
@ -73,7 +75,7 @@ function DlgCloneLibraryItem() {
if (onlySelected) { if (onlySelected) {
data.items = selected; data.items = selected;
} }
cloneItem(base.id, data, newSchema => { cloneItem(data, newSchema => {
toast.success(information.cloneComplete(newSchema.alias)); toast.success(information.cloneComplete(newSchema.alias));
router.push(urls.schema(newSchema.id)); router.push(urls.schema(newSchema.id));
}); });
@ -128,7 +130,7 @@ function DlgCloneLibraryItem() {
excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []} excluded={!user?.is_staff ? [LocationHead.LIBRARY] : []}
/> />
</div> </div>
<SelectLocationContext folderTree={folders} value={location} onChange={handleSelectLocation} /> <SelectLocationContext value={location} onChange={handleSelectLocation} />
<TextArea <TextArea
id='dlg_cst_body' id='dlg_cst_body'
label='Путь' label='Путь'

View File

@ -2,18 +2,19 @@
import { useState } from 'react'; import { useState } from 'react';
import { ICstCreateDTO } from '@/backend/rsform/api';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import usePartialUpdate from '@/hooks/usePartialUpdate'; import usePartialUpdate from '@/hooks/usePartialUpdate';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, IRSForm } from '@/models/rsform';
import { generateAlias } from '@/models/rsformAPI'; import { generateAlias } from '@/models/rsformAPI';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import FormCreateCst from './FormCreateCst'; import FormCreateCst from './FormCreateCst';
export interface DlgCreateCstProps { export interface DlgCreateCstProps {
initial?: ICstCreateData; initial?: ICstCreateDTO;
schema: IRSForm; schema: IRSForm;
onCreate: (data: ICstCreateData) => void; onCreate: (data: ICstCreateDTO) => void;
} }
function DlgCreateCst() { function DlgCreateCst() {

View File

@ -3,13 +3,14 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { ICstCreateDTO } from '@/backend/rsform/api';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
import SelectSingle from '@/components/ui/SelectSingle'; import SelectSingle from '@/components/ui/SelectSingle';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput'; import TextInput from '@/components/ui/TextInput';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { CstType, ICstCreateData, IRSForm } from '@/models/rsform'; import { CstType, IRSForm } from '@/models/rsform';
import { generateAlias, isBaseSet, isBasicConcept, isFunctional, validateNewAlias } from '@/models/rsformAPI'; import { generateAlias, isBaseSet, isBasicConcept, isFunctional, validateNewAlias } from '@/models/rsformAPI';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { labelCstType } from '@/utils/labels'; import { labelCstType } from '@/utils/labels';
@ -17,9 +18,9 @@ import { SelectorCstType } from '@/utils/selectors';
interface FormCreateCstProps { interface FormCreateCstProps {
schema: IRSForm; schema: IRSForm;
state: ICstCreateData; state: ICstCreateDTO;
partialUpdate: React.Dispatch<Partial<ICstCreateData>>; partialUpdate: React.Dispatch<Partial<ICstCreateDTO>>;
setValidated?: React.Dispatch<React.SetStateAction<boolean>>; setValidated?: React.Dispatch<React.SetStateAction<boolean>>;
} }

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