From 0a7cfa137599d058683322d90a11a7f5a0af1870 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 29 Jul 2024 22:30:24 +0300 Subject: [PATCH] F: Implement Operation edit --- .../backend/apps/oss/models/Operation.py | 12 ++ .../apps/oss/models/OperationSchema.py | 70 +++++++----- .../backend/apps/oss/serializers/__init__.py | 1 + .../apps/oss/serializers/data_access.py | 66 +++++++++++ .../backend/apps/oss/tests/s_views/t_oss.py | 76 ++++++++++++- rsconcept/backend/apps/oss/views/oss.py | 103 +++++++++++++++++- .../apps/rsform/serializers/__init__.py | 3 +- .../apps/rsform/serializers/data_access.py | 6 +- rsconcept/backend/shared/messages.py | 10 +- rsconcept/backend/shared/permissions.py | 18 +++ rsconcept/frontend/src/backend/oss.ts | 15 +++ rsconcept/frontend/src/context/OssContext.tsx | 55 +++++++++- .../DlgEditOperation/DlgEditOperation.tsx | 41 +++---- rsconcept/frontend/src/models/oss.ts | 9 ++ .../EditorOssGraph/NodeContextMenu.tsx | 7 +- .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 20 ++++ .../src/pages/OssPage/OssEditContext.tsx | 36 ++++-- 17 files changed, 477 insertions(+), 71 deletions(-) diff --git a/rsconcept/backend/apps/oss/models/Operation.py b/rsconcept/backend/apps/oss/models/Operation.py index 2be9b728..0185a4b8 100644 --- a/rsconcept/backend/apps/oss/models/Operation.py +++ b/rsconcept/backend/apps/oss/models/Operation.py @@ -7,10 +7,14 @@ from django.db.models import ( FloatField, ForeignKey, Model, + QuerySet, TextChoices, TextField ) +from .Argument import Argument +from .Substitution import Substitution + class OperationType(TextChoices): ''' Type of operation. ''' @@ -74,3 +78,11 @@ class Operation(Model): def __str__(self) -> str: return f'Операция {self.alias}' + + def getArguments(self) -> QuerySet[Argument]: + ''' Operation arguments. ''' + return Argument.objects.filter(operation=self) + + def getSubstitutions(self) -> QuerySet[Substitution]: + ''' Operation substitutions. ''' + return Substitution.objects.filter(operation=self) diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 72c08b56..b94e6963 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -100,39 +100,59 @@ class OperationSchema: self.save() @transaction.atomic - def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]: - ''' Add Argument to operation. ''' - if Argument.objects.filter(operation=operation, argument=argument).exists(): - return None - result = Argument.objects.create(operation=operation, argument=argument) - self.save() - return result - - @transaction.atomic - def clear_arguments(self, target: Operation): - ''' Clear all arguments for operation. ''' - if not Argument.objects.filter(operation=target).exists(): + def set_arguments(self, operation: Operation, arguments: list[Operation]): + ''' Set arguments to operation. ''' + processed: list[Operation] = [] + changed = False + for current in operation.getArguments(): + if current.argument not in arguments: + changed = True + current.delete() + else: + processed.append(current.argument) + for arg in arguments: + if arg not in processed: + changed = True + processed.append(arg) + Argument.objects.create(operation=operation, argument=arg) + if not changed: return - - Argument.objects.filter(operation=target).delete() - Substitution.objects.filter(operation=target).delete() - # trigger on_change effects - self.save() @transaction.atomic def set_substitutions(self, target: Operation, substitutes: list[dict]): ''' Clear all arguments for operation. ''' - Substitution.objects.filter(operation=target).delete() - for sub in substitutes: - Substitution.objects.create( - operation=target, - original=sub['original'], - substitution=sub['substitution'], - transfer_term=sub['transfer_term'] - ) + processed: list[dict] = [] + changed = False + for current in target.getSubstitutions(): + subs = [ + x for x in substitutes + if x['original'] == current.original and x['substitution'] == current.substitution + ] + if len(subs) == 0: + changed = True + current.delete() + continue + if current.transfer_term != subs[0]['transfer_term']: + current.transfer_term = subs[0]['transfer_term'] + current.save() + continue + processed.append(subs[0]) + + for sub in substitutes: + if sub not in processed: + changed = True + Substitution.objects.create( + operation=target, + original=sub['original'], + substitution=sub['substitution'], + transfer_term=sub['transfer_term'] + ) + + if not changed: + return # trigger on_change effects self.save() diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index 11b37019..d6633769 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -7,6 +7,7 @@ from .data_access import ( OperationSchemaSerializer, OperationSerializer, OperationTargetSerializer, + OperationUpdateSerializer, SetOperationInputSerializer ) from .responses import NewOperationResponse, NewSchemaResponse diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index d82949ab..cf5d5ac8 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -1,4 +1,5 @@ ''' Serializers for persistent data manipulation. ''' +import re from typing import cast from django.db.models import F @@ -7,6 +8,8 @@ from rest_framework.serializers import PrimaryKeyRelatedField as PKField from apps.library.models import LibraryItem, LibraryItemType from apps.library.serializers import LibraryItemDetailsSerializer +from apps.rsform.models import Constituenta +from apps.rsform.serializers import SubstitutionSerializerBase from shared import messages as msg from ..models import Argument, Operation, OperationSchema, OperationType @@ -47,12 +50,75 @@ class OperationCreateSerializer(serializers.Serializer): create_schema = serializers.BooleanField(default=False, required=False) item_data = OperationData() arguments = PKField(many=True, queryset=Operation.objects.all(), required=False) + positions = serializers.ListField( child=OperationPositionSerializer(), default=[] ) +class OperationUpdateSerializer(serializers.Serializer): + ''' Serializer: Operation creation. ''' + class OperationData(serializers.ModelSerializer): + ''' Serializer: Operation creation data. ''' + class Meta: + ''' serializer metadata. ''' + model = Operation + fields = 'alias', 'title', 'sync_text', 'comment' + + target = PKField(many=False, queryset=Operation.objects.all()) + item_data = OperationData() + arguments = PKField(many=True, queryset=Operation.objects.all(), required=False) + substitutions = serializers.ListField( + child=SubstitutionSerializerBase(), + required=False + ) + + positions = serializers.ListField( + child=OperationPositionSerializer(), + default=[] + ) + + def validate(self, attrs): + if 'arguments' not in attrs: + return attrs + + oss = cast(LibraryItem, self.context['oss']) + for operation in attrs['arguments']: + if operation.oss != oss: + raise serializers.ValidationError({ + 'arguments': msg.operationNotInOSS(oss.title) + }) + + if 'substitutions' not in attrs: + return attrs + schemas = [arg.result.pk for arg in attrs['arguments'] if arg.result is not None] + deleted = set() + for item in attrs['substitutions']: + original_cst = cast(Constituenta, item['original']) + substitution_cst = cast(Constituenta, item['substitution']) + if original_cst.schema.pk not in schemas: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.constituentaNotFromOperation() + }) + if substitution_cst.schema.pk not in schemas: + raise serializers.ValidationError({ + f'{substitution_cst.id}': msg.constituentaNotFromOperation() + }) + if original_cst.pk in deleted: + raise serializers.ValidationError({ + f'{original_cst.id}': msg.substituteDouble(original_cst.alias) + }) + if original_cst.schema == substitution_cst.schema: + raise serializers.ValidationError({ + 'alias': msg.substituteTrivial(original_cst.alias) + }) + deleted.add(original_cst.pk) + return attrs + + + + class OperationTargetSerializer(serializers.Serializer): ''' Serializer: Delete operation. ''' target = PKField(many=False, queryset=Operation.objects.all()) diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index 9c1054c6..332e9483 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -41,8 +41,7 @@ class TestOssViewset(EndpointTester): alias='3', operation_type=OperationType.SYNTHESIS ) - self.owned.add_argument(self.operation3, self.operation1) - self.owned.add_argument(self.operation3, self.operation2) + self.owned.set_arguments(self.operation3, [self.operation1, self.operation2]) self.owned.set_substitutions(self.operation3, [{ 'original': self.ks1x1, 'substitution': self.ks2x1, @@ -344,3 +343,76 @@ class TestOssViewset(EndpointTester): self.operation2.refresh_from_db() self.assertEqual(self.operation2.sync_text, True) self.assertEqual(self.operation2.result, self.ks2.model) + + @decl_endpoint('/api/oss/{item}/update-operation', method='patch') + def test_update_operation(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user) + ks3x1 = ks3.insert_new('X1', term_resolved='X1_1') + + data = { + 'target': self.operation3.pk, + 'item_data': { + 'alias': 'Test3 mod', + 'title': 'Test title mod', + 'comment': 'Comment mod', + 'sync_text': True + }, + 'positions': [], + 'arguments': [self.operation1.pk, self.operation2.pk], + 'substitutions': [ + { + 'original': self.ks1x1.pk, + 'substitution': ks3x1.pk, + 'transfer_term': False + } + ] + } + self.executeBadData(data=data) + + data['substitutions'][0]['substitution'] = self.ks2x1.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() + response = self.executeOK(data=data) + self.operation3.refresh_from_db() + self.assertEqual(self.operation3.sync_text, data['item_data']['sync_text']) + self.assertEqual(self.operation3.alias, data['item_data']['alias']) + self.assertEqual(self.operation3.title, data['item_data']['title']) + self.assertEqual(self.operation3.comment, data['item_data']['comment']) + self.assertEqual(set([argument.pk for argument in self.operation3.getArguments()]), set(data['arguments'])) + sub = self.operation3.getSubstitutions()[0] + self.assertEqual(sub.original.pk, data['substitutions'][0]['original']) + self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution']) + self.assertEqual(sub.transfer_term, data['substitutions'][0]['transfer_term']) + + @decl_endpoint('/api/oss/{item}/update-operation', method='patch') + def test_update_operation_sync(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'target': self.operation1.pk, + 'item_data': { + 'alias': 'Test3 mod', + 'title': 'Test title mod', + 'comment': 'Comment mod', + 'sync_text': True + }, + 'positions': [], + } + + response = self.executeOK(data=data) + self.operation1.refresh_from_db() + self.assertEqual(self.operation1.sync_text, data['item_data']['sync_text']) + self.assertEqual(self.operation1.alias, data['item_data']['alias']) + self.assertEqual(self.operation1.title, data['item_data']['title']) + self.assertEqual(self.operation1.comment, data['item_data']['comment']) + self.assertEqual(self.operation1.result.alias, data['item_data']['alias']) + self.assertEqual(self.operation1.result.title, data['item_data']['title']) + self.assertEqual(self.operation1.result.comment, data['item_data']['comment']) diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 5dc5a263..44aed8d3 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -36,7 +36,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'delete_operation', 'update_positions', 'create_input', - 'set_input' + 'set_input', + 'update_operation', + 'execute_operation', ]: permission_list = [permissions.ItemEditor] elif self.action in ['details']: @@ -117,8 +119,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev data['result'] = schema new_operation = oss.create_operation(**data) if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: - for argument in serializer.validated_data['arguments']: - oss.add_argument(operation=new_operation, argument=argument) + oss.set_arguments( + operation=new_operation, + arguments=serializer.validated_data['arguments'] + ) oss.refresh_from_db() return Response( @@ -257,3 +261,96 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(oss.model).data ) + + @extend_schema( + summary='update operation', + tags=['OSS'], + request=s.OperationUpdateSerializer(), + responses={ + c.HTTP_200_OK: s.OperationSchemaSerializer, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['patch'], url_path='update-operation') + def update_operation(self, request: Request, pk): + ''' Update operation arguments and parameters. ''' + serializer = s.OperationUpdateSerializer( + data=request.data, + context={'oss': self.get_object()} + ) + serializer.is_valid(raise_exception=True) + + operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) + oss = m.OperationSchema(self.get_object()) + with transaction.atomic(): + oss.update_positions(serializer.validated_data['positions']) + operation.alias = serializer.validated_data['item_data']['alias'] + operation.title = serializer.validated_data['item_data']['title'] + operation.comment = serializer.validated_data['item_data']['comment'] + operation.sync_text = serializer.validated_data['item_data']['sync_text'] + operation.save() + + if operation.sync_text and operation.result is not None: + can_edit = permissions.can_edit_item(request.user, operation.result) + if can_edit: + operation.result.alias = operation.alias + operation.result.title = operation.title + operation.result.comment = operation.comment + operation.result.save() + if 'arguments' in serializer.validated_data: + oss.set_arguments(operation, serializer.validated_data['arguments']) + if 'substitutions' in serializer.validated_data: + oss.set_substitutions(operation, serializer.validated_data['substitutions']) + return Response( + status=c.HTTP_200_OK, + data=s.OperationSchemaSerializer(oss.model).data + ) + + @extend_schema( + summary='execute operation', + tags=['OSS'], + request=s.OperationTargetSerializer(), + responses={ + c.HTTP_200_OK: s.OperationSchemaSerializer, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['post'], url_path='execute-operation') + def execute_operation(self, request: Request, pk): + ''' Execute operation. ''' + 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.SYNTHESIS: + raise serializers.ValidationError({ + 'target': msg.operationNotSynthesis(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']) + # operation.result.refresh_from_db() + # operation.result.title = operation.title + # operation.result.comment = operation.comment + # operation.result.alias = operation.alias + # operation.result.save() + + # update arguments + + oss.refresh_from_db() + return Response( + status=c.HTTP_200_OK, + data=s.OperationSchemaSerializer(oss.model).data + ) diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py index 1ede4f4b..b0097436 100644 --- a/rsconcept/backend/apps/rsform/serializers/__init__.py +++ b/rsconcept/backend/apps/rsform/serializers/__init__.py @@ -19,7 +19,8 @@ from .data_access import ( CstTargetSerializer, InlineSynthesisSerializer, RSFormParseSerializer, - RSFormSerializer + RSFormSerializer, + SubstitutionSerializerBase ) from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer from .io_pyconcept import PyConceptAdapter diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 2d596699..e8c27a9b 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -270,7 +270,7 @@ class CstMoveSerializer(CstListSerializer): move_to = serializers.IntegerField() -class CstSubstituteSerializerBase(serializers.Serializer): +class SubstitutionSerializerBase(serializers.Serializer): ''' Serializer: Basic substitution. ''' original = PKField(many=False, queryset=Constituenta.objects.all()) substitution = PKField(many=False, queryset=Constituenta.objects.all()) @@ -280,7 +280,7 @@ class CstSubstituteSerializerBase(serializers.Serializer): class CstSubstituteSerializer(serializers.Serializer): ''' Serializer: Constituenta substitution. ''' substitutions = serializers.ListField( - child=CstSubstituteSerializerBase(), + child=SubstitutionSerializerBase(), min_length=1 ) @@ -316,7 +316,7 @@ class InlineSynthesisSerializer(serializers.Serializer): source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore items = PKField(many=True, queryset=Constituenta.objects.all()) substitutions = serializers.ListField( - child=CstSubstituteSerializerBase() + child=SubstitutionSerializerBase() ) def validate(self, attrs): diff --git a/rsconcept/backend/shared/messages.py b/rsconcept/backend/shared/messages.py index 7cf9d105..1b2fe157 100644 --- a/rsconcept/backend/shared/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -6,8 +6,12 @@ def constituentaNotInRSform(title: str): return f'Конституента не принадлежит схеме: {title}' +def constituentaNotFromOperation(): + return f'Конституента не соответствую аргументам операции' + + def operationNotInOSS(title: str): - return f'Операция не принадлежит схеме: {title}' + return f'Операция не принадлежит ОСС: {title}' def substitutionNotInList(): @@ -22,6 +26,10 @@ def operationNotInput(title: str): return f'Операция не является Загрузкой: {title}' +def operationNotSynthesis(title: str): + return f'Операция не является Синтезом: {title}' + + def operationResultNotEmpty(title: str): return f'Результат операции не пуст: {title}' diff --git a/rsconcept/backend/shared/permissions.py b/rsconcept/backend/shared/permissions.py index d6490dd1..71c73395 100644 --- a/rsconcept/backend/shared/permissions.py +++ b/rsconcept/backend/shared/permissions.py @@ -32,6 +32,24 @@ def _extract_item(obj: Any) -> LibraryItem: }) +def can_edit_item(user, obj: Any) -> bool: + if user.is_anonymous: + return False + if hasattr(user, 'is_staff') and user.is_staff: + return True + + item = _extract_item(obj) + if item.owner == user: + return True + + if Editor.objects.filter( + item=item, + editor=cast(User, user) + ).exists() and item.access_policy != AccessPolicy.PRIVATE: + return True + return False + + class GlobalAdmin(_Base): ''' Item permission: Admin or higher. ''' diff --git a/rsconcept/frontend/src/backend/oss.ts b/rsconcept/frontend/src/backend/oss.ts index c7e1fc4b..05c8a359 100644 --- a/rsconcept/frontend/src/backend/oss.ts +++ b/rsconcept/frontend/src/backend/oss.ts @@ -8,6 +8,7 @@ import { IOperationCreatedResponse, IOperationSchemaData, IOperationSetInputData, + IOperationUpdateData, IPositionsData, ITargetOperation } from '@/models/oss'; @@ -58,3 +59,17 @@ export function patchSetInput(oss: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/oss/${oss}/update-operation`, + request: request + }); +} + +export function postExecuteOperation(oss: string, request: FrontExchange) { + AxiosPost({ + endpoint: `/api/oss/${oss}/execute-operation`, + request: request + }); +} diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index d4d51441..f0c0c039 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -16,8 +16,10 @@ import { patchCreateInput, patchDeleteOperation, patchSetInput, + patchUpdateOperation, patchUpdatePositions, - postCreateOperation + postCreateOperation, + postExecuteOperation } from '@/backend/oss'; import { type ErrorData } from '@/components/info/InfoError'; import { AccessPolicy, ILibraryItem } from '@/models/library'; @@ -28,6 +30,7 @@ import { IOperationSchema, IOperationSchemaData, IOperationSetInputData, + IOperationUpdateData, IPositionsData, ITargetOperation } from '@/models/oss'; @@ -63,6 +66,8 @@ interface IOssContext { deleteOperation: (data: ITargetOperation, callback?: () => void) => void; createInput: (data: ITargetOperation, callback?: DataCallback) => void; setInput: (data: IOperationSetInputData, callback?: () => void) => void; + updateOperation: (data: IOperationUpdateData, callback?: () => void) => void; + executeOperation: (data: ITargetOperation, callback?: () => void) => void; } const OssContext = createContext(null); @@ -362,6 +367,50 @@ export const OssState = ({ itemID, children }: OssStateProps) => { [itemID, schema, library] ); + const updateOperation = useCallback( + (data: IOperationUpdateData, callback?: () => void) => { + if (!schema) { + return; + } + setProcessingError(undefined); + patchUpdateOperation(itemID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: newData => { + library.setGlobalOSS(newData); + library.reloadItems(() => { + if (callback) callback(); + }); + } + }); + }, + [itemID, schema, library] + ); + + const executeOperation = useCallback( + (data: ITargetOperation, callback?: () => void) => { + if (!schema) { + return; + } + setProcessingError(undefined); + postExecuteOperation(itemID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: newData => { + library.setGlobalOSS(newData); + library.reloadItems(() => { + if (callback) callback(); + }); + } + }); + }, + [itemID, schema, library] + ); + return ( { createOperation, deleteOperation, createInput, - setInput + setInput, + updateOperation, + executeOperation }} > {children} diff --git a/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx b/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx index 2c243c62..e4cf7fcd 100644 --- a/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgEditOperation/DlgEditOperation.tsx @@ -10,7 +10,14 @@ import Overlay from '@/components/ui/Overlay'; import TabLabel from '@/components/ui/TabLabel'; import useRSFormCache from '@/hooks/useRSFormCache'; import { HelpTopic } from '@/models/miscellaneous'; -import { ICstSubstitute, IOperation, IOperationSchema, OperationID, OperationType } from '@/models/oss'; +import { + ICstSubstitute, + IOperation, + IOperationSchema, + IOperationUpdateData, + OperationID, + OperationType +} from '@/models/oss'; import { PARAMETER } from '@/utils/constants'; import TabArguments from './TabArguments'; @@ -21,8 +28,7 @@ interface DlgEditOperationProps { hideWindow: () => void; oss: IOperationSchema; target: IOperation; - // onSubmit: (data: IOperationEditData) => void; - onSubmit: () => void; + onSubmit: (data: IOperationUpdateData) => void; } export enum TabID { @@ -56,22 +62,19 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio }, [schemasIDs]); const handleSubmit = () => { - // const data: IOperationCreateData = { - // item_data: { - // position_x: insertPosition.x, - // position_y: insertPosition.y, - // alias: alias, - // title: title, - // comment: comment, - // sync_text: activeTab === TabID.INPUT ? syncText : true, - // operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS, - // result: activeTab === TabID.INPUT ? attachedID ?? null : null - // }, - // positions: positions, - // arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined, - // create_schema: createSchema - // }; - onSubmit(); + const data: IOperationUpdateData = { + target: target.id, + item_data: { + alias: alias, + title: title, + comment: comment, + sync_text: syncText + }, + positions: [], + arguments: target.operation_type !== OperationType.SYNTHESIS ? undefined : inputs, + substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions + }; + onSubmit(data); }; const cardPanel = useMemo( diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index 1fe00643..a6e2c6ed 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -69,6 +69,15 @@ export interface IOperationCreateData extends IPositionsData { create_schema: boolean; } +/** + * Represents {@link IOperation} data, used in update process. + */ +export interface IOperationUpdateData extends ITargetOperation { + item_data: Pick; + arguments: OperationID[] | undefined; + substitutions: ICstSubstitute[] | undefined; +} + /** * Represents {@link IOperation} data, used in setInput process. */ diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx index 0209ad27..bf4a83b7 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx @@ -1,7 +1,6 @@ 'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { toast } from 'react-toastify'; import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons'; import Dropdown from '@/components/ui/Dropdown'; @@ -25,6 +24,7 @@ interface NodeContextMenuProps extends ContextMenuData { onCreateInput: (target: OperationID) => void; onEditSchema: (target: OperationID) => void; onEditOperation: (target: OperationID) => void; + onRunOperation: (target: OperationID) => void; } function NodeContextMenu({ @@ -35,7 +35,8 @@ function NodeContextMenu({ onDelete, onCreateInput, onEditSchema, - onEditOperation + onEditOperation, + onRunOperation }: NodeContextMenuProps) { const controller = useOssEdit(); const [isOpen, setIsOpen] = useState(false); @@ -95,8 +96,8 @@ function NodeContextMenu({ }; const handleRunSynthesis = () => { - toast.error('Not implemented'); handleHide(); + onRunOperation(operation.id); }; return ( diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index 5b19ace8..14972767 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -162,6 +162,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { [controller, getPositions] ); + const handleRunOperation = useCallback( + (target: OperationID) => { + controller.runOperation(target, getPositions()); + }, + [controller, getPositions] + ); + const handleFitView = useCallback(() => { flow.fitView({ duration: PARAMETER.zoomDuration }); }, [flow]); @@ -227,6 +234,17 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { handleContextMenuHide(); }, [handleContextMenuHide]); + const handleNodeClick = useCallback( + (event: CProps.EventMouse, node: OssNode) => { + if (event.ctrlKey || event.metaKey) { + event.preventDefault(); + event.stopPropagation(); + handleEditOperation(Number(node.id)); + } + }, + [handleEditOperation] + ); + function handleKeyDown(event: React.KeyboardEvent) { if (controller.isProcessing) { return; @@ -266,6 +284,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { edges={edges} onNodesChange={handleNodesChange} onEdgesChange={onEdgesChange} + onNodeClick={handleNodeClick} fitView proOptions={{ hideAttribution: true }} nodeTypes={OssNodeTypes} @@ -309,6 +328,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { onCreateInput={handleCreateInput} onEditSchema={handleEditSchema} onEditOperation={handleEditOperation} + onRunOperation={handleRunOperation} {...menuProps} /> ) : null} diff --git a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx index 08800517..93d2a9e6 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx @@ -22,6 +22,7 @@ import { IOperationPosition, IOperationSchema, IOperationSetInputData, + IOperationUpdateData, OperationID } from '@/models/oss'; import { UserID, UserLevel } from '@/models/user'; @@ -55,6 +56,7 @@ export interface IOssEditContext { createInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void; + runOperation: (target: OperationID, positions: IOperationPosition[]) => void; } const OssEditContext = createContext(null); @@ -228,17 +230,13 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr setShowEditOperation(true); }, []); - const handleEditOperation = useCallback(() => { - // TODO: проверить наличие всех аргументов - // TODO: проверить наличие синтеза - // TODO: проверить полноту синтеза - // TODO: проверить правильность синтеза - // TODO: сохранить позиции - // TODO: обновить схему - // model.setInput(data, () => toast.success(information.changesSaved)); - - toast.error('Not implemented'); - }, []); + const handleEditOperation = useCallback( + (data: IOperationUpdateData) => { + data.positions = positions; + model.updateOperation(data, () => toast.success(information.changesSaved)); + }, + [model, positions] + ); const deleteOperation = useCallback( (target: OperationID, positions: IOperationPosition[]) => { @@ -281,6 +279,19 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr [model, targetOperationID, positions] ); + const runOperation = useCallback( + (target: OperationID, positions: IOperationPosition[]) => { + model.executeOperation( + { + target: target, + positions: positions + }, + () => toast.success(information.changesSaved) + ); + }, + [model] + ); + return ( {model.schema ? (