F: Implement RSForm to Operation dependency
Some checks failed
Frontend CI / build (22.x) (push) Waiting to run
Backend CI / build (3.12) (push) Has been cancelled

This commit is contained in:
Ivan 2024-07-28 00:37:50 +03:00
parent f254acac7f
commit ec3936cc4c
20 changed files with 267 additions and 59 deletions

View File

@ -4,8 +4,8 @@ from .basics import OperationPositionSerializer, PositionsSerializer, Substituti
from .data_access import ( from .data_access import (
ArgumentSerializer, ArgumentSerializer,
OperationCreateSerializer, OperationCreateSerializer,
OperationDeleteSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer OperationSerializer,
OperationTargetSerializer
) )
from .responses import NewOperationResponse from .responses import NewOperationResponse, NewSchemaResponse

View File

@ -53,7 +53,7 @@ class OperationCreateSerializer(serializers.Serializer):
) )
class OperationDeleteSerializer(serializers.Serializer): class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete operation. '''
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())
positions = serializers.ListField( positions = serializers.ListField(

View File

@ -1,6 +1,8 @@
''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. ''' ''' Utility serializers for REST API schema - SHOULD NOT BE ACCESSED DIRECTLY. '''
from rest_framework import serializers from rest_framework import serializers
from apps.library.serializers import LibraryItemSerializer
from .data_access import OperationSchemaSerializer, OperationSerializer from .data_access import OperationSchemaSerializer, OperationSerializer
@ -8,3 +10,9 @@ class NewOperationResponse(serializers.Serializer):
''' Serializer: Create operation response. ''' ''' Serializer: Create operation response. '''
new_operation = OperationSerializer() new_operation = OperationSerializer()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()
class NewSchemaResponse(serializers.Serializer):
''' Serializer: Create RSForm for input operation response. '''
new_schema = LibraryItemSerializer()
oss = OperationSchemaSerializer()

View File

@ -127,8 +127,6 @@ class TestOssViewset(EndpointTester):
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation(self): def test_create_operation(self):
self.populateData() self.populateData()
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
@ -231,23 +229,6 @@ class TestOssViewset(EndpointTester):
self.assertEqual(schema.access_policy, self.owned.model.access_policy) self.assertEqual(schema.access_policy, self.owned.model.access_policy)
self.assertEqual(schema.location, self.owned.model.location) self.assertEqual(schema.location, self.owned.model.location)
@decl_endpoint('/api/oss/{item}/create-operation', method='post')
def test_create_operation_result(self):
self.populateData()
data = {
'item_data': {
'alias': 'Test4',
'operation_type': OperationType.INPUT,
'result': self.ks1.model.pk
},
'positions': [],
}
response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db()
new_operation = response.data['new_operation']
self.assertEqual(new_operation['result'], self.ks1.model.pk)
@decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
def test_delete_operation(self): def test_delete_operation(self):
self.executeNotFound(item=self.invalid_id) self.executeNotFound(item=self.invalid_id)
@ -269,3 +250,40 @@ class TestOssViewset(EndpointTester):
self.login() self.login()
response = self.executeOK(data=data) response = self.executeOK(data=data)
self.assertEqual(len(response.data['items']), 2) self.assertEqual(len(response.data['items']), 2)
@decl_endpoint('/api/oss/{item}/create-input', method='patch')
def test_create_input(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'positions': []
}
self.executeBadData(data=data)
data['target'] = self.operation1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
self.executeBadData(data=data, item=self.owned_id)
self.operation1.result = None
self.operation1.comment = 'TestComment'
self.operation1.title = 'TestTitle'
self.operation1.sync_text = False
self.operation1.save()
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
new_schema = response.data['new_schema']
self.assertEqual(self.operation1.sync_text, True)
self.assertEqual(new_schema['id'], self.operation1.result.pk)
self.assertEqual(new_schema['alias'], self.operation1.alias)
self.assertEqual(new_schema['title'], self.operation1.title)
self.assertEqual(new_schema['comment'], self.operation1.comment)
data['target'] = self.operation3.pk
self.executeBadData(data=data)

View File

@ -3,7 +3,7 @@ from typing import cast
from django.db import transaction from django.db import transaction
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics from rest_framework import generics, serializers
from rest_framework import status as c from rest_framework import status as c
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
@ -12,6 +12,7 @@ from rest_framework.response import Response
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from shared import messages as msg
from shared import permissions from shared import permissions
from .. import models as m from .. import models as m
@ -33,7 +34,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
if self.action in [ if self.action in [
'create_operation', 'create_operation',
'delete_operation', 'delete_operation',
'update_positions' 'update_positions',
'create_input'
]: ]:
permission_list = [permissions.ItemEditor] permission_list = [permissions.ItemEditor]
elif self.action in ['details']: elif self.action in ['details']:
@ -117,19 +119,18 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss.add_argument(operation=new_operation, argument=argument) oss.add_argument(operation=new_operation, argument=argument)
oss.refresh_from_db() oss.refresh_from_db()
response = Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_operation': s.OperationSerializer(new_operation).data, 'new_operation': s.OperationSerializer(new_operation).data,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(oss.model).data
} }
) )
return response
@extend_schema( @extend_schema(
summary='delete operation', summary='delete operation',
tags=['OSS'], tags=['OSS'],
request=s.OperationDeleteSerializer, request=s.OperationTargetSerializer,
responses={ responses={
c.HTTP_200_OK: s.OperationSchemaSerializer, c.HTTP_200_OK: s.OperationSchemaSerializer,
c.HTTP_400_BAD_REQUEST: None, c.HTTP_400_BAD_REQUEST: None,
@ -140,7 +141,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
@action(detail=True, methods=['patch'], url_path='delete-operation') @action(detail=True, methods=['patch'], url_path='delete-operation')
def delete_operation(self, request: Request, pk): def delete_operation(self, request: Request, pk):
''' Endpoint: Delete operation. ''' ''' Endpoint: Delete operation. '''
serializer = s.OperationDeleteSerializer( serializer = s.OperationTargetSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': self.get_object()}
) )
@ -156,3 +157,59 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data
) )
@extend_schema(
summary='create input schema for target operation',
tags=['OSS'],
request=s.OperationTargetSerializer(),
responses={
c.HTTP_200_OK: s.NewSchemaResponse,
c.HTTP_400_BAD_REQUEST: None,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@action(detail=True, methods=['patch'], url_path='create-input')
def create_input(self, request: Request, pk):
''' Create new input RSForm. '''
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if operation.operation_type != m.OperationType.INPUT:
raise serializers.ValidationError({
'target': msg.operationNotInput(operation.alias)
})
if operation.result is not None:
raise serializers.ValidationError({
'target': msg.operationResultNotEmpty(operation.alias)
})
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
schema = LibraryItem.objects.create(
item_type=LibraryItemType.RSFORM,
owner=oss.model.owner,
alias=operation.alias,
title=operation.title,
comment=operation.comment,
visible=False,
access_policy=oss.model.access_policy,
location=oss.model.location
)
operation.result = schema
operation.sync_text = True
operation.save()
oss.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data={
'new_schema': LibraryItemSerializer(schema).data,
'oss': s.OperationSchemaSerializer(oss.model).data
}
)

View File

@ -18,6 +18,14 @@ def schemaNotOwned():
return 'Нет доступа к схеме' return 'Нет доступа к схеме'
def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}'
def operationResultNotEmpty(title: str):
return f'Результат операции не пуст: {title}'
def renameTrivial(name: str): def renameTrivial(name: str):
return f'Имя должно отличаться от текущего: {name}' return f'Имя должно отличаться от текущего: {name}'

View File

@ -52,7 +52,7 @@ export const urls = {
help_topic: (topic: string) => `/manuals?topic=${topic}`, help_topic: (topic: string) => `/manuals?topic=${topic}`,
schema: (id: number | string, version?: number | string) => schema: (id: number | string, version?: number | string) =>
`/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''), `/rsforms/${id}` + (version !== undefined ? `?v=${version}` : ''),
oss: (id: number | string) => `/oss/${id}`, oss: (id: number | string, tab?: number) => `/oss/${id}` + (tab !== undefined ? `?tab=${tab}` : ''),
schema_props: ({ id, tab, version, active }: SchemaProps) => { schema_props: ({ id, tab, version, active }: SchemaProps) => {
const versionStr = version !== undefined ? `v=${version}&` : ''; const versionStr = version !== undefined ? `v=${version}&` : '';
const activeStr = active !== undefined ? `&active=${active}` : ''; const activeStr = active !== undefined ? `&active=${active}` : '';

View File

@ -3,6 +3,7 @@
*/ */
import { import {
IInputCreatedResponse,
IOperationCreateData, IOperationCreateData,
IOperationCreatedResponse, IOperationCreatedResponse,
IOperationSchemaData, IOperationSchemaData,
@ -19,26 +20,33 @@ export function getOssDetails(target: string, request: FrontPull<IOperationSchem
}); });
} }
export function patchUpdatePositions(schema: string, request: FrontPush<IPositionsData>) { export function patchUpdatePositions(oss: string, request: FrontPush<IPositionsData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/oss/${schema}/update-positions`, endpoint: `/api/oss/${oss}/update-positions`,
request: request request: request
}); });
} }
export function postCreateOperation( export function postCreateOperation(
schema: string, oss: string,
request: FrontExchange<IOperationCreateData, IOperationCreatedResponse> request: FrontExchange<IOperationCreateData, IOperationCreatedResponse>
) { ) {
AxiosPost({ AxiosPost({
endpoint: `/api/oss/${schema}/create-operation`, endpoint: `/api/oss/${oss}/create-operation`,
request: request request: request
}); });
} }
export function patchDeleteOperation(schema: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) { export function patchDeleteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/oss/${schema}/delete-operation`, endpoint: `/api/oss/${oss}/delete-operation`,
request: request
});
}
export function patchCreateInput(oss: string, request: FrontExchange<ITargetOperation, IInputCreatedResponse>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/create-input`,
request: request request: request
}); });
} }

View File

@ -61,7 +61,7 @@ export { TbBriefcase as IconBusiness } from 'react-icons/tb';
export { VscLibrary as IconLibrary } from 'react-icons/vsc'; export { VscLibrary as IconLibrary } from 'react-icons/vsc';
export { IoLibrary as IconLibrary2 } from 'react-icons/io5'; export { IoLibrary as IconLibrary2 } from 'react-icons/io5';
export { BiDiamond as IconTemplates } from 'react-icons/bi'; export { BiDiamond as IconTemplates } from 'react-icons/bi';
export { FaRegObjectGroup as IconOSS } from 'react-icons/fa'; export { GiHoneycomb as IconOSS } from 'react-icons/gi';
export { RiHexagonLine as IconRSForm } from 'react-icons/ri'; export { RiHexagonLine as IconRSForm } from 'react-icons/ri';
export { LuArchive as IconArchive } from 'react-icons/lu'; export { LuArchive as IconArchive } from 'react-icons/lu';
export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuDatabase as IconDatabase } from 'react-icons/lu';
@ -105,6 +105,8 @@ export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu'; export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu'; export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWand2 as IconGenerateNames } from 'react-icons/lu'; export { LuWand2 as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr';
export { BsPlay as IconExecute } from 'react-icons/bs';
// ======== Graph UI ======= // ======== Graph UI =======
export { BiCollapse as IconGraphCollapse } from 'react-icons/bi'; export { BiCollapse as IconGraphCollapse } from 'react-icons/bi';

View File

@ -46,6 +46,7 @@ interface ILibraryContext {
processingError: ErrorData; processingError: ErrorData;
setProcessingError: (error: ErrorData) => void; setProcessingError: (error: ErrorData) => void;
reloadOSS: (callback?: () => void) => void;
reloadItems: (callback?: () => void) => void; reloadItems: (callback?: () => void) => void;
applyFilter: (params: ILibraryFilter) => ILibraryItem[]; applyFilter: (params: ILibraryFilter) => ILibraryItem[];
@ -88,9 +89,17 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
schema: globalOSS, // prettier: split lines schema: globalOSS, // prettier: split lines
error: ossError, error: ossError,
setSchema: setGlobalOSS, setSchema: setGlobalOSS,
loading: ossLoading loading: ossLoading,
reload: reloadOssInternal
} = useOssDetails({ target: ossID }); } = useOssDetails({ target: ossID });
const reloadOSS = useCallback(
(callback?: () => void) => {
reloadOssInternal(setProcessing, callback);
},
[reloadOssInternal]
);
const folders = useMemo(() => { const folders = useMemo(() => {
const result = new FolderTree(); const result = new FolderTree();
result.addPath(LocationHead.USER, 0); result.addPath(LocationHead.USER, 0);
@ -271,11 +280,17 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
1 1
); );
} }
if (callback) callback(); if (globalOSS?.schemas.includes(target)) {
reloadOSS(() => {
if (callback) callback();
});
} else {
if (callback) callback();
}
}) })
}); });
}, },
[reloadItems, user] [reloadItems, reloadOSS, user, globalOSS]
); );
const cloneItem = useCallback( const cloneItem = useCallback(
@ -321,6 +336,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
setGlobalOSS, setGlobalOSS,
ossLoading, ossLoading,
ossError, ossError,
reloadOSS,
reloadItems, reloadItems,

View File

@ -12,7 +12,7 @@ import {
patchSetOwner, patchSetOwner,
postSubscribe postSubscribe
} from '@/backend/library'; } from '@/backend/library';
import { patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss'; import { patchCreateInput, patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
import { ILibraryUpdateData } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library';
@ -54,6 +54,7 @@ interface IOssContext {
savePositions: (data: IPositionsData, callback?: () => void) => void; savePositions: (data: IPositionsData, callback?: () => void) => void;
createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback<IOperation>) => void;
deleteOperation: (data: ITargetOperation, callback?: () => void) => void; deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
} }
const OssContext = createContext<IOssContext | null>(null); const OssContext = createContext<IOssContext | null>(null);
@ -313,6 +314,25 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, library] [itemID, library]
); );
const createInput = useCallback(
(data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => {
setProcessingError(undefined);
patchCreateInput(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData.oss);
library.reloadItems(() => {
if (callback) callback(newData.new_schema);
});
}
});
},
[itemID, library]
);
return ( return (
<OssContext.Provider <OssContext.Provider
value={{ value={{
@ -335,7 +355,8 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
savePositions, savePositions,
createOperation, createOperation,
deleteOperation deleteOperation,
createInput
}} }}
> >
{children} {children}

View File

@ -3,7 +3,7 @@
*/ */
import { Graph } from './Graph'; import { Graph } from './Graph';
import { ILibraryItemData, LibraryItemID } from './library'; import { ILibraryItem, ILibraryItemData, LibraryItemID } from './library';
import { ConstituentaID } from './rsform'; import { ConstituentaID } from './rsform';
/** /**
@ -139,3 +139,11 @@ export interface IOperationCreatedResponse {
new_operation: IOperation; new_operation: IOperation;
oss: IOperationSchemaData; oss: IOperationSchemaData;
} }
/**
* Represents data response when creating {@link IRSForm} for Input {@link IOperation}.
*/
export interface IInputCreatedResponse {
new_schema: ILibraryItem;
oss: IOperationSchemaData;
}

View File

@ -26,6 +26,7 @@ function InputNode(node: OssNodeInternal) {
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover
title='Связанная КС' title='Связанная КС'
hideTitle={!controller.showTooltip}
onClick={() => { onClick={() => {
handleOpenSchema(); handleOpenSchema();
}} }}

View File

@ -3,7 +3,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { IconDestroy, IconEdit2, IconNewItem, IconRSForm } from '@/components/Icons'; import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons';
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 useClickedOutside from '@/hooks/useClickedOutside'; import useClickedOutside from '@/hooks/useClickedOutside';
@ -22,9 +22,10 @@ export interface ContextMenuData {
interface NodeContextMenuProps extends ContextMenuData { interface NodeContextMenuProps extends ContextMenuData {
onHide: () => void; onHide: () => void;
onDelete: (target: OperationID) => void; onDelete: (target: OperationID) => void;
onCreateInput: (target: OperationID) => void;
} }
function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: NodeContextMenuProps) { function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCreateInput }: NodeContextMenuProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null); const ref = useRef(null);
@ -57,11 +58,21 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: Node
onDelete(operation.id); onDelete(operation.id);
}; };
const handleCreateSchema = () => {
handleHide();
onCreateInput(operation.id);
};
const handleRunSynthesis = () => {
toast.error('Not implemented');
handleHide();
};
return ( return (
<div ref={ref} className='absolute' style={{ top: cursorY, left: cursorX, width: 10, height: 10 }}> <div ref={ref} className='absolute' style={{ top: cursorY, left: cursorX, width: 10, height: 10 }}>
<Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}> <Dropdown isOpen={isOpen} stretchLeft={cursorX >= window.innerWidth - PARAMETER.ossContextMenuWidth}>
<DropdownButton <DropdownButton
text='Свойства операции' text='Редактировать'
titleHtml={prepareTooltip('Редактировать операцию', 'Ctrl + клик')} titleHtml={prepareTooltip('Редактировать операцию', 'Ctrl + клик')}
icon={<IconEdit2 size='1rem' className='icon-primary' />} icon={<IconEdit2 size='1rem' className='icon-primary' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
@ -83,16 +94,25 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete }: Node
title='Создать пустую схему для загрузки' title='Создать пустую схему для загрузки'
icon={<IconNewItem size='1rem' className='icon-green' />} icon={<IconNewItem size='1rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleCreateSchema}
/>
) : null}
{controller.isMutable && !operation.result && operation.operation_type === OperationType.INPUT ? (
<DropdownButton
text='Загрузить схему'
title='Выбрать схему для загрузки'
icon={<IconConnect size='1rem' className='icon-primary' />}
disabled={controller.isProcessing}
onClick={handleEditSchema} onClick={handleEditSchema}
/> />
) : null} ) : null}
{controller.isMutable && operation.operation_type === OperationType.INPUT ? ( {controller.isMutable && !operation.result && operation.operation_type === OperationType.SYNTHESIS ? (
<DropdownButton <DropdownButton
text='Привязать схему' text='Выполнить синтез'
title='Выбрать схему для загрузки' title='Выполнить операцию и получить синтезированную КС'
icon={<IconRSForm size='1rem' className='icon-primary' />} icon={<IconExecute size='1rem' className='icon-green' />}
disabled={controller.isProcessing} disabled={controller.isProcessing}
onClick={handleEditSchema} onClick={handleRunSynthesis}
/> />
) : null} ) : null}

View File

@ -27,6 +27,7 @@ function OperationNode(node: OssNodeInternal) {
icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />} icon={<IconRSForm className={hasFile ? 'clr-text-green' : 'clr-text-red'} size='0.75rem' />}
noHover noHover
title='Связанная КС' title='Связанная КС'
hideTitle={!controller.showTooltip}
onClick={() => { onClick={() => {
handleOpenSchema(); handleOpenSchema();
}} }}

View File

@ -141,6 +141,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
[controller, getPositions] [controller, getPositions]
); );
const handleCreateInput = useCallback(
(target: OperationID) => {
controller.createInput(target, getPositions());
},
[controller, getPositions]
);
const handleFitView = useCallback(() => { const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration }); flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]); }, [flow]);
@ -184,7 +191,6 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: CProps.EventMouse, node: OssNode) => { (event: CProps.EventMouse, node: OssNode) => {
console.log(node);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -283,7 +289,12 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
/> />
</Overlay> </Overlay>
{menuProps ? ( {menuProps ? (
<NodeContextMenu onHide={handleContextMenuHide} onDelete={handleDeleteOperation} {...menuProps} /> <NodeContextMenu
onHide={handleContextMenuHide}
onDelete={handleDeleteOperation}
onCreateInput={handleCreateInput}
{...menuProps}
/>
) : null} ) : null}
<div className='relative' style={{ height: canvasHeight, width: canvasWidth }}> <div className='relative' style={{ height: canvasHeight, width: canvasWidth }}>
{graph} {graph}

View File

@ -44,6 +44,7 @@ export interface IOssEditContext {
savePositions: (positions: IOperationPosition[], callback?: () => void) => void; savePositions: (positions: IOperationPosition[], callback?: () => void) => void;
promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void; promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void;
deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void;
createInput: (target: OperationID, positions: IOperationPosition[]) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -210,6 +211,16 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model] [model]
); );
const createInput = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
model.createInput({ target: target, positions: positions }, new_schema => {
toast.success(information.newLibraryItem);
router.push(urls.schema(new_schema.id));
});
},
[model, router]
);
return ( return (
<OssEditContext.Provider <OssEditContext.Provider
value={{ value={{
@ -234,7 +245,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
openOperationSchema, openOperationSchema,
savePositions, savePositions,
promptCreateOperation, promptCreateOperation,
deleteOperation deleteOperation,
createInput
}} }}
> >
{model.schema ? ( {model.schema ? (

View File

@ -33,7 +33,7 @@ export enum OssTabID {
function OssTabs() { function OssTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
const activeTab = (Number(query.get('tab')) ?? OssTabID.CARD) as OssTabID; const activeTab = (Number(query.get('tab')) ?? OssTabID.GRAPH) as OssTabID;
const { calculateHeight } = useConceptOptions(); const { calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading } = useOSS(); const { schema, loading, errorLoading } = useOSS();

View File

@ -16,6 +16,7 @@ import {
IconLibrary, IconLibrary,
IconMenu, IconMenu,
IconNewItem, IconNewItem,
IconOSS,
IconOwner, IconOwner,
IconReader, IconReader,
IconReplace, IconReplace,
@ -29,6 +30,7 @@ import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
@ -36,6 +38,7 @@ import { AccessPolicy } from '@/models/library';
import { UserLevel } from '@/models/user'; import { UserLevel } from '@/models/user';
import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels'; import { describeAccessMode, labelAccessMode, tooltips } from '@/utils/labels';
import { OssTabID } from '../OssPage/OssTabs';
import { useRSEdit } from './RSEditContext'; import { useRSEdit } from './RSEditContext';
interface MenuRSTabsProps { interface MenuRSTabsProps {
@ -47,6 +50,7 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const model = useRSForm(); const model = useRSForm();
const library = useLibrary();
const { accessLevel, setAccessLevel } = useAccessMode(); const { accessLevel, setAccessLevel } = useAccessMode();
@ -181,6 +185,13 @@ function MenuRSTabs({ onDestroy }: MenuRSTabsProps) {
onClick={handleCreateNew} onClick={handleCreateNew}
/> />
) : null} ) : null}
{library.globalOSS ? (
<DropdownButton
text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH))}
/>
) : null}
<DropdownButton <DropdownButton
text='Библиотека' text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />} icon={<IconLibrary size='1rem' className='icon-primary' />}

View File

@ -22,6 +22,7 @@ import { ConstituentaID, IConstituenta, IConstituentaMeta } from '@/models/rsfor
import { PARAMETER, prefixes } from '@/utils/constants'; import { PARAMETER, prefixes } from '@/utils/constants';
import { information, labelVersion, prompts } from '@/utils/labels'; import { information, labelVersion, prompts } 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';
@ -45,7 +46,7 @@ function RSTabs() {
const { setNoFooter, calculateHeight } = useConceptOptions(); const { setNoFooter, calculateHeight } = useConceptOptions();
const { schema, loading, errorLoading, isArchive, itemID } = useRSForm(); const { schema, loading, errorLoading, isArchive, itemID } = useRSForm();
const { destroyItem } = useLibrary(); const library = useLibrary();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
useBlockNavigation(isModified); useBlockNavigation(isModified);
@ -176,11 +177,16 @@ function RSTabs() {
if (!schema || !window.confirm(prompts.deleteLibraryItem)) { if (!schema || !window.confirm(prompts.deleteLibraryItem)) {
return; return;
} }
destroyItem(schema.id, () => { const backToOSS = library.globalOSS?.schemas.includes(schema.id);
library.destroyItem(schema.id, () => {
toast.success(information.itemDestroyed); toast.success(information.itemDestroyed);
router.push(urls.library); if (backToOSS) {
router.push(urls.oss(library.globalOSS!.id, OssTabID.GRAPH));
} else {
router.push(urls.library);
}
}); });
}, [schema, destroyItem, router]); }, [schema, library, router]);
const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); const panelHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);