F: Implement react-query pt4
This commit is contained in:
parent
6543d88cbe
commit
519b5f6634
|
@ -1,63 +0,0 @@
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { createContext, useCallback, useContext, useState } from 'react';
|
|
||||||
|
|
||||||
import { useOss, useOssInvalidate, useOssUpdate } from '@/backend/oss/useOSS';
|
|
||||||
import { ErrorData } from '@/components/info/InfoError';
|
|
||||||
import { LibraryItemID } from '@/models/library';
|
|
||||||
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
|
||||||
import { contextOutsideScope } from '@/utils/labels';
|
|
||||||
|
|
||||||
interface IGlobalOssContext {
|
|
||||||
schema: IOperationSchema | undefined;
|
|
||||||
setID: (id: LibraryItemID | undefined) => void;
|
|
||||||
setData: (data: IOperationSchemaData) => void;
|
|
||||||
loading: boolean;
|
|
||||||
loadingError: ErrorData;
|
|
||||||
|
|
||||||
invalidate: () => Promise<void>;
|
|
||||||
invalidateItem: (target: LibraryItemID) => void;
|
|
||||||
partialUpdate: (data: Partial<IOperationSchema>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GlobalOssContext = createContext<IGlobalOssContext | null>(null);
|
|
||||||
export const useGlobalOss = (): IGlobalOssContext => {
|
|
||||||
const context = useContext(GlobalOssContext);
|
|
||||||
if (context === null) {
|
|
||||||
throw new Error(contextOutsideScope('useGlobalOss', 'GlobalOssState'));
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GlobalOssState = ({ children }: React.PropsWithChildren) => {
|
|
||||||
const [ossID, setID] = useState<LibraryItemID | undefined>(undefined);
|
|
||||||
const { schema: schema, error: loadingError, isLoading: loading } = useOss({ itemID: ossID });
|
|
||||||
const { update, partialUpdate } = useOssUpdate({ itemID: ossID });
|
|
||||||
const { invalidate } = useOssInvalidate({ itemID: ossID });
|
|
||||||
|
|
||||||
const invalidateItem = useCallback(
|
|
||||||
(target: LibraryItemID) => {
|
|
||||||
if (schema?.schemas.includes(target)) {
|
|
||||||
invalidate().catch(console.error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[invalidate, schema]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GlobalOssContext
|
|
||||||
value={{
|
|
||||||
schema,
|
|
||||||
setID,
|
|
||||||
setData: update,
|
|
||||||
loading,
|
|
||||||
loadingError,
|
|
||||||
partialUpdate,
|
|
||||||
invalidateItem,
|
|
||||||
invalidate
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</GlobalOssContext>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -6,7 +6,6 @@ import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { queryClient } from '@/backend/queryClient';
|
import { queryClient } from '@/backend/queryClient';
|
||||||
import { GlobalOssState } from '@/app/GlobalOssContext';
|
|
||||||
|
|
||||||
import ErrorFallback from './ErrorFallback';
|
import ErrorFallback from './ErrorFallback';
|
||||||
|
|
||||||
|
@ -31,12 +30,10 @@ function GlobalProviders({ children }: React.PropsWithChildren) {
|
||||||
>
|
>
|
||||||
<IntlProvider locale='ru' defaultLocale='ru'>
|
<IntlProvider locale='ru' defaultLocale='ru'>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<GlobalOssState>
|
|
||||||
|
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
</GlobalOssState>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ErrorBoundary>);
|
</ErrorBoundary>);
|
||||||
|
|
|
@ -2,10 +2,13 @@ import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { axiosInstance } from '@/backend/axiosInstance';
|
import { axiosInstance } from '@/backend/axiosInstance';
|
||||||
import { DELAYS } from '@/backend/configuration';
|
import { DELAYS } from '@/backend/configuration';
|
||||||
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, VersionID } from '@/models/library';
|
import { AccessPolicy, ILibraryItem, IVersionData, LibraryItemID, LibraryItemType, VersionID } from '@/models/library';
|
||||||
import { ConstituentaID, IRSFormData } from '@/models/rsform';
|
import { ConstituentaID, IRSFormData } from '@/models/rsform';
|
||||||
import { UserID } from '@/models/user';
|
import { UserID } from '@/models/user';
|
||||||
|
|
||||||
|
import { ossApi } from '../oss/api';
|
||||||
|
import { rsformsApi } from '../rsform/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents update data for renaming Location.
|
* Represents update data for renaming Location.
|
||||||
*/
|
*/
|
||||||
|
@ -67,6 +70,11 @@ export const libraryApi = {
|
||||||
})
|
})
|
||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
}),
|
}),
|
||||||
|
getItemQueryOptions: ({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) => {
|
||||||
|
return itemType === LibraryItemType.RSFORM
|
||||||
|
? rsformsApi.getRSFormQueryOptions({ itemID })
|
||||||
|
: ossApi.getOssQueryOptions({ itemID });
|
||||||
|
},
|
||||||
getTemplatesQueryOptions: () =>
|
getTemplatesQueryOptions: () =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: [libraryApi.baseKey, 'templates'],
|
queryKey: [libraryApi.baseKey, 'templates'],
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
import { useIsMutating } from '@tanstack/react-query';
|
import { useIsMutating } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { ossApi } from '../oss/api';
|
||||||
|
import { rsformsApi } from '../rsform/api';
|
||||||
import { libraryApi } from './api';
|
import { libraryApi } from './api';
|
||||||
|
|
||||||
export const useIsProcessingLibrary = () => {
|
export const useIsProcessingLibrary = () => {
|
||||||
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
|
const countMutations = useIsMutating({ mutationKey: [libraryApi.baseKey] });
|
||||||
return countMutations !== 0;
|
const countOss = useIsMutating({ mutationKey: [ossApi.baseKey] });
|
||||||
|
const countRSForm = useIsMutating({ mutationKey: [rsformsApi.baseKey] });
|
||||||
|
return countMutations + countOss + countRSForm !== 0;
|
||||||
};
|
};
|
||||||
|
|
23
rsconcept/frontend/src/backend/library/useLibraryItem.tsx
Normal file
23
rsconcept/frontend/src/backend/library/useLibraryItem.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { ILibraryItemVersioned, LibraryItemID, LibraryItemType } from '@/models/library';
|
||||||
|
|
||||||
|
import { ossApi } from '../oss/api';
|
||||||
|
import { rsformsApi } from '../rsform/api';
|
||||||
|
|
||||||
|
export function useLibraryItem({ itemID, itemType }: { itemID: LibraryItemID; itemType: LibraryItemType }) {
|
||||||
|
const { data: rsForm } = useQuery({
|
||||||
|
...rsformsApi.getRSFormQueryOptions({ itemID }),
|
||||||
|
enabled: itemType === LibraryItemType.RSFORM
|
||||||
|
});
|
||||||
|
const { data: oss } = useQuery({
|
||||||
|
...ossApi.getOssQueryOptions({ itemID }),
|
||||||
|
enabled: itemType === LibraryItemType.OSS
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
item:
|
||||||
|
itemType === LibraryItemType.RSFORM
|
||||||
|
? (rsForm as ILibraryItemVersioned | undefined)
|
||||||
|
: (oss as ILibraryItemVersioned | undefined)
|
||||||
|
};
|
||||||
|
}
|
|
@ -92,7 +92,7 @@ export interface ICstRelocateDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ossApi = {
|
export const ossApi = {
|
||||||
baseKey: 'library',
|
baseKey: 'oss',
|
||||||
|
|
||||||
getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => {
|
getOssQueryOptions: ({ itemID }: { itemID?: LibraryItemID }) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
|
||||||
import { LibraryItemID } from '@/models/library';
|
import { LibraryItemID } from '@/models/library';
|
||||||
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
|
||||||
import { OssLoader } from '@/models/OssLoader';
|
import { OssLoader } from '@/models/OssLoader';
|
||||||
|
|
||||||
import { useLibrary, useLibrarySuspense } from '@/backend/library/useLibrary';
|
|
||||||
import { ossApi } from './api';
|
import { ossApi } from './api';
|
||||||
|
|
||||||
export function useOss({ itemID }: { itemID?: LibraryItemID }) {
|
export function useOss({ itemID }: { itemID?: LibraryItemID }) {
|
||||||
|
@ -17,12 +17,12 @@ export function useOss({ itemID }: { itemID?: LibraryItemID }) {
|
||||||
return { schema: schema, isLoading: isLoading || libraryLoading, error: error };
|
return { schema: schema, isLoading: isLoading || libraryLoading, error: error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOssSuspense({ itemID }: { itemID?: LibraryItemID }) {
|
export function useOssSuspense({ itemID }: { itemID: LibraryItemID }) {
|
||||||
const { items: libraryItems } = useLibrarySuspense();
|
const { items: libraryItems } = useLibrarySuspense();
|
||||||
const { data } = useSuspenseQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...ossApi.getOssQueryOptions({ itemID })
|
...ossApi.getOssQueryOptions({ itemID })
|
||||||
});
|
});
|
||||||
const schema = data ? new OssLoader(data, libraryItems).produceOSS() : undefined;
|
const schema = new OssLoader(data!, libraryItems).produceOSS();
|
||||||
return { schema };
|
return { schema };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const useUpdatePositions = () => {
|
||||||
onSuccess: (_, variables) => updateTimestamp(variables.itemID)
|
onSuccess: (_, variables) => updateTimestamp(variables.itemID)
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
cstDelete: (
|
updatePositions: (
|
||||||
data: {
|
data: {
|
||||||
itemID: LibraryItemID; //
|
itemID: LibraryItemID; //
|
||||||
positions: IOperationPosition[];
|
positions: IOperationPosition[];
|
||||||
|
|
|
@ -107,7 +107,7 @@ export interface ICheckConstituentaDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rsformsApi = {
|
export const rsformsApi = {
|
||||||
baseKey: 'library',
|
baseKey: 'rsform',
|
||||||
|
|
||||||
getRSFormQueryOptions: ({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) => {
|
getRSFormQueryOptions: ({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
|
|
|
@ -15,11 +15,11 @@ export function useRSForm({ itemID, version }: { itemID?: LibraryItemID; version
|
||||||
return { schema, isLoading, error };
|
return { schema, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRSFormSuspense({ itemID, version }: { itemID?: LibraryItemID; version?: VersionID }) {
|
export function useRSFormSuspense({ itemID, version }: { itemID: LibraryItemID; version?: VersionID }) {
|
||||||
const { data } = useSuspenseQuery({
|
const { data } = useSuspenseQuery({
|
||||||
...rsformsApi.getRSFormQueryOptions({ itemID, version })
|
...rsformsApi.getRSFormQueryOptions({ itemID, version })
|
||||||
});
|
});
|
||||||
const schema = data ? new RSFormLoader(data).produceRSForm() : undefined;
|
const schema = new RSFormLoader(data!).produceRSForm();
|
||||||
return { schema };
|
return { schema };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { IRSFormCloneDTO } from '@/backend/library/api';
|
import { IRSFormCloneDTO } from '@/backend/library/api';
|
||||||
|
@ -18,7 +19,6 @@ import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
|
||||||
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
import { AccessPolicy, ILibraryItem, LocationHead } from '@/models/library';
|
||||||
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
|
import { cloneTitle, combineLocation, validateLocation } from '@/models/libraryAPI';
|
||||||
import { ConstituentaID } from '@/models/rsform';
|
import { ConstituentaID } from '@/models/rsform';
|
||||||
|
|
|
@ -1,34 +1,51 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
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';
|
||||||
import { IconReset, IconSave } from '@/components/Icons';
|
import { IconReset, IconSave } from '@/components/Icons';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Modal from '@/components/ui/Modal';
|
import Modal from '@/components/ui/Modal';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { IVersionData, IVersionInfo, VersionID } from '@/models/library';
|
import { ILibraryItemVersioned, IVersionData, IVersionInfo, VersionID } from '@/models/library';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
import TableVersions from './TableVersions';
|
import TableVersions from './TableVersions';
|
||||||
|
|
||||||
export interface DlgEditVersionsProps {
|
export interface DlgEditVersionsProps {
|
||||||
versions: IVersionInfo[];
|
item: ILibraryItemVersioned;
|
||||||
onDelete: (versionID: VersionID) => void;
|
|
||||||
onUpdate: (versionID: VersionID, data: IVersionData) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DlgEditVersions() {
|
function DlgEditVersions() {
|
||||||
const { versions, onDelete, onUpdate } = useDialogsStore(state => state.props as DlgEditVersionsProps);
|
const { item } = useDialogsStore(state => state.props as DlgEditVersionsProps);
|
||||||
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
|
const router = useConceptNavigation();
|
||||||
const processing = false; // TODO: fix processing hook and versions update
|
const processing = useIsProcessingLibrary();
|
||||||
|
const { versionDelete } = useVersionDelete();
|
||||||
|
const { versionUpdate } = useVersionUpdate();
|
||||||
|
|
||||||
|
const [selected, setSelected] = useState<IVersionInfo | undefined>(undefined);
|
||||||
const [version, setVersion] = useState('');
|
const [version, setVersion] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
|
|
||||||
const isValid = selected && versions.every(ver => ver.id === selected.id || ver.version != version);
|
const isValid = selected && item.versions.every(ver => ver.id === selected.id || ver.version != version);
|
||||||
const isModified = selected && (selected.version != version || selected.description != description);
|
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 handleUpdate() {
|
function handleUpdate() {
|
||||||
if (!isModified || !selected || processing || !isValid) {
|
if (!isModified || !selected || processing || !isValid) {
|
||||||
return;
|
return;
|
||||||
|
@ -37,7 +54,14 @@ function DlgEditVersions() {
|
||||||
version: version,
|
version: version,
|
||||||
description: description
|
description: description
|
||||||
};
|
};
|
||||||
onUpdate(selected.id, data);
|
versionUpdate(
|
||||||
|
{
|
||||||
|
itemID: item.id, //
|
||||||
|
versionID: selected.id,
|
||||||
|
data: data
|
||||||
|
},
|
||||||
|
() => toast.success(information.changesSaved)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
|
@ -57,9 +81,9 @@ function DlgEditVersions() {
|
||||||
<Modal readonly header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
|
<Modal readonly header='Редактирование версий' className='flex flex-col w-[40rem] px-6 gap-3 pb-6'>
|
||||||
<TableVersions
|
<TableVersions
|
||||||
processing={processing}
|
processing={processing}
|
||||||
items={versions}
|
items={item.versions}
|
||||||
onDelete={onDelete}
|
onDelete={handleDeleteVersion}
|
||||||
onSelect={versionID => setSelected(versions.find(ver => ver.id === versionID))}
|
onSelect={versionID => setSelected(item.versions.find(ver => ver.id === versionID))}
|
||||||
selected={selected?.id}
|
selected={selected?.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -100,16 +100,9 @@ export interface ILibraryItemVersioned extends ILibraryItemData {
|
||||||
* Represents common {@link ILibraryItem} editor controller.
|
* Represents common {@link ILibraryItem} editor controller.
|
||||||
*/
|
*/
|
||||||
export interface ILibraryItemEditor {
|
export interface ILibraryItemEditor {
|
||||||
schema?: ILibraryItemData;
|
schema: ILibraryItemData;
|
||||||
|
deleteSchema: () => void;
|
||||||
|
|
||||||
isMutable: boolean;
|
isMutable: boolean;
|
||||||
isProcessing: boolean;
|
|
||||||
isAttachedToOSS: boolean;
|
isAttachedToOSS: boolean;
|
||||||
|
|
||||||
setOwner: (newOwner: UserID) => void;
|
|
||||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
|
||||||
promptEditors: () => void;
|
|
||||||
promptLocation: () => void;
|
|
||||||
|
|
||||||
share: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import Loader from '@/components/ui/Loader';
|
import Loader from '@/components/ui/Loader';
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
IconEdit2,
|
IconEdit2,
|
||||||
IconEditor,
|
IconEditor,
|
||||||
IconMenu,
|
IconMenu,
|
||||||
IconNewVersion,
|
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconReader,
|
IconReader,
|
||||||
IconShare,
|
IconShare,
|
||||||
|
@ -54,9 +53,6 @@ function HelpRSMenu() {
|
||||||
<li>
|
<li>
|
||||||
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
<IconClone className='inline-icon icon-green' /> Клонировать – создать копию схемы
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<IconNewVersion size='1.25rem' className='inline-icon icon-green' /> Сохранить версию
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<IconDownload className='inline-icon' /> Выгрузить – сохранить в файле формата Экстеор
|
<IconDownload className='inline-icon' /> Выгрузить – сохранить в файле формата Экстеор
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -3,22 +3,19 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import FlexColumn from '@/components/ui/FlexColumn';
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
|
import { LibraryItemType } from '@/models/library';
|
||||||
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
|
import EditorLibraryItem from '@/pages/RSFormPage/EditorRSFormCard/EditorLibraryItem';
|
||||||
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
|
import ToolbarRSFormCard from '@/pages/RSFormPage/EditorRSFormCard/ToolbarRSFormCard';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
import FormOSS from './FormOSS';
|
import FormOSS from './FormOSS';
|
||||||
import OssStats from './OssStats';
|
import OssStats from './OssStats';
|
||||||
|
|
||||||
interface EditorOssCardProps {
|
function EditorOssCard() {
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
onDestroy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardProps) {
|
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
|
||||||
function initiateSubmit() {
|
function initiateSubmit() {
|
||||||
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
|
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
|
||||||
|
@ -38,12 +35,7 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolbarRSFormCard
|
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
|
||||||
modified={isModified}
|
|
||||||
onSubmit={initiateSubmit}
|
|
||||||
onDestroy={onDestroy}
|
|
||||||
controller={controller}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
onKeyDown={handleInput}
|
onKeyDown={handleInput}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -54,8 +46,8 @@ function EditorOssCard({ isModified, onDestroy, setIsModified }: EditorOssCardPr
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FlexColumn className='px-3'>
|
<FlexColumn className='px-3'>
|
||||||
<FormOSS id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
<FormOSS id={globals.library_item_editor} />
|
||||||
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} />
|
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.OSS} controller={controller} />
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
|
||||||
{controller.schema ? <OssStats stats={controller.schema.stats} /> : null}
|
{controller.schema ? <OssStats stats={controller.schema.stats} /> : null}
|
||||||
|
|
|
@ -6,25 +6,27 @@ import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { ILibraryUpdateDTO } from '@/backend/library/api';
|
import { ILibraryUpdateDTO } from '@/backend/library/api';
|
||||||
import { useUpdateItem } from '@/backend/library/useUpdateItem';
|
import { useUpdateItem } from '@/backend/library/useUpdateItem';
|
||||||
|
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||||
import { IconSave } from '@/components/Icons';
|
import { IconSave } from '@/components/Icons';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { LibraryItemType } from '@/models/library';
|
import { LibraryItemType } from '@/models/library';
|
||||||
import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess';
|
import ToolbarItemAccess from '@/pages/RSFormPage/EditorRSFormCard/ToolbarItemAccess';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { information } from '@/utils/labels';
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
interface FormOSSProps {
|
interface FormOSSProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
function FormOSS({ id }: FormOSSProps) {
|
||||||
const { updateItem: update } = useUpdateItem();
|
const { updateItem: update } = useUpdateItem();
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
|
const { isModified, setIsModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingOss();
|
||||||
const schema = controller.schema;
|
const schema = controller.schema;
|
||||||
|
|
||||||
const [title, setTitle] = useState(schema?.title ?? '');
|
const [title, setTitle] = useState(schema?.title ?? '');
|
||||||
|
@ -125,14 +127,14 @@ function FormOSS({ id, isModified, setIsModified }: FormOSSProps) {
|
||||||
label='Описание'
|
label='Описание'
|
||||||
rows={3}
|
rows={3}
|
||||||
value={comment}
|
value={comment}
|
||||||
disabled={!controller.isMutable || controller.isProcessing}
|
disabled={!controller.isMutable || isProcessing}
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => setComment(event.target.value)}
|
||||||
/>
|
/>
|
||||||
{controller.isMutable || isModified ? (
|
{controller.isMutable || isModified ? (
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Сохранить изменения'
|
text='Сохранить изменения'
|
||||||
className='self-center mt-4'
|
className='self-center mt-4'
|
||||||
loading={controller.isProcessing}
|
loading={isProcessing}
|
||||||
disabled={!isModified}
|
disabled={!isModified}
|
||||||
icon={<IconSave size='1.25rem' />}
|
icon={<IconSave size='1.25rem' />}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,15 +4,10 @@ import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import OssFlow from './OssFlow';
|
import OssFlow from './OssFlow';
|
||||||
|
|
||||||
interface EditorOssGraphProps {
|
function EditorOssGraph() {
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) {
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<OssFlow isModified={isModified} setIsModified={setIsModified} />
|
<OssFlow />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||||
import {
|
import {
|
||||||
IconChild,
|
IconChild,
|
||||||
IconConnect,
|
IconConnect,
|
||||||
|
@ -49,6 +50,8 @@ function NodeContextMenu({
|
||||||
onRelocateConstituents
|
onRelocateConstituents
|
||||||
}: NodeContextMenuProps) {
|
}: NodeContextMenuProps) {
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
|
const isProcessing = useIsProcessingOss();
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const readyForSynthesis = (() => {
|
const readyForSynthesis = (() => {
|
||||||
|
@ -64,7 +67,7 @@ function NodeContextMenu({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
|
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!);
|
||||||
if (argumentOperations.some(item => item.result === null)) {
|
if (argumentOperations.some(item => item.result === null)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +85,7 @@ function NodeContextMenu({
|
||||||
useEffect(() => setIsOpen(true), []);
|
useEffect(() => setIsOpen(true), []);
|
||||||
|
|
||||||
const handleOpenSchema = () => {
|
const handleOpenSchema = () => {
|
||||||
controller.openOperationSchema(operation.id);
|
controller.navigateOperationSchema(operation.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditSchema = () => {
|
const handleEditSchema = () => {
|
||||||
|
@ -126,7 +129,7 @@ function NodeContextMenu({
|
||||||
text='Редактировать'
|
text='Редактировать'
|
||||||
title='Редактировать операцию'
|
title='Редактировать операцию'
|
||||||
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
icon={<IconEdit2 size='1rem' className='icon-primary' />}
|
||||||
disabled={!controller.isMutable || controller.isProcessing}
|
disabled={!controller.isMutable || isProcessing}
|
||||||
onClick={handleEditOperation}
|
onClick={handleEditOperation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -135,7 +138,7 @@ function NodeContextMenu({
|
||||||
text='Открыть схему'
|
text='Открыть схему'
|
||||||
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
|
titleHtml={prepareTooltip('Открыть привязанную КС', 'Двойной клик')}
|
||||||
icon={<IconRSForm size='1rem' className='icon-green' />}
|
icon={<IconRSForm size='1rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleOpenSchema}
|
onClick={handleOpenSchema}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -144,7 +147,7 @@ function NodeContextMenu({
|
||||||
text='Создать схему'
|
text='Создать схему'
|
||||||
title='Создать пустую схему для загрузки'
|
title='Создать пустую схему для загрузки'
|
||||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleCreateSchema}
|
onClick={handleCreateSchema}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -153,7 +156,7 @@ function NodeContextMenu({
|
||||||
text={!operation.result ? 'Загрузить схему' : 'Изменить схему'}
|
text={!operation.result ? 'Загрузить схему' : 'Изменить схему'}
|
||||||
title='Выбрать схему для загрузки'
|
title='Выбрать схему для загрузки'
|
||||||
icon={<IconConnect size='1rem' className='icon-primary' />}
|
icon={<IconConnect size='1rem' className='icon-primary' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleEditSchema}
|
onClick={handleEditSchema}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -166,7 +169,7 @@ function NodeContextMenu({
|
||||||
: 'Необходимо предоставить все аргументы'
|
: 'Необходимо предоставить все аргументы'
|
||||||
}
|
}
|
||||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing || !readyForSynthesis}
|
disabled={isProcessing || !readyForSynthesis}
|
||||||
onClick={handleRunSynthesis}
|
onClick={handleRunSynthesis}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -176,7 +179,7 @@ function NodeContextMenu({
|
||||||
text='Конституенты'
|
text='Конституенты'
|
||||||
titleHtml='Перенос конституент</br>между схемами'
|
titleHtml='Перенос конституент</br>между схемами'
|
||||||
icon={<IconChild size='1rem' className='icon-green' />}
|
icon={<IconChild size='1rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleRelocateConstituents}
|
onClick={handleRelocateConstituents}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -184,7 +187,7 @@ function NodeContextMenu({
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Удалить операцию'
|
text='Удалить операцию'
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
disabled={!controller.isMutable || controller.isProcessing || !controller.canDelete(operation.id)}
|
disabled={!controller.isMutable || isProcessing || !controller.canDelete(operation.id)}
|
||||||
onClick={handleDeleteOperation}
|
onClick={handleDeleteOperation}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -16,15 +16,23 @@ import {
|
||||||
useReactFlow
|
useReactFlow
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
|
|
||||||
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
|
import { urls } from '@/app/urls';
|
||||||
|
import { useLibrary } from '@/backend/library/useLibrary';
|
||||||
|
import { useInputCreate } from '@/backend/oss/useInputCreate';
|
||||||
|
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||||
|
import { useOperationExecute } from '@/backend/oss/useOperationExecute';
|
||||||
|
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { OssNode } from '@/models/miscellaneous';
|
import { OssNode } from '@/models/miscellaneous';
|
||||||
import { OperationID } from '@/models/oss';
|
import { OperationID } from '@/models/oss';
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { useOSSGraphStore } from '@/stores/ossGraph';
|
import { useOSSGraphStore } from '@/stores/ossGraph';
|
||||||
import { APP_COLORS } from '@/styling/color';
|
import { APP_COLORS } from '@/styling/color';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errors } from '@/utils/labels';
|
import { errors, information } from '@/utils/labels';
|
||||||
|
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
import { OssNodeTypes } from './graph/OssNodeTypes';
|
import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||||
|
@ -34,20 +42,24 @@ import ToolbarOssGraph from './ToolbarOssGraph';
|
||||||
const ZOOM_MAX = 2;
|
const ZOOM_MAX = 2;
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
|
|
||||||
interface OssFlowProps {
|
function OssFlow() {
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { items: libraryItems } = useLibrary();
|
||||||
const flow = useReactFlow();
|
const flow = useReactFlow();
|
||||||
|
const { setIsModified } = useModificationStore();
|
||||||
|
|
||||||
|
const isProcessing = useIsProcessingOss();
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
|
|
||||||
|
const { inputCreate } = useInputCreate();
|
||||||
|
const { operationExecute } = useOperationExecute();
|
||||||
|
const { updatePositions } = useUpdatePositions();
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||||
const [toggleReset, setToggleReset] = useState(false);
|
const [toggleReset, setToggleReset] = useState(false);
|
||||||
|
@ -89,8 +101,8 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
type: edgeStraight ? 'straight' : 'simplebezier',
|
type: edgeStraight ? 'straight' : 'simplebezier',
|
||||||
animated: edgeAnimate,
|
animated: edgeAnimate,
|
||||||
targetHandle:
|
targetHandle:
|
||||||
controller.schema!.operationByID.get(argument.argument)!.position_x >
|
controller.schema.operationByID.get(argument.argument)!.position_x >
|
||||||
controller.schema!.operationByID.get(argument.operation)!.position_x
|
controller.schema.operationByID.get(argument.operation)!.position_x
|
||||||
? 'right'
|
? 'right'
|
||||||
: 'left'
|
: 'left'
|
||||||
}))
|
}))
|
||||||
|
@ -117,7 +129,18 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
controller.savePositions(getPositions(), () => setIsModified(false));
|
const positions = getPositions();
|
||||||
|
updatePositions({ itemID: controller.schema.id, positions: positions }, () => {
|
||||||
|
positions.forEach(item => {
|
||||||
|
const operation = controller.schema.operationByID.get(item.id);
|
||||||
|
if (operation) {
|
||||||
|
operation.position_x = item.position_x;
|
||||||
|
operation.position_y = item.position_y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
toast.success(information.changesSaved);
|
||||||
|
setIsModified(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateOperation(inputs: OperationID[]) {
|
function handleCreateOperation(inputs: OperationID[]) {
|
||||||
|
@ -149,8 +172,19 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
handleDeleteOperation(controller.selected[0]);
|
handleDeleteOperation(controller.selected[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateInput(target: OperationID) {
|
function handleInputCreate(target: OperationID) {
|
||||||
controller.createInput(target, getPositions());
|
const operation = controller.schema.operationByID.get(target);
|
||||||
|
if (!operation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (libraryItems.find(item => item.alias === operation.alias && item.location === controller.schema.location)) {
|
||||||
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditSchema(target: OperationID) {
|
function handleEditSchema(target: OperationID) {
|
||||||
|
@ -161,15 +195,21 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
controller.promptEditOperation(target, getPositions());
|
controller.promptEditOperation(target, getPositions());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecuteOperation(target: OperationID) {
|
function handleOperationExecute(target: OperationID) {
|
||||||
controller.executeOperation(target, getPositions());
|
operationExecute(
|
||||||
|
{
|
||||||
|
itemID: controller.schema.id, //
|
||||||
|
data: { target: target, positions: getPositions() }
|
||||||
|
},
|
||||||
|
() => toast.success(information.operationExecuted)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExecuteSelected() {
|
function handleExecuteSelected() {
|
||||||
if (controller.selected.length !== 1) {
|
if (controller.selected.length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleExecuteOperation(controller.selected[0]);
|
handleOperationExecute(controller.selected[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRelocateConstituents(target: OperationID) {
|
function handleRelocateConstituents(target: OperationID) {
|
||||||
|
@ -237,14 +277,14 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (node.data.operation.result) {
|
if (node.data.operation.result) {
|
||||||
controller.openOperationSchema(Number(node.id));
|
controller.navigateOperationSchema(Number(node.id));
|
||||||
} else {
|
} else {
|
||||||
handleEditOperation(Number(node.id));
|
handleEditOperation(Number(node.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (controller.isProcessing) {
|
if (isProcessing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!controller.isMutable) {
|
if (!controller.isMutable) {
|
||||||
|
@ -274,7 +314,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
<div tabIndex={-1} onKeyDown={handleKeyDown}>
|
<div tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||||
<Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
<Overlay position='top-[1.9rem] pt-1 right-1/2 translate-x-1/2' className='rounded-b-2xl cc-blur'>
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
isModified={isModified}
|
|
||||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
||||||
onCreate={() => handleCreateOperation(controller.selected)}
|
onCreate={() => handleCreateOperation(controller.selected)}
|
||||||
onDelete={handleDeleteSelected}
|
onDelete={handleDeleteSelected}
|
||||||
|
@ -289,10 +328,10 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
|
||||||
<NodeContextMenu
|
<NodeContextMenu
|
||||||
onHide={handleContextMenuHide}
|
onHide={handleContextMenuHide}
|
||||||
onDelete={handleDeleteOperation}
|
onDelete={handleDeleteOperation}
|
||||||
onCreateInput={handleCreateInput}
|
onCreateInput={handleInputCreate}
|
||||||
onEditSchema={handleEditSchema}
|
onEditSchema={handleEditSchema}
|
||||||
onEditOperation={handleEditOperation}
|
onEditOperation={handleEditOperation}
|
||||||
onExecuteOperation={handleExecuteOperation}
|
onExecuteOperation={handleOperationExecute}
|
||||||
onRelocateConstituents={handleRelocateConstituents}
|
onRelocateConstituents={handleRelocateConstituents}
|
||||||
{...menuProps}
|
{...menuProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||||
import {
|
import {
|
||||||
IconAnimation,
|
IconAnimation,
|
||||||
IconAnimationOff,
|
IconAnimationOff,
|
||||||
|
@ -21,6 +22,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { OperationType } from '@/models/oss';
|
import { OperationType } from '@/models/oss';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { useOSSGraphStore } from '@/stores/ossGraph';
|
import { useOSSGraphStore } from '@/stores/ossGraph';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/labels';
|
import { prepareTooltip } from '@/utils/labels';
|
||||||
|
@ -28,7 +30,6 @@ import { prepareTooltip } from '@/utils/labels';
|
||||||
import { useOssEdit } from '../OssEditContext';
|
import { useOssEdit } from '../OssEditContext';
|
||||||
|
|
||||||
interface ToolbarOssGraphProps {
|
interface ToolbarOssGraphProps {
|
||||||
isModified: boolean;
|
|
||||||
onCreate: () => void;
|
onCreate: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
|
@ -40,7 +41,6 @@ interface ToolbarOssGraphProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarOssGraph({
|
function ToolbarOssGraph({
|
||||||
isModified,
|
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
@ -51,6 +51,8 @@ function ToolbarOssGraph({
|
||||||
onResetPositions
|
onResetPositions
|
||||||
}: ToolbarOssGraphProps) {
|
}: ToolbarOssGraphProps) {
|
||||||
const controller = useOssEdit();
|
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 showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
|
@ -73,7 +75,7 @@ function ToolbarOssGraph({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentOperations = argumentIDs.map(id => controller.schema!.operationByID.get(id)!);
|
const argumentOperations = argumentIDs.map(id => controller.schema.operationByID.get(id)!);
|
||||||
if (argumentOperations.some(item => item.result === null)) {
|
if (argumentOperations.some(item => item.result === null)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -144,35 +146,31 @@ function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
disabled={controller.isProcessing || !isModified}
|
disabled={isProcessing || !isModified}
|
||||||
onClick={onSavePositions}
|
onClick={onSavePositions}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={onCreate}
|
onClick={onCreate}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Активировать операцию'
|
title='Активировать операцию'
|
||||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
disabled={isProcessing || controller.selected.length !== 1 || !readyForSynthesis}
|
||||||
onClick={onExecute}
|
onClick={onExecute}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||||
disabled={controller.selected.length !== 1 || controller.isProcessing}
|
disabled={controller.selected.length !== 1 || isProcessing}
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={
|
disabled={controller.selected.length !== 1 || isProcessing || !controller.canDelete(controller.selected[0])}
|
||||||
controller.selected.length !== 1 ||
|
|
||||||
controller.isProcessing ||
|
|
||||||
!controller.canDelete(controller.selected[0])
|
|
||||||
}
|
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
|
import { useIsProcessingOss } from '@/backend/oss/useIsProcessingOss';
|
||||||
import {
|
import {
|
||||||
IconAdmin,
|
IconAdmin,
|
||||||
IconAlert,
|
IconAlert,
|
||||||
|
@ -25,18 +26,17 @@ import useDropdown from '@/hooks/useDropdown';
|
||||||
import { UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { describeAccessMode as describeUserRole, labelAccessMode as labelUserRole } from '@/utils/labels';
|
import { describeAccessMode as describeUserRole, labelAccessMode as labelUserRole } from '@/utils/labels';
|
||||||
|
import { sharePage } from '@/utils/utils';
|
||||||
|
|
||||||
import { useOssEdit } from './OssEditContext';
|
import { useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
interface MenuOssTabsProps {
|
function MenuOssTabs() {
|
||||||
onDestroy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
|
||||||
const controller = useOssEdit();
|
const controller = useOssEdit();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const isProcessing = useIsProcessingOss();
|
||||||
|
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const setRole = useRoleStore(state => state.setRole);
|
const setRole = useRoleStore(state => state.setRole);
|
||||||
|
|
||||||
|
@ -46,12 +46,12 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
onDestroy();
|
controller.deleteSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
controller.share();
|
sharePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeRole(newMode: UserRole) {
|
function handleChangeRole(newMode: UserRole) {
|
||||||
|
@ -96,7 +96,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Удалить схему'
|
text='Удалить схему'
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
disabled={controller.isProcessing || role < UserRole.OWNER}
|
disabled={isProcessing || role < UserRole.OWNER}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -136,7 +136,7 @@ function MenuOssTabs({ onDestroy }: MenuOssTabsProps) {
|
||||||
text='Конституенты'
|
text='Конституенты'
|
||||||
titleHtml='Перенос конституент</br>между схемами'
|
titleHtml='Перенос конституент</br>между схемами'
|
||||||
icon={<IconChild size='1rem' className='icon-green' />}
|
icon={<IconChild size='1rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={handleRelocate}
|
onClick={handleRelocate}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -6,24 +6,30 @@ import { toast } from 'react-toastify';
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { useLibrary } from '@/backend/library/useLibrary';
|
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
||||||
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
|
import { useInputUpdate } from '@/backend/oss/useInputUpdate';
|
||||||
import { useSetEditors } from '@/backend/library/useSetEditors';
|
import { useOperationCreate } from '@/backend/oss/useOperationCreate';
|
||||||
import { useSetLocation } from '@/backend/library/useSetLocation';
|
import { useOperationDelete } from '@/backend/oss/useOperationDelete';
|
||||||
import { useSetOwner } from '@/backend/library/useSetOwner';
|
import { useOperationUpdate } from '@/backend/oss/useOperationUpdate';
|
||||||
import { useOssSuspense } from '@/backend/oss/useOSS';
|
import { useOssSuspense } from '@/backend/oss/useOSS';
|
||||||
import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library';
|
import { useRelocateConstituents } from '@/backend/oss/useRelocateConstituents';
|
||||||
import { Position2D } from '@/models/miscellaneous';
|
import { useUpdatePositions } from '@/backend/oss/useUpdatePositions';
|
||||||
|
import { ILibraryItemEditor, LibraryItemID } from '@/models/library';
|
||||||
import { calculateInsertPosition } from '@/models/miscellaneousAPI';
|
import { calculateInsertPosition } from '@/models/miscellaneousAPI';
|
||||||
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
import { IOperationPosition, IOperationSchema, OperationID, OperationType } from '@/models/oss';
|
||||||
import { UserID, UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { errors, information } from '@/utils/labels';
|
import { information, prompts } from '@/utils/labels';
|
||||||
|
|
||||||
import { RSTabID } from '../RSFormPage/RSTabs';
|
import { RSTabID } from '../RSFormPage/RSEditContext';
|
||||||
|
|
||||||
|
export enum OssTabID {
|
||||||
|
CARD = 0,
|
||||||
|
GRAPH = 1
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICreateOperationPrompt {
|
export interface ICreateOperationPrompt {
|
||||||
defaultX: number;
|
defaultX: number;
|
||||||
|
@ -34,36 +40,27 @@ export interface ICreateOperationPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOssEditContext extends ILibraryItemEditor {
|
export interface IOssEditContext extends ILibraryItemEditor {
|
||||||
schema?: IOperationSchema;
|
schema: IOperationSchema;
|
||||||
selected: OperationID[];
|
selected: OperationID[];
|
||||||
|
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
isMutable: boolean;
|
isMutable: boolean;
|
||||||
isProcessing: boolean;
|
|
||||||
isAttachedToOSS: boolean;
|
isAttachedToOSS: boolean;
|
||||||
|
|
||||||
showTooltip: boolean;
|
showTooltip: boolean;
|
||||||
setShowTooltip: (newValue: boolean) => void;
|
setShowTooltip: (newValue: boolean) => void;
|
||||||
|
|
||||||
setOwner: (newOwner: UserID) => void;
|
navigateTab: (tab: OssTabID) => void;
|
||||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
navigateOperationSchema: (target: OperationID) => void;
|
||||||
promptEditors: () => void;
|
|
||||||
promptLocation: () => void;
|
|
||||||
|
|
||||||
|
deleteSchema: () => void;
|
||||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
||||||
|
|
||||||
share: () => void;
|
|
||||||
|
|
||||||
openOperationSchema: (target: OperationID) => void;
|
|
||||||
|
|
||||||
savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
|
|
||||||
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
|
||||||
canDelete: (target: OperationID) => boolean;
|
canDelete: (target: OperationID) => boolean;
|
||||||
|
promptCreateOperation: (props: ICreateOperationPrompt) => void;
|
||||||
promptDeleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
promptDeleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
|
||||||
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
||||||
executeOperation: (target: OperationID, positions: IOperationPosition[]) => void;
|
|
||||||
promptRelocateConstituents: (target: OperationID | undefined, positions: IOperationPosition[]) => void;
|
promptRelocateConstituents: (target: OperationID | undefined, positions: IOperationPosition[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,325 +75,229 @@ export const useOssEdit = () => {
|
||||||
|
|
||||||
interface OssEditStateProps {
|
interface OssEditStateProps {
|
||||||
itemID: LibraryItemID;
|
itemID: LibraryItemID;
|
||||||
selected: OperationID[];
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<OperationID[]>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OssEditState = ({
|
export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEditStateProps>) => {
|
||||||
itemID,
|
|
||||||
selected,
|
|
||||||
setSelected,
|
|
||||||
children
|
|
||||||
}: React.PropsWithChildren<OssEditStateProps>) => {
|
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { items: libraryItems } = useLibrary();
|
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
|
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||||
const model = useOssSuspense({ itemID: itemID });
|
const { schema } = useOssSuspense({ itemID: itemID });
|
||||||
|
|
||||||
const { setOwner: setItemOwner } = useSetOwner();
|
const isOwned = user?.id === schema.owner || false;
|
||||||
const { setLocation: setItemLocation } = useSetLocation();
|
|
||||||
const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
|
|
||||||
const { setEditors: setItemEditors } = useSetEditors();
|
|
||||||
|
|
||||||
const isOwned = user?.id === model.schema?.owner || false;
|
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||||
|
|
||||||
const isMutable = role > UserRole.READER && !model.schema?.read_only;
|
|
||||||
|
|
||||||
const [showTooltip, setShowTooltip] = useState(true);
|
const [showTooltip, setShowTooltip] = useState(true);
|
||||||
|
|
||||||
const [insertPosition, setInsertPosition] = useState<Position2D>({ x: 0, y: 0 });
|
const [selected, setSelected] = useState<OperationID[]>([]);
|
||||||
const [createCallback, setCreateCallback] = useState<((newID: OperationID) => void) | undefined>(undefined);
|
|
||||||
|
|
||||||
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
|
||||||
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
|
||||||
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
|
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
|
||||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
||||||
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||||
|
|
||||||
const [positions, setPositions] = useState<IOperationPosition[]>([]);
|
const { deleteItem } = useDeleteItem();
|
||||||
|
const { updatePositions } = useUpdatePositions();
|
||||||
|
const { operationCreate } = useOperationCreate();
|
||||||
|
const { operationDelete } = useOperationDelete();
|
||||||
|
const { operationUpdate } = useOperationUpdate();
|
||||||
|
const { relocateConstituents } = useRelocateConstituents();
|
||||||
|
const { inputUpdate } = useInputUpdate();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
adjustRole({
|
adjustRole({
|
||||||
isOwner: model.isOwned,
|
isOwner: isOwned,
|
||||||
isEditor: (user && model.schema?.editors.includes(user?.id)) ?? false,
|
isEditor: (user && schema.editors.includes(user?.id)) ?? false,
|
||||||
isStaff: user?.is_staff ?? false,
|
isStaff: user?.is_staff ?? false,
|
||||||
adminMode: adminMode
|
adminMode: adminMode
|
||||||
}),
|
}),
|
||||||
[model.schema, adjustRole, model.isOwned, user, adminMode]
|
[schema, adjustRole, isOwned, user, adminMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSetLocation = (newLocation: string) =>
|
function navigateTab(tab: OssTabID) {
|
||||||
setItemLocation({ itemID: model.itemID!, location: newLocation }, () => toast.success(information.moveComplete));
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = urls.oss_props({
|
||||||
|
id: schema.id,
|
||||||
|
tab: tab
|
||||||
|
});
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
|
||||||
const share = useCallback(() => {
|
const navigateOperationSchema = useCallback(
|
||||||
const currentRef = window.location.href;
|
|
||||||
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() => toast.success(information.linkReady))
|
|
||||||
.catch(console.error);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setOwner = (newOwner: UserID) =>
|
|
||||||
setItemOwner({ itemID: model.itemID!, owner: newOwner }, () => toast.success(information.changesSaved));
|
|
||||||
|
|
||||||
const setAccessPolicy = (newPolicy: AccessPolicy) =>
|
|
||||||
setItemAccessPolicy({ itemID: model.itemID!, policy: newPolicy }, () => toast.success(information.changesSaved));
|
|
||||||
|
|
||||||
const handleSetEditors = (newEditors: UserID[]) =>
|
|
||||||
setItemEditors({ itemID: model.itemID!, editors: newEditors }, () => toast.success(information.changesSaved));
|
|
||||||
|
|
||||||
const openOperationSchema = useCallback(
|
|
||||||
(target: OperationID) => {
|
(target: OperationID) => {
|
||||||
const node = model.schema?.operationByID.get(target);
|
const node = schema.operationByID.get(target);
|
||||||
if (!node?.result) {
|
if (!node?.result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }));
|
router.push(urls.schema_props({ id: node.result, tab: RSTabID.CST_LIST }));
|
||||||
},
|
},
|
||||||
[router, model]
|
[router, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const savePositions = useCallback(
|
function deleteSchema() {
|
||||||
(positions: IOperationPosition[], callback?: () => void) => {
|
if (!schema || !window.confirm(prompts.deleteOSS)) {
|
||||||
model.savePositions({ positions: positions }, () => {
|
return;
|
||||||
positions.forEach(item => {
|
}
|
||||||
const operation = model.schema?.operationByID.get(item.id);
|
deleteItem(schema.id, () => {
|
||||||
if (operation) {
|
toast.success(information.itemDestroyed);
|
||||||
operation.position_x = item.position_x;
|
router.push(urls.library);
|
||||||
operation.position_y = item.position_y;
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
|
||||||
|
showCreateOperation({
|
||||||
|
oss: schema,
|
||||||
|
onCreate: data => {
|
||||||
|
const target = calculateInsertPosition(schema, data.item_data.operation_type, data.arguments ?? [], positions, {
|
||||||
|
x: defaultX,
|
||||||
|
y: defaultY
|
||||||
|
});
|
||||||
|
data.positions = positions;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
toast.success(information.changesSaved);
|
},
|
||||||
callback?.();
|
initialInputs: inputs
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
[model]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCreateOperation = useCallback(
|
function canDelete(target: OperationID) {
|
||||||
(data: IOperationCreateData) => {
|
const operation = schema.operationByID.get(target);
|
||||||
const target = calculateInsertPosition(
|
if (!operation) {
|
||||||
model.schema!,
|
return false;
|
||||||
data.item_data.operation_type,
|
}
|
||||||
data.arguments,
|
if (operation.operation_type === OperationType.INPUT) {
|
||||||
positions,
|
return true;
|
||||||
insertPosition
|
}
|
||||||
);
|
return schema.graph.expandOutputs([target]).length === 0;
|
||||||
data.positions = positions;
|
}
|
||||||
data.item_data.position_x = target.x;
|
|
||||||
data.item_data.position_y = target.y;
|
|
||||||
model.createOperation(data, operation => {
|
|
||||||
toast.success(information.newOperation(operation.alias));
|
|
||||||
if (createCallback) {
|
|
||||||
setTimeout(() => createCallback(operation.id), PARAMETER.refreshTimeout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[model, positions, insertPosition, createCallback]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEditOperation = useCallback(
|
function promptEditOperation(target: OperationID, positions: IOperationPosition[]) {
|
||||||
(data: IOperationUpdateData) => {
|
const operation = schema.operationByID.get(target);
|
||||||
data.positions = positions;
|
if (!operation) {
|
||||||
model.updateOperation(data, () => toast.success(information.changesSaved));
|
return;
|
||||||
},
|
}
|
||||||
[model, positions]
|
showEditOperation({
|
||||||
);
|
oss: schema,
|
||||||
|
target: operation,
|
||||||
const canDelete = useCallback(
|
onSubmit: data => {
|
||||||
(target: OperationID) => {
|
data.positions = positions;
|
||||||
if (!model.schema) {
|
operationUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const operation = model.schema.operationByID.get(target);
|
});
|
||||||
if (!operation) {
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (operation.operation_type === OperationType.INPUT) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return model.schema.graph.expandOutputs([target]).length === 0;
|
|
||||||
},
|
|
||||||
[model]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeleteOperation = useCallback(
|
function promptDeleteOperation(target: OperationID, positions: IOperationPosition[]) {
|
||||||
(targetID: OperationID, keepConstituents: boolean, deleteSchema: boolean) => {
|
const operation = schema.operationByID.get(target);
|
||||||
const data: IOperationDeleteData = {
|
if (!operation) {
|
||||||
target: targetID,
|
return;
|
||||||
positions: positions,
|
}
|
||||||
keep_constituents: keepConstituents,
|
showDeleteOperation({
|
||||||
delete_schema: deleteSchema
|
target: operation,
|
||||||
};
|
onSubmit: (targetID, keepConstituents, deleteSchema) => {
|
||||||
model.deleteOperation(data, () => toast.success(information.operationDestroyed));
|
operationDelete(
|
||||||
},
|
{
|
||||||
[model, positions]
|
itemID: schema.id,
|
||||||
);
|
data: {
|
||||||
|
target: targetID,
|
||||||
const createInput = useCallback(
|
positions: positions,
|
||||||
(target: OperationID, positions: IOperationPosition[]) => {
|
keep_constituents: keepConstituents,
|
||||||
const operation = model.schema?.operationByID.get(target);
|
delete_schema: deleteSchema
|
||||||
if (!model.schema || !operation) {
|
}
|
||||||
return;
|
},
|
||||||
}
|
() => toast.success(information.operationDestroyed)
|
||||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === model.schema!.location)) {
|
|
||||||
toast.error(errors.inputAlreadyExists);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.createInput({ target: target, positions: positions }, new_schema => {
|
|
||||||
toast.success(information.newLibraryItem);
|
|
||||||
router.push(urls.schema(new_schema.id));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[model, libraryItems, router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setTargetInput = useCallback(
|
|
||||||
(target: OperationID, newInput: LibraryItemID | undefined) => {
|
|
||||||
const data: IOperationSetInputData = {
|
|
||||||
target: target,
|
|
||||||
positions: positions,
|
|
||||||
input: newInput ?? null
|
|
||||||
};
|
|
||||||
model.setInput(data, () => toast.success(information.changesSaved));
|
|
||||||
},
|
|
||||||
[model, positions]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRelocateConstituents = useCallback(
|
|
||||||
(data: ICstRelocateData) => {
|
|
||||||
if (
|
|
||||||
positions.every(item => {
|
|
||||||
const operation = model.schema!.operationByID.get(item.id)!;
|
|
||||||
return operation.position_x === item.position_x && operation.position_y === item.position_y;
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
model.relocateConstituents(data, () => toast.success(information.changesSaved));
|
|
||||||
} else {
|
|
||||||
model.savePositions({ positions: positions }, () =>
|
|
||||||
model.relocateConstituents(data, () => toast.success(information.changesSaved))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
[model, positions]
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const executeOperation = useCallback(
|
function promptEditInput(target: OperationID, positions: IOperationPosition[]) {
|
||||||
(target: OperationID, positions: IOperationPosition[]) => {
|
const operation = schema.operationByID.get(target);
|
||||||
const data = {
|
if (!operation) {
|
||||||
target: target,
|
return;
|
||||||
positions: positions
|
}
|
||||||
};
|
showEditInput({
|
||||||
model.executeOperation(data, () => toast.success(information.operationExecuted));
|
oss: schema,
|
||||||
},
|
target: operation,
|
||||||
[model]
|
onSubmit: (target, newInput) => {
|
||||||
);
|
inputUpdate(
|
||||||
|
{
|
||||||
const promptEditors = () => showEditEditors({ editors: model.schema?.editors ?? [], setEditors: handleSetEditors });
|
itemID: schema.id,
|
||||||
|
data: {
|
||||||
const promptLocation = () =>
|
target: target,
|
||||||
showEditLocation({ initial: model.schema?.location ?? '', onChangeLocation: handleSetLocation });
|
positions: positions,
|
||||||
|
input: newInput ?? null
|
||||||
const promptCreateOperation = useCallback(
|
}
|
||||||
({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) => {
|
},
|
||||||
if (!model.schema) {
|
() => toast.success(information.changesSaved)
|
||||||
return;
|
);
|
||||||
}
|
}
|
||||||
setInsertPosition({ x: defaultX, y: defaultY });
|
});
|
||||||
setPositions(positions);
|
}
|
||||||
setCreateCallback(() => callback);
|
|
||||||
showCreateOperation({ oss: model.schema, onCreate: handleCreateOperation, initialInputs: inputs });
|
|
||||||
},
|
|
||||||
[model.schema, showCreateOperation, handleCreateOperation]
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptEditOperation = useCallback(
|
function promptRelocateConstituents(target: OperationID | undefined, positions: IOperationPosition[]) {
|
||||||
(target: OperationID, positions: IOperationPosition[]) => {
|
const operation = target ? schema.operationByID.get(target) : undefined;
|
||||||
const operation = model.schema?.operationByID.get(target);
|
showRelocateConstituents({
|
||||||
if (!model.schema || !operation) {
|
oss: schema,
|
||||||
return;
|
initialTarget: operation,
|
||||||
|
onSubmit: data => {
|
||||||
|
if (
|
||||||
|
positions.every(item => {
|
||||||
|
const operation = schema.operationByID.get(item.id)!;
|
||||||
|
return operation.position_x === item.position_x && operation.position_y === item.position_y;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
||||||
|
} else {
|
||||||
|
updatePositions(
|
||||||
|
{
|
||||||
|
itemID: schema.id, //
|
||||||
|
positions: positions
|
||||||
|
},
|
||||||
|
() => relocateConstituents({ itemID: schema.id, data }, () => toast.success(information.changesSaved))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setPositions(positions);
|
});
|
||||||
showEditOperation({ oss: model.schema, target: operation, onSubmit: handleEditOperation });
|
}
|
||||||
},
|
|
||||||
[model.schema, showEditOperation, handleEditOperation]
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptDeleteOperation = useCallback(
|
|
||||||
(target: OperationID, positions: IOperationPosition[]) => {
|
|
||||||
const operation = model.schema?.operationByID.get(target);
|
|
||||||
if (!model.schema || !operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPositions(positions);
|
|
||||||
showDeleteOperation({ target: operation, onSubmit: handleDeleteOperation });
|
|
||||||
},
|
|
||||||
[model.schema, showDeleteOperation, handleDeleteOperation]
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptEditInput = useCallback(
|
|
||||||
(target: OperationID, positions: IOperationPosition[]) => {
|
|
||||||
const operation = model.schema?.operationByID.get(target);
|
|
||||||
if (!model.schema || !operation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPositions(positions);
|
|
||||||
showEditInput({ oss: model.schema, target: operation, onSubmit: setTargetInput });
|
|
||||||
},
|
|
||||||
[model.schema, showEditInput, setTargetInput]
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptRelocateConstituents = useCallback(
|
|
||||||
(target: OperationID | undefined, positions: IOperationPosition[]) => {
|
|
||||||
if (!model.schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const operation = target ? model.schema?.operationByID.get(target) : undefined;
|
|
||||||
setPositions(positions);
|
|
||||||
showRelocateConstituents({ oss: model.schema, initialTarget: operation, onSubmit: handleRelocateConstituents });
|
|
||||||
},
|
|
||||||
[model.schema, showRelocateConstituents, handleRelocateConstituents]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssEditContext
|
<OssEditContext
|
||||||
value={{
|
value={{
|
||||||
schema: model.schema,
|
schema,
|
||||||
selected,
|
selected,
|
||||||
|
|
||||||
|
navigateTab,
|
||||||
|
|
||||||
|
deleteSchema,
|
||||||
|
|
||||||
showTooltip,
|
showTooltip,
|
||||||
setShowTooltip,
|
setShowTooltip,
|
||||||
|
|
||||||
isOwned,
|
isOwned,
|
||||||
isMutable,
|
isMutable,
|
||||||
isProcessing: model.processing,
|
|
||||||
isAttachedToOSS: false,
|
isAttachedToOSS: false,
|
||||||
|
|
||||||
setOwner,
|
|
||||||
setAccessPolicy,
|
|
||||||
promptEditors,
|
|
||||||
promptLocation,
|
|
||||||
|
|
||||||
share,
|
|
||||||
setSelected,
|
setSelected,
|
||||||
|
|
||||||
openOperationSchema,
|
navigateOperationSchema,
|
||||||
savePositions,
|
|
||||||
promptCreateOperation,
|
promptCreateOperation,
|
||||||
canDelete,
|
canDelete,
|
||||||
promptDeleteOperation,
|
promptDeleteOperation,
|
||||||
createInput,
|
|
||||||
promptEditInput,
|
promptEditInput,
|
||||||
promptEditOperation,
|
promptEditOperation,
|
||||||
executeOperation,
|
|
||||||
promptRelocateConstituents
|
promptRelocateConstituents
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,23 +1,69 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Suspense } from 'react';
|
import axios from 'axios';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import Loader from '@/components/ui/Loader';
|
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { OssState } from '@/pages/OssPage/OssContext';
|
import { urls } from '@/app/urls';
|
||||||
|
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
|
import TextURL from '@/components/ui/TextURL';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
|
import { OssEditState } from './OssEditContext';
|
||||||
import OssTabs from './OssTabs';
|
import OssTabs from './OssTabs';
|
||||||
|
|
||||||
function OssPage() {
|
function OssPage() {
|
||||||
|
const router = useConceptNavigation();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const itemID = params.id ? Number(params.id) : undefined;
|
const itemID = params.id ? Number(params.id) : undefined;
|
||||||
|
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
|
// useBlockNavigation(
|
||||||
|
// isModified &&
|
||||||
|
// schema !== undefined &&
|
||||||
|
// !!user &&
|
||||||
|
// (user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
|
||||||
|
// );
|
||||||
|
|
||||||
|
if (!itemID) {
|
||||||
|
router.replace(urls.page404);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loader />}>
|
<ErrorBoundary FallbackComponent={ProcessError}>
|
||||||
<OssState itemID={itemID}>
|
<OssEditState itemID={itemID}>
|
||||||
<OssTabs />
|
<OssTabs />
|
||||||
</OssState>
|
</OssEditState>
|
||||||
</Suspense>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OssPage;
|
export default OssPage;
|
||||||
|
|
||||||
|
// ====== Internals =========
|
||||||
|
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||||
|
if (axios.isAxiosError(error) && error.response) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||||
|
<p>{`Операционная схема с указанным идентификатором отсутствует`}</p>
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<TextURL text='Библиотека' href='/library' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (error.response.status === 403) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||||
|
<p>Владелец ограничил доступ к данной схеме</p>
|
||||||
|
<TextURL text='Библиотека' href='/library' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <InfoError error={error} />;
|
||||||
|
}
|
||||||
|
|
|
@ -1,79 +1,46 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
|
||||||
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
|
||||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
|
||||||
import Loader from '@/components/ui/Loader';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
import { OperationID } from '@/models/oss';
|
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { information, prompts } from '@/utils/labels';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
import EditorRSForm from './EditorOssCard';
|
import EditorRSForm from './EditorOssCard';
|
||||||
import EditorTermGraph from './EditorOssGraph';
|
import EditorTermGraph from './EditorOssGraph';
|
||||||
import MenuOssTabs from './MenuOssTabs';
|
import MenuOssTabs from './MenuOssTabs';
|
||||||
import { OssEditState } from './OssEditContext';
|
import { OssTabID, useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
export enum OssTabID {
|
|
||||||
CARD = 0,
|
|
||||||
GRAPH = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function OssTabs() {
|
function OssTabs() {
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
const activeTab = query.get('tab') ? (Number(query.get('tab')) as OssTabID) : OssTabID.GRAPH;
|
||||||
const { user } = useAuth();
|
|
||||||
const { deleteItem } = useDeleteItem();
|
const { schema, navigateTab } = useOssEdit();
|
||||||
|
|
||||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||||
const { schema, loading, loadingError: errorLoading } = useOSSControl();
|
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false);
|
const { setIsModified } = useModificationStore();
|
||||||
const [selected, setSelected] = useState<OperationID[]>([]);
|
|
||||||
useBlockNavigation(
|
useEffect(() => setIsModified(false), [setIsModified]);
|
||||||
isModified &&
|
|
||||||
schema !== undefined &&
|
|
||||||
!!user &&
|
|
||||||
(user.is_staff || user.id == schema.owner || schema.editors.includes(user.id))
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schema) {
|
const oldTitle = document.title;
|
||||||
const oldTitle = document.title;
|
document.title = schema.title;
|
||||||
document.title = schema.title;
|
return () => {
|
||||||
return () => {
|
document.title = oldTitle;
|
||||||
document.title = oldTitle;
|
};
|
||||||
};
|
}, [schema.title]);
|
||||||
}
|
|
||||||
}, [schema, schema?.title]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hideFooter(activeTab === OssTabID.GRAPH);
|
hideFooter(activeTab === OssTabID.GRAPH);
|
||||||
}, [activeTab, hideFooter]);
|
}, [activeTab, hideFooter]);
|
||||||
|
|
||||||
function navigateTab(tab: OssTabID) {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = urls.oss_props({
|
|
||||||
id: schema.id,
|
|
||||||
tab: tab
|
|
||||||
});
|
|
||||||
router.push(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectTab(index: number, last: number, event: Event) {
|
function onSelectTab(index: number, last: number, event: Event) {
|
||||||
if (last === index) {
|
if (last === index) {
|
||||||
return;
|
return;
|
||||||
|
@ -93,78 +60,34 @@ function OssTabs() {
|
||||||
navigateTab(index);
|
navigateTab(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDestroySchema() {
|
|
||||||
if (!schema || !window.confirm(prompts.deleteOSS)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
deleteItem(schema.id, () => {
|
|
||||||
toast.success(information.itemDestroyed);
|
|
||||||
router.push(urls.library);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssEditState selected={selected} setSelected={setSelected}>
|
<Tabs
|
||||||
{loading ? <Loader /> : null}
|
selectedIndex={activeTab}
|
||||||
{errorLoading ? <ProcessError error={errorLoading} /> : null}
|
onSelect={onSelectTab}
|
||||||
{schema && !loading ? (
|
defaultFocus
|
||||||
<Tabs
|
selectedTabClassName='clr-selected'
|
||||||
selectedIndex={activeTab}
|
className='flex flex-col mx-auto min-w-fit'
|
||||||
onSelect={onSelectTab}
|
>
|
||||||
defaultFocus
|
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
|
||||||
selectedTabClassName='clr-selected'
|
<TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}>
|
||||||
className='flex flex-col mx-auto min-w-fit'
|
<MenuOssTabs />
|
||||||
>
|
|
||||||
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
|
|
||||||
<TabList className={clsx('w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}>
|
|
||||||
<MenuOssTabs onDestroy={onDestroySchema} />
|
|
||||||
|
|
||||||
<TabLabel label='Карточка' title={schema.title ?? ''} />
|
<TabLabel label='Карточка' title={schema.title ?? ''} />
|
||||||
<TabLabel label='Граф' />
|
<TabLabel label='Граф' />
|
||||||
</TabList>
|
</TabList>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
||||||
<div className='overflow-x-hidden'>
|
<div className='overflow-x-hidden'>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorRSForm
|
<EditorRSForm />
|
||||||
isModified={isModified} //
|
</TabPanel>
|
||||||
setIsModified={setIsModified}
|
|
||||||
onDestroy={onDestroySchema}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorTermGraph isModified={isModified} setIsModified={setIsModified} />
|
<EditorTermGraph />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : null}
|
|
||||||
</OssEditState>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OssTabs;
|
export default OssTabs;
|
||||||
|
|
||||||
// ====== Internals =========
|
|
||||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
|
||||||
if (axios.isAxiosError(error) && error.response) {
|
|
||||||
if (error.response.status === 404) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
|
||||||
<p>{`Операционная схема с указанным идентификатором отсутствует`}</p>
|
|
||||||
<div className='flex justify-center'>
|
|
||||||
<TextURL text='Библиотека' href='/library' />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (error.response.status === 403) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
|
||||||
<p>Владелец ограничил доступ к данной схеме</p>
|
|
||||||
<TextURL text='Библиотека' href='/library' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <InfoError error={error} />;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import clsx from 'clsx';
|
|
||||||
|
|
||||||
import { IconEdit } from '@/components/Icons';
|
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
|
||||||
import { IConstituenta } from '@/models/rsform';
|
|
||||||
import { tooltips } from '@/utils/labels';
|
|
||||||
|
|
||||||
interface ControlsOverlayProps {
|
|
||||||
constituenta: IConstituenta;
|
|
||||||
disabled: boolean;
|
|
||||||
modified: boolean;
|
|
||||||
processing: boolean;
|
|
||||||
|
|
||||||
onRename: () => void;
|
|
||||||
onEditTerm: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ControlsOverlay({ constituenta, disabled, modified, processing, onRename, onEditTerm }: ControlsOverlayProps) {
|
|
||||||
return (
|
|
||||||
<Overlay position='top-1 left-[4.7rem]' className='flex select-none'>
|
|
||||||
{!disabled || processing ? (
|
|
||||||
<MiniButton
|
|
||||||
title={modified ? tooltips.unsaved : `Редактировать словоформы термина`}
|
|
||||||
noHover
|
|
||||||
onClick={onEditTerm}
|
|
||||||
icon={<IconEdit size='1rem' className='icon-primary' />}
|
|
||||||
disabled={modified}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'pt-1 sm:pl-[1.375rem] pl-1', //
|
|
||||||
'text-sm font-medium whitespace-nowrap',
|
|
||||||
'select-text cursor-default',
|
|
||||||
disabled && !processing && 'pl-[1.6rem] sm:pl-[2.8rem]'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>Имя </span>
|
|
||||||
<span className='ml-1'>{constituenta?.alias ?? ''}</span>
|
|
||||||
</div>
|
|
||||||
{!disabled || processing ? (
|
|
||||||
<MiniButton
|
|
||||||
noHover
|
|
||||||
title={modified ? tooltips.unsaved : 'Переименовать конституенту'}
|
|
||||||
onClick={onRename}
|
|
||||||
icon={<IconEdit size='1rem' className='icon-primary' />}
|
|
||||||
disabled={modified}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Overlay>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ControlsOverlay;
|
|
|
@ -2,12 +2,18 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
|
||||||
import { useMainHeight } from '@/stores/appLayout';
|
import { useMainHeight } from '@/stores/appLayout';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
import { information } from '@/utils/labels';
|
||||||
|
import { promptUnsaved } from '@/utils/utils';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import ViewConstituents from '../ViewConstituents';
|
import ViewConstituents from '../ViewConstituents';
|
||||||
|
@ -17,23 +23,20 @@ import ToolbarConstituenta from './ToolbarConstituenta';
|
||||||
// Threshold window width to switch layout.
|
// Threshold window width to switch layout.
|
||||||
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
|
const SIDELIST_LAYOUT_THRESHOLD = 1000; // px
|
||||||
|
|
||||||
interface EditorConstituentaProps {
|
function EditorConstituenta() {
|
||||||
activeCst?: IConstituenta;
|
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }: EditorConstituentaProps) {
|
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
|
|
||||||
const showList = usePreferencesStore(state => state.showCstSideList);
|
const showList = usePreferencesStore(state => state.showCstSideList);
|
||||||
|
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
|
||||||
|
const { cstUpdate } = useCstUpdate();
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
|
||||||
const [toggleReset, setToggleReset] = useState(false);
|
const [toggleReset, setToggleReset] = useState(false);
|
||||||
|
|
||||||
const disabled = !activeCst || !controller.isContentEditable || controller.isProcessing;
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
const disabled = !controller.activeCst || !controller.isContentEditable || isProcessing;
|
||||||
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
|
const isNarrow = !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD;
|
||||||
|
|
||||||
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
|
@ -56,6 +59,29 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleEditTermForms() {
|
||||||
|
if (!controller.activeCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isModified && !promptUnsaved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initiateSubmit() {
|
function initiateSubmit() {
|
||||||
const element = document.getElementById(globals.constituenta_editor) as HTMLFormElement;
|
const element = document.getElementById(globals.constituenta_editor) as HTMLFormElement;
|
||||||
if (element) {
|
if (element) {
|
||||||
|
@ -76,9 +102,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolbarConstituenta
|
<ToolbarConstituenta
|
||||||
activeCst={activeCst}
|
activeCst={controller.activeCst}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
modified={isModified}
|
|
||||||
onSubmit={initiateSubmit}
|
onSubmit={initiateSubmit}
|
||||||
onReset={() => setToggleReset(prev => !prev)}
|
onReset={() => setToggleReset(prev => !prev)}
|
||||||
/>
|
/>
|
||||||
|
@ -97,21 +122,13 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
||||||
<FormConstituenta
|
<FormConstituenta
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={globals.constituenta_editor}
|
id={globals.constituenta_editor}
|
||||||
state={activeCst}
|
|
||||||
isModified={isModified}
|
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
setIsModified={setIsModified}
|
onEditTerm={handleEditTermForms}
|
||||||
onEditTerm={controller.editTermForms}
|
|
||||||
onRename={controller.renameCst}
|
|
||||||
onOpenEdit={onOpenEdit}
|
|
||||||
/>
|
/>
|
||||||
<ViewConstituents
|
<ViewConstituents
|
||||||
isMounted={showList}
|
isMounted={showList}
|
||||||
schema={controller.schema}
|
expression={controller.activeCst?.definition_formal ?? ''}
|
||||||
expression={activeCst?.definition_formal ?? ''}
|
|
||||||
isBottom={isNarrow}
|
isBottom={isNarrow}
|
||||||
activeCst={activeCst}
|
|
||||||
onOpenEdit={onOpenEdit}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { ICstRenameDTO } from '@/backend/rsform/api';
|
||||||
|
import { useCstRename } from '@/backend/rsform/useCstRename';
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
|
import { IconEdit } from '@/components/Icons';
|
||||||
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
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 { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
|
interface EditorControlsProps {
|
||||||
|
constituenta: IConstituenta;
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
|
onEditTerm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorControls({ constituenta, disabled, onEditTerm }: EditorControlsProps) {
|
||||||
|
const { schema } = useRSEdit();
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
|
||||||
|
const showRenameCst = useDialogsStore(state => state.showRenameCst);
|
||||||
|
const { cstRename } = useCstRename();
|
||||||
|
|
||||||
|
function handleRenameCst() {
|
||||||
|
const initialData: ICstRenameDTO = {
|
||||||
|
target: constituenta.id,
|
||||||
|
alias: constituenta.alias,
|
||||||
|
cst_type: constituenta.cst_type
|
||||||
|
};
|
||||||
|
showRenameCst({
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay position='top-1 left-[4.7rem]' className='flex select-none'>
|
||||||
|
{!disabled || isProcessing ? (
|
||||||
|
<MiniButton
|
||||||
|
title={isModified ? tooltips.unsaved : `Редактировать словоформы термина`}
|
||||||
|
noHover
|
||||||
|
onClick={onEditTerm}
|
||||||
|
icon={<IconEdit size='1rem' className='icon-primary' />}
|
||||||
|
disabled={isModified}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'pt-1 sm:pl-[1.375rem] pl-1', //
|
||||||
|
'text-sm font-medium whitespace-nowrap',
|
||||||
|
'select-text cursor-default',
|
||||||
|
disabled && !isProcessing && 'pl-[1.6rem] sm:pl-[2.8rem]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Имя </span>
|
||||||
|
<span className='ml-1'>{constituenta?.alias ?? ''}</span>
|
||||||
|
</div>
|
||||||
|
{!disabled || isProcessing ? (
|
||||||
|
<MiniButton
|
||||||
|
noHover
|
||||||
|
title={isModified ? tooltips.unsaved : 'Переименовать конституенту'}
|
||||||
|
onClick={handleRenameCst}
|
||||||
|
icon={<IconEdit size='1rem' className='icon-primary' />}
|
||||||
|
disabled={isModified}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditorControls;
|
|
@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { ICstUpdateDTO } from '@/backend/rsform/api';
|
import { ICstUpdateDTO } from '@/backend/rsform/api';
|
||||||
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
|
import { IconChild, IconPredecessor, IconSave } from '@/components/Icons';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import RefsInput from '@/components/RefsInput';
|
import RefsInput from '@/components/RefsInput';
|
||||||
|
@ -13,27 +14,22 @@ import Indicator from '@/components/ui/Indicator';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import { ConstituentaID, CstType, IConstituenta } from '@/models/rsform';
|
import { ConstituentaID, CstType } from '@/models/rsform';
|
||||||
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
||||||
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
import { IExpressionParse, ParsingStatus } from '@/models/rslang';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { errors, information, labelCstTypification } from '@/utils/labels';
|
import { errors, information, labelCstTypification } from '@/utils/labels';
|
||||||
|
|
||||||
import EditorRSExpression from '../EditorRSExpression';
|
import EditorRSExpression from '../EditorRSExpression';
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import ControlsOverlay from './ControlsOverlay';
|
import EditorControls from './EditorControls';
|
||||||
|
|
||||||
interface FormConstituentaProps {
|
interface FormConstituentaProps {
|
||||||
disabled: boolean;
|
|
||||||
|
|
||||||
id?: string;
|
id?: string;
|
||||||
state?: IConstituenta;
|
disabled: boolean;
|
||||||
|
|
||||||
isModified: boolean;
|
|
||||||
toggleReset: boolean;
|
toggleReset: boolean;
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
|
|
||||||
onRename: () => void;
|
|
||||||
onEditTerm: () => void;
|
onEditTerm: () => void;
|
||||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
@ -41,72 +37,68 @@ interface FormConstituentaProps {
|
||||||
function FormConstituenta({
|
function FormConstituenta({
|
||||||
disabled,
|
disabled,
|
||||||
id,
|
id,
|
||||||
state,
|
|
||||||
|
|
||||||
isModified,
|
|
||||||
setIsModified,
|
|
||||||
|
|
||||||
toggleReset,
|
toggleReset,
|
||||||
onRename,
|
|
||||||
onEditTerm,
|
onEditTerm,
|
||||||
onOpenEdit
|
onOpenEdit
|
||||||
}: FormConstituentaProps) {
|
}: FormConstituentaProps) {
|
||||||
const { cstUpdate } = useCstUpdate();
|
const { cstUpdate } = useCstUpdate();
|
||||||
const controller = useRSEdit();
|
const { schema, activeCst } = useRSEdit();
|
||||||
const schema = controller.schema;
|
const { isModified, setIsModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
|
||||||
const [term, setTerm] = useState(state?.term_raw ?? '');
|
const [term, setTerm] = useState(activeCst?.term_raw ?? '');
|
||||||
const [textDefinition, setTextDefinition] = useState(state?.definition_raw ?? '');
|
const [textDefinition, setTextDefinition] = useState(activeCst?.definition_raw ?? '');
|
||||||
const [expression, setExpression] = useState(state?.definition_formal ?? '');
|
const [expression, setExpression] = useState(activeCst?.definition_formal ?? '');
|
||||||
const [convention, setConvention] = useState(state?.convention ?? '');
|
const [convention, setConvention] = useState(activeCst?.convention ?? '');
|
||||||
const [typification, setTypification] = useState('N/A');
|
const [typification, setTypification] = useState('N/A');
|
||||||
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
|
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
|
||||||
const typeInfo = state
|
const typeInfo = activeCst
|
||||||
? {
|
? {
|
||||||
alias: state.alias,
|
alias: activeCst.alias,
|
||||||
result: localParse ? localParse.typification : state.parse.typification,
|
result: localParse ? localParse.typification : activeCst.parse.typification,
|
||||||
args: localParse ? localParse.args : state.parse.args
|
args: localParse ? localParse.args : activeCst.parse.args
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const [forceComment, setForceComment] = useState(false);
|
const [forceComment, setForceComment] = useState(false);
|
||||||
|
|
||||||
const isBasic = !!state && isBasicConcept(state.cst_type);
|
const isBasic = !!activeCst && isBasicConcept(activeCst.cst_type);
|
||||||
const isElementary = !!state && isBaseSet(state.cst_type);
|
const isElementary = !!activeCst && isBaseSet(activeCst.cst_type);
|
||||||
const showConvention = !state || !!state.convention || forceComment || isBasic;
|
const showConvention = !activeCst || !!activeCst.convention || forceComment || isBasic;
|
||||||
|
|
||||||
const showTypification = useDialogsStore(state => state.showShowTypeGraph);
|
const showTypification = useDialogsStore(activeCst => activeCst.showShowTypeGraph);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state) {
|
if (activeCst) {
|
||||||
setConvention(state.convention);
|
setConvention(activeCst.convention);
|
||||||
setTerm(state.term_raw);
|
setTerm(activeCst.term_raw);
|
||||||
setTextDefinition(state.definition_raw);
|
setTextDefinition(activeCst.definition_raw);
|
||||||
setExpression(state.definition_formal);
|
setExpression(activeCst.definition_formal);
|
||||||
setTypification(state ? labelCstTypification(state) : 'N/A');
|
setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A');
|
||||||
setForceComment(false);
|
setForceComment(false);
|
||||||
setLocalParse(undefined);
|
setLocalParse(undefined);
|
||||||
}
|
}
|
||||||
}, [state, schema, toggleReset, setIsModified]);
|
}, [activeCst, schema, toggleReset, setIsModified]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!state) {
|
if (!activeCst) {
|
||||||
setIsModified(false);
|
setIsModified(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsModified(
|
setIsModified(
|
||||||
state.term_raw !== term ||
|
activeCst.term_raw !== term ||
|
||||||
state.definition_raw !== textDefinition ||
|
activeCst.definition_raw !== textDefinition ||
|
||||||
state.convention !== convention ||
|
activeCst.convention !== convention ||
|
||||||
state.definition_formal !== expression
|
activeCst.definition_formal !== expression
|
||||||
);
|
);
|
||||||
return () => setIsModified(false);
|
return () => setIsModified(false);
|
||||||
}, [
|
}, [
|
||||||
state,
|
activeCst,
|
||||||
state?.term_raw,
|
activeCst?.term_raw,
|
||||||
state?.definition_formal,
|
activeCst?.definition_formal,
|
||||||
state?.definition_raw,
|
activeCst?.definition_raw,
|
||||||
state?.convention,
|
activeCst?.convention,
|
||||||
term,
|
term,
|
||||||
textDefinition,
|
textDefinition,
|
||||||
expression,
|
expression,
|
||||||
|
@ -118,23 +110,23 @@ function FormConstituenta({
|
||||||
if (event) {
|
if (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
if (!state || controller.isProcessing || !schema) {
|
if (!activeCst || isProcessing || !schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: ICstUpdateDTO = {
|
const data: ICstUpdateDTO = {
|
||||||
target: state.id,
|
target: activeCst.id,
|
||||||
item_data: {
|
item_data: {
|
||||||
term_raw: state.term_raw !== term ? term : undefined,
|
term_raw: activeCst.term_raw !== term ? term : undefined,
|
||||||
definition_formal: state.definition_formal !== expression ? expression : undefined,
|
definition_formal: activeCst.definition_formal !== expression ? expression : undefined,
|
||||||
definition_raw: state.definition_raw !== textDefinition ? textDefinition : undefined,
|
definition_raw: activeCst.definition_raw !== textDefinition ? textDefinition : undefined,
|
||||||
convention: state.convention !== convention ? convention : undefined
|
convention: activeCst.convention !== convention ? convention : undefined
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
cstUpdate({ itemID: schema.id, data }, () => toast.success(information.changesSaved));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTypeGraph(event: CProps.EventMouse) {
|
function handleTypeGraph(event: CProps.EventMouse) {
|
||||||
if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
|
if (!activeCst || (localParse && !localParse.parseResult) || activeCst.parse.status !== ParsingStatus.VERIFIED) {
|
||||||
toast.error(errors.typeStructureFailed);
|
toast.error(errors.typeStructureFailed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -145,16 +137,7 @@ function FormConstituenta({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
|
<div className='mx-0 md:mx-auto pt-[2rem] xs:pt-0'>
|
||||||
{state ? (
|
{activeCst ? <EditorControls disabled={disabled} constituenta={activeCst} onEditTerm={onEditTerm} /> : null}
|
||||||
<ControlsOverlay
|
|
||||||
disabled={disabled}
|
|
||||||
modified={isModified}
|
|
||||||
processing={controller.isProcessing}
|
|
||||||
constituenta={state}
|
|
||||||
onEditTerm={onEditTerm}
|
|
||||||
onRename={onRename}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}>
|
<form id={id} className={clsx('cc-column', 'mt-1 md:w-[48.8rem] shrink-0', 'px-6 py-1')} onSubmit={handleSubmit}>
|
||||||
<RefsInput
|
<RefsInput
|
||||||
key='cst_term'
|
key='cst_term'
|
||||||
|
@ -165,12 +148,12 @@ function FormConstituenta({
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={onOpenEdit}
|
||||||
value={term}
|
value={term}
|
||||||
initialValue={state?.term_raw ?? ''}
|
initialValue={activeCst?.term_raw ?? ''}
|
||||||
resolved={state?.term_resolved ?? 'Конституента не выбрана'}
|
resolved={activeCst?.term_resolved ?? 'Конституента не выбрана'}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={newValue => setTerm(newValue)}
|
onChange={newValue => setTerm(newValue)}
|
||||||
/>
|
/>
|
||||||
{state ? (
|
{activeCst ? (
|
||||||
<TextArea
|
<TextArea
|
||||||
id='cst_typification'
|
id='cst_typification'
|
||||||
fitContent
|
fitContent
|
||||||
|
@ -184,24 +167,26 @@ function FormConstituenta({
|
||||||
colors='bg-transparent clr-text-default cursor-default'
|
colors='bg-transparent clr-text-default cursor-default'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{state ? (
|
{activeCst ? (
|
||||||
<>
|
<>
|
||||||
{!!state.definition_formal || !isElementary ? (
|
{!!activeCst.definition_formal || !isElementary ? (
|
||||||
<EditorRSExpression
|
<EditorRSExpression
|
||||||
id='cst_expression'
|
id='cst_expression'
|
||||||
label={
|
label={
|
||||||
state.cst_type === CstType.STRUCTURED
|
activeCst.cst_type === CstType.STRUCTURED
|
||||||
? 'Область определения'
|
? 'Область определения'
|
||||||
: isFunctional(state.cst_type)
|
: isFunctional(activeCst.cst_type)
|
||||||
? 'Определение функции'
|
? 'Определение функции'
|
||||||
: 'Формальное определение'
|
: 'Формальное определение'
|
||||||
}
|
}
|
||||||
placeholder={
|
placeholder={
|
||||||
state.cst_type !== CstType.STRUCTURED ? 'Родоструктурное выражение' : 'Типизация родовой структуры'
|
activeCst.cst_type !== CstType.STRUCTURED
|
||||||
|
? 'Родоструктурное выражение'
|
||||||
|
: 'Типизация родовой структуры'
|
||||||
}
|
}
|
||||||
value={expression}
|
value={expression}
|
||||||
activeCst={state}
|
activeCst={activeCst}
|
||||||
disabled={disabled || state.is_inherited}
|
disabled={disabled || activeCst.is_inherited}
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
onChangeExpression={newValue => setExpression(newValue)}
|
onChangeExpression={newValue => setExpression(newValue)}
|
||||||
onChangeTypification={setTypification}
|
onChangeTypification={setTypification}
|
||||||
|
@ -210,7 +195,7 @@ function FormConstituenta({
|
||||||
onShowTypeGraph={handleTypeGraph}
|
onShowTypeGraph={handleTypeGraph}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{!!state.definition_raw || !isElementary ? (
|
{!!activeCst.definition_raw || !isElementary ? (
|
||||||
<RefsInput
|
<RefsInput
|
||||||
id='cst_definition'
|
id='cst_definition'
|
||||||
label='Текстовое определение'
|
label='Текстовое определение'
|
||||||
|
@ -220,8 +205,8 @@ function FormConstituenta({
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={onOpenEdit}
|
||||||
value={textDefinition}
|
value={textDefinition}
|
||||||
initialValue={state.definition_raw}
|
initialValue={activeCst.definition_raw}
|
||||||
resolved={state.definition_resolved}
|
resolved={activeCst.definition_resolved}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={newValue => setTextDefinition(newValue)}
|
onChange={newValue => setTextDefinition(newValue)}
|
||||||
/>
|
/>
|
||||||
|
@ -236,12 +221,12 @@ function FormConstituenta({
|
||||||
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
label={isBasic ? 'Конвенция' : 'Комментарий'}
|
||||||
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
placeholder={isBasic ? 'Договоренность об интерпретации' : 'Пояснение разработчика'}
|
||||||
value={convention}
|
value={convention}
|
||||||
disabled={disabled || (isBasic && state.is_inherited)}
|
disabled={disabled || (isBasic && activeCst.is_inherited)}
|
||||||
onChange={event => setConvention(event.target.value)}
|
onChange={event => setConvention(event.target.value)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!showConvention && (!disabled || controller.isProcessing) ? (
|
{!showConvention && (!disabled || isProcessing) ? (
|
||||||
<button
|
<button
|
||||||
key='cst_disable_comment'
|
key='cst_disable_comment'
|
||||||
id='cst_disable_comment'
|
id='cst_disable_comment'
|
||||||
|
@ -254,7 +239,7 @@ function FormConstituenta({
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!disabled || controller.isProcessing ? (
|
{!disabled || isProcessing ? (
|
||||||
<div className='mx-auto flex'>
|
<div className='mx-auto flex'>
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
key='cst_form_submit'
|
key='cst_form_submit'
|
||||||
|
@ -264,13 +249,13 @@ function FormConstituenta({
|
||||||
icon={<IconSave size='1.25rem' />}
|
icon={<IconSave size='1.25rem' />}
|
||||||
/>
|
/>
|
||||||
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
|
<Overlay position='top-[0.1rem] left-[0.4rem]' className='cc-icons'>
|
||||||
{state.has_inherited_children && !state.is_inherited ? (
|
{activeCst.has_inherited_children && !activeCst.is_inherited ? (
|
||||||
<Indicator
|
<Indicator
|
||||||
icon={<IconPredecessor size='1.25rem' className='text-sec-600' />}
|
icon={<IconPredecessor size='1.25rem' className='text-sec-600' />}
|
||||||
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
|
titleHtml='Внимание!</br> Конституента имеет потомков<br/> в операционной схеме синтеза'
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{state.is_inherited ? (
|
{activeCst.is_inherited ? (
|
||||||
<Indicator
|
<Indicator
|
||||||
icon={<IconChild size='1.25rem' className='text-sec-600' />}
|
icon={<IconChild size='1.25rem' className='text-sec-600' />}
|
||||||
titleHtml='Внимание!</br> Конституента является наследником<br/>'
|
titleHtml='Внимание!</br> Конституента является наследником<br/>'
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
|
import { urls } from '@/app/urls';
|
||||||
|
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import {
|
import {
|
||||||
IconClone,
|
IconClone,
|
||||||
IconDestroy,
|
IconDestroy,
|
||||||
|
@ -19,17 +23,17 @@ import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip, tooltips } from '@/utils/labels';
|
import { prepareTooltip, tooltips } from '@/utils/labels';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { RSTabID, useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
interface ToolbarConstituentaProps {
|
interface ToolbarConstituentaProps {
|
||||||
activeCst?: IConstituenta;
|
activeCst?: IConstituenta;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
modified: boolean;
|
|
||||||
|
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
|
@ -38,15 +42,30 @@ interface ToolbarConstituentaProps {
|
||||||
function ToolbarConstituenta({
|
function ToolbarConstituenta({
|
||||||
activeCst,
|
activeCst,
|
||||||
disabled,
|
disabled,
|
||||||
modified,
|
|
||||||
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onReset
|
onReset
|
||||||
}: ToolbarConstituentaProps) {
|
}: ToolbarConstituentaProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { findPredecessor } = useFindPredecessor();
|
||||||
|
|
||||||
const showList = usePreferencesStore(state => state.showCstSideList);
|
const showList = usePreferencesStore(state => state.showCstSideList);
|
||||||
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
|
const toggleList = usePreferencesStore(state => state.toggleShowCstSideList);
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
|
||||||
|
function viewPredecessor(target: ConstituentaID) {
|
||||||
|
findPredecessor({ target: target }, reference =>
|
||||||
|
router.push(
|
||||||
|
urls.schema_props({
|
||||||
|
id: reference.schema,
|
||||||
|
active: reference.id,
|
||||||
|
tab: RSTabID.CST_EDIT
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay
|
<Overlay
|
||||||
|
@ -56,13 +75,13 @@ function ToolbarConstituenta({
|
||||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
items={controller.schema.oss}
|
items={controller.schema.oss}
|
||||||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{activeCst?.is_inherited ? (
|
{activeCst?.is_inherited ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Перейти к исходной конституенте в ОСС'
|
title='Перейти к исходной конституенте в ОСС'
|
||||||
onClick={() => controller.viewPredecessor(activeCst.id)}
|
onClick={() => viewPredecessor(activeCst.id)}
|
||||||
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -71,25 +90,25 @@ function ToolbarConstituenta({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||||
disabled={disabled || !modified}
|
disabled={disabled || !isModified}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Сбросить несохраненные изменения'
|
title='Сбросить несохраненные изменения'
|
||||||
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
icon={<IconReset size='1.25rem' className='icon-primary' />}
|
||||||
disabled={disabled || !modified}
|
disabled={disabled || !isModified}
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Создать конституенту после данной'
|
title='Создать конституенту после данной'
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
onClick={() => controller.createCst(activeCst?.cst_type, false)}
|
onClick={() => controller.createCst(activeCst?.cst_type, false)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={modified ? tooltips.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
titleHtml={isModified ? tooltips.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||||
disabled={disabled || modified}
|
disabled={disabled || isModified}
|
||||||
onClick={controller.cloneCst}
|
onClick={controller.cloneCst}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -112,13 +131,13 @@ function ToolbarConstituenta({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||||
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
||||||
disabled={disabled || modified || (controller.schema && controller.schema?.items.length < 2)}
|
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
|
||||||
onClick={controller.moveUp}
|
onClick={controller.moveUp}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
||||||
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
||||||
disabled={disabled || modified || (controller.schema && controller.schema?.items.length < 2)}
|
disabled={disabled || isModified || (controller.schema && controller.schema?.items.length < 2)}
|
||||||
onClick={controller.moveDown}
|
onClick={controller.moveDown}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
import RSInput from '@/components/RSInput';
|
import RSInput from '@/components/RSInput';
|
||||||
|
@ -62,6 +63,7 @@ function EditorRSExpression({
|
||||||
const parser = useRSParse({ schema: controller.schema });
|
const parser = useRSParse({ schema: controller.schema });
|
||||||
const rsInput = useRef<ReactCodeMirrorRef>(null);
|
const rsInput = useRef<ReactCodeMirrorRef>(null);
|
||||||
|
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
const showControls = usePreferencesStore(state => state.showExpressionControls);
|
const showControls = usePreferencesStore(state => state.showExpressionControls);
|
||||||
const showAST = useDialogsStore(state => state.showShowAST);
|
const showAST = useDialogsStore(state => state.showShowAST);
|
||||||
|
|
||||||
|
@ -173,7 +175,7 @@ function EditorRSExpression({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RSEditorControls
|
<RSEditorControls
|
||||||
isOpen={showControls && (!disabled || (controller.isProcessing && !activeCst.is_inherited))}
|
isOpen={showControls && (!disabled || (isProcessing && !activeCst.is_inherited))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import { Suspense, useCallback } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
|
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||||
|
import { useLibraryItem } from '@/backend/library/useLibraryItem';
|
||||||
|
import { useSetEditors } from '@/backend/library/useSetEditors';
|
||||||
|
import { useSetLocation } from '@/backend/library/useSetLocation';
|
||||||
|
import { useSetOwner } from '@/backend/library/useSetOwner';
|
||||||
import { useLabelUser } from '@/backend/users/useLabelUser';
|
import { useLabelUser } from '@/backend/users/useLabelUser';
|
||||||
import {
|
import {
|
||||||
IconDateCreate,
|
IconDateCreate,
|
||||||
|
@ -21,51 +27,80 @@ import Overlay from '@/components/ui/Overlay';
|
||||||
import Tooltip from '@/components/ui/Tooltip';
|
import Tooltip from '@/components/ui/Tooltip';
|
||||||
import ValueIcon from '@/components/ui/ValueIcon';
|
import ValueIcon from '@/components/ui/ValueIcon';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { ILibraryItemData, ILibraryItemEditor } from '@/models/library';
|
import { ILibraryItemEditor, LibraryItemID, LibraryItemType } from '@/models/library';
|
||||||
import { UserID, UserRole } from '@/models/user';
|
import { UserID, UserRole } from '@/models/user';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
import { useLibrarySearchStore } from '@/stores/librarySearch';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { prefixes } from '@/utils/constants';
|
import { prefixes } from '@/utils/constants';
|
||||||
import { prompts } from '@/utils/labels';
|
import { information, prompts } from '@/utils/labels';
|
||||||
|
|
||||||
interface EditorLibraryItemProps {
|
interface EditorLibraryItemProps {
|
||||||
item?: ILibraryItemData;
|
itemID: LibraryItemID;
|
||||||
isModified?: boolean;
|
itemType: LibraryItemType;
|
||||||
controller: ILibraryItemEditor;
|
controller: ILibraryItemEditor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemProps) {
|
function EditorLibraryItem({ itemID, itemType, controller }: EditorLibraryItemProps) {
|
||||||
const getUserLabel = useLabelUser();
|
const getUserLabel = useLabelUser();
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const setLocation = useLibrarySearchStore(state => state.setLocation);
|
const setGlobalLocation = useLibrarySearchStore(state => state.setLocation);
|
||||||
|
|
||||||
|
const { item } = useLibraryItem({ itemID, itemType });
|
||||||
|
const isProcessing = useIsProcessingLibrary();
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
|
||||||
|
const { setOwner } = useSetOwner();
|
||||||
|
const { setLocation } = useSetLocation();
|
||||||
|
const { setEditors } = useSetEditors();
|
||||||
|
|
||||||
|
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
||||||
|
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
||||||
|
|
||||||
const ownerSelector = useDropdown();
|
const ownerSelector = useDropdown();
|
||||||
const onSelectUser = useCallback(
|
const onSelectUser = function (newValue: UserID) {
|
||||||
(newValue: UserID) => {
|
ownerSelector.hide();
|
||||||
ownerSelector.hide();
|
if (newValue === item?.owner) {
|
||||||
if (newValue === item?.owner) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
if (!window.confirm(prompts.ownerChange)) {
|
||||||
if (!window.confirm(prompts.ownerChange)) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
setOwner({ itemID: itemID, owner: newValue }, () => toast.success(information.changesSaved));
|
||||||
controller.setOwner(newValue);
|
};
|
||||||
},
|
|
||||||
[controller, item?.owner, ownerSelector]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenLibrary = useCallback(
|
function handleOpenLibrary(event: CProps.EventMouse) {
|
||||||
(event: CProps.EventMouse) => {
|
if (!item) {
|
||||||
if (!item) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
setGlobalLocation(item.location);
|
||||||
setLocation(item.location);
|
router.push(urls.library, event.ctrlKey || event.metaKey);
|
||||||
router.push(urls.library, event.ctrlKey || event.metaKey);
|
}
|
||||||
},
|
|
||||||
[setLocation, item, router]
|
function handleEditLocation() {
|
||||||
);
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showEditLocation({
|
||||||
|
initial: item.location,
|
||||||
|
onChangeLocation: newLocation =>
|
||||||
|
setLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditEditors() {
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showEditEditors({
|
||||||
|
editors: item.editors,
|
||||||
|
setEditors: newEditors =>
|
||||||
|
setEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -86,8 +121,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
|
icon={<IconFolderEdit size='1.25rem' className='icon-primary' />}
|
||||||
value={item.location}
|
value={item.location}
|
||||||
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
|
title={controller.isAttachedToOSS ? 'Путь наследуется от ОСС' : 'Путь'}
|
||||||
onClick={controller.promptLocation}
|
onClick={handleEditLocation}
|
||||||
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -108,7 +143,7 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
value={getUserLabel(item.owner)}
|
value={getUserLabel(item.owner)}
|
||||||
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
|
title={controller.isAttachedToOSS ? 'Владелец наследуется от ОСС' : 'Владелец'}
|
||||||
onClick={ownerSelector.toggle}
|
onClick={ownerSelector.toggle}
|
||||||
disabled={isModified || controller.isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || controller.isAttachedToOSS || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='sm:mb-1 flex justify-between items-center'>
|
<div className='sm:mb-1 flex justify-between items-center'>
|
||||||
|
@ -117,8 +152,8 @@ function EditorLibraryItem({ item, isModified, controller }: EditorLibraryItemPr
|
||||||
dense
|
dense
|
||||||
icon={<IconEditor size='1.25rem' className='icon-primary' />}
|
icon={<IconEditor size='1.25rem' className='icon-primary' />}
|
||||||
value={item.editors.length}
|
value={item.editors.length}
|
||||||
onClick={controller.promptEditors}
|
onClick={handleEditEditors}
|
||||||
disabled={isModified || controller.isProcessing || role < UserRole.OWNER}
|
disabled={isModified || isProcessing || role < UserRole.OWNER}
|
||||||
/>
|
/>
|
||||||
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
<Tooltip anchorSelect='#editor_stats' layer='z-modalTooltip'>
|
||||||
<Suspense fallback={<Loader scale={2} />}>
|
<Suspense fallback={<Loader scale={2} />}>
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import FlexColumn from '@/components/ui/FlexColumn';
|
import FlexColumn from '@/components/ui/FlexColumn';
|
||||||
|
import { LibraryItemType } from '@/models/library';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { globals } from '@/utils/constants';
|
import { globals } from '@/utils/constants';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
@ -11,14 +13,9 @@ import FormRSForm from './FormRSForm';
|
||||||
import RSFormStats from './RSFormStats';
|
import RSFormStats from './RSFormStats';
|
||||||
import ToolbarRSFormCard from './ToolbarRSFormCard';
|
import ToolbarRSFormCard from './ToolbarRSFormCard';
|
||||||
|
|
||||||
interface EditorRSFormCardProps {
|
function EditorRSFormCard() {
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
onDestroy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSFormCardProps) {
|
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
|
||||||
function initiateSubmit() {
|
function initiateSubmit() {
|
||||||
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
|
const element = document.getElementById(globals.library_item_editor) as HTMLFormElement;
|
||||||
|
@ -38,12 +35,7 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToolbarRSFormCard
|
<ToolbarRSFormCard onSubmit={initiateSubmit} controller={controller} />
|
||||||
modified={isModified}
|
|
||||||
onSubmit={initiateSubmit}
|
|
||||||
onDestroy={onDestroy}
|
|
||||||
controller={controller}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
onKeyDown={handleInput}
|
onKeyDown={handleInput}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -53,8 +45,8 @@ function EditorRSFormCard({ isModified, onDestroy, setIsModified }: EditorRSForm
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FlexColumn className='flex-shrink'>
|
<FlexColumn className='flex-shrink'>
|
||||||
<FormRSForm id={globals.library_item_editor} isModified={isModified} setIsModified={setIsModified} />
|
<FormRSForm id={globals.library_item_editor} />
|
||||||
<EditorLibraryItem item={controller.schema} isModified={isModified} controller={controller} />
|
<EditorLibraryItem itemID={controller.schema.id} itemType={LibraryItemType.RSFORM} controller={controller} />
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
|
|
||||||
{controller.schema ? <RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> : null}
|
{controller.schema ? <RSFormStats stats={controller.schema.stats} isArchive={controller.isArchive} /> : null}
|
||||||
|
|
|
@ -3,15 +3,19 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
|
import { urls } from '@/app/urls';
|
||||||
import { ILibraryUpdateDTO } from '@/backend/library/api';
|
import { ILibraryUpdateDTO } from '@/backend/library/api';
|
||||||
import { useUpdateItem } from '@/backend/library/useUpdateItem';
|
import { useUpdateItem } from '@/backend/library/useUpdateItem';
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import { IconSave } from '@/components/Icons';
|
import { IconSave } from '@/components/Icons';
|
||||||
import SelectVersion from '@/components/select/SelectVersion';
|
import SelectVersion from '@/components/select/SelectVersion';
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import SubmitButton from '@/components/ui/SubmitButton';
|
import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import TextInput from '@/components/ui/TextInput';
|
import TextInput from '@/components/ui/TextInput';
|
||||||
import { LibraryItemType } from '@/models/library';
|
import { LibraryItemType, VersionID } from '@/models/library';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import ToolbarItemAccess from './ToolbarItemAccess';
|
import ToolbarItemAccess from './ToolbarItemAccess';
|
||||||
|
@ -19,14 +23,15 @@ import ToolbarVersioning from './ToolbarVersioning';
|
||||||
|
|
||||||
interface FormRSFormProps {
|
interface FormRSFormProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
isModified: boolean;
|
|
||||||
setIsModified: (newValue: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
function FormRSForm({ id }: FormRSFormProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const router = useConceptNavigation();
|
||||||
const schema = controller.schema;
|
const schema = controller.schema;
|
||||||
const { updateItem: update } = useUpdateItem();
|
const { updateItem: update } = useUpdateItem();
|
||||||
|
const { isModified, setIsModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
|
||||||
const [title, setTitle] = useState(schema?.title ?? '');
|
const [title, setTitle] = useState(schema?.title ?? '');
|
||||||
const [alias, setAlias] = useState(schema?.alias ?? '');
|
const [alias, setAlias] = useState(schema?.alias ?? '');
|
||||||
|
@ -34,6 +39,10 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
const [visible, setVisible] = useState(schema?.visible ?? false);
|
const [visible, setVisible] = useState(schema?.visible ?? false);
|
||||||
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
|
const [readOnly, setReadOnly] = useState(schema?.read_only ?? false);
|
||||||
|
|
||||||
|
function handleSelectVersion(version?: VersionID) {
|
||||||
|
router.push(urls.schema(schema.id, version));
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schema) {
|
if (schema) {
|
||||||
setTitle(schema.title);
|
setTitle(schema.title);
|
||||||
|
@ -127,7 +136,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
className='select-none'
|
className='select-none'
|
||||||
value={schema?.version} //
|
value={schema?.version} //
|
||||||
items={schema?.versions}
|
items={schema?.versions}
|
||||||
onSelectValue={controller.viewVersion}
|
onSelectValue={handleSelectVersion}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -137,14 +146,14 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
|
||||||
label='Описание'
|
label='Описание'
|
||||||
rows={3}
|
rows={3}
|
||||||
value={comment}
|
value={comment}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
onChange={event => setComment(event.target.value)}
|
onChange={event => setComment(event.target.value)}
|
||||||
/>
|
/>
|
||||||
{controller.isContentEditable || isModified ? (
|
{controller.isContentEditable || isModified ? (
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
text='Сохранить изменения'
|
text='Сохранить изменения'
|
||||||
className='self-center mt-4'
|
className='self-center mt-4'
|
||||||
loading={controller.isProcessing}
|
loading={isProcessing}
|
||||||
disabled={!isModified}
|
disabled={!isModified}
|
||||||
icon={<IconSave size='1.25rem' />}
|
icon={<IconSave size='1.25rem' />}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||||
|
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
|
||||||
import { VisibilityIcon } from '@/components/DomainIcons';
|
import { VisibilityIcon } from '@/components/DomainIcons';
|
||||||
import { IconImmutable, IconMutable } from '@/components/Icons';
|
import { IconImmutable, IconMutable } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
|
@ -10,6 +14,7 @@ import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
import { information } from '@/utils/labels';
|
||||||
|
|
||||||
interface ToolbarItemAccessProps {
|
interface ToolbarItemAccessProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -21,23 +26,29 @@ interface ToolbarItemAccessProps {
|
||||||
|
|
||||||
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
|
function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, controller }: ToolbarItemAccessProps) {
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const policy = controller.schema?.access_policy ?? AccessPolicy.PRIVATE;
|
const isProcessing = useIsProcessingLibrary();
|
||||||
|
const policy = controller.schema.access_policy;
|
||||||
|
const { setAccessPolicy } = useSetAccessPolicy();
|
||||||
|
|
||||||
|
function handleSetAccessPolicy(newPolicy: AccessPolicy) {
|
||||||
|
setAccessPolicy({ itemID: controller.schema.id, policy: newPolicy }, () => toast.success(information.changesSaved));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
|
<Overlay position='top-[4.5rem] right-0 w-[12rem] pr-2' className='flex' layer='z-bottom'>
|
||||||
<Label text='Доступ' className='self-center select-none' />
|
<Label text='Доступ' className='self-center select-none' />
|
||||||
<div className='ml-auto cc-icons'>
|
<div className='ml-auto cc-icons'>
|
||||||
<SelectAccessPolicy
|
<SelectAccessPolicy
|
||||||
disabled={role <= UserRole.EDITOR || controller.isProcessing || controller.isAttachedToOSS}
|
disabled={role <= UserRole.EDITOR || isProcessing || controller.isAttachedToOSS}
|
||||||
value={policy}
|
value={policy}
|
||||||
onChange={newPolicy => controller.setAccessPolicy(newPolicy)}
|
onChange={handleSetAccessPolicy}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
title={visible ? 'Библиотека: отображать' : 'Библиотека: скрывать'}
|
||||||
icon={<VisibilityIcon value={visible} />}
|
icon={<VisibilityIcon value={visible} />}
|
||||||
onClick={toggleVisible}
|
onClick={toggleVisible}
|
||||||
disabled={role === UserRole.READER || controller.isProcessing}
|
disabled={role === UserRole.READER || isProcessing}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -50,7 +61,7 @@ function ToolbarItemAccess({ visible, toggleVisible, readOnly, toggleReadOnly, c
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={toggleReadOnly}
|
onClick={toggleReadOnly}
|
||||||
disabled={role === UserRole.READER || controller.isProcessing}
|
disabled={role === UserRole.READER || isProcessing}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />
|
<BadgeHelp topic={HelpTopic.ACCESS} className={PARAMETER.TOOLTIP_WIDTH} offset={4} />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useIsProcessingLibrary } from '@/backend/library/useIsProcessingLibrary';
|
||||||
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
|
import { IconDestroy, IconSave, IconShare } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||||
|
@ -9,22 +10,24 @@ import { AccessPolicy, ILibraryItemEditor, LibraryItemType } from '@/models/libr
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip, tooltips } from '@/utils/labels';
|
import { prepareTooltip, tooltips } from '@/utils/labels';
|
||||||
|
import { sharePage } from '@/utils/utils';
|
||||||
|
|
||||||
import { IRSEditContext } from '../RSEditContext';
|
import { IRSEditContext } from '../RSEditContext';
|
||||||
|
|
||||||
interface ToolbarRSFormCardProps {
|
interface ToolbarRSFormCardProps {
|
||||||
modified: boolean;
|
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onDestroy: () => void;
|
|
||||||
controller: ILibraryItemEditor;
|
controller: ILibraryItemEditor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: ToolbarRSFormCardProps) {
|
function ToolbarRSFormCard({ controller, onSubmit }: ToolbarRSFormCardProps) {
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const canSave = modified && !controller.isProcessing;
|
const { isModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingLibrary();
|
||||||
|
const canSave = isModified && !isProcessing;
|
||||||
|
|
||||||
const ossSelector = (() => {
|
const ossSelector = (() => {
|
||||||
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
|
if (!controller.schema || controller.schema?.item_type !== LibraryItemType.RSFORM) {
|
||||||
|
@ -37,7 +40,9 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
|
||||||
return (
|
return (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
items={schema.oss}
|
items={schema.oss}
|
||||||
onSelect={(event, value) => (controller as IRSEditContext).viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
onSelect={(event, value) =>
|
||||||
|
(controller as IRSEditContext).navigateOss(value.id, event.ctrlKey || event.metaKey)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
@ -45,7 +50,7 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
|
||||||
return (
|
return (
|
||||||
<Overlay position='cc-tab-tools' className='cc-icons'>
|
<Overlay position='cc-tab-tools' className='cc-icons'>
|
||||||
{ossSelector}
|
{ossSelector}
|
||||||
{controller.isMutable || modified ? (
|
{controller.isMutable || isModified ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||||
disabled={!canSave}
|
disabled={!canSave}
|
||||||
|
@ -56,15 +61,15 @@ function ToolbarRSFormCard({ modified, controller, onSubmit, onDestroy }: Toolba
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
|
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
|
||||||
icon={<IconShare size='1.25rem' className='icon-primary' />}
|
icon={<IconShare size='1.25rem' className='icon-primary' />}
|
||||||
onClick={controller.share}
|
onClick={sharePage}
|
||||||
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
||||||
/>
|
/>
|
||||||
{controller.isMutable ? (
|
{controller.isMutable ? (
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить схему'
|
title='Удалить схему'
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={!controller.isMutable || controller.isProcessing || role < UserRole.OWNER}
|
disabled={!controller.isMutable || isProcessing || role < UserRole.OWNER}
|
||||||
onClick={onDestroy}
|
onClick={controller.deleteSchema}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />
|
<BadgeHelp topic={HelpTopic.UI_RS_CARD} offset={4} className={PARAMETER.TOOLTIP_WIDTH} />
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
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';
|
import { IconNewVersion, IconUpload, IconVersions } from '@/components/Icons';
|
||||||
import BadgeHelp from '@/components/info/BadgeHelp';
|
import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
import { information, prompts } from '@/utils/labels';
|
||||||
|
import { promptUnsaved } from '@/utils/utils';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
|
@ -13,6 +23,45 @@ interface ToolbarVersioningProps {
|
||||||
|
|
||||||
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const router = useConceptNavigation();
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
const { versionRestore } = useVersionRestore();
|
||||||
|
const { versionCreate } = useVersionCreate();
|
||||||
|
|
||||||
|
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
|
||||||
|
const showEditVersions = useDialogsStore(state => state.showEditVersions);
|
||||||
|
|
||||||
|
function handleRestoreVersion() {
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCreateVersion() {
|
||||||
|
if (isModified && !promptUnsaved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showCreateVersion({
|
||||||
|
versions: controller.schema.versions,
|
||||||
|
selected: controller.selected,
|
||||||
|
totalCount: controller.schema.items.length,
|
||||||
|
onCreate: data =>
|
||||||
|
versionCreate({ itemID: controller.schema.id, data: data }, () => {
|
||||||
|
toast.success(information.newVersion(data.version));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditVersions() {
|
||||||
|
showEditVersions({
|
||||||
|
item: controller.schema
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
|
<Overlay position='top-[-0.4rem] right-[0rem]' className='pr-2 cc-icons' layer='z-bottom'>
|
||||||
{controller.isMutable ? (
|
{controller.isMutable ? (
|
||||||
|
@ -26,19 +75,19 @@ function ToolbarVersioning({ blockReload }: ToolbarVersioningProps) {
|
||||||
: 'Переключитесь на <br/>неактуальную версию'
|
: 'Переключитесь на <br/>неактуальную версию'
|
||||||
}
|
}
|
||||||
disabled={controller.isContentEditable || blockReload}
|
disabled={controller.isContentEditable || blockReload}
|
||||||
onClick={() => controller.restoreVersion()}
|
onClick={handleRestoreVersion}
|
||||||
icon={<IconUpload size='1.25rem' className='icon-red' />}
|
icon={<IconUpload size='1.25rem' className='icon-red' />}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
|
titleHtml={controller.isContentEditable ? 'Создать версию' : 'Переключитесь <br/>на актуальную версию'}
|
||||||
disabled={!controller.isContentEditable}
|
disabled={!controller.isContentEditable}
|
||||||
onClick={controller.createVersion}
|
onClick={handleCreateVersion}
|
||||||
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
|
icon={<IconNewVersion size='1.25rem' className='icon-green' />}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
title={controller.schema?.versions.length === 0 ? 'Список версий пуст' : 'Редактировать версии'}
|
||||||
disabled={!controller.schema || controller.schema?.versions.length === 0}
|
disabled={!controller.schema || controller.schema?.versions.length === 0}
|
||||||
onClick={controller.promptEditVersions}
|
onClick={handleEditVersions}
|
||||||
icon={<IconVersions size='1.25rem' className='icon-primary' />}
|
icon={<IconVersions size='1.25rem' className='icon-primary' />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import fileDownload from 'js-file-download';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import { IconCSV } from '@/components/Icons';
|
import { IconCSV } from '@/components/Icons';
|
||||||
import { type RowSelectionState } from '@/components/ui/DataTable';
|
import { type RowSelectionState } from '@/components/ui/DataTable';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
|
@ -20,13 +21,10 @@ import { useRSEdit } from '../RSEditContext';
|
||||||
import TableRSList from './TableRSList';
|
import TableRSList from './TableRSList';
|
||||||
import ToolbarRSList from './ToolbarRSList';
|
import ToolbarRSList from './ToolbarRSList';
|
||||||
|
|
||||||
interface EditorRSListProps {
|
function EditorRSList() {
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const controller = useRSEdit();
|
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('');
|
const [filterText, setFilterText] = useState('');
|
||||||
|
@ -91,7 +89,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
controller.deselectAll();
|
controller.deselectAll();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!controller.isContentEditable || controller.isProcessing) {
|
if (!controller.isContentEditable || isProcessing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Delete' && controller.canDeleteSelected) {
|
if (event.key === 'Delete' && controller.canDeleteSelected) {
|
||||||
|
@ -170,7 +168,7 @@ function EditorRSList({ onOpenEdit }: EditorRSListProps) {
|
||||||
enableSelection={controller.isContentEditable}
|
enableSelection={controller.isContentEditable}
|
||||||
selected={rowSelection}
|
selected={rowSelection}
|
||||||
setSelected={handleRowSelection}
|
setSelected={handleRowSelection}
|
||||||
onEdit={onOpenEdit}
|
onEdit={controller.navigateCst}
|
||||||
onCreateNew={() => controller.createCst(undefined, false)}
|
onCreateNew={() => controller.createCst(undefined, false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import { CstTypeIcon } from '@/components/DomainIcons';
|
import { CstTypeIcon } from '@/components/DomainIcons';
|
||||||
import {
|
import {
|
||||||
IconClone,
|
IconClone,
|
||||||
|
@ -24,6 +25,7 @@ import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
function ToolbarRSList() {
|
function ToolbarRSList() {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
const insertMenu = useDropdown();
|
const insertMenu = useDropdown();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -34,7 +36,7 @@ function ToolbarRSList() {
|
||||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
items={controller.schema.oss}
|
items={controller.schema.oss}
|
||||||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -47,7 +49,7 @@ function ToolbarRSList() {
|
||||||
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
|
||||||
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
icon={<IconMoveUp size='1.25rem' className='icon-primary' />}
|
||||||
disabled={
|
disabled={
|
||||||
controller.isProcessing ||
|
isProcessing ||
|
||||||
controller.selected.length === 0 ||
|
controller.selected.length === 0 ||
|
||||||
(controller.schema && controller.selected.length === controller.schema.items.length)
|
(controller.schema && controller.selected.length === controller.schema.items.length)
|
||||||
}
|
}
|
||||||
|
@ -57,7 +59,7 @@ function ToolbarRSList() {
|
||||||
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
|
||||||
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
icon={<IconMoveDown size='1.25rem' className='icon-primary' />}
|
||||||
disabled={
|
disabled={
|
||||||
controller.isProcessing ||
|
isProcessing ||
|
||||||
controller.selected.length === 0 ||
|
controller.selected.length === 0 ||
|
||||||
(controller.schema && controller.selected.length === controller.schema.items.length)
|
(controller.schema && controller.selected.length === controller.schema.items.length)
|
||||||
}
|
}
|
||||||
|
@ -68,7 +70,7 @@ function ToolbarRSList() {
|
||||||
title='Добавить пустую конституенту'
|
title='Добавить пустую конституенту'
|
||||||
hideTitle={insertMenu.isOpen}
|
hideTitle={insertMenu.isOpen}
|
||||||
icon={<IconOpenList size='1.25rem' className='icon-green' />}
|
icon={<IconOpenList size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={insertMenu.toggle}
|
onClick={insertMenu.toggle}
|
||||||
/>
|
/>
|
||||||
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
|
<Dropdown isOpen={insertMenu.isOpen} className='-translate-x-1/2 md:translate-x-0'>
|
||||||
|
@ -86,19 +88,19 @@ function ToolbarRSList() {
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={() => controller.createCst(undefined, false)}
|
onClick={() => controller.createCst(undefined, false)}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
|
||||||
icon={<IconClone size='1.25rem' className='icon-green' />}
|
icon={<IconClone size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing || controller.selected.length !== 1}
|
disabled={isProcessing || controller.selected.length !== 1}
|
||||||
onClick={controller.cloneCst}
|
onClick={controller.cloneCst}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={controller.isProcessing || !controller.canDeleteSelected}
|
disabled={isProcessing || !controller.canDeleteSelected}
|
||||||
onClick={controller.promptDeleteCst}
|
onClick={controller.promptDeleteCst}
|
||||||
/>
|
/>
|
||||||
<BadgeHelp topic={HelpTopic.UI_RS_LIST} offset={5} />
|
<BadgeHelp topic={HelpTopic.UI_RS_LIST} offset={5} />
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
import { ConstituentaID } from '@/models/rsform';
|
|
||||||
|
|
||||||
import TGFlow from './TGFlow';
|
import TGFlow from './TGFlow';
|
||||||
|
|
||||||
interface EditorTermGraphProps {
|
function EditorTermGraph() {
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) {
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<TGFlow onOpenEdit={onOpenEdit} />
|
<TGFlow />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
import { useStoreApi } from 'reactflow';
|
import { useStoreApi } from 'reactflow';
|
||||||
import { useDebounce } from 'use-debounce';
|
import { useDebounce } from 'use-debounce';
|
||||||
|
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
import InfoConstituenta from '@/components/info/InfoConstituenta';
|
||||||
import SelectedCounter from '@/components/info/SelectedCounter';
|
import SelectedCounter from '@/components/info/SelectedCounter';
|
||||||
import { CProps } from '@/components/props';
|
import { CProps } from '@/components/props';
|
||||||
|
@ -47,18 +48,13 @@ import ViewHidden from './ViewHidden';
|
||||||
const ZOOM_MAX = 3;
|
const ZOOM_MAX = 3;
|
||||||
const ZOOM_MIN = 0.25;
|
const ZOOM_MIN = 0.25;
|
||||||
|
|
||||||
interface TGFlowProps {
|
function TGFlow() {
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TGFlow({ onOpenEdit }: TGFlowProps) {
|
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
||||||
const [edges, setEdges] = useEdgesState([]);
|
|
||||||
const flow = useReactFlow();
|
const flow = useReactFlow();
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { addSelectedNodes } = store.getState();
|
const { addSelectedNodes } = store.getState();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
|
||||||
const showParams = useDialogsStore(state => state.showGraphParams);
|
const showParams = useDialogsStore(state => state.showGraphParams);
|
||||||
|
|
||||||
|
@ -67,6 +63,9 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
const coloring = useTermGraphStore(state => state.coloring);
|
const coloring = useTermGraphStore(state => state.coloring);
|
||||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||||
|
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||||
|
const [edges, setEdges] = useEdgesState([]);
|
||||||
|
|
||||||
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
const [focusCst, setFocusCst] = useState<IConstituenta | undefined>(undefined);
|
||||||
const filteredGraph = useGraphFilter(controller.schema, filter, focusCst);
|
const filteredGraph = useGraphFilter(controller.schema, filter, focusCst);
|
||||||
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
const [hidden, setHidden] = useState<ConstituentaID[]>([]);
|
||||||
|
@ -113,7 +112,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
const resetNodes = useCallback(() => {
|
const resetNodes = useCallback(() => {
|
||||||
const newNodes: Node<TGNodeData>[] = [];
|
const newNodes: Node<TGNodeData>[] = [];
|
||||||
filteredGraph.nodes.forEach(node => {
|
filteredGraph.nodes.forEach(node => {
|
||||||
const cst = controller.schema!.cstByID.get(node.id);
|
const cst = controller.schema.cstByID.get(node.id);
|
||||||
if (cst) {
|
if (cst) {
|
||||||
newNodes.push({
|
newNodes.push({
|
||||||
id: String(node.id),
|
id: String(node.id),
|
||||||
|
@ -184,7 +183,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
if (!controller.schema) {
|
if (!controller.schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const definition = controller.selected.map(id => controller.schema!.cstByID.get(id)!.alias).join(' ');
|
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);
|
controller.createCst(controller.selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,7 +231,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
if (controller.isProcessing) {
|
if (isProcessing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
|
@ -282,7 +281,7 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
function handleNodeDoubleClick(event: CProps.EventMouse, cstID: ConstituentaID) {
|
function handleNodeDoubleClick(event: CProps.EventMouse, cstID: ConstituentaID) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onOpenEdit(cstID);
|
controller.navigateCst(cstID);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNodeEnter(event: CProps.EventMouse, cstID: ConstituentaID) {
|
function handleNodeEnter(event: CProps.EventMouse, cstID: ConstituentaID) {
|
||||||
|
@ -314,11 +313,11 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
/>
|
/>
|
||||||
{!focusCst ? (
|
{!focusCst ? (
|
||||||
<ToolbarGraphSelection
|
<ToolbarGraphSelection
|
||||||
graph={controller.schema!.graph}
|
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={
|
isOwned={
|
||||||
controller.schema && controller.schema.inheritance.length > 0
|
controller.schema && controller.schema.inheritance.length > 0
|
||||||
? cstID => !controller.schema!.cstByID.get(cstID)?.is_inherited
|
? cstID => !controller.schema.cstByID.get(cstID)?.is_inherited
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
selected={controller.selected}
|
selected={controller.selected}
|
||||||
|
@ -386,7 +385,6 @@ function TGFlow({ onOpenEdit }: TGFlowProps) {
|
||||||
coloringScheme={coloring}
|
coloringScheme={coloring}
|
||||||
toggleSelection={controller.toggleSelect}
|
toggleSelection={controller.toggleSelect}
|
||||||
setFocus={handleSetFocus}
|
setFocus={handleSetFocus}
|
||||||
onEdit={onOpenEdit}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
import {
|
import {
|
||||||
IconClustering,
|
IconClustering,
|
||||||
IconClusteringOff,
|
IconClusteringOff,
|
||||||
|
@ -16,6 +17,7 @@ import BadgeHelp from '@/components/info/BadgeHelp';
|
||||||
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
import MiniSelectorOSS from '@/components/select/MiniSelectorOSS';
|
||||||
import MiniButton from '@/components/ui/MiniButton';
|
import MiniButton from '@/components/ui/MiniButton';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
import { useRSEdit } from '../RSEditContext';
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
@ -46,13 +48,24 @@ function ToolbarTermGraph({
|
||||||
onSaveImage
|
onSaveImage
|
||||||
}: ToolbarTermGraphProps) {
|
}: ToolbarTermGraphProps) {
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
||||||
|
|
||||||
|
function handleShowTypeGraph() {
|
||||||
|
const typeInfo = controller.schema?.items.map(item => ({
|
||||||
|
alias: item.alias,
|
||||||
|
result: item.parse.typification,
|
||||||
|
args: item.parse.args
|
||||||
|
}));
|
||||||
|
showTypeGraph({ items: typeInfo ?? [] });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='cc-icons'>
|
<div className='cc-icons'>
|
||||||
{controller.schema && controller.schema?.oss.length > 0 ? (
|
{controller.schema && controller.schema?.oss.length > 0 ? (
|
||||||
<MiniSelectorOSS
|
<MiniSelectorOSS
|
||||||
items={controller.schema.oss}
|
items={controller.schema.oss}
|
||||||
onSelect={(event, value) => controller.viewOSS(value.id, event.ctrlKey || event.metaKey)}
|
onSelect={(event, value) => controller.navigateOss(value.id, event.ctrlKey || event.metaKey)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<MiniButton
|
<MiniButton
|
||||||
|
@ -91,7 +104,7 @@ function ToolbarTermGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Новая конституента'
|
title='Новая конституента'
|
||||||
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
icon={<IconNewItem size='1.25rem' className='icon-green' />}
|
||||||
disabled={controller.isProcessing}
|
disabled={isProcessing}
|
||||||
onClick={onCreate}
|
onClick={onCreate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -99,14 +112,14 @@ function ToolbarTermGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Удалить выбранные'
|
title='Удалить выбранные'
|
||||||
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
icon={<IconDestroy size='1.25rem' className='icon-red' />}
|
||||||
disabled={!controller.canDeleteSelected || controller.isProcessing}
|
disabled={!controller.canDeleteSelected || isProcessing}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
|
||||||
title='Граф ступеней'
|
title='Граф ступеней'
|
||||||
onClick={() => controller.showTypeGraph()}
|
onClick={handleShowTypeGraph}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
icon={<IconImage size='1.25rem' className='icon-primary' />}
|
||||||
|
|
|
@ -15,6 +15,8 @@ import { useTooltipsStore } from '@/stores/tooltips';
|
||||||
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
import { APP_COLORS, colorBgGraphNode } from '@/styling/color';
|
||||||
import { globals, PARAMETER, prefixes } from '@/utils/constants';
|
import { globals, PARAMETER, prefixes } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
|
|
||||||
interface ViewHiddenProps {
|
interface ViewHiddenProps {
|
||||||
items: ConstituentaID[];
|
items: ConstituentaID[];
|
||||||
selected: ConstituentaID[];
|
selected: ConstituentaID[];
|
||||||
|
@ -23,13 +25,13 @@ interface ViewHiddenProps {
|
||||||
|
|
||||||
toggleSelection: (cstID: ConstituentaID) => void;
|
toggleSelection: (cstID: ConstituentaID) => void;
|
||||||
setFocus: (cstID: ConstituentaID) => void;
|
setFocus: (cstID: ConstituentaID) => void;
|
||||||
onEdit: (cstID: ConstituentaID) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme, onEdit }: ViewHiddenProps) {
|
function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme }: ViewHiddenProps) {
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const localSelected = items.filter(id => selected.includes(id));
|
const localSelected = items.filter(id => selected.includes(id));
|
||||||
|
|
||||||
|
const { navigateCst } = useRSEdit();
|
||||||
const isFolded = useTermGraphStore(state => state.foldHidden);
|
const isFolded = useTermGraphStore(state => state.foldHidden);
|
||||||
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
||||||
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
|
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
|
||||||
|
@ -108,7 +110,7 @@ function ViewHidden({ items, selected, toggleSelection, setFocus, schema, colori
|
||||||
: {})
|
: {})
|
||||||
}}
|
}}
|
||||||
onClick={event => handleClick(cstID, event)}
|
onClick={event => handleClick(cstID, event)}
|
||||||
onDoubleClick={() => onEdit(cstID)}
|
onDoubleClick={() => navigateCst(cstID)}
|
||||||
data-tooltip-id={globals.constituenta_tooltip}
|
data-tooltip-id={globals.constituenta_tooltip}
|
||||||
onMouseEnter={() => setActiveCst(cst)}
|
onMouseEnter={() => setActiveCst(cst)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useGlobalOss } from '@/app/GlobalOssContext';
|
import fileDownload from 'js-file-download';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
|
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
|
||||||
|
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
|
||||||
|
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
|
||||||
|
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
||||||
|
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
|
||||||
|
import { useResetAliases } from '@/backend/rsform/useResetAliases';
|
||||||
|
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
|
||||||
import {
|
import {
|
||||||
IconAdmin,
|
IconAdmin,
|
||||||
IconAlert,
|
IconAlert,
|
||||||
|
@ -19,7 +28,6 @@ import {
|
||||||
IconLibrary,
|
IconLibrary,
|
||||||
IconMenu,
|
IconMenu,
|
||||||
IconNewItem,
|
IconNewItem,
|
||||||
IconNewVersion,
|
|
||||||
IconOSS,
|
IconOSS,
|
||||||
IconOwner,
|
IconOwner,
|
||||||
IconQR,
|
IconQR,
|
||||||
|
@ -35,79 +43,142 @@ import Divider from '@/components/ui/Divider';
|
||||||
import Dropdown from '@/components/ui/Dropdown';
|
import Dropdown from '@/components/ui/Dropdown';
|
||||||
import DropdownButton from '@/components/ui/DropdownButton';
|
import DropdownButton from '@/components/ui/DropdownButton';
|
||||||
import useDropdown from '@/hooks/useDropdown';
|
import useDropdown from '@/hooks/useDropdown';
|
||||||
import { AccessPolicy } from '@/models/library';
|
import { AccessPolicy, LocationHead } from '@/models/library';
|
||||||
|
import { CstType } from '@/models/rsform';
|
||||||
import { UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
|
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
||||||
|
import { describeAccessMode, information, labelAccessMode, tooltips } from '@/utils/labels';
|
||||||
|
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
|
||||||
|
|
||||||
import { OssTabID } from '../OssPage/OssTabs';
|
import { OssTabID } from '../OssPage/OssEditContext';
|
||||||
import { useRSEdit } from './RSEditContext';
|
import { useRSEdit } from './RSEditContext';
|
||||||
|
|
||||||
interface MenuRSTabsProps {
|
function MenuRSTabs() {
|
||||||
onDestroy: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
|
||||||
const controller = useRSEdit();
|
const controller = useRSEdit();
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const oss = useGlobalOss();
|
|
||||||
|
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const setRole = useRoleStore(state => state.setRole);
|
const setRole = useRoleStore(state => state.setRole);
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
const isProcessing = useIsProcessingRSForm();
|
||||||
|
|
||||||
|
const { resetAliases } = useResetAliases();
|
||||||
|
const { restoreOrder } = useRestoreOrder();
|
||||||
|
const { produceStructure } = useProduceStructure();
|
||||||
|
const { inlineSynthesis } = useInlineSynthesis();
|
||||||
|
const { cstSubstitute } = useCstSubstitute();
|
||||||
|
const { download } = useDownloadRSForm();
|
||||||
|
|
||||||
|
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
|
||||||
|
const showQR = useDialogsStore(state => state.showQR);
|
||||||
|
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
|
||||||
|
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
|
||||||
|
const showUpload = useDialogsStore(state => state.showUploadRSForm);
|
||||||
|
|
||||||
const schemaMenu = useDropdown();
|
const schemaMenu = useDropdown();
|
||||||
const editMenu = useDropdown();
|
const editMenu = useDropdown();
|
||||||
const accessMenu = useDropdown();
|
const accessMenu = useDropdown();
|
||||||
|
|
||||||
|
// TODO: move into separate function
|
||||||
|
const canProduceStructure =
|
||||||
|
!!controller.activeCst &&
|
||||||
|
!!controller.activeCst.parse.typification &&
|
||||||
|
controller.activeCst.cst_type !== CstType.BASE &&
|
||||||
|
controller.activeCst.cst_type !== CstType.CONSTANT;
|
||||||
|
|
||||||
|
function calculateCloneLocation() {
|
||||||
|
const location = controller.schema.location;
|
||||||
|
const head = location.substring(0, 2) as LocationHead;
|
||||||
|
if (head === LocationHead.LIBRARY) {
|
||||||
|
return user?.is_staff ? location : LocationHead.USER;
|
||||||
|
}
|
||||||
|
if (controller.schema.owner === user?.id) {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
return head === LocationHead.USER ? LocationHead.USER : location;
|
||||||
|
}
|
||||||
|
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
onDestroy();
|
controller.deleteSchema();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDownload() {
|
function handleDownload() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
controller.download();
|
if (isModified && !promptUnsaved()) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpload() {
|
function handleUpload() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
controller.promptUpload();
|
showUpload({ itemID: controller.schema.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClone() {
|
function handleClone() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
controller.promptClone();
|
if (isModified && !promptUnsaved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showClone({
|
||||||
|
base: controller.schema,
|
||||||
|
initialLocation: calculateCloneLocation(),
|
||||||
|
selected: controller.selected,
|
||||||
|
totalCount: controller.schema.items.length
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShare() {
|
function handleShare() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
controller.share();
|
sharePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleShowQR() {
|
function handleShowQR() {
|
||||||
schemaMenu.hide();
|
schemaMenu.hide();
|
||||||
controller.showQR();
|
showQR({ target: generatePageQR() });
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreateVersion() {
|
|
||||||
schemaMenu.hide();
|
|
||||||
controller.createVersion();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReindex() {
|
function handleReindex() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
controller.reindex();
|
resetAliases(controller.schema.id, () => toast.success(information.reindexComplete));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRestoreOrder() {
|
function handleRestoreOrder() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
controller.reorder();
|
restoreOrder(controller.schema.id, () => toast.success(information.reorderComplete));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubstituteCst() {
|
function handleSubstituteCst() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
controller.substitute();
|
if (isModified && !promptUnsaved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showSubstituteCst({
|
||||||
|
schema: controller.schema,
|
||||||
|
onSubstitute: data =>
|
||||||
|
cstSubstitute(
|
||||||
|
{
|
||||||
|
itemID: controller.schema.id, //
|
||||||
|
data
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
controller.setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
|
||||||
|
toast.success(information.substituteSingle);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTemplates() {
|
function handleTemplates() {
|
||||||
|
@ -117,12 +188,41 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
|
|
||||||
function handleProduceStructure() {
|
function handleProduceStructure() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
controller.produceStructure();
|
if (!controller.activeCst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isModified && !promptUnsaved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
produceStructure(
|
||||||
|
{
|
||||||
|
itemID: controller.schema.id, //
|
||||||
|
data: { target: controller.activeCst.id }
|
||||||
|
},
|
||||||
|
cstList => {
|
||||||
|
toast.success(information.addedConstituents(cstList.length));
|
||||||
|
if (cstList.length !== 0) {
|
||||||
|
controller.setSelected(cstList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInlineSynthesis() {
|
function handleInlineSynthesis() {
|
||||||
editMenu.hide();
|
editMenu.hide();
|
||||||
controller.inlineSynthesis();
|
if (isModified && !promptUnsaved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChangeMode(newMode: UserRole) {
|
function handleChangeMode(newMode: UserRole) {
|
||||||
|
@ -155,7 +255,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
<Dropdown isOpen={schemaMenu.isOpen}>
|
<Dropdown isOpen={schemaMenu.isOpen}>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Поделиться'
|
text='Поделиться'
|
||||||
titleHtml={tooltips.shareItem(controller.schema?.access_policy)}
|
titleHtml={tooltips.shareItem(controller.schema.access_policy)}
|
||||||
icon={<IconShare size='1rem' className='icon-primary' />}
|
icon={<IconShare size='1rem' className='icon-primary' />}
|
||||||
onClick={handleShare}
|
onClick={handleShare}
|
||||||
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
disabled={controller.schema?.access_policy !== AccessPolicy.PUBLIC}
|
||||||
|
@ -174,12 +274,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
onClick={handleClone}
|
onClick={handleClone}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownButton
|
|
||||||
text='Сохранить версию'
|
|
||||||
disabled={!controller.isContentEditable}
|
|
||||||
onClick={handleCreateVersion}
|
|
||||||
icon={<IconNewVersion size='1rem' className='icon-green' />}
|
|
||||||
/>
|
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Выгрузить в Экстеор'
|
text='Выгрузить в Экстеор'
|
||||||
icon={<IconDownload size='1rem' className='icon-primary' />}
|
icon={<IconDownload size='1rem' className='icon-primary' />}
|
||||||
|
@ -189,7 +283,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Загрузить из Экстеор'
|
text='Загрузить из Экстеор'
|
||||||
icon={<IconUpload size='1rem' className='icon-red' />}
|
icon={<IconUpload size='1rem' className='icon-red' />}
|
||||||
disabled={controller.isProcessing || controller.schema?.oss.length !== 0}
|
disabled={isProcessing || controller.schema?.oss.length !== 0}
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -197,7 +291,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Удалить схему'
|
text='Удалить схему'
|
||||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||||
disabled={controller.isProcessing || role < UserRole.OWNER}
|
disabled={isProcessing || role < UserRole.OWNER}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -211,11 +305,11 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
onClick={handleCreateNew}
|
onClick={handleCreateNew}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{oss.schema ? (
|
{controller.schema.oss.length > 0 ? (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Перейти к ОСС'
|
text='Перейти к ОСС'
|
||||||
icon={<IconOSS size='1rem' className='icon-primary' />}
|
icon={<IconOSS size='1rem' className='icon-primary' />}
|
||||||
onClick={() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH))}
|
onClick={() => router.push(urls.oss(controller.schema.oss[0].id, OssTabID.GRAPH))}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
|
@ -225,7 +319,6 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!controller.isArchive && user ? (
|
{!controller.isArchive && user ? (
|
||||||
<div ref={editMenu.ref}>
|
<div ref={editMenu.ref}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -244,14 +337,14 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
text='Шаблоны'
|
text='Шаблоны'
|
||||||
title='Создать конституенту из шаблона'
|
title='Создать конституенту из шаблона'
|
||||||
icon={<IconTemplates size='1rem' className='icon-green' />}
|
icon={<IconTemplates size='1rem' className='icon-green' />}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
onClick={handleTemplates}
|
onClick={handleTemplates}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Встраивание'
|
text='Встраивание'
|
||||||
titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
|
titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
|
||||||
icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
|
icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
onClick={handleInlineSynthesis}
|
onClick={handleInlineSynthesis}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -261,21 +354,21 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
text='Упорядочить список'
|
text='Упорядочить список'
|
||||||
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
|
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
|
||||||
icon={<IconSortList size='1rem' className='icon-primary' />}
|
icon={<IconSortList size='1rem' className='icon-primary' />}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
onClick={handleRestoreOrder}
|
onClick={handleRestoreOrder}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Порядковые имена'
|
text='Порядковые имена'
|
||||||
titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
|
titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
|
||||||
icon={<IconGenerateNames size='1rem' className='icon-primary' />}
|
icon={<IconGenerateNames size='1rem' className='icon-primary' />}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
onClick={handleReindex}
|
onClick={handleReindex}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
text='Порождение структуры'
|
text='Порождение структуры'
|
||||||
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
|
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
|
||||||
icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
|
icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
|
||||||
disabled={!controller.isContentEditable || !controller.canProduceStructure || controller.isProcessing}
|
disabled={!controller.isContentEditable || !canProduceStructure || isProcessing}
|
||||||
onClick={handleProduceStructure}
|
onClick={handleProduceStructure}
|
||||||
/>
|
/>
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
|
@ -283,11 +376,12 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
titleHtml='Заменить вхождения <br/>одной конституенты на другую'
|
titleHtml='Заменить вхождения <br/>одной конституенты на другую'
|
||||||
icon={<IconReplace size='1rem' className='icon-red' />}
|
icon={<IconReplace size='1rem' className='icon-red' />}
|
||||||
onClick={handleSubstituteCst}
|
onClick={handleSubstituteCst}
|
||||||
disabled={!controller.isContentEditable || controller.isProcessing}
|
disabled={!controller.isContentEditable || isProcessing}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
router.push(urls.schema(itemID, version), newTab);
|
||||||
{controller.isArchive && user ? (
|
{controller.isArchive && user ? (
|
||||||
<Button
|
<Button
|
||||||
dense
|
dense
|
||||||
|
@ -298,10 +392,9 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
|
||||||
hideTitle={accessMenu.isOpen}
|
hideTitle={accessMenu.isOpen}
|
||||||
className='h-full px-2'
|
className='h-full px-2'
|
||||||
icon={<IconArchive size='1.25rem' className='icon-primary' />}
|
icon={<IconArchive size='1.25rem' className='icon-primary' />}
|
||||||
onClick={event => controller.viewVersion(undefined, event.ctrlKey || event.metaKey)}
|
onClick={event => router.push(urls.schema(controller.schema.id), event.ctrlKey || event.metaKey)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<div ref={accessMenu.ref}>
|
<div ref={accessMenu.ref}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -1,69 +1,55 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { urls } from '@/app/urls';
|
import { urls } from '@/app/urls';
|
||||||
import { useAuth } from '@/backend/auth/useAuth';
|
import { useAuth } from '@/backend/auth/useAuth';
|
||||||
import { useSetAccessPolicy } from '@/backend/library/useSetAccessPolicy';
|
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
||||||
import { useSetEditors } from '@/backend/library/useSetEditors';
|
import { ICstCreateDTO } from '@/backend/rsform/api';
|
||||||
import { useSetLocation } from '@/backend/library/useSetLocation';
|
|
||||||
import { useSetOwner } from '@/backend/library/useSetOwner';
|
|
||||||
import { useFindPredecessor } from '@/backend/oss/useFindPredecessor';
|
|
||||||
import { ICstCreateDTO, ICstRenameDTO, ICstUpdateDTO, IInlineSynthesisDTO } from '@/backend/rsform/api';
|
|
||||||
import { useCstCreate } from '@/backend/rsform/useCstCreate';
|
import { useCstCreate } from '@/backend/rsform/useCstCreate';
|
||||||
import { useCstDelete } from '@/backend/rsform/useCstDelete';
|
import { useCstDelete } from '@/backend/rsform/useCstDelete';
|
||||||
import { useCstMove } from '@/backend/rsform/useCstMove';
|
import { useCstMove } from '@/backend/rsform/useCstMove';
|
||||||
import { useCstRename } from '@/backend/rsform/useCstRename';
|
|
||||||
import { useCstSubstitute } from '@/backend/rsform/useCstSubstitute';
|
|
||||||
import { useCstUpdate } from '@/backend/rsform/useCstUpdate';
|
|
||||||
import { useDownloadRSForm } from '@/backend/rsform/useDownloadRSForm';
|
|
||||||
import { useInlineSynthesis } from '@/backend/rsform/useInlineSynthesis';
|
|
||||||
import { useIsProcessingRSForm } from '@/backend/rsform/useIsProcessingRSForm';
|
|
||||||
import { useProduceStructure } from '@/backend/rsform/useProduceStructure';
|
|
||||||
import { useResetAliases } from '@/backend/rsform/useResetAliases';
|
|
||||||
import { useRestoreOrder } from '@/backend/rsform/useRestoreOrder';
|
|
||||||
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
|
import { useRSFormSuspense } from '@/backend/rsform/useRSForm';
|
||||||
import {
|
import { ILibraryItemEditor, LibraryItemID, VersionID } from '@/models/library';
|
||||||
AccessPolicy,
|
import { ConstituentaID, CstType, IConstituenta, IRSForm } from '@/models/rsform';
|
||||||
ILibraryItemEditor,
|
|
||||||
IVersionData,
|
|
||||||
LibraryItemID,
|
|
||||||
LocationHead,
|
|
||||||
VersionID
|
|
||||||
} from '@/models/library';
|
|
||||||
import { ICstSubstitutions } from '@/models/oss';
|
|
||||||
import { ConstituentaID, CstType, IConstituenta, IConstituentaMeta, IRSForm, TermForm } from '@/models/rsform';
|
|
||||||
import { generateAlias } from '@/models/rsformAPI';
|
import { generateAlias } from '@/models/rsformAPI';
|
||||||
import { UserID, UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { usePreferencesStore } from '@/stores/preferences';
|
import { usePreferencesStore } from '@/stores/preferences';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { EXTEOR_TRS_FILE } from '@/utils/constants';
|
import { PARAMETER, prefixes } from '@/utils/constants';
|
||||||
import { information, prompts } from '@/utils/labels';
|
import { information, prompts } from '@/utils/labels';
|
||||||
import { promptUnsaved } from '@/utils/utils';
|
import { promptUnsaved } from '@/utils/utils';
|
||||||
|
|
||||||
import { RSTabID } from './RSTabs';
|
import { OssTabID } from '../OssPage/OssEditContext';
|
||||||
|
|
||||||
|
export enum RSTabID {
|
||||||
|
CARD = 0,
|
||||||
|
CST_LIST = 1,
|
||||||
|
CST_EDIT = 2,
|
||||||
|
TERM_GRAPH = 3
|
||||||
|
}
|
||||||
|
|
||||||
export interface IRSEditContext extends ILibraryItemEditor {
|
export interface IRSEditContext extends ILibraryItemEditor {
|
||||||
schema?: IRSForm;
|
schema: IRSForm;
|
||||||
selected: ConstituentaID[];
|
selected: ConstituentaID[];
|
||||||
|
activeCst?: IConstituenta;
|
||||||
|
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
isArchive: boolean;
|
isArchive: boolean;
|
||||||
isMutable: boolean;
|
isMutable: boolean;
|
||||||
isContentEditable: boolean;
|
isContentEditable: boolean;
|
||||||
isProcessing: boolean;
|
|
||||||
isAttachedToOSS: boolean;
|
isAttachedToOSS: boolean;
|
||||||
canProduceStructure: boolean;
|
|
||||||
canDeleteSelected: boolean;
|
canDeleteSelected: boolean;
|
||||||
|
|
||||||
setOwner: (newOwner: UserID) => void;
|
navigateRSForm: ({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) => void;
|
||||||
setAccessPolicy: (newPolicy: AccessPolicy) => void;
|
navigateCst: (cstID: ConstituentaID) => void;
|
||||||
promptEditors: () => void;
|
navigateOss: (target: LibraryItemID, newTab?: boolean) => void;
|
||||||
promptLocation: () => void;
|
|
||||||
|
deleteSchema: () => void;
|
||||||
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
||||||
select: (target: ConstituentaID) => void;
|
select: (target: ConstituentaID) => void;
|
||||||
|
@ -71,35 +57,12 @@ export interface IRSEditContext extends ILibraryItemEditor {
|
||||||
toggleSelect: (target: ConstituentaID) => void;
|
toggleSelect: (target: ConstituentaID) => void;
|
||||||
deselectAll: () => void;
|
deselectAll: () => void;
|
||||||
|
|
||||||
viewOSS: (target: LibraryItemID, newTab?: boolean) => void;
|
|
||||||
viewVersion: (version?: VersionID, newTab?: boolean) => void;
|
|
||||||
viewPredecessor: (target: ConstituentaID) => void;
|
|
||||||
createVersion: () => void;
|
|
||||||
restoreVersion: () => void;
|
|
||||||
promptEditVersions: () => void;
|
|
||||||
|
|
||||||
moveUp: () => void;
|
moveUp: () => void;
|
||||||
moveDown: () => void;
|
moveDown: () => void;
|
||||||
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
|
createCst: (type: CstType | undefined, skipDialog: boolean, definition?: string) => void;
|
||||||
renameCst: () => void;
|
|
||||||
cloneCst: () => void;
|
cloneCst: () => void;
|
||||||
promptDeleteCst: () => void;
|
promptDeleteCst: () => void;
|
||||||
editTermForms: () => void;
|
|
||||||
|
|
||||||
promptTemplate: () => void;
|
promptTemplate: () => void;
|
||||||
promptClone: () => void;
|
|
||||||
promptUpload: () => void;
|
|
||||||
share: () => void;
|
|
||||||
download: () => void;
|
|
||||||
|
|
||||||
reindex: () => void;
|
|
||||||
reorder: () => void;
|
|
||||||
produceStructure: () => void;
|
|
||||||
inlineSynthesis: () => void;
|
|
||||||
substitute: () => void;
|
|
||||||
|
|
||||||
showTypeGraph: () => void;
|
|
||||||
showQR: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const RSEditContext = createContext<IRSEditContext | null>(null);
|
const RSEditContext = createContext<IRSEditContext | null>(null);
|
||||||
|
@ -113,28 +76,11 @@ export const useRSEdit = () => {
|
||||||
|
|
||||||
interface RSEditStateProps {
|
interface RSEditStateProps {
|
||||||
itemID: LibraryItemID;
|
itemID: LibraryItemID;
|
||||||
|
activeTab: RSTabID;
|
||||||
versionID?: VersionID;
|
versionID?: VersionID;
|
||||||
|
|
||||||
selected: ConstituentaID[];
|
|
||||||
isModified: boolean;
|
|
||||||
setSelected: React.Dispatch<React.SetStateAction<ConstituentaID[]>>;
|
|
||||||
activeCst?: IConstituenta;
|
|
||||||
|
|
||||||
onCreateCst?: (newCst: IConstituentaMeta) => void;
|
|
||||||
onDeleteCst?: (newActive?: ConstituentaID) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RSEditState = ({
|
export const RSEditState = ({ itemID, versionID, activeTab, children }: React.PropsWithChildren<RSEditStateProps>) => {
|
||||||
itemID,
|
|
||||||
versionID,
|
|
||||||
selected,
|
|
||||||
setSelected,
|
|
||||||
activeCst,
|
|
||||||
isModified,
|
|
||||||
onCreateCst,
|
|
||||||
onDeleteCst,
|
|
||||||
children
|
|
||||||
}: React.PropsWithChildren<RSEditStateProps>) => {
|
|
||||||
const router = useConceptNavigation();
|
const router = useConceptNavigation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const adminMode = usePreferencesStore(state => state.adminMode);
|
const adminMode = usePreferencesStore(state => state.adminMode);
|
||||||
|
@ -142,72 +88,33 @@ export const RSEditState = ({
|
||||||
const adjustRole = useRoleStore(state => state.adjustRole);
|
const adjustRole = useRoleStore(state => state.adjustRole);
|
||||||
|
|
||||||
const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
|
const { schema } = useRSFormSuspense({ itemID: itemID, version: versionID });
|
||||||
const isProcessing = useIsProcessingRSForm();
|
const { isModified } = useModificationStore();
|
||||||
|
|
||||||
const { download: downloadFile } = useDownloadRSForm();
|
|
||||||
const { findPredecessor } = useFindPredecessor();
|
|
||||||
|
|
||||||
const { setOwner: setItemOwner } = useSetOwner();
|
|
||||||
const { setLocation: setItemLocation } = useSetLocation();
|
|
||||||
const { setAccessPolicy: setItemAccessPolicy } = useSetAccessPolicy();
|
|
||||||
const { setEditors: setItemEditors } = useSetEditors();
|
|
||||||
|
|
||||||
const { cstCreate } = useCstCreate();
|
|
||||||
const { cstRename } = useCstRename();
|
|
||||||
const { cstSubstitute } = useCstSubstitute();
|
|
||||||
const { cstMove } = useCstMove();
|
|
||||||
const { cstDelete } = useCstDelete();
|
|
||||||
const { cstUpdate } = useCstUpdate();
|
|
||||||
|
|
||||||
const { produceStructure: produceStructureInternal } = useProduceStructure();
|
|
||||||
const { inlineSynthesis: inlineSynthesisInternal } = useInlineSynthesis();
|
|
||||||
const { restoreOrder: restoreOrderInternal } = useRestoreOrder();
|
|
||||||
const { resetAliases: resetAliasesInternal } = useResetAliases();
|
|
||||||
|
|
||||||
const isOwned = user?.id === schema?.owner || false;
|
const isOwned = user?.id === schema?.owner || false;
|
||||||
|
|
||||||
const isArchive = !!versionID;
|
const isArchive = !!versionID;
|
||||||
|
const isMutable = role > UserRole.READER && !schema.read_only;
|
||||||
const isMutable = role > UserRole.READER && !schema?.read_only;
|
|
||||||
const isContentEditable = isMutable && !isArchive;
|
const isContentEditable = isMutable && !isArchive;
|
||||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema?.cstByID.get(id)?.is_inherited);
|
const isAttachedToOSS = schema.oss.length > 0;
|
||||||
const isAttachedToOSS =
|
|
||||||
!!schema && schema.oss.length > 0 && (schema.stats.count_inherited > 0 || schema.items.length === 0);
|
|
||||||
|
|
||||||
const [renameInitialData, setRenameInitialData] = useState<ICstRenameDTO>();
|
const [selected, setSelected] = useState<ConstituentaID[]>([]);
|
||||||
|
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
||||||
|
|
||||||
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
|
const activeCst: IConstituenta | undefined = (() => {
|
||||||
const showCreateVersion = useDialogsStore(state => state.showCreateVersion);
|
if (!schema || selected.length === 0) {
|
||||||
const showEditVersions = useDialogsStore(state => state.showEditVersions);
|
return undefined;
|
||||||
const showEditEditors = useDialogsStore(state => state.showEditEditors);
|
} else {
|
||||||
const showEditLocation = useDialogsStore(state => state.showChangeLocation);
|
return schema.cstByID.get(selected[-1]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const { cstCreate } = useCstCreate();
|
||||||
|
const { cstMove } = useCstMove();
|
||||||
|
const { cstDelete } = useCstDelete();
|
||||||
|
const { deleteItem } = useDeleteItem();
|
||||||
|
|
||||||
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
const showCreateCst = useDialogsStore(state => state.showCreateCst);
|
||||||
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
const showDeleteCst = useDialogsStore(state => state.showDeleteCst);
|
||||||
|
|
||||||
const showRenameCst = useDialogsStore(state => state.showRenameCst);
|
|
||||||
const showEditTerm = useDialogsStore(state => state.showEditWordForms);
|
|
||||||
|
|
||||||
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
|
|
||||||
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
const showCstTemplate = useDialogsStore(state => state.showCstTemplate);
|
||||||
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
|
|
||||||
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
|
|
||||||
const showUpload = useDialogsStore(state => state.showUploadRSForm);
|
|
||||||
const showQR = useDialogsStore(state => state.showQR);
|
|
||||||
|
|
||||||
const typeInfo = schema
|
|
||||||
? schema.items.map(item => ({
|
|
||||||
alias: item.alias,
|
|
||||||
result: item.parse.typification,
|
|
||||||
args: item.parse.args
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const canProduceStructure =
|
|
||||||
!!activeCst &&
|
|
||||||
!!activeCst.parse.typification &&
|
|
||||||
activeCst.cst_type !== CstType.BASE &&
|
|
||||||
activeCst.cst_type !== CstType.CONSTANT;
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
|
@ -220,79 +127,78 @@ export const RSEditState = ({
|
||||||
[schema, adjustRole, isOwned, user, adminMode]
|
[schema, adjustRole, isOwned, user, adminMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
function viewVersion(version?: VersionID, newTab?: boolean) {
|
function navigateOss(target: LibraryItemID, newTab?: boolean) {
|
||||||
router.push(urls.schema(itemID, version), newTab);
|
|
||||||
}
|
|
||||||
|
|
||||||
function viewPredecessor(target: ConstituentaID) {
|
|
||||||
findPredecessor({ target: target }, reference =>
|
|
||||||
router.push(
|
|
||||||
urls.schema_props({
|
|
||||||
id: reference.schema,
|
|
||||||
active: reference.id,
|
|
||||||
tab: RSTabID.CST_EDIT
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function viewOSS(target: LibraryItemID, newTab?: boolean) {
|
|
||||||
router.push(urls.oss(target), newTab);
|
router.push(urls.oss(target), newTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreVersion() {
|
function navigateRSForm({ tab, activeID }: { tab: RSTabID; activeID?: ConstituentaID }) {
|
||||||
if (!versionID || !window.confirm(prompts.restoreArchive)) {
|
if (!schema) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
model.versionRestore(versionID, () => {
|
const data = {
|
||||||
toast.success(information.versionRestored);
|
id: schema.id,
|
||||||
viewVersion(undefined);
|
tab: tab,
|
||||||
|
active: activeID,
|
||||||
|
version: versionID
|
||||||
|
};
|
||||||
|
const url = urls.schema_props(data);
|
||||||
|
if (activeID) {
|
||||||
|
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
||||||
|
router.replace(url);
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
||||||
|
data.active = schema.items[0].id;
|
||||||
|
router.replace(urls.schema_props(data));
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateCst(cstID: ConstituentaID) {
|
||||||
|
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
||||||
|
navigateRSForm({ tab: RSTabID.CST_EDIT, activeID: cstID });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSchema() {
|
||||||
|
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
router.push(urls.library);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateCloneLocation() {
|
|
||||||
if (!schema) {
|
|
||||||
return LocationHead.USER;
|
|
||||||
}
|
|
||||||
const location = schema.location;
|
|
||||||
const head = schema.location.substring(0, 2) as LocationHead;
|
|
||||||
if (head === LocationHead.LIBRARY) {
|
|
||||||
return user?.is_staff ? location : LocationHead.USER;
|
|
||||||
}
|
|
||||||
if (schema.owner === user?.id) {
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
return head === LocationHead.USER ? LocationHead.USER : location;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreateCst(data: ICstCreateDTO) {
|
function handleCreateCst(data: ICstCreateDTO) {
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data.alias = data.alias || generateAlias(data.cst_type, schema);
|
data.alias = data.alias || generateAlias(data.cst_type, schema);
|
||||||
cstCreate({ itemID: itemID, data }, newCst => {
|
cstCreate({ itemID: itemID, data }, newCst => {
|
||||||
toast.success(information.newConstituent(newCst.alias));
|
toast.success(information.newConstituent(newCst.alias));
|
||||||
setSelected([newCst.id]);
|
setSelected([newCst.id]);
|
||||||
if (onCreateCst) onCreateCst(newCst);
|
navigateRSForm({ tab: activeTab, activeID: newCst.id });
|
||||||
});
|
if (activeTab === RSTabID.CST_LIST) {
|
||||||
}
|
setTimeout(() => {
|
||||||
|
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||||
function handleRenameCst(data: ICstRenameDTO) {
|
if (element) {
|
||||||
const oldAlias = renameInitialData?.alias ?? '';
|
element.scrollIntoView({
|
||||||
cstRename({ itemID: itemID, data }, () => toast.success(information.renameComplete(oldAlias, data.alias)));
|
behavior: 'smooth',
|
||||||
}
|
block: 'nearest',
|
||||||
|
inline: 'end'
|
||||||
function handleSubstituteCst(data: ICstSubstitutions) {
|
});
|
||||||
cstSubstitute({ itemID: itemID, data }, () => {
|
}
|
||||||
setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)));
|
}, PARAMETER.refreshTimeout);
|
||||||
toast.success(information.substituteSingle);
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteCst(deleted: ConstituentaID[]) {
|
function handleDeleteCst(deleted: ConstituentaID[]) {
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data = {
|
const data = {
|
||||||
items: deleted
|
items: deleted
|
||||||
};
|
};
|
||||||
|
@ -304,88 +210,18 @@ export const RSEditState = ({
|
||||||
cstDelete({ itemID: itemID, data }, () => {
|
cstDelete({ itemID: itemID, data }, () => {
|
||||||
toast.success(information.constituentsDestroyed(deletedNames));
|
toast.success(information.constituentsDestroyed(deletedNames));
|
||||||
setSelected(nextActive ? [nextActive] : []);
|
setSelected(nextActive ? [nextActive] : []);
|
||||||
onDeleteCst?.(nextActive);
|
if (!nextActive) {
|
||||||
});
|
navigateRSForm({ tab: RSTabID.CST_LIST });
|
||||||
}
|
} else if (activeTab === RSTabID.CST_EDIT) {
|
||||||
|
navigateRSForm({ tab: activeTab, activeID: nextActive });
|
||||||
function handleSaveWordforms(forms: TermForm[]) {
|
} else {
|
||||||
if (!activeCst) {
|
navigateRSForm({ tab: activeTab });
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data: ICstUpdateDTO = {
|
|
||||||
target: activeCst.id,
|
|
||||||
item_data: { term_forms: forms }
|
|
||||||
};
|
|
||||||
cstUpdate({ itemID: itemID, data }, () => toast.success(information.changesSaved));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCreateVersion(data: IVersionData) {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.versionCreate(data, () => {
|
|
||||||
toast.success(information.newVersion(data.version));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteVersion(versionID: VersionID) {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.versionDelete(versionID, () => {
|
|
||||||
toast.success(information.versionDestroyed);
|
|
||||||
if (versionID === versionID) {
|
|
||||||
viewVersion(undefined);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateVersion(versionID: VersionID, data: IVersionData) {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.versionUpdate(versionID, data, () => toast.success(information.changesSaved));
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSetLocation = (newLocation: string) =>
|
|
||||||
setItemLocation({ itemID: itemID, location: newLocation }, () => toast.success(information.moveComplete));
|
|
||||||
|
|
||||||
function handleInlineSynthesis(data: IInlineSynthesisDTO) {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const oldCount = schema.items.length;
|
|
||||||
inlineSynthesisInternal({ itemID: itemID, data }, newSchema => {
|
|
||||||
setSelected([]);
|
|
||||||
toast.success(information.addedConstituents(newSchema.items.length - oldCount));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVersion() {
|
|
||||||
if (!schema || (isModified && !promptUnsaved())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showCreateVersion({
|
|
||||||
versions: schema.versions,
|
|
||||||
onCreate: handleCreateVersion,
|
|
||||||
selected: selected,
|
|
||||||
totalCount: schema.items.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptEditVersions() {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditVersions({
|
|
||||||
versions: schema.versions,
|
|
||||||
onDelete: handleDeleteVersion,
|
|
||||||
onUpdate: handleUpdateVersion
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveUp() {
|
function moveUp() {
|
||||||
if (!schema?.items || selected.length === 0) {
|
if (!schema.items || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
const currentIndex = schema.items.reduce((prev, cst, index) => {
|
||||||
|
@ -407,7 +243,7 @@ export const RSEditState = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveDown() {
|
function moveDown() {
|
||||||
if (!schema?.items || selected.length === 0) {
|
if (!schema.items || selected.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
@ -433,9 +269,6 @@ export const RSEditState = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
|
function createCst(type: CstType | undefined, skipDialog: boolean, definition?: string) {
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
|
const targetType = type ?? activeCst?.cst_type ?? CstType.BASE;
|
||||||
const data: ICstCreateDTO = {
|
const data: ICstCreateDTO = {
|
||||||
insert_after: activeCst?.id ?? null,
|
insert_after: activeCst?.id ?? null,
|
||||||
|
@ -455,7 +288,7 @@ export const RSEditState = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneCst() {
|
function cloneCst() {
|
||||||
if (!activeCst || !schema) {
|
if (!activeCst) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: ICstCreateDTO = {
|
const data: ICstCreateDTO = {
|
||||||
|
@ -471,213 +304,50 @@ export const RSEditState = ({
|
||||||
handleCreateCst(data);
|
handleCreateCst(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameCst() {
|
|
||||||
if (!activeCst || !schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const data: ICstRenameDTO = {
|
|
||||||
target: activeCst.id,
|
|
||||||
alias: activeCst.alias,
|
|
||||||
cst_type: activeCst.cst_type
|
|
||||||
};
|
|
||||||
setRenameInitialData(data);
|
|
||||||
showRenameCst({
|
|
||||||
schema: schema,
|
|
||||||
initial: data,
|
|
||||||
allowChangeType: !activeCst.is_inherited,
|
|
||||||
onRename: handleRenameCst
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function substitute() {
|
|
||||||
if (!schema || (isModified && !promptUnsaved())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showSubstituteCst({ schema: schema, onSubstitute: handleSubstituteCst });
|
|
||||||
}
|
|
||||||
|
|
||||||
function inlineSynthesis() {
|
|
||||||
if (!schema || (isModified && !promptUnsaved())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showInlineSynthesis({ receiver: schema, onInlineSynthesis: handleInlineSynthesis });
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptDeleteCst() {
|
function promptDeleteCst() {
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
|
showDeleteCst({ schema: schema, selected: selected, onDelete: handleDeleteCst });
|
||||||
}
|
}
|
||||||
|
|
||||||
function editTermForms() {
|
|
||||||
if (!activeCst) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isModified && !promptUnsaved()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditTerm({ target: activeCst, onSave: handleSaveWordforms });
|
|
||||||
}
|
|
||||||
|
|
||||||
function reindex() {
|
|
||||||
if (!itemID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetAliasesInternal(itemID, () => toast.success(information.reindexComplete));
|
|
||||||
}
|
|
||||||
|
|
||||||
function reorder() {
|
|
||||||
if (!itemID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
restoreOrderInternal(itemID, () => toast.success(information.reorderComplete));
|
|
||||||
}
|
|
||||||
|
|
||||||
function produceStructure() {
|
|
||||||
if (!activeCst) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isModified && !promptUnsaved()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
produceStructureInternal({ itemID: itemID, data: { target: activeCst.id } }, cstList => {
|
|
||||||
toast.success(information.addedConstituents(cstList.length));
|
|
||||||
if (cstList.length !== 0) {
|
|
||||||
setSelected(cstList);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSetEditors(newEditors: UserID[]) {
|
|
||||||
setItemEditors({ itemID: itemID, editors: newEditors }, () => toast.success(information.changesSaved));
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptTemplate() {
|
function promptTemplate() {
|
||||||
if ((isModified && !promptUnsaved()) || !schema) {
|
if (isModified && !promptUnsaved()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
|
showCstTemplate({ schema: schema, onCreate: handleCreateCst, insertAfter: activeCst?.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptClone() {
|
|
||||||
if (!schema || (isModified && !promptUnsaved())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showClone({
|
|
||||||
base: schema,
|
|
||||||
initialLocation: calculateCloneLocation(),
|
|
||||||
selected: selected,
|
|
||||||
totalCount: schema.items.length
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptEditors() {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditEditors({ editors: schema.editors, setEditors: handleSetEditors });
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptLocation() {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showEditLocation({ initial: schema.location, onChangeLocation: handleSetLocation });
|
|
||||||
}
|
|
||||||
|
|
||||||
function download() {
|
|
||||||
if ((isModified && !promptUnsaved()) || !schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
|
|
||||||
downloadFile({ itemID: itemID, version: versionID }, (data: Blob) => {
|
|
||||||
try {
|
|
||||||
fileDownload(data, fileName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function share() {
|
|
||||||
const currentRef = window.location.href;
|
|
||||||
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() => toast.success(information.linkReady))
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOwner(newOwner: UserID) {
|
|
||||||
setItemOwner({ itemID: itemID, owner: newOwner }, () => toast.success(information.changesSaved));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAccessPolicy(newPolicy: AccessPolicy) {
|
|
||||||
setItemAccessPolicy({ itemID: itemID, policy: newPolicy }, () => toast.success(information.changesSaved));
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateQR(): string {
|
|
||||||
const currentRef = window.location.href;
|
|
||||||
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RSEditContext
|
<RSEditContext
|
||||||
value={{
|
value={{
|
||||||
schema: schema,
|
schema,
|
||||||
selected,
|
selected,
|
||||||
|
activeCst,
|
||||||
|
|
||||||
isOwned,
|
isOwned,
|
||||||
isArchive,
|
isArchive,
|
||||||
isMutable,
|
isMutable,
|
||||||
isContentEditable,
|
isContentEditable,
|
||||||
isProcessing,
|
|
||||||
isAttachedToOSS,
|
isAttachedToOSS,
|
||||||
canProduceStructure,
|
|
||||||
canDeleteSelected,
|
canDeleteSelected,
|
||||||
|
|
||||||
setOwner,
|
navigateRSForm,
|
||||||
setAccessPolicy,
|
navigateCst,
|
||||||
promptEditors,
|
|
||||||
promptLocation,
|
|
||||||
|
|
||||||
setSelected: setSelected,
|
deleteSchema,
|
||||||
|
navigateOss,
|
||||||
|
|
||||||
|
setSelected,
|
||||||
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
select: (target: ConstituentaID) => setSelected(prev => [...prev, target]),
|
||||||
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
deselect: (target: ConstituentaID) => setSelected(prev => prev.filter(id => id !== target)),
|
||||||
toggleSelect: (target: ConstituentaID) =>
|
toggleSelect: (target: ConstituentaID) =>
|
||||||
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
setSelected(prev => (prev.includes(target) ? prev.filter(id => id !== target) : [...prev, target])),
|
||||||
deselectAll: () => setSelected([]),
|
deselectAll: () => setSelected([]),
|
||||||
|
|
||||||
viewOSS,
|
|
||||||
viewVersion,
|
|
||||||
viewPredecessor,
|
|
||||||
createVersion,
|
|
||||||
restoreVersion,
|
|
||||||
promptEditVersions,
|
|
||||||
|
|
||||||
moveUp,
|
moveUp,
|
||||||
moveDown,
|
moveDown,
|
||||||
createCst,
|
createCst,
|
||||||
cloneCst,
|
cloneCst,
|
||||||
renameCst,
|
|
||||||
promptDeleteCst,
|
promptDeleteCst,
|
||||||
editTermForms,
|
|
||||||
|
|
||||||
promptTemplate,
|
promptTemplate
|
||||||
promptClone,
|
|
||||||
promptUpload: () => showUpload({ itemID: model.itemID! }),
|
|
||||||
download,
|
|
||||||
share,
|
|
||||||
|
|
||||||
reindex,
|
|
||||||
reorder,
|
|
||||||
inlineSynthesis,
|
|
||||||
produceStructure,
|
|
||||||
substitute,
|
|
||||||
|
|
||||||
showTypeGraph: () => showTypeGraph({ items: typeInfo }),
|
|
||||||
showQR: () => showQR({ target: generateQR() })
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,21 +1,81 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { RSFormState } from '@/context/RSFormContext';
|
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
|
import { urls } from '@/app/urls';
|
||||||
|
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
||||||
|
import Divider from '@/components/ui/Divider';
|
||||||
|
import TextURL from '@/components/ui/TextURL';
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
|
import { LibraryItemID } from '@/models/library';
|
||||||
|
import { useModificationStore } from '@/stores/modification';
|
||||||
|
|
||||||
|
import { RSEditState, RSTabID } from './RSEditContext';
|
||||||
import RSTabs from './RSTabs';
|
import RSTabs from './RSTabs';
|
||||||
|
|
||||||
function RSFormPage() {
|
function RSFormPage() {
|
||||||
const params = useParams();
|
const router = useConceptNavigation();
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
const version = query.get('v') ?? undefined;
|
const params = useParams();
|
||||||
|
const itemID = params.id ? Number(params.id) : undefined;
|
||||||
|
const version = query.get('v') ? Number(query.get('v')) : undefined;
|
||||||
|
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
|
||||||
|
|
||||||
|
const { isModified } = useModificationStore();
|
||||||
|
useBlockNavigation(isModified);
|
||||||
|
|
||||||
|
if (!itemID) {
|
||||||
|
router.replace(urls.page404);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<RSFormState itemID={params.id ?? ''} versionID={version}>
|
<ErrorBoundary
|
||||||
<RSTabs />
|
FallbackComponent={({ error }) => (
|
||||||
</RSFormState>
|
<ProcessError error={error as ErrorData} isArchive={!!version} itemID={itemID} />
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<RSEditState itemID={itemID} versionID={version} activeTab={activeTab}>
|
||||||
|
<RSTabs />
|
||||||
|
</RSEditState>
|
||||||
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RSFormPage;
|
export default RSFormPage;
|
||||||
|
|
||||||
|
// ====== Internals =========
|
||||||
|
function ProcessError({
|
||||||
|
error,
|
||||||
|
isArchive,
|
||||||
|
itemID
|
||||||
|
}: {
|
||||||
|
error: ErrorData;
|
||||||
|
isArchive: boolean;
|
||||||
|
itemID?: LibraryItemID;
|
||||||
|
}): React.ReactElement {
|
||||||
|
if (axios.isAxiosError(error) && error.response) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||||
|
<p>{`Концептуальная схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}</p>
|
||||||
|
<div className='flex justify-center'>
|
||||||
|
<TextURL text='Библиотека' href='/library' />
|
||||||
|
{isArchive ? <Divider vertical margins='mx-3' /> : null}
|
||||||
|
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (error.response.status === 403) {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center p-2 mx-auto'>
|
||||||
|
<p>Владелец ограничил доступ к данной схеме</p>
|
||||||
|
<TextURL text='Библиотека' href='/library' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <InfoError error={error} />;
|
||||||
|
}
|
||||||
|
|
|
@ -1,79 +1,43 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams } from 'react-router';
|
|
||||||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
import { useGlobalOss } from '@/app/GlobalOssContext';
|
import { useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
||||||
import { useBlockNavigation, useConceptNavigation } from '@/app/Navigation/NavigationContext';
|
|
||||||
import { urls } from '@/app/urls';
|
|
||||||
import { useDeleteItem } from '@/backend/library/useDeleteItem';
|
|
||||||
import InfoError, { ErrorData } from '@/components/info/InfoError';
|
|
||||||
import Divider from '@/components/ui/Divider';
|
|
||||||
import Loader from '@/components/ui/Loader';
|
|
||||||
import Overlay from '@/components/ui/Overlay';
|
import Overlay from '@/components/ui/Overlay';
|
||||||
import TabLabel from '@/components/ui/TabLabel';
|
import TabLabel from '@/components/ui/TabLabel';
|
||||||
import TextURL from '@/components/ui/TextURL';
|
|
||||||
import useQueryStrings from '@/hooks/useQueryStrings';
|
import useQueryStrings from '@/hooks/useQueryStrings';
|
||||||
import { LibraryItemID } from '@/models/library';
|
|
||||||
import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsform';
|
|
||||||
import { useAppLayoutStore } from '@/stores/appLayout';
|
import { useAppLayoutStore } from '@/stores/appLayout';
|
||||||
import { PARAMETER, prefixes } from '@/utils/constants';
|
import { useModificationStore } from '@/stores/modification';
|
||||||
import { information, labelVersion, prompts } from '@/utils/labels';
|
import { labelVersion } from '@/utils/labels';
|
||||||
|
|
||||||
import { OssTabID } from '../OssPage/OssTabs';
|
|
||||||
import EditorConstituenta from './EditorConstituenta';
|
import EditorConstituenta from './EditorConstituenta';
|
||||||
import EditorRSForm from './EditorRSFormCard';
|
import EditorRSForm from './EditorRSFormCard';
|
||||||
import EditorRSList from './EditorRSList';
|
import EditorRSList from './EditorRSList';
|
||||||
import EditorTermGraph from './EditorTermGraph';
|
import EditorTermGraph from './EditorTermGraph';
|
||||||
import MenuRSTabs from './MenuRSTabs';
|
import MenuRSTabs from './MenuRSTabs';
|
||||||
import { RSEditState } from './RSEditContext';
|
import { RSTabID, useRSEdit } from './RSEditContext';
|
||||||
|
|
||||||
export enum RSTabID {
|
|
||||||
CARD = 0,
|
|
||||||
CST_LIST = 1,
|
|
||||||
CST_EDIT = 2,
|
|
||||||
TERM_GRAPH = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
function RSTabs() {
|
function RSTabs() {
|
||||||
const params = useParams();
|
|
||||||
const router = useConceptNavigation();
|
|
||||||
const query = useQueryStrings();
|
const query = useQueryStrings();
|
||||||
|
const router = useConceptNavigation();
|
||||||
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
|
const activeTab = query.get('tab') ? (Number(query.get('tab')) as RSTabID) : RSTabID.CARD;
|
||||||
const itemID = params.id ? Number(params.id) : undefined;
|
|
||||||
const version = query.get('v') ? Number(query.get('v')) : undefined;
|
|
||||||
const cstQuery = query.get('active');
|
const cstQuery = query.get('active');
|
||||||
|
|
||||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||||
const { schema, loading, errorLoading, isArchive } = useRSFormControl();
|
const { setIsModified } = useModificationStore();
|
||||||
const { deleteItem } = useDeleteItem();
|
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
|
||||||
const oss = useGlobalOss();
|
|
||||||
|
|
||||||
const [isModified, setIsModified] = useState(false);
|
useEffect(() => setIsModified(false), [setIsModified]);
|
||||||
useBlockNavigation(isModified);
|
|
||||||
|
|
||||||
const [selected, setSelected] = useState<ConstituentaID[]>([]);
|
|
||||||
const activeCst: IConstituenta | undefined = (() => {
|
|
||||||
if (!schema || selected.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
} else {
|
|
||||||
return schema.cstByID.get(selected.at(-1)!);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (schema) {
|
const oldTitle = document.title;
|
||||||
const oldTitle = document.title;
|
document.title = schema.title;
|
||||||
document.title = schema.title;
|
return () => {
|
||||||
return () => {
|
document.title = oldTitle;
|
||||||
document.title = oldTitle;
|
};
|
||||||
};
|
}, [schema?.title]);
|
||||||
}
|
|
||||||
}, [schema, schema?.title]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hideFooter(activeTab !== RSTabID.CARD);
|
hideFooter(activeTab !== RSTabID.CARD);
|
||||||
|
@ -89,30 +53,6 @@ function RSTabs() {
|
||||||
return () => hideFooter(false);
|
return () => hideFooter(false);
|
||||||
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
|
}, [activeTab, cstQuery, setSelected, schema, hideFooter, setIsModified]);
|
||||||
|
|
||||||
function navigateTab(tab: RSTabID, activeID?: ConstituentaID) {
|
|
||||||
if (!schema) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = urls.schema_props({
|
|
||||||
id: schema.id,
|
|
||||||
tab: tab,
|
|
||||||
active: activeID,
|
|
||||||
version: version
|
|
||||||
});
|
|
||||||
if (activeID) {
|
|
||||||
if (tab === activeTab && tab !== RSTabID.CST_EDIT) {
|
|
||||||
router.replace(url);
|
|
||||||
} else {
|
|
||||||
router.push(url);
|
|
||||||
}
|
|
||||||
} else if (tab !== activeTab && tab === RSTabID.CST_EDIT && schema.items.length > 0) {
|
|
||||||
activeID = schema.items[0].id;
|
|
||||||
router.replace(url);
|
|
||||||
} else {
|
|
||||||
router.push(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectTab(index: number, last: number, event: Event) {
|
function onSelectTab(index: number, last: number, event: Event) {
|
||||||
if (last === index) {
|
if (last === index) {
|
||||||
return;
|
return;
|
||||||
|
@ -129,160 +69,54 @@ function RSTabs() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
navigateTab(index, selected.length > 0 ? selected.at(-1) : undefined);
|
navigateRSForm({ tab: index, activeID: selected.length > 0 ? selected.at(-1) : undefined });
|
||||||
}
|
|
||||||
|
|
||||||
function onCreateCst(newCst: IConstituentaMeta) {
|
|
||||||
navigateTab(activeTab, newCst.id);
|
|
||||||
if (activeTab === RSTabID.CST_LIST) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'nearest',
|
|
||||||
inline: 'end'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, PARAMETER.refreshTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeleteCst(newActive?: ConstituentaID) {
|
|
||||||
if (!newActive) {
|
|
||||||
navigateTab(RSTabID.CST_LIST);
|
|
||||||
} else if (activeTab === RSTabID.CST_EDIT) {
|
|
||||||
navigateTab(activeTab, newActive);
|
|
||||||
} else {
|
|
||||||
navigateTab(activeTab);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onOpenCst(cstID: ConstituentaID) {
|
|
||||||
if (cstID !== activeCst?.id || activeTab !== RSTabID.CST_EDIT) {
|
|
||||||
navigateTab(RSTabID.CST_EDIT, cstID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDestroySchema() {
|
|
||||||
if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const backToOSS = oss.schema?.schemas.includes(schema.id);
|
|
||||||
deleteItem(schema.id, () => {
|
|
||||||
toast.success(information.itemDestroyed);
|
|
||||||
if (backToOSS) {
|
|
||||||
oss
|
|
||||||
.invalidate()
|
|
||||||
.then(() => router.push(urls.oss(oss.schema!.id, OssTabID.GRAPH)))
|
|
||||||
.catch(console.error);
|
|
||||||
} else {
|
|
||||||
router.push(urls.library);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RSEditState
|
<>
|
||||||
selected={selected}
|
<Tabs
|
||||||
setSelected={setSelected}
|
selectedIndex={activeTab}
|
||||||
activeCst={activeCst}
|
onSelect={onSelectTab}
|
||||||
isModified={isModified}
|
defaultFocus
|
||||||
onCreateCst={onCreateCst}
|
selectedTabClassName='clr-selected'
|
||||||
onDeleteCst={onDeleteCst}
|
className='flex flex-col mx-auto min-w-fit'
|
||||||
>
|
>
|
||||||
{loading ? <Loader /> : null}
|
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
|
||||||
{errorLoading ? <ProcessError error={errorLoading} isArchive={isArchive} itemID={itemID} /> : null}
|
<TabList
|
||||||
{schema && !loading ? (
|
className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}
|
||||||
<Tabs
|
>
|
||||||
selectedIndex={activeTab}
|
<MenuRSTabs />
|
||||||
onSelect={onSelectTab}
|
|
||||||
defaultFocus
|
|
||||||
selectedTabClassName='clr-selected'
|
|
||||||
className='flex flex-col mx-auto min-w-fit'
|
|
||||||
>
|
|
||||||
<Overlay position='top-0 right-1/2 translate-x-1/2' layer='z-sticky'>
|
|
||||||
<TabList
|
|
||||||
className={clsx('mx-auto w-fit', 'flex items-stretch', 'border-b-2 border-x-2 divide-x-2', 'bg-prim-200')}
|
|
||||||
>
|
|
||||||
<MenuRSTabs onDestroy={onDestroySchema} />
|
|
||||||
|
|
||||||
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
|
<TabLabel label='Карточка' titleHtml={`${schema.title ?? ''}<br />Версия: ${labelVersion(schema)}`} />
|
||||||
<TabLabel
|
<TabLabel
|
||||||
label='Содержание'
|
label='Содержание'
|
||||||
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${
|
titleHtml={`Конституент: ${schema.stats?.count_all ?? 0}<br />Ошибок: ${schema.stats?.count_errors ?? 0}`}
|
||||||
schema.stats?.count_errors ?? 0
|
/>
|
||||||
}`}
|
<TabLabel label='Редактор' />
|
||||||
/>
|
<TabLabel label='Граф термов' />
|
||||||
<TabLabel label='Редактор' />
|
</TabList>
|
||||||
<TabLabel label='Граф термов' />
|
</Overlay>
|
||||||
</TabList>
|
|
||||||
</Overlay>
|
|
||||||
|
|
||||||
<div className='overflow-x-hidden'>
|
<div className='overflow-x-hidden'>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorRSForm
|
<EditorRSForm />
|
||||||
isModified={isModified} //
|
</TabPanel>
|
||||||
setIsModified={setIsModified}
|
|
||||||
onDestroy={onDestroySchema}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorRSList onOpenEdit={onOpenCst} />
|
<EditorRSList />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorConstituenta
|
<EditorConstituenta />
|
||||||
isModified={isModified}
|
</TabPanel>
|
||||||
setIsModified={setIsModified}
|
|
||||||
activeCst={activeCst}
|
|
||||||
onOpenEdit={onOpenCst}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<EditorTermGraph onOpenEdit={onOpenCst} />
|
<EditorTermGraph />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
) : null}
|
</>
|
||||||
</RSEditState>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RSTabs;
|
export default RSTabs;
|
||||||
|
|
||||||
// ====== Internals =========
|
|
||||||
function ProcessError({
|
|
||||||
error,
|
|
||||||
isArchive,
|
|
||||||
itemID
|
|
||||||
}: {
|
|
||||||
error: ErrorData;
|
|
||||||
isArchive: boolean;
|
|
||||||
itemID?: LibraryItemID;
|
|
||||||
}): React.ReactElement {
|
|
||||||
if (axios.isAxiosError(error) && error.response) {
|
|
||||||
if (error.response.status === 404) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
|
||||||
<p>{`Концептуальная схема с указанным идентификатором ${isArchive ? 'и версией ' : ''}отсутствует`}</p>
|
|
||||||
<div className='flex justify-center'>
|
|
||||||
<TextURL text='Библиотека' href='/library' />
|
|
||||||
{isArchive ? <Divider vertical margins='mx-3' /> : null}
|
|
||||||
{isArchive ? <TextURL text='Актуальная версия' href={`/rsforms/${itemID}`} /> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (error.response.status === 403) {
|
|
||||||
return (
|
|
||||||
<div className='flex flex-col items-center p-2 mx-auto'>
|
|
||||||
<p>Владелец ограничил доступ к данной схеме</p>
|
|
||||||
<TextURL text='Библиотека' href='/library' />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return <InfoError error={error} />;
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,12 +4,13 @@ import clsx from 'clsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import useWindowSize from '@/hooks/useWindowSize';
|
import useWindowSize from '@/hooks/useWindowSize';
|
||||||
import { ConstituentaID, IConstituenta, IRSForm } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
import { UserRole } from '@/models/user';
|
import { UserRole } from '@/models/user';
|
||||||
import { useFitHeight } from '@/stores/appLayout';
|
import { useFitHeight } from '@/stores/appLayout';
|
||||||
import { useRoleStore } from '@/stores/role';
|
import { useRoleStore } from '@/stores/role';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { useRSEdit } from '../RSEditContext';
|
||||||
import ConstituentsSearch from './ConstituentsSearch';
|
import ConstituentsSearch from './ConstituentsSearch';
|
||||||
import TableSideConstituents from './TableSideConstituents';
|
import TableSideConstituents from './TableSideConstituents';
|
||||||
|
|
||||||
|
@ -19,16 +20,14 @@ const COLUMN_DENSE_SEARCH_THRESHOLD = 1100;
|
||||||
interface ViewConstituentsProps {
|
interface ViewConstituentsProps {
|
||||||
expression: string;
|
expression: string;
|
||||||
isBottom?: boolean;
|
isBottom?: boolean;
|
||||||
activeCst?: IConstituenta;
|
|
||||||
schema?: IRSForm;
|
|
||||||
onOpenEdit: (cstID: ConstituentaID) => void;
|
|
||||||
isMounted: boolean;
|
isMounted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit, isMounted }: ViewConstituentsProps) {
|
function ViewConstituents({ expression, isBottom, isMounted }: ViewConstituentsProps) {
|
||||||
const windowSize = useWindowSize();
|
const windowSize = useWindowSize();
|
||||||
const role = useRoleStore(state => state.role);
|
const role = useRoleStore(state => state.role);
|
||||||
const listHeight = useFitHeight(!isBottom ? '8.2rem' : role !== UserRole.READER ? '42rem' : '35rem', '10rem');
|
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 ?? []);
|
||||||
|
|
||||||
|
@ -60,7 +59,7 @@ function ViewConstituents({ expression, schema, activeCst, isBottom, onOpenEdit,
|
||||||
maxHeight={listHeight}
|
maxHeight={listHeight}
|
||||||
items={filteredData}
|
items={filteredData}
|
||||||
activeCst={activeCst}
|
activeCst={activeCst}
|
||||||
onOpenEdit={onOpenEdit}
|
onOpenEdit={navigateCst}
|
||||||
autoScroll={!isBottom}
|
autoScroll={!isBottom}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
11
rsconcept/frontend/src/stores/modification.ts
Normal file
11
rsconcept/frontend/src/stores/modification.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface ModificationStore {
|
||||||
|
isModified: boolean;
|
||||||
|
setIsModified: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModificationStore = create<ModificationStore>()(set => ({
|
||||||
|
isModified: false,
|
||||||
|
setIsModified: value => set({ isModified: value })
|
||||||
|
}));
|
|
@ -3,10 +3,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
|
import axios, { AxiosError, AxiosHeaderValue, AxiosResponse } from 'axios';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import { AliasMapping } from '@/models/rslang';
|
import { AliasMapping } from '@/models/rslang';
|
||||||
|
|
||||||
import { prompts } from './labels';
|
import { information, prompts } from './labels';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if arguments is Node.
|
* Checks if arguments is Node.
|
||||||
|
@ -202,3 +203,23 @@ export function convertToCSV(targetObj: object[]): Blob {
|
||||||
|
|
||||||
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
return new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a QR code for the current page.
|
||||||
|
*/
|
||||||
|
export function generatePageQR(): string {
|
||||||
|
const currentRef = window.location.href;
|
||||||
|
return currentRef.includes('?') ? currentRef + '&qr' : currentRef + '?qr';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies sharable link to the current page.
|
||||||
|
*/
|
||||||
|
export function sharePage() {
|
||||||
|
const currentRef = window.location.href;
|
||||||
|
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(url)
|
||||||
|
.then(() => toast.success(information.linkReady))
|
||||||
|
.catch(console.error);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user