R: Move notifications into transport layer

This commit is contained in:
Ivan 2025-01-28 19:45:31 +03:00
parent 421cd98429
commit 54a01b31b3
91 changed files with 894 additions and 892 deletions

View File

@ -4,7 +4,7 @@ export default defineConfig({
testDir: 'tests',
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'html',
reporter: 'list',
projects: [
{
name: 'Desktop Chrome',

View File

@ -10,9 +10,9 @@ interface UserButtonProps {
}
function UserButton({ onLogin, onClickUser }: UserButtonProps) {
const { user } = useAuthSuspense();
const { user, isAnonymous } = useAuthSuspense();
const adminMode = usePreferencesStore(state => state.adminMode);
if (!user) {
if (isAnonymous) {
return (
<NavigationButton
className='cc-fade-in'

View File

@ -1,5 +1,5 @@
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useLogout } from '@/backend/auth/useLogout';
import {
IconAdmin,
@ -29,7 +29,7 @@ interface UserDropdownProps {
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const router = useConceptNavigation();
const { user } = useAuth();
const { user } = useAuthSuspense();
const { logout } = useLogout();
const darkMode = usePreferencesStore(state => state.darkMode);
@ -77,7 +77,7 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
return (
<Dropdown className='mt-[1.5rem] min-w-[18ch] max-w-[12rem]' stretchLeft isOpen={isOpen}>
<DropdownButton
text={user?.username}
text={user.username}
title='Профиль пользователя'
icon={<IconUser size='1rem' />}
onClick={navigateProfile}
@ -94,7 +94,7 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
title='Отображение иконок подсказок'
onClick={toggleShowHelp}
/>
{user?.is_staff ? (
{user.is_staff ? (
<DropdownButton
text={adminMode ? 'Админ: Вкл' : 'Админ: Выкл'}
icon={adminMode ? <IconAdmin size='1rem' /> : <IconAdminOff size='1rem' />}
@ -102,7 +102,7 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
onClick={toggleAdminMode}
/>
) : null}
{user?.is_staff ? (
{user.is_staff ? (
<DropdownButton
text='REST API' // prettier: split-line
icon={<IconRESTapi size='1rem' />}
@ -110,7 +110,7 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
onClick={gotoRestApi}
/>
) : null}
{user?.is_staff ? (
{user.is_staff ? (
<DropdownButton
text='База данных' // prettier: split-line
icon={<IconDatabase size='1rem' />}
@ -124,7 +124,7 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
onClick={gotoIcons}
/>
) : null}
{user?.is_staff ? (
{user.is_staff ? (
<DropdownButton
text='Структура БД' // prettier: split-line
icon={<IconDBStructure size='1rem' />}

View File

@ -1,115 +1,132 @@
/**
* Module: generic API for backend REST communications.
* Module: generic API for backend REST communications using axios library.
*/
import axios from 'axios';
import { AxiosError, AxiosRequestConfig } from 'axios';
import { toast } from 'react-toastify';
import { ErrorData } from '@/components/info/InfoError';
import { buildConstants } from '@/utils/buildConstants';
import { extractErrorMessage } from '@/utils/utils';
import { axiosInstance } from './axiosInstance';
const defaultOptions = {
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'x-csrftoken',
baseURL: `${buildConstants.backend}`,
withCredentials: true
};
const axiosInstance = axios.create(defaultOptions);
axiosInstance.interceptors.request.use(config => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
if (token) {
config.headers['x-csrftoken'] = token;
}
return config;
});
// ================ Data transfer types ================
export type DataCallback<ResponseData = undefined> = (data: ResponseData) => void;
export interface IFrontRequest<RequestData, ResponseData> {
data?: RequestData;
onSuccess?: DataCallback<ResponseData>;
onError?: (error: ErrorData) => void;
setLoading?: (loading: boolean) => void;
showError?: boolean;
successMessage?: string | ((data: ResponseData) => string);
}
export interface FrontPush<DataType> extends IFrontRequest<DataType, undefined> {
data: DataType;
}
export interface FrontPull<DataType> extends IFrontRequest<undefined, DataType> {
onSuccess: DataCallback<DataType>;
}
export interface FrontExchange<RequestData, ResponseData> extends IFrontRequest<RequestData, ResponseData> {
data: RequestData;
onSuccess: DataCallback<ResponseData>;
}
export interface FrontAction extends IFrontRequest<undefined, undefined> {}
export interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string;
request: IFrontRequest<RequestData, ResponseData>;
request?: IFrontRequest<RequestData, ResponseData>;
options?: AxiosRequestConfig;
}
export interface IAxiosGetRequest {
endpoint: string;
options?: AxiosRequestConfig;
signal?: AbortSignal;
}
// ================ Transport API calls ================
export function AxiosGet<ResponseData>({ endpoint, request, options }: IAxiosRequest<undefined, ResponseData>) {
request.setLoading?.(true);
axiosInstance
export function axiosGet<ResponseData>({ endpoint, options }: IAxiosGetRequest) {
return axiosInstance
.get<ResponseData>(endpoint, options)
.then(response => {
request.setLoading?.(false);
request.onSuccess?.(response.data);
})
.then(response => response.data)
.catch((error: Error | AxiosError) => {
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
request.onError?.(error);
toast.error(extractErrorMessage(error));
console.error(error);
throw error;
});
}
export function AxiosPost<RequestData, ResponseData>({
export function axiosPost<RequestData, ResponseData = void>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
request.setLoading?.(true);
axiosInstance
.post<ResponseData>(endpoint, request.data, options)
return axiosInstance
.post<ResponseData>(endpoint, request?.data, options)
.then(response => {
request.setLoading?.(false);
request.onSuccess?.(response.data);
})
.catch((error: Error | AxiosError) => {
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
request.onError?.(error);
});
}
export function AxiosDelete<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
request.setLoading?.(true);
axiosInstance
.delete<ResponseData>(endpoint, options)
.then(response => {
request.setLoading?.(false);
request.onSuccess?.(response.data);
})
.catch((error: Error | AxiosError) => {
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
request.onError?.(error);
});
}
export function AxiosPatch<RequestData, ResponseData>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
request.setLoading?.(true);
axiosInstance
.patch<ResponseData>(endpoint, request.data, options)
.then(response => {
request.setLoading?.(false);
request.onSuccess?.(response.data);
if (request?.successMessage) {
if (typeof request.successMessage === 'string') {
toast.success(request.successMessage);
} else {
toast.success(request.successMessage(response.data));
}
}
return response.data;
})
.catch((error: Error | AxiosError) => {
request.setLoading?.(false);
if (request.showError) toast.error(extractErrorMessage(error));
request.onError?.(error);
toast.error(extractErrorMessage(error));
console.error(error);
throw error;
});
}
export function axiosDelete<RequestData, ResponseData = void>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
return axiosInstance
.delete<ResponseData>(endpoint, options)
.then(response => {
if (request?.successMessage) {
if (typeof request.successMessage === 'string') {
toast.success(request.successMessage);
} else {
toast.success(request.successMessage(response.data));
}
}
return response.data;
})
.catch((error: Error | AxiosError) => {
toast.error(extractErrorMessage(error));
console.error(error);
throw error;
});
}
export function axiosPatch<RequestData, ResponseData = void>({
endpoint,
request,
options
}: IAxiosRequest<RequestData, ResponseData>) {
return axiosInstance
.patch<ResponseData>(endpoint, request?.data, options)
.then(response => {
if (request?.successMessage) {
if (typeof request.successMessage === 'string') {
toast.success(request.successMessage);
} else {
toast.success(request.successMessage(response.data));
}
}
return response.data;
})
.catch((error: Error | AxiosError) => {
toast.error(extractErrorMessage(error));
console.error(error);
throw error;
});
}

View File

@ -1,8 +1,10 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { ICurrentUser } from '@/models/user';
import { information } from '@/utils/labels';
import { axiosGet, axiosPost } from '../apiTransport';
/**
* Represents login data, used to authenticate users.
@ -53,19 +55,41 @@ export const authApi = {
queryKey: [authApi.baseKey, 'user'],
staleTime: DELAYS.staleLong,
queryFn: meta =>
axiosInstance
.get<ICurrentUser>('/users/api/auth', {
signal: meta.signal
})
.then(response => (response.data.id === null ? null : response.data)),
placeholderData: null
axiosGet<ICurrentUser>({
endpoint: '/users/api/auth',
options: { signal: meta.signal }
})
});
},
logout: () => axiosInstance.post('/users/api/logout'),
login: (data: IUserLoginDTO) => axiosInstance.post('/users/api/login', data),
changePassword: (data: IChangePasswordDTO) => axiosInstance.post('/users/api/change-password', data),
requestPasswordReset: (data: IRequestPasswordDTO) => axiosInstance.post('/users/api/password-reset', data),
validatePasswordToken: (data: IPasswordTokenDTO) => axiosInstance.post('/users/api/password-reset/validate', data),
resetPassword: (data: IResetPasswordDTO) => axiosInstance.post('/users/api/password-reset/confirm', data)
logout: () => axiosPost({ endpoint: '/users/api/logout' }),
login: (data: IUserLoginDTO) =>
axiosPost({
endpoint: '/users/api/login',
request: { data: data }
}),
changePassword: (data: IChangePasswordDTO) =>
axiosPost({
endpoint: '/users/api/change-password',
request: {
data: data,
successMessage: information.changesSaved
}
}),
requestPasswordReset: (data: IRequestPasswordDTO) =>
axiosPost({
endpoint: '/users/api/password-reset',
request: { data: data }
}),
validatePasswordToken: (data: IPasswordTokenDTO) =>
axiosPost({
endpoint: '/users/api/password-reset/validate',
request: { data: data }
}),
resetPassword: (data: IResetPasswordDTO) =>
axiosPost({
endpoint: '/users/api/password-reset/confirm',
request: { data: data }
})
};

View File

@ -10,12 +10,12 @@ export function useAuth() {
} = useQuery({
...authApi.getAuthQueryOptions()
});
return { user, isLoading, error };
return { user, isLoading, isAnonymous: user?.id === null || user === undefined, error };
}
export function useAuthSuspense() {
const { data: user } = useSuspenseQuery({
...authApi.getAuthQueryOptions()
});
return { user };
return { user, isAnonymous: user.id === null };
}

View File

@ -1,25 +0,0 @@
/**
* Module: communication setup.
*/
import axios from 'axios';
import { buildConstants } from '@/utils/buildConstants';
const defaultOptions = {
xsrfCookieName: 'csrftoken',
xsrfHeaderName: 'x-csrftoken',
baseURL: `${buildConstants.backend}`,
withCredentials: true
};
export const axiosInstance = axios.create(defaultOptions);
axiosInstance.interceptors.request.use(config => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='))
?.split('=')[1];
if (token) {
config.headers['x-csrftoken'] = token;
}
return config;
});

View File

@ -1,6 +1,7 @@
import { axiosInstance } from '@/backend/axiosInstance';
import { ILexemeData, IWordFormPlain } from '@/models/language';
import { axiosPost } from '../apiTransport';
/**
* Represents API result for text output.
*/
@ -12,15 +13,18 @@ export const cctextApi = {
baseKey: 'cctext',
inflectText: (data: IWordFormPlain) =>
axiosInstance //
.post<ITextResult>('/api/cctext/inflect', data)
.then(response => response.data),
axiosPost<IWordFormPlain, ITextResult>({
endpoint: '/api/cctext/inflect',
request: { data: data }
}),
parseText: (data: { text: string }) =>
axiosInstance //
.post<ITextResult>('/api/cctext/parse', data)
.then(response => response.data),
axiosPost<{ text: string }, ITextResult>({
endpoint: '/api/cctext/parse',
request: { data: data }
}),
generateLexeme: (data: { text: string }) =>
axiosInstance //
.post<ILexemeData>('/api/cctext/generate-lexeme', data)
.then(response => response.data)
axiosPost<{ text: string }, ILexemeData>({
endpoint: '/api/cctext/generate-lexeme',
request: { data: data }
})
};

View File

@ -1,11 +1,20 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
import {
AccessPolicy,
ILibraryItem,
IVersionData,
IVersionInfo,
LibraryItemID,
LibraryItemType,
VersionID
} from '@/models/library';
import { ConstituentaID, IRSFormData } from '@/models/rsform';
import { UserID } from '@/models/user';
import { information } from '@/utils/labels';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '../apiTransport';
import { ossApi } from '../oss/api';
import { rsformsApi } from '../rsform/api';
@ -59,87 +68,140 @@ export const libraryApi = {
baseKey: 'library',
libraryListKey: ['library', 'list'],
getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) =>
queryOptions({
queryKey: libraryApi.libraryListKey,
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosInstance
.get<ILibraryItem[]>(isAdmin ? '/api/library/all' : '/api/library/active', {
signal: meta.signal
})
.then(response => response.data)
}),
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
return itemType === LibraryItemType.RSFORM
? rsformsApi.getRSFormQueryOptions({ itemID })
: ossApi.getOssQueryOptions({ itemID });
},
getLibraryQueryOptions: ({ isAdmin }: { isAdmin: boolean }) =>
queryOptions({
queryKey: libraryApi.libraryListKey,
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosGet<ILibraryItem[]>({
endpoint: isAdmin ? '/api/library/all' : '/api/library/active',
options: { signal: meta.signal }
})
}),
getTemplatesQueryOptions: () =>
queryOptions({
queryKey: [libraryApi.baseKey, 'templates'],
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosInstance
.get<ILibraryItem[]>('/api/library/templates', {
signal: meta.signal
})
.then(response => response.data)
axiosGet<ILibraryItem[]>({
endpoint: '/api/library/templates',
options: { signal: meta.signal }
})
}),
createItem: (data: ILibraryCreateDTO) =>
data.file
? axiosInstance
.post<ILibraryItem>('/api/rsforms/create-detailed', data, {
axiosPost<ILibraryCreateDTO, ILibraryItem>({
endpoint: !data.file ? '/api/library' : '/api/rsforms/create-detailed',
request: {
data: data,
successMessage: information.newLibraryItem
},
options: !data.file
? undefined
: {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => response.data)
: axiosInstance //
.post<ILibraryItem>('/api/library', data)
.then(response => response.data),
}
}),
updateItem: (data: ILibraryUpdateDTO) =>
axiosInstance //
.patch<ILibraryItem>(`/api/library/${data.id}`, data)
.then(response => response.data),
setOwner: (data: { itemID: LibraryItemID; owner: UserID }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-owner`, { user: data.owner }),
setLocation: (data: { itemID: LibraryItemID; location: string }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-location`, { location: data.location }),
setAccessPolicy: (data: { itemID: LibraryItemID; policy: AccessPolicy }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-access-policy`, { access_policy: data.policy }),
setEditors: (data: { itemID: LibraryItemID; editors: UserID[] }) =>
axiosInstance //
.patch(`/api/library/${data.itemID}/set-editors`, { users: data.editors }),
axiosPatch<ILibraryUpdateDTO, ILibraryItem>({
endpoint: `/api/library/${data.id}`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
setOwner: ({ itemID, owner }: { itemID: LibraryItemID; owner: UserID }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-owner`,
request: {
data: { user: owner },
successMessage: information.changesSaved
}
}),
setLocation: ({ itemID, location }: { itemID: LibraryItemID; location: string }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-location`,
request: {
data: { location: location },
successMessage: information.moveComplete
}
}),
setAccessPolicy: ({ itemID, policy }: { itemID: LibraryItemID; policy: AccessPolicy }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-access-policy`,
request: {
data: { access_policy: policy },
successMessage: information.changesSaved
}
}),
setEditors: ({ itemID, editors }: { itemID: LibraryItemID; editors: UserID[] }) =>
axiosPatch({
endpoint: `/api/library/${itemID}/set-editors`,
request: {
data: { users: editors },
successMessage: information.changesSaved
}
}),
deleteItem: (target: LibraryItemID) =>
axiosInstance //
.delete(`/api/library/${target}`),
axiosDelete({
endpoint: `/api/library/${target}`,
request: {
successMessage: information.itemDestroyed
}
}),
cloneItem: (data: IRSFormCloneDTO) =>
axiosInstance //
.post<IRSFormData>(`/api/library/${data.id}/clone`, data)
.then(response => response.data),
axiosPost<IRSFormCloneDTO, IRSFormData>({
endpoint: `/api/library/${data.id}/clone`,
request: {
data: data,
successMessage: newSchema => information.cloneComplete(newSchema.alias)
}
}),
renameLocation: (data: IRenameLocationDTO) =>
axiosInstance //
.patch('/api/library/rename-location', data),
axiosPatch({
endpoint: '/api/library/rename-location',
request: {
data: data,
successMessage: information.locationRenamed
}
}),
versionCreate: (data: { itemID: LibraryItemID; data: IVersionData }) =>
axiosInstance //
.post<IVersionCreatedResponse>(`/api/library/${data.itemID}/versions`, data.data)
.then(response => response.data),
versionRestore: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
axiosInstance //
.patch<IRSFormData>(`/api/versions/${data.versionID}/restore`)
.then(response => response.data),
versionUpdate: (data: { itemID: LibraryItemID; versionID: VersionID; data: IVersionData }) =>
axiosInstance //
.patch(`/api/versions/${data.versionID}`, data.data),
versionCreate: ({ itemID, data }: { itemID: LibraryItemID; data: IVersionData }) =>
axiosPost<IVersionData, IVersionCreatedResponse>({
endpoint: `/api/library/${itemID}/create-version`,
request: {
data: data,
successMessage: information.newVersion(data.version)
}
}),
versionRestore: ({ versionID }: { versionID: VersionID }) =>
axiosPatch<undefined, IRSFormData>({
endpoint: `/api/versions/${versionID}/restore`,
request: {
successMessage: information.versionRestored
}
}),
versionUpdate: ({ versionID, data }: { versionID: VersionID; data: IVersionData }) =>
axiosPatch<IVersionData, IVersionInfo>({
endpoint: `/api/versions/${versionID}`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
versionDelete: (data: { itemID: LibraryItemID; versionID: VersionID }) =>
axiosInstance //
.delete(`/api/versions/${data.versionID}`)
axiosDelete({
endpoint: `/api/versions/${data.versionID}`,
request: {
successMessage: information.versionDestroyed
}
})
};

View File

@ -16,7 +16,7 @@ export function useLibrary() {
// NOTE: Using suspense here to avoid duplicated library data requests
const { user } = useAuthSuspense();
const { data: items, isLoading } = useQuery({
...libraryApi.getLibraryQueryOptions({ isAdmin: user?.is_staff ?? false })
...libraryApi.getLibraryQueryOptions({ isAdmin: user.is_staff })
});
return { items: items ?? [], isLoading };
}

View File

@ -30,12 +30,6 @@ export const useSetAccessPolicy = () => {
});
return {
setAccessPolicy: (
data: {
itemID: LibraryItemID; //
policy: AccessPolicy;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
setAccessPolicy: (data: { itemID: LibraryItemID; policy: AccessPolicy }) => mutation.mutate(data)
};
};

View File

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

View File

@ -11,6 +11,28 @@ 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))
// );
// }
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === variables.itemID ? { ...item, location: variables.location } : item))
);

View File

@ -31,12 +31,6 @@ export const useSetOwner = () => {
});
return {
setOwner: (
data: {
itemID: LibraryItemID; //
owner: UserID;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
setOwner: (data: { itemID: LibraryItemID; owner: UserID }) => mutation.mutate(data)
};
};

View File

@ -1,6 +1,5 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { rsformsApi } from '@/backend/rsform/api';
import { ILibraryItem } from '@/models/library';
@ -13,7 +12,7 @@ export const useUpdateItem = () => {
mutationFn: libraryApi.updateItem,
onSuccess: (data: ILibraryItem) => {
client
.cancelQueries({ queryKey: [libraryApi.libraryListKey] })
.cancelQueries({ queryKey: libraryApi.libraryListKey })
.then(async () => {
client.setQueryData(libraryApi.libraryListKey, (prev: ILibraryItem[] | undefined) =>
prev?.map(item => (item.id === data.id ? data : item))
@ -26,9 +25,6 @@ export const useUpdateItem = () => {
}
});
return {
updateItem: (
data: ILibraryUpdateDTO, //
onSuccess?: DataCallback<ILibraryItem>
) => mutation.mutate(data, { onSuccess })
updateItem: (data: ILibraryUpdateDTO) => mutation.mutate(data)
};
};

View File

@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { rsformsApi } from '@/backend/rsform/api';
import { IVersionData, LibraryItemID } from '@/models/library';
import { IVersionData, LibraryItemID, VersionID } from '@/models/library';
import { libraryApi } from './api';
@ -14,7 +14,7 @@ export const useVersionCreate = () => {
mutationKey: [libraryApi.baseKey, 'create-version'],
mutationFn: libraryApi.versionCreate,
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);
}
});
@ -24,7 +24,7 @@ export const useVersionCreate = () => {
itemID: LibraryItemID; //
data: IVersionData;
},
onSuccess?: DataCallback<IVersionData>
) => mutation.mutate(data, { onSuccess: () => onSuccess?.(data.data) })
onSuccess?: DataCallback<VersionID>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.version) })
};
};

View File

@ -13,11 +13,14 @@ export const useVersionDelete = () => {
mutationFn: libraryApi.versionDelete,
onSuccess: (_, variables) => {
client.setQueryData(
[rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey],
(prev: IRSFormData) => ({
...prev,
versions: prev.versions.filter(version => version.id !== variables.versionID)
})
rsformsApi.getRSFormQueryOptions({ itemID: variables.itemID }).queryKey,
(prev: IRSFormData | undefined) =>
!prev
? undefined
: {
...prev,
versions: prev.versions.filter(version => version.id !== variables.versionID)
}
);
}
});

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration';
import { ILibraryItem, LibraryItemID } from '@/models/library';
import {
@ -12,6 +12,7 @@ import {
OperationType
} from '@/models/oss';
import { ConstituentaID, IConstituentaReference, ITargetCst } from '@/models/rsform';
import { information } from '@/utils/labels';
/**
* Represents {@link IOperation} data, used in creation process.
@ -101,48 +102,90 @@ export const ossApi = {
queryFn: meta =>
!itemID
? undefined
: axiosInstance
.get<IOperationSchemaData>(`/api/oss/${itemID}/details`, {
signal: meta.signal
})
.then(response => response.data)
: axiosGet<IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/details`,
options: { signal: meta.signal }
})
});
},
updatePositions: (data: { itemID: LibraryItemID; positions: IOperationPosition[] }) =>
axiosInstance //
.patch(`/api/oss/${data.itemID}/update-positions`, { positions: data.positions }),
operationCreate: (data: { itemID: LibraryItemID; data: IOperationCreateDTO }) =>
axiosInstance //
.post<IOperationCreatedResponse>(`/api/oss/${data.itemID}/create-operation`, data.data)
.then(response => response.data),
operationDelete: (data: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
axiosInstance //
.patch<IOperationSchemaData>(`/api/oss/${data.itemID}/delete-operation`, data.data)
.then(response => response.data),
inputCreate: (data: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosInstance //
.patch<IInputCreatedResponse>(`/api/oss/${data.itemID}/create-input`, data.data)
.then(response => response.data),
inputUpdate: (data: { itemID: LibraryItemID; data: IInputUpdateDTO }) =>
axiosInstance //
.patch<IOperationSchemaData>(`/api/oss/${data.itemID}/set-input`, data.data)
.then(response => response.data),
operationUpdate: (data: { itemID: LibraryItemID; data: IOperationUpdateDTO }) =>
axiosInstance //
.patch<IOperationSchemaData>(`/api/oss/${data.itemID}/update-operation`, data.data)
.then(response => response.data),
operationExecute: (data: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosInstance //
.post<IOperationSchemaData>(`/api/oss/${data.itemID}/execute-operation`, data.data)
.then(response => response.data),
updatePositions: ({
itemID,
positions,
isSilent
}: {
itemID: LibraryItemID;
positions: IOperationPosition[];
isSilent?: boolean;
}) =>
axiosPatch({
endpoint: `/api/oss/${itemID}/update-positions`,
request: {
data: { positions: positions },
successMessage: isSilent ? undefined : information.changesSaved
}
}),
relocateConstituents: (data: { itemID: LibraryItemID; data: ICstRelocateDTO }) =>
axiosInstance //
.post<IOperationSchemaData>(`/api/oss/${data.itemID}/relocate-constituents`, data.data)
.then(response => response.data),
operationCreate: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationCreateDTO }) =>
axiosPost<IOperationCreateDTO, IOperationCreatedResponse>({
endpoint: `/api/oss/${itemID}/create-operation`,
request: {
data: data,
successMessage: response => information.newOperation(response.new_operation.alias)
}
}),
operationDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationDeleteDTO }) =>
axiosDelete<IOperationDeleteDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/delete-operation`,
request: {
data: data,
successMessage: information.operationDestroyed
}
}),
inputCreate: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosPatch<ITargetOperation, IInputCreatedResponse>({
endpoint: `/api/oss/${itemID}/create-input`,
request: {
data: data,
successMessage: information.newLibraryItem
}
}),
inputUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IInputUpdateDTO }) =>
axiosPatch<IInputUpdateDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/set-input`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
operationUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: IOperationUpdateDTO }) =>
axiosPatch<IOperationUpdateDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/update-operation`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
operationExecute: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetOperation }) =>
axiosPost<ITargetOperation, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/execute-operation`,
request: {
data: data,
successMessage: information.operationExecuted
}
}),
relocateConstituents: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRelocateDTO }) =>
axiosPost<ICstRelocateDTO, IOperationSchemaData>({
endpoint: `/api/oss/${itemID}/relocate-constituents`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
getPredecessor: (data: ITargetCst) =>
axiosInstance //
.post<IConstituentaReference>(`/api/oss/get-predecessor`, data)
.then(response => response.data)
axiosPost<ITargetCst, IConstituentaReference>({
endpoint: '/api/oss/get-predecessor',
request: { data: data }
})
};

View File

@ -12,7 +12,7 @@ export const useInputCreate = () => {
mutationKey: [ossApi.baseKey, 'input-create'],
mutationFn: ossApi.inputCreate,
onSuccess: async 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] });
}
});

View File

@ -11,17 +11,11 @@ export const useInputUpdate = () => {
mutationKey: [ossApi.baseKey, 'input-update'],
mutationFn: ossApi.inputUpdate,
onSuccess: async 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 {
inputUpdate: (
data: {
itemID: LibraryItemID; //
data: IInputUpdateDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
inputUpdate: (data: { itemID: LibraryItemID; data: IInputUpdateDTO }) => mutation.mutate(data)
};
};

View File

@ -14,7 +14,7 @@ export const useOperationCreate = () => {
mutationKey: [ossApi.baseKey, 'operation-create'],
mutationFn: ossApi.operationCreate,
onSuccess: data => {
client.setQueryData([ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey], data.oss);
client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
updateTimestamp(data.oss.id);
}
});

View File

@ -11,17 +11,11 @@ export const useOperationDelete = () => {
mutationKey: [ossApi.baseKey, 'operation-delete'],
mutationFn: ossApi.operationDelete,
onSuccess: async 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 {
operationDelete: (
data: {
itemID: LibraryItemID; //
data: IOperationDeleteDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
operationDelete: (data: { itemID: LibraryItemID; data: IOperationDeleteDTO }) => mutation.mutate(data)
};
};

View File

@ -11,17 +11,11 @@ export const useOperationExecute = () => {
mutationKey: [ossApi.baseKey, 'operation-execute'],
mutationFn: ossApi.operationExecute,
onSuccess: async 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 {
operationExecute: (
data: {
itemID: LibraryItemID; //
data: ITargetOperation;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
operationExecute: (data: { itemID: LibraryItemID; data: ITargetOperation }) => mutation.mutate(data)
};
};

View File

@ -11,17 +11,11 @@ export const useOperationUpdate = () => {
mutationKey: [ossApi.baseKey, 'operation-update'],
mutationFn: ossApi.operationUpdate,
onSuccess: async 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 {
operationUpdate: (
data: {
itemID: LibraryItemID; //
data: IOperationUpdateDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
operationUpdate: (data: { itemID: LibraryItemID; data: IOperationUpdateDTO }) => mutation.mutate(data)
};
};

View File

@ -11,17 +11,11 @@ export const useRelocateConstituents = () => {
mutationKey: [ossApi.baseKey, 'relocate-constituents'],
mutationFn: ossApi.relocateConstituents,
onSuccess: async 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 {
relocateConstituents: (
data: {
itemID: LibraryItemID; //
data: ICstRelocateDTO;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })
relocateConstituents: (data: { itemID: LibraryItemID; data: ICstRelocateDTO }) => mutation.mutate(data)
};
};

View File

@ -18,6 +18,7 @@ export const useUpdatePositions = () => {
data: {
itemID: LibraryItemID; //
positions: IOperationPosition[];
isSilent?: boolean;
},
onSuccess?: () => void
) => mutation.mutate(data, { onSuccess })

View File

@ -1,6 +1,6 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { axiosDelete, axiosGet, axiosPatch, axiosPost } from '@/backend/apiTransport';
import { DELAYS } from '@/backend/configuration';
import { LibraryItemID, VersionID } from '@/models/library';
import { ICstSubstitute, ICstSubstitutions } from '@/models/oss';
@ -14,6 +14,7 @@ import {
TermForm
} from '@/models/rsform';
import { IExpressionParse } from '@/models/rslang';
import { information } from '@/utils/labels';
/**
* Represents data, used for uploading {@link IRSForm} as file.
@ -116,76 +117,108 @@ export const rsformsApi = {
queryFn: meta =>
!itemID
? undefined
: axiosInstance
.get<IRSFormData>(
version ? `/api/library/${itemID}/versions/${version}` : `/api/rsforms/${itemID}/details`,
{
signal: meta.signal
}
)
.then(response => response.data)
: axiosGet<IRSFormData>({
endpoint: version ? `/api/library/${itemID}/versions/${version}` : `/api/rsforms/${itemID}/details`,
options: { signal: meta.signal }
})
});
},
download: ({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) =>
axiosInstance //
.get<Blob>(version ? `/api/versions/${version}/export-file` : `/api/rsforms/${itemID}/export-trs`, {
responseType: 'blob'
})
.then(response => response.data),
axiosGet<Blob>({
endpoint: version ? `/api/versions/${version}/export-file` : `/api/rsforms/${itemID}/export-trs`,
options: { responseType: 'blob' }
}),
upload: (data: IRSFormUploadDTO) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${data.itemID}/load-trs`, data, {
axiosPatch<IRSFormUploadDTO, IRSFormData>({
endpoint: `/api/rsforms/${data.itemID}/load-trs`,
request: {
data: data,
successMessage: information.uploadSuccess
},
options: {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => response.data),
}
}),
cstCreate: ({ itemID, data }: { itemID: LibraryItemID; data: ICstCreateDTO }) =>
axiosInstance //
.post<ICstCreatedResponse>(`/api/rsforms/${itemID}/create-cst`, data)
.then(response => response.data),
axiosPost<ICstCreateDTO, ICstCreatedResponse>({
endpoint: `/api/rsforms/${itemID}/create-cst`,
request: {
data: data,
successMessage: response => information.newConstituent(response.new_cst.alias)
}
}),
cstUpdate: ({ itemID, data }: { itemID: LibraryItemID; data: ICstUpdateDTO }) =>
axiosInstance //
.patch<IConstituentaMeta>(`/api/rsforms/${itemID}/update-cst`, data)
.then(response => response.data),
axiosPatch<ICstUpdateDTO, IConstituentaMeta>({
endpoint: `/api/rsforms/${itemID}/update-cst`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
cstDelete: ({ itemID, data }: { itemID: LibraryItemID; data: IConstituentaList }) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/delete-multiple-cst`, data)
.then(response => response.data),
axiosDelete<IConstituentaList, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/delete-multiple-cst`,
request: {
data: data,
successMessage: information.constituentsDestroyed(data.items.length)
}
}),
cstRename: ({ itemID, data }: { itemID: LibraryItemID; data: ICstRenameDTO }) =>
axiosInstance //
.patch<ICstCreatedResponse>(`/api/rsforms/${itemID}/rename-cst`, data)
.then(response => response.data),
axiosPatch<ICstRenameDTO, ICstCreatedResponse>({
endpoint: `/api/rsforms/${itemID}/rename-cst`,
request: {
data: data,
successMessage: information.changesSaved
}
}),
cstSubstitute: ({ itemID, data }: { itemID: LibraryItemID; data: ICstSubstitutions }) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/substitute`, data)
.then(response => response.data),
axiosPatch<ICstSubstitutions, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/substitute`,
request: {
data: data,
successMessage: information.substituteSingle
}
}),
cstMove: ({ itemID, data }: { itemID: LibraryItemID; data: ICstMoveDTO }) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/move-cst`, data)
.then(response => response.data),
axiosPatch<ICstMoveDTO, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/move-cst`,
request: { data: data }
}),
produceStructure: ({ itemID, data }: { itemID: LibraryItemID; data: ITargetCst }) =>
axiosInstance //
.post<IProduceStructureResponse>(`/api/rsforms/${itemID}/produce-structure`, data)
.then(response => response.data),
axiosPost<ITargetCst, IProduceStructureResponse>({
endpoint: `/api/rsforms/${itemID}/produce-structure`,
request: {
data: data,
successMessage: response => information.addedConstituents(response.cst_list.length)
}
}),
inlineSynthesis: ({ itemID, data }: { itemID: LibraryItemID; data: IInlineSynthesisDTO }) =>
axiosInstance //
.post<IRSFormData>(`/api/rsforms/${itemID}/inline-synthesis`, data)
.then(response => response.data),
restoreOrder: (itemID: LibraryItemID) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/restore-order`)
.then(response => response.data),
resetAliases: (itemID: LibraryItemID) =>
axiosInstance //
.patch<IRSFormData>(`/api/rsforms/${itemID}/reset-aliases`)
.then(response => response.data),
axiosPost<IInlineSynthesisDTO, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/inline-synthesis`,
request: {
data: data,
successMessage: information.inlineSynthesisComplete
}
}),
restoreOrder: ({ itemID }: { itemID: LibraryItemID }) =>
axiosPatch<undefined, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/restore-order`,
request: { successMessage: information.reorderComplete }
}),
resetAliases: ({ itemID }: { itemID: LibraryItemID }) =>
axiosPatch<undefined, IRSFormData>({
endpoint: `/api/rsforms/${itemID}/reset-aliases`,
request: { successMessage: information.reindexComplete }
}),
checkConstituenta: ({ itemID, data }: { itemID: LibraryItemID; data: ICheckConstituentaDTO }) =>
axiosInstance //
.post<IExpressionParse>(`/api/rsforms/${itemID}/check-constituenta`, data)
.then(response => response.data)
axiosPost<ICheckConstituentaDTO, IExpressionParse>({
endpoint: `/api/rsforms/${itemID}/check-constituenta`,
request: { data: data }
})
};

View File

@ -14,7 +14,7 @@ export const useCstCreate = () => {
mutationKey: [rsformsApi.baseKey, 'create-cst'],
mutationFn: rsformsApi.cstCreate,
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);
// TODO: invalidate OSS?
}

View File

@ -13,7 +13,7 @@ export const useCstDelete = () => {
mutationKey: [rsformsApi.baseKey, 'delete-multiple-cst'],
mutationFn: rsformsApi.cstDelete,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}

View File

@ -12,7 +12,7 @@ export const useCstMove = () => {
mutationKey: [rsformsApi.baseKey, 'move-cst'],
mutationFn: rsformsApi.cstMove,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}

View File

@ -2,9 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform';
import { DataCallback } from '../apiTransport';
import { ICstRenameDTO, rsformsApi } from './api';
export const useCstRename = () => {
@ -14,18 +12,12 @@ export const useCstRename = () => {
mutationKey: [rsformsApi.baseKey, 'rename-cst'],
mutationFn: rsformsApi.cstRename,
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);
// TODO: invalidate OSS?
}
});
return {
cstRename: (
data: {
itemID: LibraryItemID; //
data: ICstRenameDTO;
},
onSuccess?: DataCallback<IConstituentaMeta>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.new_cst) })
cstRename: (data: { itemID: LibraryItemID; data: ICstRenameDTO }) => mutation.mutate(data)
};
};

View File

@ -13,7 +13,7 @@ export const useCstSubstitute = () => {
mutationKey: [rsformsApi.baseKey, 'substitute-cst'],
mutationFn: rsformsApi.cstSubstitute,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}

View File

@ -1,9 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { DataCallback } from '@/backend/apiTransport';
import { useUpdateTimestamp } from '@/backend/library/useUpdateTimestamp';
import { LibraryItemID } from '@/models/library';
import { IConstituentaMeta } from '@/models/rsform';
import { ICstUpdateDTO, rsformsApi } from './api';
@ -22,12 +20,6 @@ export const useCstUpdate = () => {
}
});
return {
cstUpdate: (
data: {
itemID: LibraryItemID; //
data: ICstUpdateDTO;
},
onSuccess?: DataCallback<IConstituentaMeta>
) => mutation.mutate(data, { onSuccess })
cstUpdate: (data: { itemID: LibraryItemID; data: ICstUpdateDTO }) => mutation.mutate(data)
};
};

View File

@ -14,7 +14,7 @@ export const useInlineSynthesis = () => {
mutationKey: [rsformsApi.baseKey, 'inline-synthesis'],
mutationFn: rsformsApi.inlineSynthesis,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}

View File

@ -14,7 +14,7 @@ export const useProduceStructure = () => {
mutationKey: [rsformsApi.baseKey, 'produce-structure'],
mutationFn: rsformsApi.produceStructure,
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);
// TODO: invalidate OSS?
}

View File

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

View File

@ -12,15 +12,12 @@ export const useResetAliases = () => {
mutationKey: [rsformsApi.baseKey, 'reset-aliases'],
mutationFn: rsformsApi.resetAliases,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id);
// TODO: invalidate OSS?
}
});
return {
resetAliases: (
itemID: LibraryItemID, //
onSuccess?: () => void
) => mutation.mutate(itemID, { onSuccess })
resetAliases: (data: { itemID: LibraryItemID }) => mutation.mutate(data)
};
};

View File

@ -12,14 +12,11 @@ export const useRestoreOrder = () => {
mutationKey: [rsformsApi.baseKey, 'restore-order'],
mutationFn: rsformsApi.restoreOrder,
onSuccess: data => {
client.setQueryData([rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey], data);
client.setQueryData(rsformsApi.getRSFormQueryOptions({ itemID: data.id }).queryKey, data);
updateTimestamp(data.id);
}
});
return {
restoreOrder: (
itemID: LibraryItemID, //
onSuccess?: () => void
) => mutation.mutate(itemID, { onSuccess })
restoreOrder: (data: { itemID: LibraryItemID }) => mutation.mutate(data)
};
};

View File

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

View File

@ -1,8 +1,10 @@
import { queryOptions } from '@tanstack/react-query';
import { axiosInstance } from '@/backend/axiosInstance';
import { DELAYS } from '@/backend/configuration';
import { IUser, IUserInfo, IUserProfile, IUserSignupData } from '@/models/user';
import { information } from '@/utils/labels';
import { axiosGet, axiosPatch, axiosPost } from '../apiTransport';
/**
* Represents user data, intended to update user profile in persistent storage.
@ -16,24 +18,37 @@ export const usersApi = {
queryKey: [usersApi.baseKey, 'list'],
staleTime: DELAYS.staleMedium,
queryFn: meta =>
axiosInstance
.get<IUserInfo[]>('/users/api/active-users', {
signal: meta.signal
})
.then(response => response.data)
axiosGet<IUserInfo[]>({
endpoint: '/users/api/active-users',
options: { signal: meta.signal }
})
}),
getProfileQueryOptions: () =>
queryOptions({
queryKey: [usersApi.baseKey, 'profile'],
staleTime: DELAYS.staleShort,
queryFn: meta =>
axiosInstance
.get<IUserProfile>('/users/api/profile', {
signal: meta.signal
})
.then(response => response.data)
axiosGet<IUserProfile>({
endpoint: '/users/api/profile',
options: { signal: meta.signal }
})
}),
signup: (data: IUserSignupData) => axiosInstance.post('/users/api/signup', data),
updateProfile: (data: IUpdateProfileDTO) => axiosInstance.patch('/users/api/profile', data)
signup: (data: IUserSignupData) =>
axiosPost<IUserSignupData, IUserProfile>({
endpoint: '/users/api/signup',
request: {
data: data,
successMessage: createdUser => information.newUser(createdUser.username)
}
}),
updateProfile: (data: IUpdateProfileDTO) =>
axiosPatch<IUpdateProfileDTO, IUserProfile>({
endpoint: '/users/api/profile',
request: {
data: data,
successMessage: information.changesSaved
}
})
};

View File

@ -15,7 +15,7 @@ export const useSignup = () => {
signup: (
data: IUserSignupData, //
onSuccess?: DataCallback<IUserProfile>
) => mutation.mutate(data, { onSuccess: response => onSuccess?.(response.data as IUserProfile) }),
) => mutation.mutate(data, { onSuccess }),
isPending: mutation.isPending,
error: mutation.error,
reset: mutation.reset

View File

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

View File

@ -31,14 +31,13 @@ function SelectUser({
const getUserLabel = useLabelUser();
const items = filter ? users.filter(user => filter(user.id)) : users;
const options =
items?.map(user => ({
value: user.id,
label: getUserLabel(user.id)
})) ?? [];
const options = items.map(user => ({
value: user.id,
label: getUserLabel(user.id)
}));
function filterLabel(option: { value: UserID | undefined; label: string }, inputValue: string) {
const user = items?.find(item => item.id === option.value);
const user = items.find(item => item.id === option.value);
return !user ? false : matchUser(user, inputValue);
}

View File

@ -79,23 +79,23 @@ function SelectTree<ItemType>({
}
return (
<div {...restProps}>
<div tabIndex={-1} {...restProps}>
{items.map((item, index) => {
const isActive = getParent(item) === item || !folded.includes(getParent(item));
return (
<div
tabIndex={-1}
key={`${prefix}${index}`}
className={clsx(
'pr-3 pl-6 border-b',
'cc-scroll-row',
'bg-prim-200 clr-hover cc-animate-color',
'cursor-pointer',
value === item && 'clr-selected'
value === item && 'clr-selected',
!isActive && 'pointer-events-none'
)}
data-tooltip-id={globals.tooltip}
data-tooltip-html={isActive ? getDescription(item) : undefined}
onClick={isActive ? event => handleSetValue(event, item) : undefined}
data-tooltip-html={getDescription(item)}
onClick={event => handleSetValue(event, item)}
style={{
borderBottomWidth: isActive ? '1px' : '0px',
transitionProperty: 'height, opacity, padding',

View File

@ -1,19 +1,13 @@
'use client';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import Loader from '../ui/Loader';
import TextURL from '../ui/TextURL';
function RequireAuth({ children }: React.PropsWithChildren) {
const { user, isLoading } = useAuth();
const { isAnonymous } = useAuthSuspense();
if (isLoading) {
return <Loader key='auth-loader' />;
}
if (user) {
return <>{children}</>;
} else {
if (isAnonymous) {
return (
<div key='auth-no-user' className='flex flex-col items-center gap-1 mt-2'>
<p className='mb-2'>Пожалуйста войдите в систему</p>
@ -23,6 +17,7 @@ function RequireAuth({ children }: React.PropsWithChildren) {
</div>
);
}
return <>{children}</>;
}
export default RequireAuth;

View File

@ -2,7 +2,6 @@
import clsx from 'clsx';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
@ -23,7 +22,6 @@ import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
import { ConstituentaID } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { information } from '@/utils/labels';
export interface DlgCloneLibraryItemProps {
base: ILibraryItem;
@ -75,10 +73,7 @@ function DlgCloneLibraryItem() {
if (onlySelected) {
data.items = selected;
}
cloneItem(data, newSchema => {
toast.success(information.cloneComplete(newSchema.alias));
router.push(urls.schema(newSchema.id));
});
cloneItem(data, newSchema => router.push(urls.schema(newSchema.id)));
}
return (

View File

@ -189,7 +189,7 @@ function TabArguments({ state, schema, partialUpdate }: TabArgumentsProps) {
<PickConstituenta
id='dlg_argument_picker'
value={selectedCst}
data={schema?.items}
data={schema.items}
onSelectValue={handleSelectConstituenta}
prefixID={prefixes.cst_modal_list}
rows={7}

View File

@ -16,16 +16,16 @@ import TableUsers from './TableUsers';
export interface DlgEditEditorsProps {
editors: UserID[];
setEditors: (newValue: UserID[]) => void;
onChangeEditors: (newValue: UserID[]) => void;
}
function DlgEditEditors() {
const { editors, setEditors } = useDialogsStore(state => state.props as DlgEditEditorsProps);
const { editors, onChangeEditors } = useDialogsStore(state => state.props as DlgEditEditorsProps);
const [selected, setSelected] = useState<UserID[]>(editors);
const { users } = useUsers();
function handleSubmit() {
setEditors(selected);
onChangeEditors(selected);
}
function onDeleteEditor(target: UserID) {

View File

@ -1,10 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { useVersionDelete } from '@/backend/library/useVersionDelete';
import { useVersionUpdate } from '@/backend/library/useVersionUpdate';
@ -13,19 +10,18 @@ import MiniButton from '@/components/ui/MiniButton';
import Modal from '@/components/ui/Modal';
import TextArea from '@/components/ui/TextArea';
import TextInput from '@/components/ui/TextInput';
import { ILibraryItemVersioned, IVersionData, IVersionInfo, VersionID } from '@/models/library';
import { ILibraryItemVersioned, IVersionInfo, VersionID } from '@/models/library';
import { useDialogsStore } from '@/stores/dialogs';
import { information } from '@/utils/labels';
import TableVersions from './TableVersions';
export interface DlgEditVersionsProps {
item: ILibraryItemVersioned;
afterDelete: (targetVersion: VersionID) => void;
}
function DlgEditVersions() {
const { item } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const router = useConceptNavigation();
const { item, afterDelete } = useDialogsStore(state => state.props as DlgEditVersionsProps);
const processing = useIsProcessingLibrary();
const { versionDelete } = useVersionDelete();
const { versionUpdate } = useVersionUpdate();
@ -37,31 +33,21 @@ function DlgEditVersions() {
const isValid = selected && item.versions.every(ver => ver.id === selected.id || ver.version != version);
const isModified = selected && (selected.version != version || selected.description != description);
function handleDeleteVersion(versionID: VersionID) {
versionDelete({ itemID: item.id, versionID: versionID }, () => {
toast.success(information.versionDestroyed);
if (versionID === versionID) {
router.push(urls.schema(item.id));
}
});
function handleDeleteVersion(targetVersion: VersionID) {
versionDelete({ itemID: item.id, versionID: targetVersion }, () => afterDelete(targetVersion));
}
function handleUpdate() {
if (!isModified || !selected || processing || !isValid) {
return;
}
const data: IVersionData = {
version: version,
description: description
};
versionUpdate(
{
itemID: item.id, //
versionID: selected.id,
data: data
},
() => toast.success(information.changesSaved)
);
versionUpdate({
versionID: selected.id,
data: {
version: version,
description: description
}
});
}
function handleReset() {

View File

@ -45,7 +45,7 @@ function DlgInlineSynthesis() {
return;
}
onInlineSynthesis({
source: source.schema?.id,
source: source.schema.id,
receiver: receiver.id,
items: selected,
substitutions: substitutions
@ -53,7 +53,7 @@ function DlgInlineSynthesis() {
}
useEffect(() => {
setSelected(source.schema ? source.schema?.items.map(cst => cst.id) : []);
setSelected(source.schema ? source.schema.items.map(cst => cst.id) : []);
setSubstitutions([]);
}, [source.schema]);

View File

@ -28,15 +28,13 @@ function DlgRenameCst() {
const [cstData, updateData] = usePartialUpdate(initial);
useEffect(() => {
if (schema && initial && cstData.cst_type !== initial.cst_type) {
if (initial && cstData.cst_type !== initial.cst_type) {
updateData({ alias: generateAlias(cstData.cst_type, schema) });
}
}, [initial, cstData.cst_type, updateData, schema]);
useEffect(() => {
setValidated(
!!schema && cstData.alias !== initial.alias && validateNewAlias(cstData.alias, cstData.cst_type, schema)
);
setValidated(cstData.alias !== initial.alias && validateNewAlias(cstData.alias, cstData.cst_type, schema));
}, [cstData.cst_type, cstData.alias, initial, schema]);
return (

View File

@ -1,9 +1,7 @@
'use client';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { IRSFormUploadDTO } from '@/backend/rsform/api';
import { useUploadTRS } from '@/backend/rsform/useUploadTRS';
import Checkbox from '@/components/ui/Checkbox';
import FileInput from '@/components/ui/FileInput';
@ -26,13 +24,12 @@ function DlgUploadRSForm() {
if (!file) {
return;
}
const data: IRSFormUploadDTO = {
upload({
itemID: itemID,
load_metadata: loadMetadata,
file: file,
fileName: file.name
};
upload(data, () => toast.success('Схема загружена из файла'));
});
};
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -48,6 +48,7 @@ export type VersionID = number;
*/
export interface IVersionInfo {
id: VersionID;
item: LibraryItemID;
version: string;
description: string;
time_create: string;
@ -56,7 +57,7 @@ export interface IVersionInfo {
/**
* Represents version data, intended to update version metadata in persistent storage.
*/
export interface IVersionData extends Omit<IVersionInfo, 'id' | 'time_create'> {}
export interface IVersionData extends Omit<IVersionInfo, 'id' | 'time_create' | 'item'> {}
/**
* Represents library item common data typical for all item types.

View File

@ -25,7 +25,10 @@ export interface IUser {
/**
* Represents CurrentUser information.
*/
export interface ICurrentUser extends Pick<IUser, 'id' | 'username' | 'is_staff'> {
export interface ICurrentUser {
id: UserID | null;
username: string;
is_staff: boolean;
editor: LibraryItemID[];
}

View File

@ -2,7 +2,6 @@
import clsx from 'clsx';
import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
@ -27,7 +26,6 @@ import { AccessPolicy, LibraryItemType, LocationHead } from '@/models/library';
import { combineLocation, validateLocation } from '@/models/libraryAPI';
import { useLibrarySearchStore } from '@/stores/librarySearch';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { information } from '@/utils/labels';
function FormCreateItem() {
const router = useConceptNavigation();
@ -85,7 +83,6 @@ function FormCreateItem() {
};
setSearchLocation(location);
createItem(data, newItem => {
toast.success(information.newLibraryItem);
if (itemType == LibraryItemType.RSFORM) {
router.push(urls.schema(newItem.id));
} else {

View File

@ -2,27 +2,25 @@ import { useEffect } from 'react';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import Loader from '@/components/ui/Loader';
import { PARAMETER } from '@/utils/constants';
function HomePage() {
const router = useConceptNavigation();
const { user, isLoading } = useAuth();
const { isAnonymous } = useAuthSuspense();
useEffect(() => {
if (!isLoading) {
if (!user) {
setTimeout(() => {
router.replace(urls.manuals);
}, PARAMETER.refreshTimeout);
} else {
setTimeout(() => {
router.replace(urls.library);
}, PARAMETER.refreshTimeout);
}
if (isAnonymous) {
setTimeout(() => {
router.replace(urls.manuals);
}, PARAMETER.refreshTimeout);
} else {
setTimeout(() => {
router.replace(urls.library);
}, PARAMETER.refreshTimeout);
}
}, [router, user, isLoading]);
}, [router, isAnonymous]);
return <Loader />;
}

View File

@ -40,10 +40,7 @@ function LibraryPage() {
target: location,
new_location: newLocation
},
() => {
setLocation(newLocation);
toast.success(information.locationRenamed);
}
() => setLocation(newLocation)
);
}

View File

@ -23,7 +23,7 @@ interface ViewSideLocationProps {
}
function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps) {
const { user } = useAuth();
const { user, isAnonymous } = useAuth();
const { items } = useLibrary();
const windowSize = useWindowSize();
@ -34,7 +34,7 @@ function ViewSideLocation({ isVisible, onRenameLocation }: ViewSideLocationProps
const toggleSubfolders = useLibrarySearchStore(state => state.toggleSubfolders);
const canRename = (() => {
if (location.length <= 3 || !user) {
if (location.length <= 3 || isAnonymous || !user) {
return false;
}
if (user.is_staff) {

View File

@ -21,7 +21,7 @@ function LoginPage() {
const query = useQueryStrings();
const userQuery = query.get('username');
const { user } = useAuth();
const { isAnonymous } = useAuth();
const { login, isPending, error, reset } = useLogin();
const [username, setUsername] = useState(userQuery || '');
@ -44,7 +44,7 @@ function LoginPage() {
}
}
if (user) {
if (!isAnonymous) {
return <ExpectedAnonymous />;
}
return (

View File

@ -2,7 +2,6 @@
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { ILibraryUpdateDTO } from '@/backend/library/api';
import { useUpdateItem } from '@/backend/library/useUpdateItem';
@ -14,7 +13,6 @@ import TextInput from '@/components/ui/TextInput';
import { LibraryItemType } from '@/models/library';
import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess';
import { useModificationStore } from '@/stores/modification';
import { information } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
@ -29,11 +27,11 @@ function FormOSS({ id }: FormOSSProps) {
const isProcessing = useIsProcessingOss();
const schema = controller.schema;
const [title, setTitle] = useState(schema?.title ?? '');
const [alias, setAlias] = useState(schema?.alias ?? '');
const [comment, setComment] = useState(schema?.comment ?? '');
const [visible, setVisible] = useState(schema?.visible ?? false);
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
const [title, setTitle] = useState(schema.title);
const [alias, setAlias] = useState(schema.alias);
const [comment, setComment] = useState(schema.comment);
const [visible, setVisible] = useState(schema.visible);
const [readOnly, setReadOnly] = useState(schema.read_only);
useEffect(() => {
if (schema) {
@ -46,10 +44,6 @@ function FormOSS({ id }: FormOSSProps) {
}, [schema]);
useEffect(() => {
if (!schema) {
setIsModified(false);
return;
}
setIsModified(
schema.title !== title ||
schema.alias !== alias ||
@ -59,12 +53,11 @@ function FormOSS({ id }: FormOSSProps) {
);
return () => setIsModified(false);
}, [
schema,
schema?.title,
schema?.alias,
schema?.comment,
schema?.visible,
schema?.read_only,
schema.title,
schema.alias,
schema.comment,
schema.visible,
schema.read_only,
title,
alias,
comment,
@ -89,7 +82,7 @@ function FormOSS({ id }: FormOSSProps) {
visible: visible,
read_only: readOnly
};
update(data, () => toast.success(information.changesSaved));
update(data);
};
return (

View File

@ -58,7 +58,7 @@ function NodeContextMenu({
if (operation.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (!controller.schema || operation.result) {
if (operation.result) {
return false;
}

View File

@ -32,7 +32,7 @@ import { useModificationStore } from '@/stores/modification';
import { useOSSGraphStore } from '@/stores/ossGraph';
import { APP_COLORS } from '@/styling/color';
import { PARAMETER } from '@/utils/constants';
import { errors, information } from '@/utils/labels';
import { errors } from '@/utils/labels';
import { useOssEdit } from '../OssEditContext';
import { OssNodeTypes } from './graph/OssNodeTypes';
@ -81,33 +81,29 @@ function OssFlow() {
});
useEffect(() => {
if (!controller.schema) {
setNodes([]);
setEdges([]);
} else {
setNodes(
controller.schema.items.map(operation => ({
id: String(operation.id),
data: { label: operation.alias, operation: operation },
position: { x: operation.position_x, y: operation.position_y },
type: operation.operation_type.toString()
}))
);
setEdges(
controller.schema.arguments.map((argument, index) => ({
id: String(index),
source: String(argument.argument),
target: String(argument.operation),
type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate,
targetHandle:
controller.schema.operationByID.get(argument.argument)!.position_x >
controller.schema.operationByID.get(argument.operation)!.position_x
? 'right'
: 'left'
}))
);
}
setNodes(
controller.schema.items.map(operation => ({
id: String(operation.id),
data: { label: operation.alias, operation: operation },
position: { x: operation.position_x, y: operation.position_y },
type: operation.operation_type.toString()
}))
);
setEdges(
controller.schema.arguments.map((argument, index) => ({
id: String(index),
source: String(argument.argument),
target: String(argument.operation),
type: edgeStraight ? 'straight' : 'simplebezier',
animated: edgeAnimate,
targetHandle:
controller.schema.operationByID.get(argument.argument)!.position_x >
controller.schema.operationByID.get(argument.operation)!.position_x
? 'right'
: 'left'
}))
);
setTimeout(() => {
setIsModified(false);
}, PARAMETER.graphRefreshDelay);
@ -138,15 +134,11 @@ function OssFlow() {
operation.position_y = item.position_y;
}
});
toast.success(information.changesSaved);
setIsModified(false);
});
}
function handleCreateOperation(inputs: OperationID[]) {
if (!controller.schema) {
return;
}
const positions = getPositions();
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
controller.promptCreateOperation({
@ -181,10 +173,9 @@ function OssFlow() {
toast.error(errors.inputAlreadyExists);
return;
}
inputCreate({ itemID: controller.schema.id, data: { target: target, positions: getPositions() } }, new_schema => {
toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id));
});
inputCreate({ itemID: controller.schema.id, data: { target: target, positions: getPositions() } }, new_schema =>
router.push(urls.schema(new_schema.id))
);
}
function handleEditSchema(target: OperationID) {
@ -196,13 +187,10 @@ function OssFlow() {
}
function handleOperationExecute(target: OperationID) {
operationExecute(
{
itemID: controller.schema.id, //
data: { target: target, positions: getPositions() }
},
() => toast.success(information.operationExecuted)
);
operationExecute({
itemID: controller.schema.id, //
data: { target: target, positions: getPositions() }
});
}
function handleExecuteSelected() {
@ -217,9 +205,6 @@ function OssFlow() {
}
function handleSaveImage() {
if (!controller.schema) {
return;
}
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
if (canvas === null) {
toast.error(errors.imageFailed);
@ -242,7 +227,7 @@ function OssFlow() {
})
.then(dataURL => {
const a = document.createElement('a');
a.setAttribute('download', `${controller.schema?.alias ?? 'oss'}.png`);
a.setAttribute('download', `${controller.schema.alias}.png`);
a.setAttribute('href', dataURL);
a.click();
})

View File

@ -53,7 +53,7 @@ function ToolbarOssGraph({
const controller = useOssEdit();
const { isModified } = useModificationStore();
const isProcessing = useIsProcessingOss();
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 edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -66,7 +66,7 @@ function ToolbarOssGraph({
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
return false;
}
if (!controller.schema || selectedOperation.result) {
if (selectedOperation.result) {
return false;
}

View File

@ -2,7 +2,7 @@
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
import {
IconAdmin,
@ -33,7 +33,7 @@ import { useOssEdit } from './OssEditContext';
function MenuOssTabs() {
const controller = useOssEdit();
const router = useConceptNavigation();
const { user } = useAuth();
const { user, isAnonymous } = useAuthSuspense();
const isProcessing = useIsProcessingOss();
@ -103,7 +103,7 @@ function MenuOssTabs() {
<Divider margins='mx-3 my-1' />
{user ? (
{!isAnonymous ? (
<DropdownButton
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
@ -118,7 +118,7 @@ function MenuOssTabs() {
</Dropdown>
</div>
{user ? (
{!isAnonymous ? (
<div ref={editMenu.ref}>
<Button
dense
@ -143,7 +143,7 @@ function MenuOssTabs() {
</div>
) : null}
{user ? (
{!isAnonymous ? (
<div ref={accessMenu.ref}>
<Button
dense
@ -177,7 +177,7 @@ function MenuOssTabs() {
text={labelUserRole(UserRole.EDITOR)}
title={describeUserRole(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!controller.isOwned && !controller.schema?.editors.includes(user.id)}
disabled={!controller.isOwned && (!user.id || !controller.schema.editors.includes(user.id))}
onClick={() => handleChangeRole(UserRole.EDITOR)}
/>
<DropdownButton
@ -191,13 +191,13 @@ function MenuOssTabs() {
text={labelUserRole(UserRole.ADMIN)}
title={describeUserRole(UserRole.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user?.is_staff}
disabled={!user.is_staff}
onClick={() => handleChangeRole(UserRole.ADMIN)}
/>
</Dropdown>
</div>
) : null}
{!user ? (
{isAnonymous ? (
<Button
dense
noBorder

View File

@ -1,11 +1,10 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import { useInputUpdate } from '@/backend/oss/useInputUpdate';
import { useOperationCreate } from '@/backend/oss/useOperationCreate';
@ -22,7 +21,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { prompts } from '@/utils/labels';
import { RSTabID } from '../RSFormPage/RSEditContext';
@ -79,14 +78,15 @@ interface OssEditStateProps {
export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEditStateProps>) => {
const router = useConceptNavigation();
const { user } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode);
const role = useRoleStore(state => state.role);
const adjustRole = useRoleStore(state => state.adjustRole);
const { user } = useAuthSuspense();
const { schema } = useOssSuspense({ itemID: itemID });
const isOwned = user?.id === schema.owner || false;
const isOwned = !!user.id && user.id === schema.owner;
const isMutable = role > UserRole.READER && !schema.read_only;
@ -112,8 +112,8 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
() =>
adjustRole({
isOwner: isOwned,
isEditor: (user && schema.editors.includes(user?.id)) ?? false,
isStaff: user?.is_staff ?? false,
isEditor: !!user.id && schema.editors.includes(user.id),
isStaff: user.is_staff,
adminMode: adminMode
}),
[schema, adjustRole, isOwned, user, adminMode]
@ -139,13 +139,10 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
}
function deleteSchema() {
if (!schema || !window.confirm(prompts.deleteOSS)) {
if (!window.confirm(prompts.deleteOSS)) {
return;
}
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
router.push(urls.library);
});
deleteItem(schema.id, () => router.push(urls.library));
}
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
@ -160,7 +157,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
data.item_data.position_x = target.x;
data.item_data.position_y = target.y;
operationCreate({ itemID: schema.id, data }, operation => {
toast.success(information.newOperation(operation.alias));
if (callback) {
setTimeout(() => callback(operation.id), PARAMETER.refreshTimeout);
}
@ -191,7 +187,7 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
target: operation,
onSubmit: data => {
data.positions = positions;
operationUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
operationUpdate({ itemID: schema.id, data });
}
});
}
@ -204,18 +200,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
showDeleteOperation({
target: operation,
onSubmit: (targetID, keepConstituents, deleteSchema) => {
operationDelete(
{
itemID: schema.id,
data: {
target: targetID,
positions: positions,
keep_constituents: keepConstituents,
delete_schema: deleteSchema
}
},
() => toast.success(information.operationDestroyed)
);
operationDelete({
itemID: schema.id,
data: {
target: targetID,
positions: positions,
keep_constituents: keepConstituents,
delete_schema: deleteSchema
}
});
}
});
}
@ -229,17 +222,14 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
oss: schema,
target: operation,
onSubmit: (target, newInput) => {
inputUpdate(
{
itemID: schema.id,
data: {
target: target,
positions: positions,
input: newInput ?? null
}
},
() => toast.success(information.changesSaved)
);
inputUpdate({
itemID: schema.id,
data: {
target: target,
positions: positions,
input: newInput ?? null
}
});
}
});
}
@ -256,14 +246,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
return operation.position_x === item.position_x && operation.position_y === item.position_y;
})
) {
relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
relocateConstituents({ itemID: schema.id, data });
} else {
updatePositions(
{
isSilent: true,
itemID: schema.id, //
positions: positions
},
() => relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved))
() => relocateConstituents({ itemID: schema.id, data })
);
}
}

View File

@ -2,7 +2,6 @@
import clsx from 'clsx';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
@ -12,7 +11,6 @@ import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { globals } from '@/utils/constants';
import { information } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext';
@ -69,16 +67,13 @@ function EditorConstituenta() {
showEditTerm({
target: controller.activeCst,
onSave: forms =>
cstUpdate(
{
itemID: controller.schema.id,
data: {
target: controller.activeCst!.id,
item_data: { term_forms: forms }
}
},
() => toast.success(information.changesSaved)
)
cstUpdate({
itemID: controller.schema.id,
data: {
target: controller.activeCst!.id,
item_data: { term_forms: forms }
}
})
});
}

View File

@ -1,5 +1,4 @@
import clsx from 'clsx';
import { toast } from 'react-toastify';
import { ICstRenameDTO } from '@/backend/rsform/api';
import { useCstRename } from '@/backend/rsform/useCstRename';
@ -10,7 +9,7 @@ import Overlay from '@/components/ui/Overlay';
import { IConstituenta } from '@/models/rsform';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { information, tooltips } from '@/utils/labels';
import { tooltips } from '@/utils/labels';
import { useRSEdit } from '../RSEditContext';
@ -39,10 +38,7 @@ function EditorControls({ constituenta, disabled, onEditTerm }: EditorControlsPr
schema: schema,
initial: initialData,
allowChangeType: !constituenta.is_inherited,
onRename: data => {
const oldAlias = initialData.alias;
cstRename({ itemID: schema.id, data }, () => toast.success(information.renameComplete(oldAlias, data.alias)));
}
onRename: data => cstRename({ itemID: schema.id, data })
});
}

View File

@ -19,7 +19,7 @@ import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { errors, information, labelCstTypification } from '@/utils/labels';
import { errors, labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression';
import { useRSEdit } from '../RSEditContext';
@ -122,7 +122,7 @@ function FormConstituenta({
convention: activeCst.convention !== convention ? convention : undefined
}
};
cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
cstUpdate({ itemID: schema.id, data });
}
function handleTypeGraph(event: CProps.EventMouse) {

View File

@ -72,7 +72,7 @@ function ToolbarConstituenta({
position='cc-tab-tools right-1/2 translate-x-1/2 xs:right-4 xs:translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons cc-animate-position outline-none cc-blur px-1 rounded-b-2xl'
>
{controller.schema && controller.schema?.oss.length > 0 ? (
{controller.schema.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
@ -131,13 +131,13 @@ function ToolbarConstituenta({
<MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
disabled={disabled || isModified || controller.schema.items.length < 2}
onClick={controller.moveUp}
/>
<MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
disabled={disabled || isModified || controller.schema.items.length < 2}
onClick={controller.moveDown}
/>
</>

View File

@ -1,6 +1,5 @@
import { Suspense } from 'react';
import { useIntl } from 'react-intl';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
@ -34,7 +33,7 @@ import { useLibrarySearchStore } from '@/stores/librarySearch';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role';
import { prefixes } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { prompts } from '@/utils/labels';
interface EditorLibraryItemProps {
itemID: LibraryItemID;
@ -69,7 +68,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
if (!window.confirm(prompts.ownerChange)) {
return;
}
setOwner({ itemID: itemID, owner: newValue }, () => toast.success(information.changesSaved));
setOwner({ itemID: itemID, owner: newValue });
};
function handleOpenLibrary(event: CProps.EventMouse) {
@ -86,8 +85,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
}
showEditLocation({
initial: item.location,
onChangeLocation: newLocation =>
setLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete))
onChangeLocation: newLocation => setLocation({ itemID: itemID, location: newLocation })
});
}
@ -97,8 +95,7 @@ function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemPr
}
showEditEditors({
editors: item.editors,
setEditors: newEditors =>
setEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved))
onChangeEditors: newEditors => setEditors({ itemID: itemID, editors: newEditors })
});
}

View File

@ -33,11 +33,11 @@ function FormRSForm({ id }: FormRSFormProps) {
const { isModified, setIsModified } = useModificationStore();
const isProcessing = useIsProcessingRSForm();
const [title, setTitle] = useState(schema?.title ?? '');
const [alias, setAlias] = useState(schema?.alias ?? '');
const [comment, setComment] = useState(schema?.comment ?? '');
const [visible, setVisible] = useState(schema?.visible ?? false);
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
const [title, setTitle] = useState(schema.title);
const [alias, setAlias] = useState(schema.alias);
const [comment, setComment] = useState(schema.comment);
const [visible, setVisible] = useState(schema.visible);
const [readOnly, setReadOnly] = useState(schema.read_only);
function handleSelectVersion(version?: VersionID) {
router.push(urls.schema(schema.id, version));
@ -54,10 +54,6 @@ function FormRSForm({ id }: FormRSFormProps) {
}, [schema]);
useEffect(() => {
if (!schema) {
setIsModified(false);
return;
}
setIsModified(
schema.title !== title ||
schema.alias !== alias ||
@ -67,12 +63,11 @@ function FormRSForm({ id }: FormRSFormProps) {
);
return () => setIsModified(false);
}, [
schema,
schema?.title,
schema?.alias,
schema?.comment,
schema?.visible,
schema?.read_only,
schema.title,
schema.alias,
schema.comment,
schema.visible,
schema.read_only,
title,
alias,
comment,
@ -122,7 +117,7 @@ function FormRSForm({ id }: FormRSFormProps) {
onChange={event => setAlias(event.target.value)}
/>
<div className='flex flex-col'>
<ToolbarVersioning blockReload={schema && schema?.oss.length > 0} />
<ToolbarVersioning blockReload={schema.oss.length > 0} />
<ToolbarItemAccess
visible={visible}
toggleVisible={() => setVisible(prev => !prev)}
@ -134,8 +129,8 @@ function FormRSForm({ id }: FormRSFormProps) {
<SelectVersion
id='schema_version'
className='select-none'
value={schema?.version} //
items={schema?.versions}
value={schema.version} //
items={schema.versions}
onSelectValue={handleSelectVersion}
/>
</div>

View File

@ -1,5 +1,3 @@
import { toast } from 'react-toastify';
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
import { VisibilityIcon } from '@/components/DomainIcons';
@ -14,7 +12,6 @@ import { HelpTopic } from '@/models/miscellaneous';
import { UserRole } from '@/models/user';
import { useRoleStore } from '@/stores/role';
import { PARAMETER } from '@/utils/constants';
import { information } from '@/utils/labels';
interface ToolbarItemAccessProps {
visible: boolean;
@ -31,7 +28,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
const { setAccessPolicy } = useSetAccessPolicy();
function handleSetAccessPolicy(newPolicy: AccessPolicy) {
setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy }, () => toast.success(information.changesSaved));
setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy });
}
return (

View File

@ -30,7 +30,7 @@ function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
const canSave = isModified && !isProcessing;
const ossSelector = (() => {
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
if (controller.schema.item_type !== LibraryItemType.RSFORM) {
return null;
}
const schema = controller.schema as IRSForm;
@ -59,10 +59,10 @@ function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
/>
) : null}
<MiniButton
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
titleHtml={tooltips.shareItem(controller.schema.access_policy)}
icon={<IconShare size='1.25rem' className='icon-primary' />}
onClick={sharePage}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
disabled={controller.schema.access_policy !== AccessPolicy.PUBLIC}
/>
{controller.isMutable ? (
<MiniButton

View File

@ -1,7 +1,5 @@
import { toast } from 'react-toastify';
'use client';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useVersionCreate } from '@/backend/library/useVersionCreate';
import { useVersionRestore } from '@/backend/library/useVersionRestore';
import { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
@ -12,7 +10,7 @@ import { HelpTopic } from '@/models/miscellaneous';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { PARAMETER } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { useRSEdit } from '../RSEditContext';
@ -23,7 +21,6 @@ interface ToolbarVersioningProps {
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
const controller = useRSEdit();
const router = useConceptNavigation();
const { isModified } = useModificationStore();
const { versionRestore } = useVersionRestore();
const { versionCreate } = useVersionCreate();
@ -35,10 +32,7 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
if (!controller.schema.version || !window.confirm(prompts.restoreArchive)) {
return;
}
versionRestore({ itemID: controller.schema.id, versionID: controller.schema.version }, () => {
toast.success(information.versionRestored);
router.push(urls.schema(controller.schema.id));
});
versionRestore({ versionID: controller.schema.version }, () => controller.navigateVersion(undefined));
}
function handleCreateVersion() {
@ -50,15 +44,22 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
selected: controller.selected,
totalCount: controller.schema.items.length,
onCreate: data =>
versionCreate({ itemID: controller.schema.id, data: data }, () => {
toast.success(information.newVersion(data.version));
})
versionCreate(
{
itemID: controller.schema.id, //
data: data
},
newVersion => controller.navigateVersion(newVersion)
)
});
}
function handleEditVersions() {
showEditVersions({
item: controller.schema
item: controller.schema,
afterDelete: targetVersion => {
if (targetVersion === controller.activeVersion) controller.navigateVersion(undefined);
}
});
}
@ -85,8 +86,8 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
/>
<MiniButton
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
disabled={!controller.schema || controller.schema?.versions.length === 0}
title={controller.schema.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
disabled={controller.schema.versions.length === 0}
onClick={handleEditVersions}
icon={<IconVersions size='1.25rem' className='icon-primary' />}
/>

View File

@ -26,7 +26,7 @@ function EditorRSList() {
const controller = useRSEdit();
const isProcessing = useIsProcessingRSForm();
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema?.items ?? []);
const [filtered, setFiltered] = useState<IConstituenta[]>(controller.schema.items);
const [filterText, setFilterText] = useState('');
useEffect(() => {
@ -42,17 +42,17 @@ function EditorRSList() {
}, [filtered, setRowSelection, controller.selected]);
useEffect(() => {
if (!controller.schema || controller.schema.items.length === 0) {
if (controller.schema.items.length === 0) {
setFiltered([]);
} else if (filterText) {
setFiltered(controller.schema.items.filter(cst => matchConstituenta(cst, filterText, CstMatchMode.ALL)));
} else {
setFiltered(controller.schema.items);
}
}, [filterText, controller.schema?.items, controller.schema]);
}, [filterText, controller.schema.items]);
function handleDownloadCSV() {
if (!controller.schema || filtered.length === 0) {
if (filtered.length === 0) {
toast.error(information.noDataToExport);
return;
}
@ -65,21 +65,17 @@ function EditorRSList() {
}
function handleRowSelection(updater: React.SetStateAction<RowSelectionState>) {
if (!controller.schema) {
controller.deselectAll();
} else {
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
const newSelection: ConstituentaID[] = [];
filtered.forEach((cst, index) => {
if (newRowSelection[String(index)] === true) {
newSelection.push(cst.id);
}
});
controller.setSelected(prev => [
...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)),
...newSelection
]);
}
const newRowSelection = typeof updater === 'function' ? updater(rowSelection) : updater;
const newSelection: ConstituentaID[] = [];
filtered.forEach((cst, index) => {
if (newRowSelection[String(index)] === true) {
newSelection.push(cst.id);
}
});
controller.setSelected(prev => [
...prev.filter(cst_id => !filtered.find(cst => cst.id === cst_id)),
...newSelection
]);
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
@ -142,7 +138,7 @@ function EditorRSList() {
{controller.isContentEditable ? (
<div className='flex items-center border-b'>
<div className='px-2'>
Выбор {controller.selected.length} из {controller.schema?.stats?.count_all ?? 0}
Выбор {controller.selected.length} из {controller.schema.stats?.count_all}
</div>
<SearchBar
id='constituents_search'

View File

@ -33,7 +33,7 @@ function ToolbarRSList() {
position='cc-tab-tools right-4 translate-x-0 md:right-1/2 md:translate-x-1/2'
className='cc-icons cc-animate-position items-start outline-none'
>
{controller.schema && controller.schema?.oss.length > 0 ? (
{controller.schema.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
@ -51,7 +51,7 @@ function ToolbarRSList() {
disabled={
isProcessing ||
controller.selected.length === 0 ||
(controller.schema && controller.selected.length === controller.schema.items.length)
controller.selected.length === controller.schema.items.length
}
onClick={controller.moveUp}
/>
@ -61,7 +61,7 @@ function ToolbarRSList() {
disabled={
isProcessing ||
controller.selected.length === 0 ||
(controller.schema && controller.selected.length === controller.schema.items.length)
controller.selected.length === controller.schema.items.length
}
onClick={controller.moveDown}
/>

View File

@ -9,7 +9,7 @@ import { SelectorGraphColoring } from '@/utils/selectors';
import SchemasGuide from './SchemasGuide';
interface GraphSelectorsProps {
schema?: IRSForm;
schema: IRSForm;
coloring: GraphColoring;
onChangeColoring: (newValue: GraphColoring) => void;
}
@ -20,7 +20,7 @@ function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSelectorsPr
<Overlay position='right-[2.5rem] top-[0.25rem]'>
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
{coloring === 'schemas' && !!schema ? <SchemasGuide schema={schema} /> : null}
{coloring === 'schemas' ? <SchemasGuide schema={schema} /> : null}
</Overlay>
<SelectSingle
noBorder

View File

@ -72,7 +72,7 @@ function TGFlow() {
const [isDragging, setIsDragging] = useState(false);
const [hoverID, setHoverID] = useState<ConstituentaID | undefined>(undefined);
const hoverCst = hoverID && controller.schema?.cstByID.get(hoverID);
const hoverCst = hoverID && controller.schema.cstByID.get(hoverID);
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
const [hoverLeft, setHoverLeft] = useState(true);
@ -96,9 +96,6 @@ function TGFlow() {
});
useEffect(() => {
if (!controller.schema) {
return;
}
const newDismissed: ConstituentaID[] = [];
controller.schema.items.forEach(cst => {
if (!filteredGraph.nodes.has(cst.id)) {
@ -161,7 +158,7 @@ function TGFlow() {
}, [controller.schema, filter.noText, focusCst, coloring, flow.viewportInitialized]);
useEffect(() => {
if (!controller.schema || !needReset || !flow.viewportInitialized) {
if (!needReset || !flow.viewportInitialized) {
return;
}
setNeedReset(false);
@ -180,24 +177,18 @@ function TGFlow() {
}
function handleCreateCst() {
if (!controller.schema) {
return;
}
const definition = controller.selected.map(id => controller.schema.cstByID.get(id)!.alias).join(' ');
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
}
function handleDeleteCst() {
if (!controller.schema || !controller.canDeleteSelected) {
if (!controller.canDeleteSelected) {
return;
}
controller.promptDeleteCst();
}
function handleSaveImage() {
if (!controller.schema) {
return;
}
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
if (canvas === null) {
toast.error(errors.imageFailed);
@ -220,7 +211,7 @@ function TGFlow() {
})
.then(dataURL => {
const a = document.createElement('a');
a.setAttribute('download', `${controller.schema?.alias ?? 'graph'}.png`);
a.setAttribute('download', `${controller.schema.alias}.png`);
a.setAttribute('href', dataURL);
a.click();
})
@ -263,7 +254,7 @@ function TGFlow() {
}
function handleSetFocus(cstID: ConstituentaID | undefined) {
const target = cstID !== undefined ? controller.schema?.cstByID.get(cstID) : cstID;
const target = cstID !== undefined ? controller.schema.cstByID.get(cstID) : cstID;
setFocusCst(prev => (prev === target ? undefined : target));
if (target) {
controller.setSelected([]);
@ -314,9 +305,9 @@ function TGFlow() {
{!focusCst ? (
<ToolbarGraphSelection
graph={controller.schema.graph}
isCore={cstID => isBasicConcept(controller.schema?.cstByID.get(cstID)?.cst_type)}
isCore={cstID => isBasicConcept(controller.schema.cstByID.get(cstID)?.cst_type)}
isOwned={
controller.schema && controller.schema.inheritance.length > 0
controller.schema.inheritance.length > 0
? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited
: undefined
}
@ -350,7 +341,7 @@ function TGFlow() {
<div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}>
<SelectedCounter
hideZero
totalCount={controller.schema?.stats?.count_all ?? 0}
totalCount={controller.schema.stats?.count_all ?? 0}
selectedCount={controller.selected.length}
position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]'
/>

View File

@ -52,17 +52,17 @@ function ToolbarTermGraph({
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
function handleShowTypeGraph() {
const typeInfo = controller.schema?.items.map(item => ({
const typeInfo = controller.schema.items.map(item => ({
alias: item.alias,
result: item.parse.typification,
args: item.parse.args
}));
showTypeGraph({ items: typeInfo ?? [] });
showTypeGraph({ items: typeInfo });
}
return (
<div className='cc-icons'>
{controller.schema && controller.schema?.oss.length > 0 ? (
{controller.schema.oss.length > 0 ? (
<MiniSelectorOSS
items={controller.schema.oss}
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}

View File

@ -18,9 +18,9 @@ import { globals, PARAMETER, prefixes } from '@/utils/constants';
import { useRSEdit } from '../RSEditContext';
interface ViewHiddenProps {
schema: IRSForm;
items: ConstituentaID[];
selected: ConstituentaID[];
schema?: IRSForm;
coloringScheme: GraphColoring;
toggleSelection: (cstID: ConstituentaID) => void;
@ -45,7 +45,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
}
}
if (!schema || items.length <= 0) {
if (items.length <= 0) {
return null;
}
return (

View File

@ -1,11 +1,10 @@
'use client';
import fileDownload from 'js-file-download';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
@ -50,7 +49,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { useRoleStore } from '@/stores/role';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { describeAccessMode, information, labelAccessMode, tooltips } from '@/utils/labels';
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { OssTabID } from '../OssPage/OssEditContext';
@ -59,7 +58,7 @@ import { useRSEdit } from './RSEditContext';
function MenuRSTabs() {
const controller = useRSEdit();
const router = useConceptNavigation();
const { user } = useAuth();
const { user, isAnonymous } = useAuthSuspense();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
@ -94,9 +93,9 @@ function MenuRSTabs() {
const location = controller.schema.location;
const head = location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) {
return user?.is_staff ? location : LocationHead.USER;
return user.is_staff ? location : LocationHead.USER;
}
if (controller.schema.owner === user?.id) {
if (controller.schema.owner === user.id) {
return location;
}
return head === LocationHead.USER ? LocationHead.USER : location;
@ -113,13 +112,19 @@ function MenuRSTabs() {
return;
}
const fileName = (controller.schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
download({ itemID: controller.schema.id, version: controller.schema.version }, (data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
download(
{
itemID: controller.schema.id, //
version: controller.schema.version
},
(data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
}
});
);
}
function handleUpload() {
@ -152,12 +157,12 @@ function MenuRSTabs() {
function handleReindex() {
editMenu.hide();
resetAliases(controller.schema.id, () => toast.success(information.reindexComplete));
resetAliases({ itemID: controller.schema.id });
}
function handleRestoreOrder() {
editMenu.hide();
restoreOrder(controller.schema.id, () => toast.success(information.reorderComplete));
restoreOrder({ itemID: controller.schema.id });
}
function handleSubstituteCst() {
@ -170,12 +175,11 @@ function MenuRSTabs() {
onSubstitute: data =>
cstSubstitute(
{
itemID: controller.schema.id, //
itemID: controller.schema.id,
data
},
() => {
controller.setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
toast.success(information.substituteSingle);
}
)
});
@ -200,7 +204,6 @@ function MenuRSTabs() {
data: { target: controller.activeCst.id }
},
cstList => {
toast.success(information.addedConstituents(cstList.length));
if (cstList.length !== 0) {
controller.setSelected(cstList);
}
@ -216,11 +219,7 @@ function MenuRSTabs() {
showInlineSynthesis({
receiver: controller.schema,
onInlineSynthesis: data => {
const oldCount = controller.schema.items.length;
inlineSynthesis({ itemID: controller.schema.id, data }, newSchema => {
controller.deselectAll();
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
});
inlineSynthesis({ itemID: controller.schema.id, data }, () => controller.deselectAll());
}
});
}
@ -258,7 +257,7 @@ function MenuRSTabs() {
titleHtml={tooltips.shareItem(controller.schema.access_policy)}
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
disabled={controller.schema.access_policy !== AccessPolicy.PUBLIC}
/>
<DropdownButton
text='QR-код'
@ -266,7 +265,7 @@ function MenuRSTabs() {
icon={<IconQR size='1rem' className='icon-primary' />}
onClick={handleShowQR}
/>
{user ? (
{!isAnonymous ? (
<DropdownButton
text='Клонировать'
icon={<IconClone size='1rem' className='icon-green' />}
@ -283,7 +282,7 @@ function MenuRSTabs() {
<DropdownButton
text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />}
disabled={isProcessing || controller.schema?.oss.length !== 0}
disabled={isProcessing || controller.schema.oss.length !== 0}
onClick={handleUpload}
/>
) : null}
@ -298,7 +297,7 @@ function MenuRSTabs() {
<Divider margins='mx-3 my-1' />
{user ? (
{!isAnonymous ? (
<DropdownButton
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
@ -319,7 +318,7 @@ function MenuRSTabs() {
/>
</Dropdown>
</div>
{!controller.isArchive && user ? (
{!controller.isArchive && !isAnonymous ? (
<div ref={editMenu.ref}>
<Button
dense
@ -381,7 +380,7 @@ function MenuRSTabs() {
</Dropdown>
</div>
) : null}
{controller.isArchive && user ? (
{controller.isArchive && !isAnonymous ? (
<Button
dense
noBorder
@ -394,7 +393,7 @@ function MenuRSTabs() {
onClick={event => router.push(urls.schema(controller.schema.id), event.ctrlKey || event.metaKey)}
/>
) : null}
{user ? (
{!isAnonymous ? (
<div ref={accessMenu.ref}>
<Button
dense
@ -428,7 +427,7 @@ function MenuRSTabs() {
text={labelAccessMode(UserRole.EDITOR)}
title={describeAccessMode(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!controller.isOwned && !controller.schema?.editors.includes(user.id)}
disabled={!controller.isOwned && (!user.id || !controller.schema.editors.includes(user.id))}
onClick={() => handleChangeMode(UserRole.EDITOR)}
/>
<DropdownButton
@ -442,13 +441,13 @@ function MenuRSTabs() {
text={labelAccessMode(UserRole.ADMIN)}
title={describeAccessMode(UserRole.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user?.is_staff}
disabled={!user.is_staff}
onClick={() => handleChangeMode(UserRole.ADMIN)}
/>
</Dropdown>
</div>
) : null}
{!user ? (
{isAnonymous ? (
<Button
dense
noBorder

View File

@ -1,11 +1,10 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
import { useAuth } from '@/backend/auth/useAuth';
import { useAuthSuspense } from '@/backend/auth/useAuth';
import { useDeleteItem } from '@/backend/library/useDeleteItem';
import { ICstCreateDTO } from '@/backend/rsform/api';
import { useCstCreate } from '@/backend/rsform/useCstCreate';
@ -21,7 +20,7 @@ import { useModificationStore } from '@/stores/modification';
import { usePreferencesStore } from '@/stores/preferences';
import { useRoleStore } from '@/stores/role';
import { PARAMETER, prefixes } from '@/utils/constants';
import { information, prompts } from '@/utils/labels';
import { prompts } from '@/utils/labels';
import { promptUnsaved } from '@/utils/utils';
import { OssTabID } from '../OssPage/OssEditContext';
@ -37,6 +36,7 @@ export interface IRSEditContext extends ILibraryItemEditor {
schema: IRSForm;
selected: ConstituentaID[];
activeCst?: IConstituenta;
activeVersion?: VersionID;
isOwned: boolean;
isArchive: boolean;
@ -45,6 +45,7 @@ export interface IRSEditContext extends ILibraryItemEditor {
isAttachedToOSS: boolean;
canDeleteSelected: boolean;
navigateVersion: (versionID: VersionID | undefined) => void;
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) => void;
navigateCst: (cstID: ConstituentaID) => void;
navigateOss: (target: LibraryItemID, newTab?: boolean) => void;
@ -77,21 +78,26 @@ export const useRSEdit = () => {
interface RSEditStateProps {
itemID: LibraryItemID;
activeTab: RSTabID;
versionID?: VersionID;
activeVersion?: VersionID;
}
export const RSEditState = ({ itemID, versionID, activeTab, children }: React.PropsWithChildren<RSEditStateProps>) => {
export const RSEditState = ({
itemID,
activeVersion,
activeTab,
children
}: React.PropsWithChildren<RSEditStateProps>) => {
const router = useConceptNavigation();
const { user } = useAuth();
const adminMode = usePreferencesStore(state => state.adminMode);
const role = useRoleStore(state => state.role);
const adjustRole = useRoleStore(state => state.adjustRole);
const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
const { user } = useAuthSuspense();
const { schema } = useRSFormSuspense({ itemID: itemID, version: activeVersion });
const { isModified } = useModificationStore();
const isOwned = user?.id === schema?.owner || false;
const isArchive = !!versionID;
const isOwned = !!user.id && user.id === schema.owner;
const isArchive = !!activeVersion;
const isMutable = role > UserRole.READER && !schema.read_only;
const isContentEditable = isMutable && !isArchive;
const isAttachedToOSS = schema.oss.length > 0;
@ -114,13 +120,17 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
() =>
adjustRole({
isOwner: isOwned,
isEditor: (user && schema?.editors.includes(user?.id)) ?? false,
isStaff: user?.is_staff ?? false,
isEditor: !!user.id && schema.editors.includes(user.id),
isStaff: user.is_staff,
adminMode: adminMode
}),
[schema, adjustRole, isOwned, user, adminMode]
);
function navigateVersion(versionID: VersionID | undefined) {
router.push(urls.schema(schema.id, versionID));
}
function navigateOss(target: LibraryItemID, newTab?: boolean) {
router.push(urls.oss(target), newTab);
}
@ -133,7 +143,7 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
id: schema.id,
tab: tab,
active: activeID,
version: versionID
version: activeVersion
};
const url = urls.schema_props(data);
if (activeID) {
@ -162,7 +172,6 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
}
const ossID = schema.oss.length > 0 ? schema.oss[0].id : undefined;
deleteItem(schema.id, () => {
toast.success(information.itemDestroyed);
if (ossID) {
router.push(urls.oss(ossID, OssTabID.GRAPH));
} else {
@ -174,7 +183,6 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
function handleCreateCst(data: ICstCreateDTO) {
data.alias = data.alias || generateAlias(data.cst_type, schema);
cstCreate({ itemID: itemID, data }, newCst => {
toast.success(information.newConstituent(newCst.alias));
setSelected([newCst.id]);
navigateRSForm({ tab: activeTab, activeID: newCst.id });
if (activeTab === RSTabID.CST_LIST) {
@ -197,12 +205,10 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
items: deleted
};
const deletedNames = deleted.map(id => schema.cstByID.get(id)!.alias).join(', ');
const isEmpty = deleted.length === schema.items.length;
const nextActive = isEmpty ? undefined : getNextActiveOnDelete(activeCst?.id, schema.items, deleted);
cstDelete({ itemID: itemID, data }, () => {
toast.success(information.constituentsDestroyed(deletedNames));
setSelected(nextActive ? [nextActive] : []);
if (!nextActive) {
navigateRSForm({ tab: RSTabID.CST_LIST });
@ -314,6 +320,7 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
schema,
selected,
activeCst,
activeVersion,
isOwned,
isArchive,
@ -322,11 +329,12 @@ export const RSEditState = ({ itemID, versionID, activeTab, children }: React.Pr
isAttachedToOSS,
canDeleteSelected,
navigateVersion,
navigateRSForm,
navigateCst,
navigateOss,
deleteSchema,
navigateOss,
setSelected,
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),

View File

@ -37,7 +37,7 @@ function RSFormPage() {
<ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
)}
>
<RSEditState itemID={itemID} versionID={version} activeTab={activeTab}>
<RSEditState itemID={itemID} activeVersion={version} activeTab={activeTab}>
<RSTabs />
</RSEditState>
</ErrorBoundary>

View File

@ -13,7 +13,7 @@ import { matchConstituenta } from '@/models/rsformAPI';
import { useCstSearchStore } from '@/stores/cstSearch';
interface ConstituentsSearchProps {
schema?: IRSForm;
schema: IRSForm;
dense?: boolean;
activeID?: ConstituentaID;
activeExpression: string;
@ -31,7 +31,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
const toggleInherited = useCstSearchStore(state => state.toggleInherited);
useEffect(() => {
if (!schema || schema.items.length === 0) {
if (schema.items.length === 0) {
setFiltered([]);
return;
}
@ -53,7 +53,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
setFiltered,
filterSource,
activeExpression,
schema?.items,
schema.items,
schema,
filterMatch,
activeID,
@ -71,7 +71,7 @@ function ConstituentsSearch({ schema, activeID, activeExpression, dense, setFilt
/>
<SelectMatchMode value={filterMatch} onChange={newValue => setMatch(newValue)} dense={dense} />
<SelectGraphFilter value={filterSource} onChange={newValue => setSource(newValue)} dense={dense} />
{schema && schema?.stats.count_inherited > 0 ? (
{schema.stats.count_inherited > 0 ? (
<MiniButton
noHover
titleHtml={`Наследованные: <b>${includeInherited ? 'отображать' : 'скрывать'}</b>`}

View File

@ -29,7 +29,7 @@ function ViewConstituents({ expression, isBottom, isMounted }: ViewConstituentsP
const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
const { schema, activeCst, navigateCst } = useRSEdit();
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema?.items ?? []);
const [filteredData, setFilteredData] = useState<IConstituenta[]>(schema.items);
return (
<div

View File

@ -3,7 +3,6 @@
import axios from 'axios';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
import { urls } from '@/app/urls';
@ -20,9 +19,7 @@ import TextInput from '@/components/ui/TextInput';
import TextURL from '@/components/ui/TextURL';
import Tooltip from '@/components/ui/Tooltip';
import { HelpTopic } from '@/models/miscellaneous';
import { IUserSignupData } from '@/models/user';
import { globals, patterns } from '@/utils/constants';
import { information } from '@/utils/labels';
function FormSignup() {
const router = useConceptNavigation();
@ -54,20 +51,20 @@ function FormSignup() {
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!isPending) {
const data: IUserSignupData = {
if (isPending) {
return;
}
signup(
{
username,
email,
password,
password2,
first_name: firstName,
last_name: lastName
};
signup(data, createdUser => {
router.push(urls.login_hint(createdUser.username));
toast.success(information.newUser(createdUser.username));
});
}
},
createdUser => router.push(urls.login_hint(createdUser.username))
);
}
return (
<form className={clsx('cc-fade-in cc-column', 'mx-auto w-[36rem]', 'px-6 py-3')} onSubmit={handleSubmit}>

View File

@ -13,7 +13,7 @@ import InfoError, { ErrorData } from '@/components/info/InfoError';
import FlexColumn from '@/components/ui/FlexColumn';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import { errors, information } from '@/utils/labels';
import { errors } from '@/utils/labels';
function EditorPassword() {
const router = useConceptNavigation();
@ -38,10 +38,7 @@ function EditorPassword() {
old_password: oldPassword,
new_password: newPassword
};
changePassword(data, () => {
toast.success(information.changesSaved);
router.push(urls.login);
});
changePassword(data, () => router.push(urls.login));
}
useEffect(() => {

View File

@ -2,7 +2,6 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useBlockNavigation } from '@/app/Navigation/NavigationContext';
import { IUpdateProfileDTO } from '@/backend/users/api';
@ -11,7 +10,6 @@ import { useUpdateProfile } from '@/backend/users/useUpdateProfile';
import InfoError, { ErrorData } from '@/components/info/InfoError';
import SubmitButton from '@/components/ui/SubmitButton';
import TextInput from '@/components/ui/TextInput';
import { information } from '@/utils/labels';
function EditorProfile() {
const { profile } = useProfileSuspense();
@ -41,7 +39,7 @@ function EditorProfile() {
first_name: first_name,
last_name: last_name
};
updateProfile(data, () => toast.success(information.changesSaved));
updateProfile(data);
}
return (

View File

@ -90,7 +90,7 @@ export function prepareTooltip(text: string, hotkey?: string) {
* Generates label for {@link IVersionInfo} of {@link IRSForm}.
*/
export function labelVersion(schema?: IRSForm) {
const version = schema?.versions.find(ver => ver.id === schema.version);
const version = schema?.versions.find(ver => ver.id === schema?.version);
return version ? version.version : 'актуальная';
}
@ -957,6 +957,8 @@ export const information = {
cloneComplete: (alias: string) => `Копия создана: ${alias}`,
noDataToExport: 'Нет данных для экспорта',
substitutionsCorrect: 'Таблица отождествлений прошла проверку',
uploadSuccess: 'Схема загружена из файла',
inlineSynthesisComplete: 'Встраивание завершено',
newLibraryItem: 'Схема успешно создана',
addedConstituents: (count: number) => `Добавлены конституенты: ${count}`,
@ -964,14 +966,13 @@ export const information = {
newVersion: (version: string) => `Версия создана: ${version}`,
newConstituent: (alias: string) => `Конституента добавлена: ${alias}`,
newOperation: (alias: string) => `Операция добавлена: ${alias}`,
renameComplete: (oldAlias: string, newAlias: string) => `Переименование: ${oldAlias} -> ${newAlias}`,
versionDestroyed: 'Версия удалена',
itemDestroyed: 'Схема удалена',
operationDestroyed: 'Операция удалена',
operationExecuted: 'Операция выполнена',
allOperationExecuted: 'Все операции выполнены',
constituentsDestroyed: (aliases: string) => `Конституенты удалены: ${aliases}`
constituentsDestroyed: (count: number) => `Конституенты удалены: ${count}`
};
/**