B: Fix cache invalidation and error handling

This commit is contained in:
Ivan 2025-01-29 14:52:07 +03:00
parent 44b0705521
commit d182b2d34e
68 changed files with 419 additions and 309 deletions

View File

@ -1,16 +1,29 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Outlet } from 'react-router'; import { Outlet } from 'react-router';
import ConceptToaster from '@/app/ConceptToaster'; 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 { NavigationState } from '@/app/Navigation/NavigationContext';
import Loader from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout'; import { useAppLayoutStore, useMainHeight, useViewportHeight } from '@/stores/appLayout';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
import ErrorFallback from './ErrorFallback';
import { GlobalDialogs } from './GlobalDialogs'; import { GlobalDialogs } from './GlobalDialogs';
import { GlobalTooltips } from './GlobalTooltips'; import { GlobalTooltips } from './GlobalTooltips';
import { NavigationState } from './Navigation/NavigationContext';
const resetState = () => {
console.log('Resetting state after error fallback');
};
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
console.log('Error fallback: ' + error.message);
if (info.componentStack) {
console.log('Component stack: ' + info.componentStack);
}
};
function ApplicationLayout() { function ApplicationLayout() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
@ -23,37 +36,39 @@ function ApplicationLayout() {
// TODO: prefetch data // TODO: prefetch data
return ( return (
<NavigationState> <ErrorBoundary FallbackComponent={ErrorFallback} onError={logError} onReset={resetState}>
<div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'> <NavigationState>
<ConceptToaster <div className='min-w-[20rem] antialiased h-full max-w-[120rem] mx-auto'>
className='text-[14px] cc-animate-position' <ConceptToaster
style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }} className='text-[14px] cc-animate-position'
autoClose={3000} style={{ marginTop: noNavigationAnimation ? '1.5rem' : '3.5rem' }}
draggable={false} autoClose={3000}
pauseOnFocusLoss={false} draggable={false}
/> pauseOnFocusLoss={false}
/>
<GlobalDialogs /> <GlobalDialogs />
<GlobalTooltips /> <GlobalTooltips />
<Navigation /> <Navigation />
<div <div
id={globals.main_scroll} id={globals.main_scroll}
className='overflow-x-auto max-w-[100vw]' className='overflow-x-auto max-w-[100vw]'
style={{ style={{
maxHeight: viewportHeight maxHeight: viewportHeight
}} }}
> >
<main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}> <main className='cc-scroll-y' style={{ overflowY: showScroll ? 'scroll' : 'auto', minHeight: mainHeight }}>
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<Outlet /> <Outlet />
</Suspense> </Suspense>
</main> </main>
{!noNavigation && !noFooter ? <Footer /> : null} {!noNavigation && !noFooter ? <Footer /> : null}
</div>
</div> </div>
</div> </NavigationState>
</NavigationState> </ErrorBoundary>
); );
} }

View File

@ -5,8 +5,8 @@ import Button from '@/components/ui/Button';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return ( return (
<div className='flex flex-col gap-3 items-center antialiased' role='alert'> <div className='flex flex-col gap-3 my-3 items-center antialiased' role='alert'>
<h1>Что-то пошло не так!</h1> <h1 className='my-2'>Что-то пошло не так!</h1>
<Button onClick={resetErrorBoundary} text='Попробовать еще раз' /> <Button onClick={resetErrorBoundary} text='Попробовать еще раз' />
<InfoError error={error as Error} /> <InfoError error={error as Error} />
</div> </div>

View File

@ -2,32 +2,13 @@
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
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 ErrorFallback from './ErrorFallback';
const resetState = () => {
console.log('Resetting state after error fallback');
};
const logError = (error: Error, info: { componentStack?: string | null | undefined }) => {
console.log('Error fallback: ' + error.message);
if (info.componentStack) {
console.log('Component stack: ' + info.componentStack);
}
};
// prettier-ignore // prettier-ignore
function GlobalProviders({ children }: React.PropsWithChildren) { function GlobalProviders({ children }: React.PropsWithChildren) {
return ( return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={resetState}
onError={logError}
>
<IntlProvider locale='ru' defaultLocale='ru'> <IntlProvider locale='ru' defaultLocale='ru'>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@ -35,8 +16,7 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
{children} {children}
</QueryClientProvider> </QueryClientProvider>
</IntlProvider> </IntlProvider>);
</ErrorBoundary>);
} }
export default GlobalProviders; export default GlobalProviders;

View File

@ -7,7 +7,7 @@ export const useChangePassword = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ['change-password'], mutationKey: ['change-password'],
mutationFn: authApi.changePassword, mutationFn: authApi.changePassword,
onSettled: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
return { return {
changePassword: ( changePassword: (

View File

@ -7,7 +7,7 @@ export const useLogin = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ['login'], mutationKey: ['login'],
mutationFn: authApi.login, mutationFn: authApi.login,
onSettled: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
return { return {
login: ( login: (

View File

@ -7,7 +7,7 @@ export const useLogout = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ['logout'], mutationKey: ['logout'],
mutationFn: authApi.logout, mutationFn: authApi.logout,
onSettled: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) onSettled: () => client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) }; return { logout: (onSuccess?: () => void) => mutation.mutate(undefined, { onSuccess }) };
}; };

View File

@ -7,12 +7,12 @@ export const useResetPassword = () => {
const validateMutation = useMutation({ const validateMutation = useMutation({
mutationKey: ['reset-password'], mutationKey: ['reset-password'],
mutationFn: authApi.validatePasswordToken, mutationFn: authApi.validatePasswordToken,
onSuccess: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) onSuccess: () => client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
const resetMutation = useMutation({ const resetMutation = useMutation({
mutationKey: ['reset-password'], mutationKey: ['reset-password'],
mutationFn: authApi.resetPassword, mutationFn: authApi.resetPassword,
onSuccess: async () => await client.invalidateQueries({ queryKey: [authApi.baseKey] }) onSuccess: () => client.invalidateQueries({ queryKey: [authApi.baseKey] })
}); });
return { return {
validateToken: ( validateToken: (

View File

@ -2,6 +2,8 @@ import { queryOptions } from '@tanstack/react-query';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport'; import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration'; import { DELAYS } from '@/backend/configuration';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api';
import { import {
AccessPolicy, AccessPolicy,
ILibraryItem, ILibraryItem,
@ -15,9 +17,6 @@ import { ConstituentaID, IRSFormData } from '@/models/rsform';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { information } from '@/utils/labels'; import { information } from '@/utils/labels';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
/** /**
* Represents update data for renaming Location. * Represents update data for renaming Location.
*/ */

View File

@ -10,7 +10,7 @@ export const useCloneItem = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'clone-item'], mutationKey: [libraryApi.baseKey, 'clone-item'],
mutationFn: libraryApi.cloneItem, mutationFn: libraryApi.cloneItem,
onSuccess: async () => await client.invalidateQueries({ queryKey: [libraryApi.baseKey] }) onSuccess: () => client.invalidateQueries({ queryKey: [libraryApi.baseKey] })
}); });
return { return {
cloneItem: ( cloneItem: (

View File

@ -1,5 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -9,11 +11,14 @@ export const useDeleteItem = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'delete-item'], mutationKey: [libraryApi.baseKey, 'delete-item'],
mutationFn: libraryApi.deleteItem, mutationFn: libraryApi.deleteItem,
onSuccess: async (_, variables) => { onSuccess: (_, variables) => {
await client.cancelQueries({ queryKey: [libraryApi.libraryListKey] });
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.filter(item => item.id !== variables) prev?.filter(item => item.id !== variables)
); );
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({ queryKey: rsformsApi.getRSFormQueryOptions({ itemID: variables }).queryKey })
]);
} }
}); });
return { return {

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api';
import { IRenameLocationDTO, libraryApi } from './api'; import { IRenameLocationDTO, libraryApi } from './api';
export const useRenameLocation = () => { export const useRenameLocation = () => {
@ -7,7 +10,12 @@ export const useRenameLocation = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'rename-location'], mutationKey: [libraryApi.baseKey, 'rename-location'],
mutationFn: libraryApi.renameLocation, mutationFn: libraryApi.renameLocation,
onSuccess: () => client.invalidateQueries({ queryKey: [libraryApi.baseKey] }) onSuccess: () =>
Promise.allSettled([
client.invalidateQueries({ queryKey: [libraryApi.baseKey] }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] }),
client.invalidateQueries({ queryKey: [ossApi.baseKey] })
])
}); });
return { return {
renameLocation: ( renameLocation: (

View File

@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api'; import { rsformsApi } from '@/backend/rsform/api';
import { AccessPolicy, ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { AccessPolicy, ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -11,21 +13,29 @@ export const useSetAccessPolicy = () => {
mutationKey: [libraryApi.baseKey, 'set-location'], mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setAccessPolicy, mutationFn: libraryApi.setAccessPolicy,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
const ossData: IOperationSchemaData | undefined = client.getQueryData(ossKey);
if (ossData) {
client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy });
return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.items
.map(item => {
if (!item.result) {
return;
}
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
return client.invalidateQueries({ queryKey: itemKey });
})
.filter(item => !!item)
]);
}
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, access_policy: variables.policy }));
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, access_policy: variables.policy } : item)) 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
};
});
} }
}); });

View File

@ -1,10 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api'; import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { ossApi } from '../oss/api';
import { libraryApi } from './api'; import { libraryApi } from './api';
export const useSetEditors = () => { export const useSetEditors = () => {
@ -17,19 +17,21 @@ export const useSetEditors = () => {
const ossData = client.getQueryData(ossKey); const ossData = client.getQueryData(ossKey);
if (ossData) { if (ossData) {
client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); client.setQueryData(ossKey, { ...ossData, editors: variables.editors });
Promise.allSettled([ return Promise.allSettled(
...ossData.items.map(item => { ossData.items
if (!item.result) { .map(item => {
return; if (!item.result) {
} return;
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey; }
return client.invalidateQueries({ queryKey: itemKey }); const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
}) return client.invalidateQueries({ queryKey: itemKey });
]).catch(console.error); })
} else { .filter(item => !!item)
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey; );
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, editors: variables.editors }));
} }
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, editors: variables.editors }));
} }
}); });

View File

@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api'; import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -11,43 +13,29 @@ export const useSetLocation = () => {
mutationKey: [libraryApi.baseKey, 'set-location'], mutationKey: [libraryApi.baseKey, 'set-location'],
mutationFn: libraryApi.setLocation, mutationFn: libraryApi.setLocation,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
// const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey; const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
// const ossData = client.getQueryData(ossKey); const ossData: IOperationSchemaData | undefined = client.getQueryData(ossKey);
// if (ossData) { if (ossData) {
// client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); client.setQueryData(ossKey, { ...ossData, location: variables.location });
// Promise.allSettled([ return Promise.allSettled([
// client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
// ...ossData.items.map(item => { ...ossData.items
// if (!item.result) { .map(item => {
// return; if (!item.result) {
// } return;
// const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey; }
// return client.invalidateQueries({ queryKey: itemKey }); const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
// }) return client.invalidateQueries({ queryKey: itemKey });
// ]).catch(console.error); })
// } else { .filter(item => !!item)
// const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey; ]);
// client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, editors: variables.editors })); }
// client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
// prev?.map(item => (item.id === variables.itemID ? { ...item, editors: variables.editors } : item))
// );
// }
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, location: variables.location }));
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item)) 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
};
});
} }
}); });

View File

@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api'; import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { UserID } from '@/models/user'; import { UserID } from '@/models/user';
import { libraryApi } from './api'; import { libraryApi } from './api';
@ -12,21 +14,29 @@ export const useSetOwner = () => {
mutationKey: [libraryApi.baseKey, 'set-owner'], mutationKey: [libraryApi.baseKey, 'set-owner'],
mutationFn: libraryApi.setOwner, mutationFn: libraryApi.setOwner,
onSuccess: (_, variables) => { onSuccess: (_, variables) => {
const ossKey = ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey;
const ossData: IOperationSchemaData | undefined = client.getQueryData(ossKey);
if (ossData) {
client.setQueryData(ossKey, { ...ossData, owner: variables.owner });
return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
...ossData.items
.map(item => {
if (!item.result) {
return;
}
const itemKey = rsformsApi.getRSFormQueryOptions({ itemID: item.result }).queryKey;
return client.invalidateQueries({ queryKey: itemKey });
})
.filter(item => !!item)
]);
}
const rsKey = rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey;
client.setQueryData(rsKey, prev => (!prev ? undefined : { ...prev, owner: variables.owner }));
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, owner: variables.owner } : item)) 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
};
});
} }
}); });

View File

@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { rsformsApi } from '@/backend/rsform/api'; import { ossApi } from '@/backend/oss/api';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem, LibraryItemType } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { IRSFormData } from '@/models/rsform';
import { ILibraryUpdateDTO, libraryApi } from './api'; import { ILibraryUpdateDTO, libraryApi } from './api';
@ -11,17 +13,23 @@ export const useUpdateItem = () => {
mutationKey: [libraryApi.baseKey, 'update-item'], mutationKey: [libraryApi.baseKey, 'update-item'],
mutationFn: libraryApi.updateItem, mutationFn: libraryApi.updateItem,
onSuccess: (data: ILibraryItem) => { onSuccess: (data: ILibraryItem) => {
client const itemKey = libraryApi.getItemQueryOptions({ itemID: data.id, itemType: data.item_type }).queryKey;
.cancelQueries({ queryKey: libraryApi.libraryListKey }) client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
.then(async () => { prev?.map(item => (item.id === data.id ? data : item))
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => );
prev?.map(item => (item.id === data.id ? data : item)) client.setQueryData(itemKey, (prev: IRSFormData | IOperationSchemaData | undefined) =>
!prev ? undefined : { ...prev, ...data }
);
if (data.item_type === LibraryItemType.RSFORM) {
const schema: IRSFormData | undefined = client.getQueryData(itemKey);
if (schema) {
return Promise.allSettled(
schema.oss.map(item =>
client.invalidateQueries({ queryKey: ossApi.getOssQueryOptions({ itemID: item.id }).queryKey })
)
); );
await client.invalidateQueries({ }
queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey] }
});
})
.catch(console.error);
} }
}); });
return { return {

View File

@ -1,6 +1,5 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { rsformsApi } from '@/backend/rsform/api'; import { rsformsApi } from '@/backend/rsform/api';
import { VersionID } from '@/models/library'; import { VersionID } from '@/models/library';
@ -8,13 +7,12 @@ import { libraryApi } from './api';
export const useVersionRestore = () => { export const useVersionRestore = () => {
const client = useQueryClient(); const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'restore-version'], mutationKey: [libraryApi.baseKey, 'restore-version'],
mutationFn: libraryApi.versionRestore, mutationFn: libraryApi.versionRestore,
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); return client.invalidateQueries({ queryKey: [libraryApi.baseKey] });
} }
}); });
return { return {

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID } from '@/models/library'; import { ILibraryItem, LibraryItemID } from '@/models/library';
import { ITargetOperation, ossApi } from './api'; import { ITargetOperation, ossApi } from './api';
@ -11,9 +12,12 @@ export const useInputCreate = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'input-create'], mutationKey: [ossApi.baseKey, 'input-create'],
mutationFn: ossApi.inputCreate, mutationFn: ossApi.inputCreate,
onSuccess: async data => { onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] })
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IInputUpdateDTO, ossApi } from './api'; import { IInputUpdateDTO, ossApi } from './api';
@ -10,9 +11,12 @@ export const useInputUpdate = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'input-update'], mutationKey: [ossApi.baseKey, 'input-update'],
mutationFn: ossApi.inputUpdate, mutationFn: ossApi.inputUpdate,
onSuccess: async data => { onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] })
]);
} }
}); });
return { return {

View File

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

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IOperationDeleteDTO, ossApi } from './api'; import { IOperationDeleteDTO, ossApi } from './api';
@ -10,9 +11,12 @@ export const useOperationDelete = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-delete'], mutationKey: [ossApi.baseKey, 'operation-delete'],
mutationFn: ossApi.operationDelete, mutationFn: ossApi.operationDelete,
onSuccess: async data => { onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] })
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ITargetOperation, ossApi } from './api'; import { ITargetOperation, ossApi } from './api';
@ -10,9 +11,12 @@ export const useOperationExecute = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-execute'], mutationKey: [ossApi.baseKey, 'operation-execute'],
mutationFn: ossApi.operationExecute, mutationFn: ossApi.operationExecute,
onSuccess: async data => { onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] })
]);
} }
}); });
return { return {

View File

@ -1,7 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { LibraryItemID } from '@/models/library'; import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { IOperationUpdateDTO, ossApi } from './api'; import { IOperationUpdateDTO, ossApi } from './api';
@ -10,9 +11,22 @@ export const useOperationUpdate = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-update'], mutationKey: [ossApi.baseKey, 'operation-update'],
mutationFn: ossApi.operationUpdate, mutationFn: ossApi.operationUpdate,
onSuccess: async data => { onSuccess: (data, variables) => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); const schemaID = data.items.find(item => item.id === variables.data.target)?.result;
if (!schemaID) {
return;
}
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
!prev
? undefined
: prev.map(item =>
item.id === schemaID ? { ...item, ...variables.data.item_data, time_update: Date() } : item
)
);
return client.invalidateQueries({
queryKey: rsformsApi.getRSFormQueryOptions({ itemID: schemaID }).queryKey
});
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstRelocateDTO, ossApi } from './api'; import { ICstRelocateDTO, ossApi } from './api';
@ -10,9 +11,12 @@ export const useRelocateConstituents = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'relocate-constituents'], mutationKey: [ossApi.baseKey, 'relocate-constituents'],
mutationFn: ossApi.relocateConstituents, mutationFn: ossApi.relocateConstituents,
onSuccess: async data => { onSuccess: data => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
await client.invalidateQueries({ queryKey: [libraryApi.libraryListKey] }); return Promise.allSettled([
client.invalidateQueries({ queryKey: libraryApi.libraryListKey }),
client.invalidateQueries({ queryKey: [rsformsApi.baseKey] })
]);
} }
}); });
return { return {

View File

@ -14,7 +14,7 @@ export const queryClient = new QueryClient({
queries: { queries: {
staleTime: DELAYS.staleDefault, staleTime: DELAYS.staleDefault,
gcTime: DELAYS.garbageCollection, gcTime: DELAYS.garbageCollection,
retry: 3, retry: false,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchOnMount: true, refetchOnMount: true,
refetchOnReconnect: true refetchOnReconnect: true

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform'; import { IConstituentaMeta } from '@/models/rsform';
@ -16,7 +17,14 @@ export const useCstCreate = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.schema.id
})
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IConstituentaList } from '@/models/rsform'; import { IConstituentaList } from '@/models/rsform';
@ -15,7 +16,14 @@ export const useCstDelete = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.id
})
]);
} }
}); });
return { return {

View File

@ -14,7 +14,6 @@ export const useCstMove = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
// TODO: invalidate OSS?
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstRenameDTO, rsformsApi } from './api'; import { ICstRenameDTO, rsformsApi } from './api';
@ -14,7 +15,14 @@ export const useCstRename = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.schema.id
})
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstSubstitutions } from '@/models/oss'; import { ICstSubstitutions } from '@/models/oss';
@ -15,7 +16,14 @@ export const useCstSubstitute = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.id
})
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ICstUpdateDTO, rsformsApi } from './api'; import { ICstUpdateDTO, rsformsApi } from './api';
@ -11,12 +12,24 @@ export const useCstUpdate = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'update-cst'], mutationKey: [rsformsApi.baseKey, 'update-cst'],
mutationFn: rsformsApi.cstUpdate, mutationFn: rsformsApi.cstUpdate,
onSuccess: async (_, variables) => { onSuccess: (newCst, variables) => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey, prev =>
!prev
? undefined
: {
...prev,
items: prev.items.map(item => (item.id === newCst.id ? { ...item, ...newCst } : item))
}
);
updateTimestamp(variables.itemID); updateTimestamp(variables.itemID);
await client.invalidateQueries({
queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey] return Promise.allSettled([
}); client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
// TODO: invalidate OSS? client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== variables.itemID
})
]);
} }
}); });
return { return {

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { IRSFormData } from '@/models/rsform'; import { IRSFormData } from '@/models/rsform';
@ -16,7 +17,14 @@ export const useInlineSynthesis = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.id
})
]);
} }
}); });
return { return {

View File

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

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { ConstituentaID, ITargetCst } from '@/models/rsform'; import { ConstituentaID, ITargetCst } from '@/models/rsform';
@ -16,7 +17,14 @@ export const useProduceStructure = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
updateTimestamp(data.schema.id); updateTimestamp(data.schema.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.schema.id
})
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp'; import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library'; import { LibraryItemID } from '@/models/library';
import { rsformsApi } from './api'; import { rsformsApi } from './api';
@ -14,7 +15,14 @@ export const useResetAliases = () => {
onSuccess: data => { onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data); client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id); updateTimestamp(data.id);
// TODO: invalidate OSS?
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.id
})
]);
} }
}); });
return { return {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api'; import { libraryApi } from '@/backend/library/api';
import { ossApi } from '@/backend/oss/api';
import { ILibraryItem } from '@/models/library'; import { ILibraryItem } from '@/models/library';
import { IRSFormUploadDTO, rsformsApi } from './api'; import { IRSFormUploadDTO, rsformsApi } from './api';
@ -15,6 +16,14 @@ export const useUploadTRS = () => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) => client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item)) prev?.map(item => (item.id === data.id ? data : item))
); );
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({
queryKey: [rsformsApi.baseKey],
predicate: query => query.queryKey.length > 2 && query.queryKey[2] !== data.id
})
]);
} }
}); });
return { return {

View File

@ -9,7 +9,7 @@ export const useSignup = () => {
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ['signup'], mutationKey: ['signup'],
mutationFn: usersApi.signup, mutationFn: usersApi.signup,
onSuccess: async () => await client.invalidateQueries({ queryKey: [usersApi.baseKey] }) onSuccess: () => client.invalidateQueries({ queryKey: usersApi.getUsersQueryOptions().queryKey })
}); });
return { return {
signup: ( signup: (

View File

@ -2,14 +2,15 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { IUpdateProfileDTO, usersApi } from './api'; import { IUpdateProfileDTO, usersApi } from './api';
// TODO: reload users / optimistic update
export const useUpdateProfile = () => { export const useUpdateProfile = () => {
const client = useQueryClient(); const client = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: ['update-profile'], mutationKey: ['update-profile'],
mutationFn: usersApi.updateProfile, mutationFn: usersApi.updateProfile,
onSuccess: async () => await client.invalidateQueries({ queryKey: [usersApi.baseKey] }) onSuccess: data => {
client.setQueryData(usersApi.getProfileQueryOptions().queryKey, data);
return client.invalidateQueries({ queryKey: usersApi.getUsersQueryOptions().queryKey });
}
}); });
return { return {
updateProfile: (data: IUpdateProfileDTO) => mutation.mutate(data), updateProfile: (data: IUpdateProfileDTO) => mutation.mutate(data),

View File

@ -16,7 +16,28 @@ function DescribeError({ error }: { error: ErrorData }) {
} else if (typeof error === 'string') { } else if (typeof error === 'string') {
return <p>{error}</p>; return <p>{error}</p>;
} else if (!axios.isAxiosError(error)) { } else if (!axios.isAxiosError(error)) {
return <PrettyJson data={error} />; return (
<div className='mt-6'>
<p>
<b>Error:</b> {error.name}
</p>
<p>
<b>Message:</b> {error.message}
</p>
{error.stack && (
<pre
style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
padding: '6px',
overflowX: 'auto'
}}
>
{error.stack}
</pre>
)}
</div>
);
} }
if (!error?.response) { if (!error?.response) {
return <p>Нет ответа от сервера</p>; return <p>Нет ответа от сервера</p>;

View File

@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary'; import { useMutatingLibrary } from '@/backend/library/useMutatingLibrary';
import { useVersionDelete } from '@/backend/library/useVersionDelete'; import { useVersionDelete } from '@/backend/library/useVersionDelete';
import { useVersionUpdate } from '@/backend/library/useVersionUpdate'; import { useVersionUpdate } from '@/backend/library/useVersionUpdate';
import { IconReset, IconSave } from '@/components/Icons'; import { IconReset, IconSave } from '@/components/Icons';
@ -22,7 +22,7 @@ export interface DlgEditVersionsProps {
function DlgEditVersions() { function DlgEditVersions() {
const { item, afterDelete } = useDialogsStore(state => state.props as DlgEditVersionsProps); const { item, afterDelete } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const processing = useIsProcessingLibrary(); const processing = useMutatingLibrary();
const { versionDelete } = useVersionDelete(); const { versionDelete } = useVersionDelete();
const { versionUpdate } = useVersionUpdate(); const { versionUpdate } = useVersionUpdate();

View File

@ -3,7 +3,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem'; import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard'; import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
@ -47,7 +46,7 @@ function EditorOssCard() {
> >
<FlexColumn className='px-3'> <FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} /> <FormOSS id={globals.library_item_editor} />
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.OSS} controller={controller} /> <EditorLibraryItem controller={controller} />
</FlexColumn> </FlexColumn>
{controller.schema ? <OssStats stats={controller.schema.stats} /> : null} {controller.schema ? <OssStats stats={controller.schema.stats} /> : null}

View File

@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import { ILibraryUpdateDTO } from '@/backend/library/api'; import { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem'; import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss'; import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import { IconSave } from '@/components/Icons'; import { IconSave } from '@/components/Icons';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea'; import TextArea from '@/components/ui/TextArea';
@ -24,7 +24,7 @@ function FormOSS({ id }: FormOSSProps) {
const { updateItem: update } = useUpdateItem(); const { updateItem: update } = useUpdateItem();
const controller = useOssEdit(); const controller = useOssEdit();
const { isModified, setIsModified } = useModificationStore(); const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss(); const isProcessing = useMutatingOss();
const schema = controller.schema; const schema = controller.schema;
const [title, setTitle] = useState(schema.title); const [title, setTitle] = useState(schema.title);

View File

@ -2,7 +2,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss'; import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import { import {
IconChild, IconChild,
IconConnect, IconConnect,
@ -50,7 +50,7 @@ function NodeContextMenu({
onRelocateConstituents onRelocateConstituents
}: NodeContextMenuProps) { }: NodeContextMenuProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const isProcessing = useIsProcessingOss(); const isProcessing = useMutatingOss();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);

View File

@ -20,7 +20,7 @@ import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useLibrary } from '@/backend/library/useLibrary'; import { useLibrary } from '@/backend/library/useLibrary';
import { useInputCreate } from '@/backend/oss/useInputCreate'; import { useInputCreate } from '@/backend/oss/useInputCreate';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss'; import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import { useOperationExecute } from '@/backend/oss/useOperationExecute'; import { useOperationExecute } from '@/backend/oss/useOperationExecute';
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions'; import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
@ -50,7 +50,7 @@ function OssFlow() {
const flow = useReactFlow(); const flow = useReactFlow();
const { setIsModified } = useModificationStore(); const { setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss(); const isProcessing = useMutatingOss();
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -118,7 +118,7 @@ function OssFlow() {
} }
function handleNodesChange(changes: NodeChange[]) { function handleNodesChange(changes: NodeChange[]) {
if (changes.some(change => change.type === 'position' && change.position)) { if (controller.isMutable && changes.some(change => change.type === 'position' && change.position)) {
setIsModified(true); setIsModified(true);
} }
onNodesChange(changes); onNodesChange(changes);

View File

@ -2,7 +2,7 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss'; import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import { import {
IconAnimation, IconAnimation,
IconAnimationOff, IconAnimationOff,
@ -52,7 +52,7 @@ function ToolbarOssGraph({
}: ToolbarOssGraphProps) { }: ToolbarOssGraphProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useIsProcessingOss(); const isProcessing = useMutatingOss();
const selectedOperation = controller.schema.operationByID.get(controller.selected[0]); const selectedOperation = controller.schema.operationByID.get(controller.selected[0]);
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
@ -89,7 +89,6 @@ function ToolbarOssGraph({
<MiniButton <MiniButton
title='Сбросить изменения' title='Сбросить изменения'
icon={<IconReset size='1.25rem' className='icon-primary' />} icon={<IconReset size='1.25rem' className='icon-primary' />}
disabled={!isModified}
onClick={onResetPositions} onClick={onResetPositions}
/> />
<MiniButton <MiniButton

View File

@ -3,7 +3,7 @@
import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useAuthSuspense } from '@/backend/auth/useAuth'; import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss'; import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import { import {
IconAdmin, IconAdmin,
IconAlert, IconAlert,
@ -35,7 +35,7 @@ function MenuOssTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, isAnonymous } = useAuthSuspense(); const { user, isAnonymous } = useAuthSuspense();
const isProcessing = useIsProcessingOss(); const isProcessing = useMutatingOss();
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole); const setRole = useRoleStore(state => state.setRole);

View File

@ -86,11 +86,9 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
const { schema } = useOssSuspense({ itemID: itemID }); const { schema } = useOssSuspense({ itemID: itemID });
const isOwned = !!user.id && user.id === schema.owner; const isOwned = !!user.id && user.id === schema.owner;
const isMutable = role > UserRole.READER && !schema.read_only; const isMutable = role > UserRole.READER && !schema.read_only;
const [showTooltip, setShowTooltip] = useState(true); const [showTooltip, setShowTooltip] = useState(true);
const [selected, setSelected] = useState<OperationID[]>([]); const [selected, setSelected] = useState<OperationID[]>([]);
const showEditInput = useDialogsStore(state => state.showChangeInputSchema); const showEditInput = useDialogsStore(state => state.showChangeInputSchema);

View File

@ -21,13 +21,6 @@ function OssPage() {
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
useBlockNavigation(isModified); useBlockNavigation(isModified);
// useBlockNavigation(
// isModified &&
// schema !== undefined &&
// !!user &&
// (user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
// );
if (!itemID) { if (!itemID) {
router.replace(urls.page404); router.replace(urls.page404);
return null; return null;

View File

@ -6,7 +6,7 @@ import { useEffect, useState } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { IPasswordTokenDTO, IResetPasswordDTO } from '@/backend/auth/api'; import { IResetPasswordDTO } from '@/backend/auth/api';
import { useResetPassword } from '@/backend/auth/useResetPassword'; import { useResetPassword } from '@/backend/auth/useResetPassword';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
@ -48,10 +48,7 @@ function PasswordChangePage() {
}, [newPassword, newPasswordRepeat, reset]); }, [newPassword, newPasswordRepeat, reset]);
useEffect(() => { useEffect(() => {
const data: IPasswordTokenDTO = { validateToken({ token: token ?? '' }, () => setIsTokenValid(true));
token: token ?? ''
};
validateToken(data, () => setIsTokenValid(true));
}, [token, validateToken]); }, [token, validateToken]);
return ( return (

View File

@ -4,7 +4,7 @@ import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate'; import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import useWindowSize from '@/hooks/useWindowSize'; import useWindowSize from '@/hooks/useWindowSize';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
@ -33,7 +33,7 @@ function EditorConstituenta() {
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing; const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing;
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD; const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;

View File

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { ICstRenameDTO } from '@/backend/rsform/api'; import { ICstRenameDTO } from '@/backend/rsform/api';
import { useCstRename } from '@/backend/rsform/useCstRename'; import { useCstRename } from '@/backend/rsform/useCstRename';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconEdit } from '@/components/Icons'; import { IconEdit } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -23,7 +23,7 @@ interface EditorControlsProps {
function EditorControls({ constituenta, disabled, onEditTerm }: EditorControlsProps) { function EditorControls({ constituenta, disabled, onEditTerm }: EditorControlsProps) {
const { schema } = useRSEdit(); const { schema } = useRSEdit();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const showRenameCst = useDialogsStore(state => state.showRenameCst); const showRenameCst = useDialogsStore(state => state.showRenameCst);
const { cstRename } = useCstRename(); const { cstRename } = useCstRename();

View File

@ -6,7 +6,7 @@ import { toast } from 'react-toastify';
import { ICstUpdateDTO } from '@/backend/rsform/api'; import { ICstUpdateDTO } from '@/backend/rsform/api';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate'; import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons'; import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import RefsInput from '@/components/RefsInput'; import RefsInput from '@/components/RefsInput';
@ -45,7 +45,7 @@ function FormConstituenta({
const { cstUpdate } = useCstUpdate(); const { cstUpdate } = useCstUpdate();
const { schema, activeCst } = useRSEdit(); const { schema, activeCst } = useRSEdit();
const { isModified, setIsModified } = useModificationStore(); const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const [term, setTerm] = useState(activeCst?.term_raw ?? ''); const [term, setTerm] = useState(activeCst?.term_raw ?? '');
const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? ''); const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? '');

View File

@ -5,7 +5,7 @@ import clsx from 'clsx';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor'; import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { import {
IconClone, IconClone,
IconDestroy, IconDestroy,
@ -53,7 +53,7 @@ function ToolbarConstituenta({
const showList = usePreferencesStore(state => state.showCstSideList); const showList = usePreferencesStore(state => state.showCstSideList);
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList); const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
function viewPredecessor(target: ConstituentaID) { function viewPredecessor(target: ConstituentaID) {
findPredecessor({ target: target }, reference => findPredecessor({ target: target }, reference =>

View File

@ -7,7 +7,7 @@ import { toast } from 'react-toastify';
import { DataCallback } from '@/backend/apiTransport'; import { DataCallback } from '@/backend/apiTransport';
import { ICheckConstituentaDTO } from '@/backend/rsform/api'; import { ICheckConstituentaDTO } from '@/backend/rsform/api';
import { useCheckConstituenta } from '@/backend/rsform/useCheckConstituenta'; import { useCheckConstituenta } from '@/backend/rsform/useCheckConstituenta';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import RSInput from '@/components/RSInput'; import RSInput from '@/components/RSInput';
@ -65,7 +65,7 @@ function EditorRSExpression({
const rsInput = useRef<ReactCodeMirrorRef>(null); const rsInput = useRef<ReactCodeMirrorRef>(null);
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined); const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls); const showControls = usePreferencesStore(state => state.showExpressionControls);
const showAST = useDialogsStore(state => state.showShowAST); const showAST = useDialogsStore(state => state.showShowAST);

View File

@ -1,4 +1,4 @@
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconControls, IconTree, IconTypeGraph } from '@/components/Icons'; import { IconControls, IconTree, IconTypeGraph } from '@/components/Icons';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -12,7 +12,7 @@ interface ToolbarRSExpressionProps {
} }
function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) { function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls); const showControls = usePreferencesStore(state => state.showExpressionControls);
const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls); const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls);

View File

@ -3,8 +3,7 @@ import { useIntl } from 'react-intl';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary'; import { useMutatingLibrary } from '@/backend/library/useMutatingLibrary';
import { useLibraryItem } from '@/backend/library/useLibraryItem';
import { useSetEditors } from '@/backend/library/useSetEditors'; import { useSetEditors } from '@/backend/library/useSetEditors';
import { useSetLocation } from '@/backend/library/useSetLocation'; import { useSetLocation } from '@/backend/library/useSetLocation';
import { useSetOwner } from '@/backend/library/useSetOwner'; import { useSetOwner } from '@/backend/library/useSetOwner';
@ -26,7 +25,7 @@ import Overlay from '@/components/ui/Overlay';
import Tooltip from '@/components/ui/Tooltip'; import Tooltip from '@/components/ui/Tooltip';
import ValueIcon from '@/components/ui/ValueIcon'; import ValueIcon from '@/components/ui/ValueIcon';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { ILibraryItemEditor, LibraryItemID, LibraryItemType } from '@/models/library'; import { ILibraryItemEditor } from '@/models/library';
import { UserID, UserRole } from '@/models/user'; import { UserID, UserRole } from '@/models/user';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { useLibrarySearchStore } from '@/stores/librarySearch'; import { useLibrarySearchStore } from '@/stores/librarySearch';
@ -36,20 +35,17 @@ import { prefixes } from '@/utils/constants';
import { prompts } from '@/utils/labels'; import { prompts } from '@/utils/labels';
interface EditorLibraryItemProps { interface EditorLibraryItemProps {
itemID: LibraryItemID;
itemType: LibraryItemType;
controller: ILibraryItemEditor; controller: ILibraryItemEditor;
} }
function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemProps) { function EditorLibraryItem({ controller }: EditorLibraryItemProps) {
const getUserLabel = useLabelUser(); const getUserLabel = useLabelUser();
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const intl = useIntl(); const intl = useIntl();
const router = useConceptNavigation(); const router = useConceptNavigation();
const setGlobalLocation = useLibrarySearchStore(state => state.setLocation); const setGlobalLocation = useLibrarySearchStore(state => state.setLocation);
const { item } = useLibraryItem({ itemID, itemType }); const isProcessing = useMutatingLibrary();
const isProcessing = useIsProcessingLibrary();
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const { setOwner } = useSetOwner(); const { setOwner } = useSetOwner();
@ -62,47 +58,34 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
const ownerSelector = useDropdown(); const ownerSelector = useDropdown();
const onSelectUser = function (newValue: UserID) { const onSelectUser = function (newValue: UserID) {
ownerSelector.hide(); ownerSelector.hide();
if (newValue === item?.owner) { if (newValue === controller.schema.owner) {
return; return;
} }
if (!window.confirm(prompts.ownerChange)) { if (!window.confirm(prompts.ownerChange)) {
return; return;
} }
setOwner({ itemID: itemID, owner: newValue }); setOwner({ itemID: controller.schema.id, owner: newValue });
}; };
function handleOpenLibrary(event: CProps.EventMouse) { function handleOpenLibrary(event: CProps.EventMouse) {
if (!item) { setGlobalLocation(controller.schema.location);
return;
}
setGlobalLocation(item.location);
router.push(urls.library, event.ctrlKey || event.metaKey); router.push(urls.library, event.ctrlKey || event.metaKey);
} }
function handleEditLocation() { function handleEditLocation() {
if (!item) {
return;
}
showEditLocation({ showEditLocation({
initial: item.location, initial: controller.schema.location,
onChangeLocation: newLocation => setLocation({ itemID: itemID, location: newLocation }) onChangeLocation: newLocation => setLocation({ itemID: controller.schema.id, location: newLocation })
}); });
} }
function handleEditEditors() { function handleEditEditors() {
if (!item) {
return;
}
showEditEditors({ showEditEditors({
editors: item.editors, editors: controller.schema.editors,
onChangeEditors: newEditors => setEditors({ itemID: itemID, editors: newEditors }) onChangeEditors: newEditors => setEditors({ itemID: controller.schema.id, editors: newEditors })
}); });
} }
if (!item) {
return null;
}
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
<div className='flex justify-stretch sm:mb-1 max-w-[30rem] gap-3'> <div className='flex justify-stretch sm:mb-1 max-w-[30rem] gap-3'>
@ -116,7 +99,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
<ValueIcon <ValueIcon
className='text-ellipsis flex-grow' className='text-ellipsis flex-grow'
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />} icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
value={item.location} value={controller.schema.location}
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'} title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
onClick={handleEditLocation} onClick={handleEditLocation}
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER} disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
@ -128,7 +111,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
{ownerSelector.isOpen ? ( {ownerSelector.isOpen ? (
<SelectUser <SelectUser
className='w-[25rem] sm:w-[26rem] text-sm' className='w-[25rem] sm:w-[26rem] text-sm'
value={item.owner ?? undefined} value={controller.schema.owner ?? undefined}
onSelectValue={onSelectUser} onSelectValue={onSelectUser}
/> />
) : null} ) : null}
@ -137,7 +120,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
<ValueIcon <ValueIcon
className='sm:mb-1' className='sm:mb-1'
icon={<IconOwner size='1.25rem' className='icon-primary' />} icon={<IconOwner size='1.25rem' className='icon-primary' />}
value={getUserLabel(item.owner)} value={getUserLabel(controller.schema.owner)}
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'} title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
onClick={ownerSelector.toggle} onClick={ownerSelector.toggle}
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER} disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
@ -148,13 +131,13 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
id='editor_stats' id='editor_stats'
dense dense
icon={<IconEditor size='1.25rem' className='icon-primary' />} icon={<IconEditor size='1.25rem' className='icon-primary' />}
value={item.editors.length} value={controller.schema.editors.length}
onClick={handleEditEditors} onClick={handleEditEditors}
disabled={isModified || isProcessing || role < UserRole.OWNER} disabled={isModified || isProcessing || role < UserRole.OWNER}
/> />
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'> <Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
<Suspense fallback={<Loader scale={2} />}> <Suspense fallback={<Loader scale={2} />}>
<InfoUsers items={item?.editors ?? []} prefix={prefixes.user_editors} header='Редакторы' /> <InfoUsers items={controller.schema.editors} prefix={prefixes.user_editors} header='Редакторы' />
</Suspense> </Suspense>
</Tooltip> </Tooltip>
@ -162,7 +145,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
dense dense
disabled disabled
icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />} icon={<IconDateUpdate size='1.25rem' className='text-ok-600' />}
value={new Date(item.time_update).toLocaleString(intl.locale)} value={new Date(controller.schema.time_update).toLocaleString(intl.locale)}
title='Дата обновления' title='Дата обновления'
/> />
@ -170,7 +153,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
dense dense
disabled disabled
icon={<IconDateCreate size='1.25rem' className='text-ok-600' />} icon={<IconDateCreate size='1.25rem' className='text-ok-600' />}
value={new Date(item.time_create).toLocaleString(intl.locale, { value={new Date(controller.schema.time_create).toLocaleString(intl.locale, {
year: '2-digit', year: '2-digit',
month: '2-digit', month: '2-digit',
day: '2-digit' day: '2-digit'

View File

@ -3,7 +3,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn'; import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import { useModificationStore } from '@/stores/modification'; import { useModificationStore } from '@/stores/modification';
import { globals } from '@/utils/constants'; import { globals } from '@/utils/constants';
@ -46,7 +45,7 @@ function EditorRSFormCard() {
> >
<FlexColumn className='flex-shrink'> <FlexColumn className='flex-shrink'>
<FormRSForm id={globals.library_item_editor} /> <FormRSForm id={globals.library_item_editor} />
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.RSFORM} controller={controller} /> <EditorLibraryItem controller={controller} />
</FlexColumn> </FlexColumn>
{controller.schema ? <RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> : null} {controller.schema ? <RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> : null}

View File

@ -7,7 +7,7 @@ import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import { ILibraryUpdateDTO } from '@/backend/library/api'; import { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem'; import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconSave } from '@/components/Icons'; import { IconSave } from '@/components/Icons';
import SelectVersion from '@/components/select/SelectVersion'; import SelectVersion from '@/components/select/SelectVersion';
import Label from '@/components/ui/Label'; import Label from '@/components/ui/Label';
@ -31,7 +31,7 @@ function FormRSForm({ id }: FormRSFormProps) {
const schema = controller.schema; const schema = controller.schema;
const { updateItem: update } = useUpdateItem(); const { updateItem: update } = useUpdateItem();
const { isModified, setIsModified } = useModificationStore(); const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const [title, setTitle] = useState(schema.title); const [title, setTitle] = useState(schema.title);
const [alias, setAlias] = useState(schema.alias); const [alias, setAlias] = useState(schema.alias);

View File

@ -1,4 +1,4 @@
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary'; import { useMutatingLibrary } from '@/backend/library/useMutatingLibrary';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy'; import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
import { VisibilityIcon } from '@/components/DomainIcons'; import { VisibilityIcon } from '@/components/DomainIcons';
import { IconImmutable, IconMutable } from '@/components/Icons'; import { IconImmutable, IconMutable } from '@/components/Icons';
@ -23,7 +23,7 @@ interface ToolbarItemAccessProps {
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) { function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const isProcessing = useIsProcessingLibrary(); const isProcessing = useMutatingLibrary();
const policy = controller.schema.access_policy; const policy = controller.schema.access_policy;
const { setAccessPolicy } = useSetAccessPolicy(); const { setAccessPolicy } = useSetAccessPolicy();

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary'; import { useMutatingLibrary } from '@/backend/library/useMutatingLibrary';
import { IconDestroy, IconSave, IconShare } from '@/components/Icons'; import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS'; import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
@ -26,7 +26,7 @@ interface ToolbarRSFormCardProps {
function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) { function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useIsProcessingLibrary(); const isProcessing = useMutatingLibrary();
const canSave = isModified && !isProcessing; const canSave = isModified && !isProcessing;
const ossSelector = (() => { const ossSelector = (() => {

View File

@ -4,7 +4,7 @@ import fileDownload from 'js-file-download';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconCSV } from '@/components/Icons'; import { IconCSV } from '@/components/Icons';
import { type RowSelectionState } from '@/components/ui/DataTable'; import { type RowSelectionState } from '@/components/ui/DataTable';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
@ -24,7 +24,7 @@ import ToolbarRSList from './ToolbarRSList';
function EditorRSList() { function EditorRSList() {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const controller = useRSEdit(); const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema.items); const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema.items);
const [filterText, setFilterText] = useState(''); const [filterText, setFilterText] = useState('');

View File

@ -1,4 +1,4 @@
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { CstTypeIcon } from '@/components/DomainIcons'; import { CstTypeIcon } from '@/components/DomainIcons';
import { import {
IconClone, IconClone,
@ -25,7 +25,7 @@ import { useRSEdit } from '../RSEditContext';
function ToolbarRSList() { function ToolbarRSList() {
const controller = useRSEdit(); const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const insertMenu = useDropdown(); const insertMenu = useDropdown();
return ( return (

View File

@ -19,7 +19,7 @@ import {
import { useStoreApi } from 'reactflow'; import { useStoreApi } from 'reactflow';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import InfoConstituenta from '@/components/info/InfoConstituenta'; import InfoConstituenta from '@/components/info/InfoConstituenta';
import SelectedCounter from '@/components/info/SelectedCounter'; import SelectedCounter from '@/components/info/SelectedCounter';
import { CProps } from '@/components/props'; import { CProps } from '@/components/props';
@ -54,7 +54,7 @@ function TGFlow() {
const flow = useReactFlow(); const flow = useReactFlow();
const store = useStoreApi(); const store = useStoreApi();
const { addSelectedNodes } = store.getState(); const { addSelectedNodes } = store.getState();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const showParams = useDialogsStore(state => state.showGraphParams); const showParams = useDialogsStore(state => state.showGraphParams);

View File

@ -1,6 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { import {
IconClustering, IconClustering,
IconClusteringOff, IconClusteringOff,
@ -48,7 +48,7 @@ function ToolbarTermGraph({
onSaveImage onSaveImage
}: ToolbarTermGraphProps) { }: ToolbarTermGraphProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph); const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
function handleShowTypeGraph() { function handleShowTypeGraph() {

View File

@ -8,7 +8,7 @@ import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute'; import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm'; import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis'; import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm'; import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { useProduceStructure } from '@/backend/rsform/useProduceStructure'; import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
import { useResetAliases } from '@/backend/rsform/useResetAliases'; import { useResetAliases } from '@/backend/rsform/useResetAliases';
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder'; import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
@ -63,7 +63,7 @@ function MenuRSTabs() {
const role = useRoleStore(state => state.role); const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole); const setRole = useRoleStore(state => state.setRole);
const { isModified } = useModificationStore(); const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm(); const isProcessing = useMutatingRSForm();
const { resetAliases } = useResetAliases(); const { resetAliases } = useResetAliases();
const { restoreOrder } = useRestoreOrder(); const { restoreOrder } = useRestoreOrder();

View File

@ -6,7 +6,7 @@ import { useParams } from 'react-router';
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext'; import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls'; import { urls } from '@/app/urls';
import InfoError, { ErrorData } from '@/components/info/InfoError'; import { ErrorData } from '@/components/info/InfoError';
import Divider from '@/components/ui/Divider'; import Divider from '@/components/ui/Divider';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import useQueryStrings from '@/hooks/useQueryStrings'; import useQueryStrings from '@/hooks/useQueryStrings';
@ -33,6 +33,7 @@ function RSFormPage() {
} }
return ( return (
<ErrorBoundary <ErrorBoundary
onError={filterErrors}
FallbackComponent={({ error }) => ( FallbackComponent={({ error }) => (
<ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} /> <ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
)} )}
@ -47,6 +48,13 @@ function RSFormPage() {
export default RSFormPage; export default RSFormPage;
// ====== Internals ========= // ====== Internals =========
const filterErrors = (error: Error) => {
if (axios.isAxiosError(error) && error.response && (error.response.status === 404 || error.response.status === 403)) {
return;
}
throw error;
};
function ProcessError({ function ProcessError({
error, error,
isArchive, isArchive,
@ -55,7 +63,7 @@ function ProcessError({
error: ErrorData; error: ErrorData;
isArchive: boolean; isArchive: boolean;
itemID?: LibraryItemID; itemID?: LibraryItemID;
}): React.ReactElement { }): React.ReactElement | null {
if (axios.isAxiosError(error) && error.response) { if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) { if (error.response.status === 404) {
return ( return (
@ -77,5 +85,5 @@ function ProcessError({
); );
} }
} }
return <InfoError error={error} />; return null;
} }