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

View File

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

View File

@ -2,32 +2,13 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { ErrorBoundary } from 'react-error-boundary';
import { IntlProvider } from 'react-intl';
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
function GlobalProviders({ children }: React.PropsWithChildren) {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={resetState}
onError={logError}
>
<IntlProvider locale='ru' defaultLocale='ru'>
<QueryClientProvider client={queryClient}>
@ -35,8 +16,7 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
{children}
</QueryClientProvider>
</IntlProvider>
</ErrorBoundary>);
</IntlProvider>);
}
export default GlobalProviders;

View File

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

View File

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

View File

@ -7,7 +7,7 @@ export const useLogout = () => {
const mutation = useMutation({
mutationKey: ['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 }) };
};

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
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 { libraryApi } from './api';
@ -9,11 +11,14 @@ export const useDeleteItem = () => {
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'delete-item'],
mutationFn: libraryApi.deleteItem,
onSuccess: async (_, variables) => {
await client.cancelQueries({ queryKey: [libraryApi.libraryListKey] });
onSuccess: (_, variables) => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.filter(item => item.id !== variables)
);
return Promise.allSettled([
client.invalidateQueries({ queryKey: [ossApi.baseKey] }),
client.invalidateQueries({ queryKey: rsformsApi.getRSFormQueryOptions({ itemID: variables }).queryKey })
]);
}
});
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 { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api';
import { libraryApi } from './api';
export const useIsProcessingLibrary = () => {
export const useMutatingLibrary = () => {
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });

View File

@ -1,5 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/api';
import { rsformsApi } from '@/backend/rsform/api';
import { IRenameLocationDTO, libraryApi } from './api';
export const useRenameLocation = () => {
@ -7,7 +10,12 @@ export const useRenameLocation = () => {
const mutation = useMutation({
mutationKey: [libraryApi.baseKey, 'rename-location'],
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 {
renameLocation: (

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ossApi } from '@/backend/oss/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 { libraryApi } from './api';
@ -12,21 +14,29 @@ export const useSetOwner = () => {
mutationKey: [libraryApi.baseKey, 'set-owner'],
mutationFn: libraryApi.setOwner,
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) =>
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 { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem } from '@/models/library';
import { ossApi } from '@/backend/oss/api';
import { ILibraryItem, LibraryItemType } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss';
import { IRSFormData } from '@/models/rsform';
import { ILibraryUpdateDTO, libraryApi } from './api';
@ -11,17 +13,23 @@ export const useUpdateItem = () => {
mutationKey: [libraryApi.baseKey, 'update-item'],
mutationFn: libraryApi.updateItem,
onSuccess: (data: ILibraryItem) => {
client
.cancelQueries({ queryKey: libraryApi.libraryListKey })
.then(async () => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item))
const itemKey = libraryApi.getItemQueryOptions({ itemID: data.id, itemType: data.item_type }).queryKey;
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 {

View File

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

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import { ITargetOperation, ossApi } from './api';
@ -11,9 +12,12 @@ export const useInputCreate = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'input-create'],
mutationFn: ossApi.inputCreate,
onSuccess: async data => {
onSuccess: data => {
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library';
import { IInputUpdateDTO, ossApi } from './api';
@ -10,9 +11,12 @@ export const useInputUpdate = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'input-update'],
mutationFn: ossApi.inputUpdate,
onSuccess: async data => {
onSuccess: 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 {

View File

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

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library';
import { IOperationDeleteDTO, ossApi } from './api';
@ -10,9 +11,12 @@ export const useOperationDelete = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-delete'],
mutationFn: ossApi.operationDelete,
onSuccess: async data => {
onSuccess: 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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library';
import { ITargetOperation, ossApi } from './api';
@ -10,9 +11,12 @@ export const useOperationExecute = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-execute'],
mutationFn: ossApi.operationExecute,
onSuccess: async data => {
onSuccess: 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 {

View File

@ -1,7 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
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';
@ -10,9 +11,22 @@ export const useOperationUpdate = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'operation-update'],
mutationFn: ossApi.operationUpdate,
onSuccess: async data => {
onSuccess: (data, variables) => {
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { rsformsApi } from '@/backend/rsform/api';
import { LibraryItemID } from '@/models/library';
import { ICstRelocateDTO, ossApi } from './api';
@ -10,9 +11,12 @@ export const useRelocateConstituents = () => {
const mutation = useMutation({
mutationKey: [ossApi.baseKey, 'relocate-constituents'],
mutationFn: ossApi.relocateConstituents,
onSuccess: async data => {
onSuccess: 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 {

View File

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

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform';
@ -16,7 +17,14 @@ export const useCstCreate = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { IConstituentaList } from '@/models/rsform';
@ -15,7 +16,14 @@ export const useCstDelete = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
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 {

View File

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

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { ICstRenameDTO, rsformsApi } from './api';
@ -14,7 +15,14 @@ export const useCstRename = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { ICstSubstitutions } from '@/models/oss';
@ -15,7 +16,14 @@ export const useCstSubstitute = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { ICstUpdateDTO, rsformsApi } from './api';
@ -11,12 +12,24 @@ export const useCstUpdate = () => {
const mutation = useMutation({
mutationKey: [rsformsApi.baseKey, 'update-cst'],
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);
await client.invalidateQueries({
queryKey: [rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey]
});
// 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] !== variables.itemID
})
]);
}
});
return {

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { IRSFormData } from '@/models/rsform';
@ -16,7 +17,14 @@ export const useInlineSynthesis = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
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 {

View File

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

View File

@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { ConstituentaID, ITargetCst } from '@/models/rsform';
@ -16,7 +17,14 @@ export const useProduceStructure = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.schema.id }).queryKey, data.schema);
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { ossApi } from '@/backend/oss/api';
import { LibraryItemID } from '@/models/library';
import { rsformsApi } from './api';
@ -14,7 +15,14 @@ export const useResetAliases = () => {
onSuccess: data => {
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
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 {

View File

@ -1,6 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { libraryApi } from '@/backend/library/api';
import { ossApi } from '@/backend/oss/api';
import { ILibraryItem } from '@/models/library';
import { IRSFormUploadDTO, rsformsApi } from './api';
@ -15,6 +16,14 @@ export const useUploadTRS = () => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
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 {

View File

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

View File

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

View File

@ -16,7 +16,28 @@ function DescribeError({ error }: { error: ErrorData }) {
} else if (typeof error === 'string') {
return <p>{error}</p>;
} 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) {
return <p>Нет ответа от сервера</p>;

View File

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

View File

@ -3,7 +3,6 @@
import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
import { useModificationStore } from '@/stores/modification';
@ -47,7 +46,7 @@ function EditorOssCard() {
>
<FlexColumn className='px-3'>
<FormOSS id={globals.library_item_editor} />
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.OSS} controller={controller} />
<EditorLibraryItem controller={controller} />
</FlexColumn>
{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 { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import { IconSave } from '@/components/Icons';
import SubmitButton from '@/components/ui/SubmitButton';
import TextArea from '@/components/ui/TextArea';
@ -24,7 +24,7 @@ function FormOSS({ id }: FormOSSProps) {
const { updateItem: update } = useUpdateItem();
const controller = useOssEdit();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
const isProcessing = useMutatingOss();
const schema = controller.schema;
const [title, setTitle] = useState(schema.title);

View File

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

View File

@ -20,7 +20,7 @@ import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useLibrary } from '@/backend/library/useLibrary';
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 { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
import { CProps } from '@/components/props';
@ -50,7 +50,7 @@ function OssFlow() {
const flow = useReactFlow();
const { setIsModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
const isProcessing = useMutatingOss();
const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -118,7 +118,7 @@ function OssFlow() {
}
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);
}
onNodesChange(changes);

View File

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

View File

@ -3,7 +3,7 @@
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import { useMutatingOss } from '@/backend/oss/useMutatingOss';
import {
IconAdmin,
IconAlert,
@ -35,7 +35,7 @@ function MenuOssTabs() {
const router = useConceptNavigation();
const { user, isAnonymous } = useAuthSuspense();
const isProcessing = useIsProcessingOss();
const isProcessing = useMutatingOss();
const role = useRoleStore(state => state.role);
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 isOwned = !!user.id && user.id === schema.owner;
const isMutable = role > UserRole.READER && !schema.read_only;
const [showTooltip, setShowTooltip] = useState(true);
const [selected, setSelected] = useState<OperationID[]>([]);
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import clsx from 'clsx';
import { useState } from 'react';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import useWindowSize from '@/hooks/useWindowSize';
import { useMainHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs';
@ -33,7 +33,7 @@ function EditorConstituenta() {
const [toggleReset, setToggleReset] = useState(false);
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing;
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 { useCstRename } from '@/backend/rsform/useCstRename';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconEdit } from '@/components/Icons';
import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay';
@ -23,7 +23,7 @@ interface EditorControlsProps {
function EditorControls({ constituenta, disabled, onEditTerm }: EditorControlsProps) {
const { schema } = useRSEdit();
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
const showRenameCst = useDialogsStore(state => state.showRenameCst);
const { cstRename } = useCstRename();

View File

@ -6,7 +6,7 @@ import { toast } from 'react-toastify';
import { ICstUpdateDTO } from '@/backend/rsform/api';
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 { CProps } from '@/components/props';
import RefsInput from '@/components/RefsInput';
@ -45,7 +45,7 @@ function FormConstituenta({
const { cstUpdate } = useCstUpdate();
const { schema, activeCst } = useRSEdit();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
const [term, setTerm] = useState(activeCst?.term_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 { urls } from '@/app/urls';
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import {
IconClone,
IconDestroy,
@ -53,7 +53,7 @@ function ToolbarConstituenta({
const showList = usePreferencesStore(state => state.showCstSideList);
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
function viewPredecessor(target: ConstituentaID) {
findPredecessor({ target: target }, reference =>

View File

@ -7,7 +7,7 @@ import { toast } from 'react-toastify';
import { DataCallback } from '@/backend/apiTransport';
import { ICheckConstituentaDTO } from '@/backend/rsform/api';
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 { CProps } from '@/components/props';
import RSInput from '@/components/RSInput';
@ -65,7 +65,7 @@ function EditorRSExpression({
const rsInput = useRef<ReactCodeMirrorRef>(null);
const [parseData, setParseData] = useState<IExpressionParse | undefined>(undefined);
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
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 { CProps } from '@/components/props';
import MiniButton from '@/components/ui/MiniButton';
@ -12,7 +12,7 @@ interface ToolbarRSExpressionProps {
}
function ToolbarRSExpression({ disabled, showTypeGraph, showAST }: ToolbarRSExpressionProps) {
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
const showControls = usePreferencesStore(state => state.showExpressionControls);
const toggleControls = usePreferencesStore(state => state.toggleShowExpressionControls);

View File

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

View File

@ -3,7 +3,6 @@
import clsx from 'clsx';
import FlexColumn from '@/components/ui/FlexColumn';
import { LibraryItemType } from '@/models/library';
import { useModificationStore } from '@/stores/modification';
import { globals } from '@/utils/constants';
@ -46,7 +45,7 @@ function EditorRSFormCard() {
>
<FlexColumn className='flex-shrink'>
<FormRSForm id={globals.library_item_editor} />
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.RSFORM} controller={controller} />
<EditorLibraryItem controller={controller} />
</FlexColumn>
{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 { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
import { useMutatingRSForm } from '@/backend/rsform/useMutatingRSForm';
import { IconSave } from '@/components/Icons';
import SelectVersion from '@/components/select/SelectVersion';
import Label from '@/components/ui/Label';
@ -31,7 +31,7 @@ function FormRSForm({ id }: FormRSFormProps) {
const schema = controller.schema;
const { updateItem: update } = useUpdateItem();
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const isProcessing = useMutatingRSForm();
const [title, setTitle] = useState(schema.title);
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 { VisibilityIcon } from '@/components/DomainIcons';
import { IconImmutable, IconMutable } from '@/components/Icons';
@ -23,7 +23,7 @@ interface ToolbarItemAccessProps {
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
const role = useRoleStore(state => state.role);
const isProcessing = useIsProcessingLibrary();
const isProcessing = useMutatingLibrary();
const policy = controller.schema.access_policy;
const { setAccessPolicy } = useSetAccessPolicy();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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