From 73bbde3acbd85a7990edc35ea0da249117f717ff Mon Sep 17 00:00:00 2001 From: khadanovichba <1415926535w@gmail.com> Date: Fri, 21 Jun 2024 18:47:46 +0300 Subject: [PATCH] synthesis --- ...ph_operationnode_synthesisedge_and_more.py | 69 ++++ .../backend/apps/rsform/models/LibraryItem.py | 4 - .../backend/apps/rsform/models/Synthesis.py | 141 ++++--- .../apps/rsform/serializers/synthesis.py | 118 +++++- rsconcept/backend/apps/rsform/urls.py | 6 +- .../backend/apps/rsform/views/__init__.py | 4 +- .../backend/apps/rsform/views/synthesis.py | 151 ++++++-- rsconcept/frontend/.vscode/launch.json | 15 + rsconcept/frontend/src/app/backendAPI.ts | 70 ++-- .../src/components/ui/Synthesis/InputNode.tsx | 75 +++- .../components/ui/Synthesis/OperationNode.tsx | 117 ++++-- .../components/ui/Synthesis/SynthesisFlow.css | 9 +- .../components/ui/Synthesis/SynthesisFlow.tsx | 99 ++--- .../DlgInlineSynthesis/SubstitutionsTab.tsx | 3 +- .../DlgOssGraph/DlgSelectInputScheme.tsx | 93 +++++ .../src/dialogs/DlgOssGraph/DlgSynthesis.tsx | 120 ++++++ .../frontend/src/dialogs/DlgSynthesis.tsx | 113 ------ rsconcept/frontend/src/hooks/useOssDetails.ts | 25 +- rsconcept/frontend/src/models/OssLoader.ts | 3 +- rsconcept/frontend/src/models/oss.ts | 51 ++- rsconcept/frontend/src/models/synthesis.ts | 9 - .../OssPage/EditorOssGraph/EditorOssGraph.tsx | 11 +- .../src/pages/OssPage/SynthesisContext.tsx | 345 ++++++++++++++++++ .../SynthesisOperation.tsx | 0 .../OssPage/SynthesisSubstitutionsTab.tsx | 40 ++ .../src/pages/OssPage/SynthesisToolbar.tsx | 45 +++ .../pages/SynthesisPage/SynthesisContext.tsx | 62 ---- .../src/pages/SynthesisPage/SynthesisPage.tsx | 16 +- .../SynthesisSubstitutionsTab.tsx | 42 --- .../pages/SynthesisPage/SynthesisToolbar.tsx | 39 -- 30 files changed, 1358 insertions(+), 537 deletions(-) create mode 100644 rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py create mode 100644 rsconcept/frontend/.vscode/launch.json create mode 100644 rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx create mode 100644 rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx delete mode 100644 rsconcept/frontend/src/dialogs/DlgSynthesis.tsx delete mode 100644 rsconcept/frontend/src/models/synthesis.ts create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx rename rsconcept/frontend/src/pages/{SynthesisPage => OssPage}/SynthesisOperation.tsx (100%) create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx delete mode 100644 rsconcept/frontend/src/pages/SynthesisPage/SynthesisContext.tsx delete mode 100644 rsconcept/frontend/src/pages/SynthesisPage/SynthesisSubstitutionsTab.tsx delete mode 100644 rsconcept/frontend/src/pages/SynthesisPage/SynthesisToolbar.tsx diff --git a/rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py b/rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py new file mode 100644 index 00000000..c2c381d4 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0.5 on 2024-06-21 15:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rsform', '0007_location_and_flags'), + ] + + operations = [ + migrations.CreateModel( + name='InputNode', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vertical_coordinate', models.IntegerField(verbose_name='Вертикальная координата звена')), + ('horizontal_coordinate', models.IntegerField(verbose_name='Горизонтальная координата звена')), + ('rsform_id', models.IntegerField(null=True, verbose_name='Схема')), + ], + ), + migrations.CreateModel( + name='SynthesisGraph', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('Draft', 'Draft'), ('Completed', 'Completed'), ('Warning', 'Warning'), ('Failed', 'Failed')], max_length=20, verbose_name='Статус операции слияния')), + ], + ), + migrations.CreateModel( + name='OperationNode', + fields=[ + ('inputnode_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='rsform.inputnode')), + ('name', models.CharField(max_length=20, verbose_name='Название')), + ('status', models.CharField(choices=[('Draft', 'Draft'), ('Completed', 'Completed'), ('Warning', 'Warning'), ('Failed', 'Failed')], max_length=20, verbose_name='Статус операции слияния')), + ('left_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rsform_library_item_left', to='rsform.libraryitem', verbose_name='Левый предок')), + ('right_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rsform_library_item_right', to='rsform.libraryitem', verbose_name='Правый предок')), + ], + bases=('rsform.inputnode',), + ), + migrations.CreateModel( + name='SynthesisEdge', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('decoded_id', models.CharField(max_length=30, verbose_name='Id ребра на фронте')), + ('source_handle', models.CharField(max_length=30, verbose_name='')), + ('node_from', models.IntegerField(verbose_name='Звено-предок')), + ('node_to', models.IntegerField(verbose_name='Звено-наследник')), + ('graph_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.synthesisgraph', verbose_name='Схема синтеза')), + ], + ), + migrations.AddField( + model_name='inputnode', + name='graph_id', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.synthesisgraph', verbose_name='Схема синтеза'), + ), + migrations.CreateModel( + name='SynthesisSubstitution', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('deleteRight', models.BooleanField(verbose_name='Удалить правую')), + ('takeLeftTerm', models.BooleanField(verbose_name='Использовать термин левой')), + ('graph_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.synthesisgraph', verbose_name='Схема синтеза')), + ('leftCst', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='constituenta_original', to='rsform.constituenta', verbose_name='Конституента')), + ('rightCst', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='constituenta_substitution', to='rsform.constituenta', verbose_name='Подстановка')), + ('operation_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.operationnode', verbose_name='Операция синтеза')), + ], + ), + ] diff --git a/rsconcept/backend/apps/rsform/models/LibraryItem.py b/rsconcept/backend/apps/rsform/models/LibraryItem.py index f42b1b2c..56813796 100644 --- a/rsconcept/backend/apps/rsform/models/LibraryItem.py +++ b/rsconcept/backend/apps/rsform/models/LibraryItem.py @@ -102,10 +102,6 @@ class LibraryItem(Model): auto_now=True ) - #is_hidden: BooleanField = BooleanField( - # default=False - #) - class Meta: ''' Model metadata. ''' verbose_name = 'Схема' diff --git a/rsconcept/backend/apps/rsform/models/Synthesis.py b/rsconcept/backend/apps/rsform/models/Synthesis.py index 8eebe041..02cfa6d6 100644 --- a/rsconcept/backend/apps/rsform/models/Synthesis.py +++ b/rsconcept/backend/apps/rsform/models/Synthesis.py @@ -1,90 +1,139 @@ from django.db.models import ( CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet, - TextChoices, TextField, BooleanField, CharField, DateTimeField, JSONField, IntegerField + TextChoices, TextField, BooleanField, CharField, DateTimeField, JSONField, IntegerField, AutoField ) -from rsconcept.backend.apps.rsform.models.api_RSForm import RSForm, LibraryItem, LibraryItemType -import rsconcept.backend.apps.rsform.messages as messages - - -class GraphStatus(TextChoices): - DRAFT = 'Draft', - IN_PROGRESS = 'In progress', - COMPLETED = 'Completed', - FAILED = 'Failed' - class OperationStatus(TextChoices): DRAFT = 'Draft', - # IN_PROGRESS = 'In progress', COMPLETED = 'Completed', WARNING = 'Warning', FAILED = 'Failed' -class SynthesisNodeType: - LIBRARY = 'Library', - SYNTHESIZED = 'Synthesized' +class GraphStatus(TextChoices): + DRAFT = 'Draft', + COMPLETED = 'Completed', + WARNING = 'Warning', + FAILED = 'Failed' -class ConceptOperation(Model): +class SynthesisGraph(Model): + status: CharField = CharField( + verbose_name='Статус операции слияния', + max_length=20, + choices=GraphStatus + ) + + +class InputNode(Model): + graph_id: ForeignKey = ForeignKey( + verbose_name='Схема синтеза', + to=SynthesisGraph, + on_delete=CASCADE + ) + + vertical_coordinate: IntegerField = IntegerField( + verbose_name='Вертикальная координата звена', + ) + + horizontal_coordinate: IntegerField = IntegerField( + verbose_name='Горизонтальная координата звена', + ) + + rsform_id: IntegerField = IntegerField( + verbose_name='Схема', + null=True + ) + + +class OperationNode(InputNode): name: CharField = CharField( verbose_name='Название', max_length=20 ) - node_type: CharField = CharField( - verbose_name='Тип звена операции слияния', - max_length=20, - choices=SynthesisNodeType - ) - - status: CharField( + status: CharField = CharField( verbose_name='Статус операции слияния', max_length=20, choices=OperationStatus ) - vertical_coordinate = IntegerField( - verbose_name='Вертикальная координата звена', + left_parent: ForeignKey = ForeignKey( + verbose_name='Левый предок', + to='rsform.LibraryItem', + related_name='rsform_library_item_left', + on_delete=SET_NULL, + null=True ) - horizontal_coordinate = IntegerField( - verbose_name='Горизонтальная координата звена', + right_parent: ForeignKey = ForeignKey( + verbose_name='Правый предок', + to='rsform.LibraryItem', + related_name='rsform_library_item_right', + on_delete=SET_NULL, + null=True ) - rsform = ForeignKey( - verbose_name='Схема', - to='rsform.LibraryItem' + +class SynthesisSubstitution(Model): + graph_id: ForeignKey = ForeignKey( + verbose_name='Схема синтеза', + to=SynthesisGraph, + on_delete=CASCADE ) - operation_type = CharField() - - -class SynthesisGraph(Model): - name: CharField = CharField( - verbose_name='Название', - max_length=20 + operation_id: ForeignKey = ForeignKey( + verbose_name='Операция синтеза', + to=OperationNode, + on_delete=CASCADE ) - status: CharField = CharField( - verbose_name='Статус схемы слияния', - max_length=20, - choices=GraphStatus, + + leftCst: ForeignKey = ForeignKey( + verbose_name='Конституента', + to='Constituenta', + related_name='constituenta_original', + on_delete=SET_NULL, + null=True + ) + + rightCst: ForeignKey = ForeignKey( + verbose_name='Подстановка', + to='Constituenta', + related_name='constituenta_substitution', + on_delete=SET_NULL, + null=True + ) + + deleteRight: BooleanField = BooleanField( + verbose_name='Удалить правую' + ) + + takeLeftTerm: BooleanField = BooleanField( + verbose_name='Использовать термин левой' ) class SynthesisEdge(Model): - synthesis_graph: ForeignKey( + decoded_id: CharField = CharField( + verbose_name='Id ребра на фронте', + max_length=30, + ) + + source_handle: CharField = CharField( + verbose_name='', + max_length=30, + ) + graph_id: ForeignKey = ForeignKey( verbose_name='Схема синтеза', to=SynthesisGraph, + on_delete=CASCADE ) - node_from: ForeignKey( + node_from: IntegerField = IntegerField( verbose_name='Звено-предок', - to='rsform.LibraryItem' ) - node_to: ForeignKey( + node_to: IntegerField = IntegerField( verbose_name='Звено-наследник', - to='rsform.LibraryItem' ) diff --git a/rsconcept/backend/apps/rsform/serializers/synthesis.py b/rsconcept/backend/apps/rsform/serializers/synthesis.py index d6c99639..83040da6 100644 --- a/rsconcept/backend/apps/rsform/serializers/synthesis.py +++ b/rsconcept/backend/apps/rsform/serializers/synthesis.py @@ -1,18 +1,112 @@ from rest_framework import serializers -from .data_access import InlineSynthesisSerializer +from .data_access import CstSubstituteSerializerBase +from rest_framework.serializers import PrimaryKeyRelatedField as PKField -class SynthesisEdgeSerializer(serializers.Serializer): - schema_from = serializers.IntegerField() - schema_to = serializers.IntegerField() +from ..models import Constituenta, LibraryItem +from ..models.Synthesis import SynthesisGraph, SynthesisEdge, InputNode, OperationNode, SynthesisSubstitution -class SynthesisNodeSerializer(serializers.Serializer): - pk = serializers.IntegerField() - vertical_coordinate = serializers.IntegerField() - horizontal_coordinate = serializers.IntegerField() +class SynthesisGraphSerializer(serializers.ModelSerializer): + class Meta: + model = SynthesisGraph + fields = '__all__' + + def create(self, validated_data): + graph, created = SynthesisGraph.objects.update_or_create( + id=validated_data['id'], + defaults={'status': validated_data['status']} + ) + return graph -class SynthesisGraphSerializer(serializers.Serializer): - pk = serializers.IntegerField() - user = serializers.CharField() - edges_list = serializers.ListField(child=SynthesisEdgeSerializer()) +class InputNodeSerializer(serializers.ModelSerializer): + class Meta: + model = InputNode + fields = '__all__' + + def create(self, validated_data_list): + for validated_data in validated_data_list: + input_node, created = InputNode.objects.update_or_create( + id=validated_data['id'] if validated_data.get('id') else None, + defaults={ + 'graph_id': validated_data['graph_id'], + 'vertical_coordinate': validated_data['vertical_coordinate'], + 'horizontal_coordinate': validated_data['horizontal_coordinate'], + 'rsform_id': validated_data['rsform_id'], + } + ) + return + + +class OperationNodeSerializer(serializers.ModelSerializer): + class Meta: + model = OperationNode + fields = '__all__' + + def create(self, validated_data_list): + operations = [] + for validated_data in validated_data_list: + operation_node, created = OperationNode.objects.update_or_create( + id=validated_data['id'], + defaults={ + 'graph_id': validated_data['graph_id'], + 'vertical_coordinate': validated_data['vertical_coordinate'], + 'horizontal_coordinate': validated_data['horizontal_coordinate'], + 'rsform_id': validated_data['rsform_id'], + 'left_parent': validated_data.get('left_parent'), + 'right_parent': validated_data.get('right_parent'), + } + ) + operations.append(operation_node) + return operations + + +class SynthesisSubstitutionSerializer(serializers.ModelSerializer): + class Meta: + model = SynthesisSubstitution + fields = '__all__' + + def create(self, validated_data_list): + substitutions = [] + for validated_data in validated_data_list: + substitution, created = SynthesisSubstitution.objects.update_or_create( + id=validated_data['id'], + defaults={ + 'operation_id': validated_data['operation_id'], + 'graph_id': validated_data['graph_id'], + 'leftCst': validated_data['leftCst'], + 'rightCst': validated_data['rightCst'], + 'deleteRight': validated_data['deleteRight'], + 'takeLeftTerm': validated_data['takeLeftTerm'], + } + ) + substitutions.append(substitution) + return substitutions + + +class SynthesisEdgeSerializer(serializers.ModelSerializer): + class Meta: + model = SynthesisEdge + fields = '__all__' + + def create(self, validated_data_list): + for validated_data in validated_data_list: + substitution, created = SynthesisEdge.objects.update_or_create( + id=validated_data['id'], + defaults={ + 'graph_id': validated_data['graph_id'], + 'decoded_id': validated_data['decoded_id'], + 'source_handle': validated_data['source_handle'], + 'node_from': validated_data['node_from'], + 'node_to': validated_data['node_to'], + } + ) + return + + +class RunSingleSynthesis(serializers.Serializer): + operationId = serializers.IntegerField() + + +class RunSingleSynthesisResponse(serializers.Serializer): + rsformId = serializers.IntegerField() diff --git a/rsconcept/backend/apps/rsform/urls.py b/rsconcept/backend/apps/rsform/urls.py index d6ebda25..8d2743d1 100644 --- a/rsconcept/backend/apps/rsform/urls.py +++ b/rsconcept/backend/apps/rsform/urls.py @@ -30,7 +30,9 @@ urlpatterns = [ path('cctext/inflect', views.inflect), path('cctext/generate-lexeme', views.generate_lexeme), path('cctext/parse', views.parse_text), - path('synthesis/single', views.run_synthesis_view), - path('synthesis/graph', views.run_sythesis_graph_view), + path('synthesis/run_single', views.run_synthesis_view), + path('synthesis/run_all', views.run_sythesis_graph_view), + path('synthesis/save', views.save_synthesis_graph), + path('synthesis/', views.get_synthesis_graph), path('', include(library_router.urls)), ] diff --git a/rsconcept/backend/apps/rsform/views/__init__.py b/rsconcept/backend/apps/rsform/views/__init__.py index bd23a57b..5e00018f 100644 --- a/rsconcept/backend/apps/rsform/views/__init__.py +++ b/rsconcept/backend/apps/rsform/views/__init__.py @@ -9,5 +9,7 @@ from .versions import VersionViewset, create_version, export_file, retrieve_vers from .synthesis import ( run_synthesis_view, - run_sythesis_graph_view + run_sythesis_graph_view, + save_synthesis_graph, + get_synthesis_graph, ) diff --git a/rsconcept/backend/apps/rsform/views/synthesis.py b/rsconcept/backend/apps/rsform/views/synthesis.py index 73a79e38..441eda07 100644 --- a/rsconcept/backend/apps/rsform/views/synthesis.py +++ b/rsconcept/backend/apps/rsform/views/synthesis.py @@ -5,72 +5,158 @@ from rest_framework import status from rest_framework.decorators import api_view from rest_framework.request import Request from rest_framework.response import Response +from django.db.models import Q +from rest_framework.views import APIView from ..models.Constituenta import Constituenta from ..models.LibraryItem import LibraryItem from ..models.api_RSForm import RSForm +from ..models.Synthesis import SynthesisGraph, InputNode, OperationNode, SynthesisSubstitution, SynthesisEdge from ..serializers import RSFormSerializer, SynthesisGraphSerializer, InlineSynthesisSerializer from typing import cast from django.contrib.auth.models import User +from ..serializers.data_access import CstBaseSerializer, CstSerializer +from ..serializers.synthesis import OperationNodeSerializer, InputNodeSerializer, \ + SynthesisSubstitutionSerializer, SynthesisEdgeSerializer, RunSingleSynthesis, RunSingleSynthesisResponse from ..utils import clone_rsform @extend_schema( - summary='Run synthesis operation', + summary='Get synthesis graph', tags=['Synthesis'], - request=InlineSynthesisSerializer, + auth=None +) +@api_view(['GET']) +def get_synthesis_graph(request: Request, pk_item: int): + input_nodes = InputNode.objects.filter(graph_id=pk_item) + operation_nodes = OperationNode.objects.filter(graph_id=pk_item) + edges = SynthesisEdge.objects.filter(graph_id=pk_item) + substitutions = [] + for operation_node in operation_nodes: + substitution_batch = SynthesisSubstitution.objects.filter(operation_id=operation_node.id) + for substitution in substitution_batch: + substitutions.append(substitution) + + synthesis_graph = SynthesisGraphSerializer() + synthesis_graph.create(validated_data={'id': pk_item, 'status': 'Draft'}) + input_nodes = InputNodeSerializer(instance=input_nodes, many=True) + operation_nodes = (OperationNodeSerializer(instance=operation_nodes, many=True)) + edges = SynthesisEdgeSerializer(instance=edges, many=True) + substitutions = SynthesisSubstitutionSerializer(instance=substitutions, many=True) + for substitution in substitutions.data: + substitution['leftCst'] = CstSerializer(instance=Constituenta.objects.get(id=substitution['leftCst'])).data + substitution['rightCst'] = CstSerializer(instance=Constituenta.objects.get(id=substitution['rightCst'])).data + return Response(data={ + 'graph': synthesis_graph.data, + 'input_nodes': input_nodes.data, + 'operation_nodes': operation_nodes.data, + 'edges': edges.data, + 'substitutions': substitutions.data, + }) + + +@extend_schema( + summary='Save synthesis graph', + tags=['Synthesis'], + request=SynthesisGraphSerializer, responses={status.HTTP_200_OK: RSFormSerializer}, auth=None ) @api_view(['POST']) -def run_synthesis_view(request: Request): - serializer = InlineSynthesisSerializer( - data=request.data, - context={'user': request.user} - ) - serializer.is_valid(raise_exception=True) - return run_synthesis(serializer=serializer) +def save_synthesis_graph(request: Request): + graph_data = request.data.get('graph') + input_nodes_data = request.data.get('input_nodes') + operation_nodes_data = request.data.get('operation_nodes') + edges_data = request.data.get('edges') + substitutions_data = request.data.get('substitutions') + + synthesis_graph_serializer = SynthesisGraphSerializer() + graph = synthesis_graph_serializer.create(validated_data=graph_data) + + InputNode.objects.filter(graph_id=graph).delete() + OperationNode.objects.filter(graph_id=graph).delete() + SynthesisEdge.objects.filter(graph_id=graph).delete() + SynthesisSubstitution.objects.filter(graph_id=graph).delete() + + input_node_serializer = InputNodeSerializer() + for input_node in input_nodes_data: + input_node['graph_id'] = graph + input_node_serializer.create(validated_data_list=input_nodes_data) + + for operation_node in operation_nodes_data: + operation_node['graph_id'] = graph + operation_node['left_parent'] = LibraryItem.objects.get(id=operation_node['left_parent']) + operation_node['right_parent'] = LibraryItem.objects.get(id=operation_node['right_parent']) + operation_node_serializer = OperationNodeSerializer() + operations = operation_node_serializer.create(validated_data_list=operation_nodes_data) + + for edge in edges_data: + edge['graph_id'] = graph + + edge_serializer = SynthesisEdgeSerializer() + edge_serializer.create(validated_data_list=edges_data) + + operations_dict = {operation.id: operation for operation in operations} + for substitution_data in substitutions_data: + substitution_data['operation_id'] = operations_dict[substitution_data['operation_id']] + substitution_data['rightCst'] = Constituenta.objects.get(id=substitution_data['rightCst']['id']) + substitution_data['leftCst'] = Constituenta.objects.get(id=substitution_data['leftCst']['id']) + substitution_data['graph_id'] = graph + + substitution_serializer = SynthesisSubstitutionSerializer() + substitutions = substitution_serializer.create(validated_data_list=substitutions_data) + + return Response(synthesis_graph_serializer.data, status=status.HTTP_201_CREATED) @extend_schema( summary='Run synthesis graph', tags=['Synthesis'], - request=InlineSynthesisSerializer, - responses={status.HTTP_200_OK: RSFormSerializer}, + request=RunSingleSynthesis, + responses={status.HTTP_200_OK: RunSingleSynthesisResponse}, auth=None ) @api_view(['POST']) def run_sythesis_graph_view(request: Request): - serializer = SynthesisGraphSerializer(data=request.data) + serializer = RunSingleSynthesis(data=request.data) serializer.is_valid(raise_exception=True) for atomic_synthesis in serializer.validated_data: run_synthesis(atomic_synthesis) -def run_synthesis(serializer: InlineSynthesisSerializer): - left_schema = RSForm(serializer.validated_data['source']) - right_schema = RSForm(serializer.validated_data['receiver']) - constituents = cast(list[Constituenta], serializer.validated_data['items']) +@extend_schema( + summary='Run synthesis operation', + tags=['Synthesis'], + request=RunSingleSynthesis, + responses={status.HTTP_200_OK: RunSingleSynthesisResponse}, + auth=None +) +@api_view(['POST']) +def run_synthesis_view(request: Request): + serializer = RunSingleSynthesis( + data=request.data + ) + serializer.is_valid(raise_exception=True) + return run_synthesis(serializer=serializer) + + +def run_synthesis(serializer: RunSingleSynthesis): + operation_id = serializer.data['operationId'] + operation = OperationNode.objects.get(id=operation_id) + + left_schema = RSForm(operation.left_parent) + right_schema = RSForm(operation.right_parent) + substitutions = SynthesisSubstitution.objects.filter(operation_id=operation_id) left_schema_copy = clone_rsform(left_schema) - copied_constituents = left_schema_copy.item.constituenta_set - used_constiuents = set() + right_constituents = right_schema.item.constituenta_set.filter() + left_schema_copy.insert_copy(right_constituents) - for substitution in serializer.validated_data['substitutions']: - original = cast(Constituenta, substitution['original']) - replacement = cast(Constituenta, substitution['substitution']) - if original in constituents: - index = next(i for (i, cst) in enumerate(constituents) if cst == original) - original = copied_constituents[index] - else: - index = next(i for (i, cst) in enumerate(constituents) if cst == replacement) - replacement = copied_constituents[index] - left_schema_copy.substitute(original, replacement, substitution['transfer_term']) - if substitution['transfer_term']: - used_constiuents.add(replacement.pk) - unused_constitunents = {cst for cst in right_schema.item.constituenta_set() if cst.pk not in used_constiuents} - left_schema_copy.insert_copy(list(unused_constitunents)) + for substitution in substitutions: + original = cast(Constituenta, substitution.leftCst) + replacement = cast(Constituenta, substitution.rightCst) + left_schema_copy.substitute(original, replacement, (not substitution.deleteRight) and substitution.takeLeftTerm) left_schema.restore_order() return Response( @@ -78,7 +164,6 @@ def run_synthesis(serializer: InlineSynthesisSerializer): data=RSFormSerializer(left_schema_copy.item).data ) - return right_rsform_copy = clone_rsform(right_schema) serializer.is_valid(raise_exception=True) diff --git a/rsconcept/frontend/.vscode/launch.json b/rsconcept/frontend/.vscode/launch.json new file mode 100644 index 00000000..2ba986f6 --- /dev/null +++ b/rsconcept/frontend/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:8080", + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/rsconcept/frontend/src/app/backendAPI.ts b/rsconcept/frontend/src/app/backendAPI.ts index 017a1d62..a71a65ed 100644 --- a/rsconcept/frontend/src/app/backendAPI.ts +++ b/rsconcept/frontend/src/app/backendAPI.ts @@ -17,7 +17,7 @@ import { LibraryItemType } from '@/models/library'; import { ILibraryCreateData } from '@/models/library'; -import { IOperationSchemaData } from '@/models/oss'; +import { IOperationSchemaData, IRunSynthesis, IRunSynthesisResponse } from '@/models/oss'; import { IConstituentaList, IConstituentaMeta, @@ -51,7 +51,7 @@ import { IUserUpdatePassword } from '@/models/user'; import { buildConstants } from '@/utils/buildConstants'; -import {ISynthesisData} from "@/models/synthesis.ts"; +import { ISynthesisGraphData } from '@/models/oss.ts'; const defaultOptions = { xsrfCookieName: 'csrftoken', @@ -86,6 +86,7 @@ interface IFrontRequest { export interface FrontPush extends IFrontRequest { data: DataType; } + export interface FrontPull extends IFrontRequest { onSuccess: DataCallback; } @@ -95,7 +96,8 @@ export interface FrontExchange extends IFrontRequest< onSuccess: DataCallback; } -export interface FrontAction extends IFrontRequest {} +export interface FrontAction extends IFrontRequest { +} interface IAxiosRequest { endpoint: string; @@ -235,26 +237,10 @@ export function postCloneLibraryItem(target: string, request: FrontExchange) { - request.onSuccess({ - id: Number(target), - comment: '123', - alias: 'oss1', - access_policy: AccessPolicy.PUBLIC, - editors: [], - owner: 1, - item_type: LibraryItemType.OSS, - location: '/U', - read_only: false, - subscribers: [], - time_create: '0', - time_update: '0', - title: 'TestOss', - visible: false + AxiosGet({ + endpoint: `/api/synthesis/${target}`, + request: request }); - // AxiosGet({ - // endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS - // request: request - // }); } export function getRSFormDetails(target: string, version: string, request: FrontPull) { @@ -438,16 +424,24 @@ export function patchUploadTRS(target: string, request: FrontExchange) { + +export function patchInlineSynthesis(request: FrontExchange) { AxiosPatch({ endpoint: `/api/operations/inline-synthesis`, request: request }); } -export function postSynthesis(request: FrontExchange){ - AxiosPatch({ - endpoint: `/api/synthesis/single`, +export function runSingleSynthesis(request: FrontExchange) { + AxiosPost({ + endpoint: `/api/synthesis/run_single`, + request: request + }); +} + +export function postSynthesisGraph(request: FrontExchange) { + AxiosPost({ + endpoint: `/api/synthesis/save`, request: request }); } @@ -528,10 +522,10 @@ function AxiosGet({ endpoint, request, options }: IAxiosRequest({ - endpoint, - request, - options -}: IAxiosRequest) { + endpoint, + request, + options + }: IAxiosRequest) { if (request.setLoading) request.setLoading(true); axiosInstance .post(endpoint, request.data, options) @@ -547,10 +541,10 @@ function AxiosPost({ } function AxiosDelete({ - endpoint, - request, - options -}: IAxiosRequest) { + endpoint, + request, + options + }: IAxiosRequest) { if (request.setLoading) request.setLoading(true); axiosInstance .delete(endpoint, options) @@ -566,10 +560,10 @@ function AxiosDelete({ } function AxiosPatch({ - endpoint, - request, - options -}: IAxiosRequest) { + endpoint, + request, + options + }: IAxiosRequest) { if (request.setLoading) request.setLoading(true); axiosInstance .patch(endpoint, request.data, options) diff --git a/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx b/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx index 00aca772..a59d8f3c 100644 --- a/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx +++ b/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx @@ -1,24 +1,61 @@ -import {memo, type FC} from 'react'; -import {Handle, Position, type NodeProps} from '@reactflow/core'; +import { memo, type FC } from 'react'; +import { Handle, Position, type NodeProps } from '@reactflow/core'; +import Button from '@/components/ui/Button.tsx'; +import { PiPlugsConnected } from 'react-icons/pi'; +import { CiSquareRemove } from 'react-icons/ci'; +import MiniButton from '@/components/ui/MiniButton.tsx'; +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; -const InputNode: FC = ({data, xPos, yPos}) => { - return ( - <> - -
-
- Тип: {data.label} -
-
- Схема из библиотеки:{' '} - - RSForm - -
-
+interface InputNodeProps { + id: string; + data: { + label: string; + onDelete: (nodeId: string) => void; + }; + bound_rsform_id: number; +} - - ); + +const InputNode: FC = ({ id, data,bound_rsform_id }) => { + const controller = useSynthesis(); + const { label, onDelete } = data; + + const handleDelete = () => { + onDelete(id); + }; + + const handleClick = () =>{ + controller.selectNode(id); + controller.showSelectInput(); + } + + return ( + <> + +
+ } + title="Удалить" + onClick={handleDelete} + color={'red'} + /> +
+ Тип: Ввод +
+
+ Схема:{controller.getBind(id) === undefined? '': controller.getBind(id)} + + } + title="Привязать схему" + onClick={() => {handleClick()}} + /> + +
+
+ + + ); }; export default memo(InputNode); diff --git a/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx b/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx index 38c249c5..9e1d5564 100644 --- a/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx +++ b/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx @@ -1,42 +1,91 @@ -import {memo, type FC, type CSSProperties} from 'react'; -import {Handle, Position, type NodeProps} from '@reactflow/core'; +import { memo, type FC, type CSSProperties } from 'react'; +import { Handle, Position, type NodeProps } from '@reactflow/core'; +import MiniButton from '@/components/ui/MiniButton.tsx'; +import { IoGitNetworkSharp } from 'react-icons/io5'; +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; +import { CiSquareRemove } from 'react-icons/ci'; +import { VscDebugStart } from "react-icons/vsc"; -const sourceHandleStyleA: CSSProperties = {left: 50}; +const sourceHandleStyleA: CSSProperties = { left: 50 }; const sourceHandleStyleB: CSSProperties = { - right: 50, - left: 'auto', + right: 50, + left: 'auto' }; -const OperationNode: FC = ({data, xPos, yPos}) => { - return ( - <> - -
-
- Тип: {data.label} -
-
- Схема:{' '} - - {xPos.toFixed(2)},{yPos.toFixed(2)} - -
-
+interface OperationNodeProps { + id: string; + data: { + label: string; + onDelete: (nodeId: string) => void; + }; + xPos: number, + yPos: number, +} - - - - ); +const OperationNode: FC = ({ id, data, xPos, yPos }) => { + const controller = useSynthesis(); + const { label, onDelete } = data; + + const handleDelete = () => { + onDelete(id); + }; + + const handleSubstitution = () =>{ + controller.selectNode(id); + controller.showSynthesis(); + } + + const handleSynthesis = () => { + controller.singleSynthesis(id) + } + + return ( + <> + +
+ } + title="Удалить" + onClick={handleDelete} + color={'red'} + /> +
+ Тип: Отождествление +
+
+ Схема:{' '} + + + } + title="Синтез" + onClick={() => handleSynthesis()} + /> + } + title="Отождествления" + onClick={() => handleSubstitution()} + /> + +
+
+ + + + + ); }; export default memo(OperationNode); diff --git a/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css index 38f84a05..72fdcd19 100644 --- a/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css +++ b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css @@ -3,9 +3,16 @@ font-size: 12px; } +.react-flow__node-input { + border: 1px solid #555; + padding: 10px; + width: 150px; + border-radius: 5px; +} + .react-flow__node-custom { border: 1px solid #555; padding: 10px; - width: 200px; + width: 250px; border-radius: 5px; } diff --git a/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx index a62e8aec..03fba955 100644 --- a/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx +++ b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx @@ -1,14 +1,15 @@ -import {useCallback} from 'react'; +import { useCallback, useMemo } from 'react'; import { - ReactFlow, - addEdge, - useNodesState, - useEdgesState, - type Connection, - type Edge, - type Node, + ReactFlow, + addEdge, + useNodesState, + useEdgesState, + type Connection, + type Edge, + type Node, OnSelectionChangeParams } from '@reactflow/core'; + import OperationNode from './OperationNode'; import InputNode from './InputNode'; @@ -16,68 +17,38 @@ import InputNode from './InputNode'; import '@reactflow/core/dist/style.css'; import './SynthesisFlow.css'; -import DlgSynthesis from "@/dialogs/DlgSynthesis.tsx"; +import { useState } from 'react'; +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; +import { useConceptOptions } from '@/context/OptionsContext.tsx'; + const nodeTypes = { - custom: OperationNode, - input: InputNode, + custom: OperationNode, + input: InputNode }; -const initialNodes: Node[] = [ - { - id: '1', - type: 'input', - data: {label: 'Node 1'}, - position: {x: 250, y: 5}, - }, - { - type: 'input', - id: '2', - data: {label: 'Node 2'}, - position: {x: 100, y: 100}, - }, - { - id: '3', - data: {label: 'Node 3'}, - position: {x: 400, y: 100}, - type: 'custom', - - }, - { - id: '4', - data: {label: 'Node 4'}, - position: {x: 400, y: 200}, - type: 'custom', - }, -]; - -const initialEdges: Edge[] = [ - //{ id: 'e1-2', source: '1', target: '2', animated: true }, - //{ id: 'e1-3', source: '1', target: '3', animated: true }, -]; - function Flow() { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const onConnect = useCallback( - (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), - [setEdges] - ); + const controller = useSynthesis(); + const { calculateHeight, darkMode } = useConceptOptions(); + const canvasWidth = useMemo(() => { + return 'calc(100vw - 1rem)'; + }, []); - - return ( -
- -
- ); + const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); + return ( +
+ +
+ ); } export default Flow; diff --git a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx index 2225c448..568fb105 100644 --- a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx +++ b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx @@ -6,6 +6,7 @@ import { ConstituentaID, IRSForm, ISubstitution } from '@/models/rsform'; import { prefixes } from '@/utils/constants'; import PickSubstitutions from '../../components/select/PickSubstitutions'; +import { ISynthesisSubstitution } from '@/models/oss.ts'; interface SubstitutionsTabProps { receiver?: IRSForm; @@ -16,7 +17,7 @@ interface SubstitutionsTabProps { error?: ErrorData; substitutions: ISubstitution[]; - setSubstitutions: React.Dispatch>; + setSubstitutions: React.Dispatch>; } function SubstitutionsTab({ diff --git a/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx new file mode 100644 index 00000000..384a50d2 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx @@ -0,0 +1,93 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; +import { toast } from 'react-toastify'; + +import Checkbox from '@/components/ui/Checkbox.tsx'; +import FileInput from '@/components/ui/FileInput.tsx'; +import Modal, { ModalProps } from '@/components/ui/Modal.tsx'; +import { useRSForm } from '@/context/RSFormContext.tsx'; +import { IRSForm, IRSFormUploadData, ISubstitution } from '@/models/rsform.ts'; +import { EXTEOR_TRS_FILE } from '@/utils/constants.ts'; +import { TabList, TabPanel, Tabs } from 'react-tabs'; +import clsx from 'clsx'; +import TabLabel from '@/components/ui/TabLabel.tsx'; +import SubstitutionsTab from '@/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx'; +import useRSFormDetails from '@/hooks/useRSFormDetails.ts'; +import { LibraryItemID } from '@/models/library.ts'; +import { ISynthesisData } from '@/models/synthesis.ts'; +import { TabID } from '@/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx'; +import SynthesisSubstitutionsTab from '@/pages/OssPage/SynthesisSubstitutionsTab.tsx'; +import SchemaTab from '@/dialogs/DlgInlineSynthesis/SchemaTab.tsx'; +import { + Node +} from '@reactflow/core'; +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; + +interface DlgCreateSynthesisProps extends Pick { + nodeId: string; +} + +export enum SynthesisTabID { + SCHEMA = 0, + SUBSTITUTIONS = 1 +} + +function DlgSelectInputScheme({ nodeId, hideWindow }: DlgCreateSynthesisProps) { + const controller = useSynthesis(); + + const [activeTab, setActiveTab] = useState(SynthesisTabID.SCHEMA); + const [selected, setSelected] = useState([]); + const [substitutions, setSubstitutions] = useState([]); + const [donorID, setDonorID] = useState(undefined); + + + const schemaPanel = useMemo( + () => ( + + + + ), + [donorID] + ); + + function handleSubmit() { + if (donorID !== undefined) { + controller.updateBounds(nodeId, donorID); + } + } + + function validate() { + if (donorID === undefined) { + toast.error('Выберите источник конституент'); + return false; + } + return true; + } + + return ( + + + + + + {schemaPanel} + + + ); +} + +export default DlgSelectInputScheme; diff --git a/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx new file mode 100644 index 00000000..308298a3 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useMemo, useState } from 'react'; + +import Modal, { ModalProps } from '@/components/ui/Modal.tsx'; +import { TabList, TabPanel, Tabs } from 'react-tabs'; +import clsx from 'clsx'; +import TabLabel from '@/components/ui/TabLabel.tsx'; +import useRSFormDetails from '@/hooks/useRSFormDetails.ts'; +import SynthesisSubstitutionsTab from '@/pages/OssPage/SynthesisSubstitutionsTab.tsx'; +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; +import { ISynthesisSubstitution } from '@/models/oss.ts'; + +interface DlgCreateSynthesisProps extends Pick { + nodeId: string; + onSynthesis: (data: ISynthesisSubstitution[]) => void; +} + +export enum SynthesisTabID { + SCHEMA = 0, + SUBSTITUTIONS = 1 +} + +function DlgSynthesis({ hideWindow, nodeId, onSynthesis }: DlgCreateSynthesisProps) { + const controller = useSynthesis(); + + const [activeTab, setActiveTab] = useState(SynthesisTabID.SCHEMA); + const sourceLeft = useRSFormDetails({ + target: controller.getNodeParentsRsform(nodeId)[0] ? + String(controller.getNodeParentsRsform(nodeId)[0]) : undefined + }); + const sourceRight = useRSFormDetails({ + target: controller.getNodeParentsRsform(nodeId)[1] ? + String(controller.getNodeParentsRsform(nodeId)[1]) : undefined + }); + + + //const validated = useMemo(() => !!source.schema && selected.length > 0, [source.schema, selected]); + function handleSubmit() { + const parents = controller.getNodeParentsRsform(nodeId); + + if (parents.length != 2) { + return; + } + const data: ISynthesisSubstitution[] = controller.substitutions.map((item) => ({ + id: null, + operation_id: nodeId, + leftCst: item.leftCst, + rightCst: item.rightCst, + deleteRight: item.deleteRight, + takeLeftTerm: item.takeLeftTerm + })); + controller.setSubstitutions(data); + } + + function validated() { + const parents = controller.getNodeParentsRsform(nodeId); + return parents.length == 2; + } + + const schemaPanel = useMemo( + () => ( + + ), [] + ); + + const selectedSubstitutions = useMemo( + () => controller.getSubstitution(nodeId), + [controller, nodeId] + ); + + const setSelectedSubstitutions = (newElement: ISynthesisSubstitution[]) => { + controller.updateSubstitution(nodeId, newElement, controller.setSubstitutions); + }; + + + const substitutesPanel = useMemo( + () => ( + + + + ), + [sourceLeft.schema, sourceRight.schema, controller] + ); + + return ( + + + + + + + {schemaPanel} + {substitutesPanel} + + + + ); +} + +export default DlgSynthesis; diff --git a/rsconcept/frontend/src/dialogs/DlgSynthesis.tsx b/rsconcept/frontend/src/dialogs/DlgSynthesis.tsx deleted file mode 100644 index ee93f32c..00000000 --- a/rsconcept/frontend/src/dialogs/DlgSynthesis.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client'; - -import {useEffect, useMemo, useState} from 'react'; -import {toast} from 'react-toastify'; - -import Checkbox from '@/components/ui/Checkbox'; -import FileInput from '@/components/ui/FileInput'; -import Modal, {ModalProps} from '@/components/ui/Modal'; -import {useRSForm} from '@/context/RSFormContext'; -import {IRSForm, IRSFormUploadData, ISubstitution} from '@/models/rsform'; -import {EXTEOR_TRS_FILE} from '@/utils/constants'; -import {TabList, TabPanel, Tabs} from "react-tabs"; -import clsx from "clsx"; -import TabLabel from "@/components/ui/TabLabel.tsx"; -import SubstitutionsTab from "@/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx"; -import useRSFormDetails from "@/hooks/useRSFormDetails.ts"; -import {LibraryItemID} from "@/models/library.ts"; -import {ISynthesisData} from "@/models/synthesis.ts"; -import {TabID} from "@/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx"; -import SynthesisSubstitutionsTab from "@/pages/SynthesisPage/SynthesisSubstitutionsTab.tsx"; - -interface DlgCreateSynthesisProps extends Pick { - schemaLeftID: number; - schemaRightID: number; - onSynthesis: (data: ISynthesisData) => void; - -} - -export enum SynthesisTabID { - SCHEMA = 0, - SUBSTITUTIONS = 1 -} - -function DlgSynthesis({hideWindow, schemaLeftID, schemaRightID, onSynthesis}: DlgCreateSynthesisProps) { - const [activeTab, setActiveTab] = useState(SynthesisTabID.SCHEMA); - const sourceLeft = useRSFormDetails({target: schemaLeftID ? String(schemaLeftID) : undefined}); - const sourceRight = useRSFormDetails({target: schemaRightID ? String(schemaRightID) : undefined}); - const [selected, setSelected] = useState([]); - const [substitutions, setSubstitutions] = useState([]); - - //const validated = useMemo(() => !!source.schema && selected.length > 0, [source.schema, selected]); - - function handleSubmit() { - if (!sourceLeft.schema || !sourceRight.schema) { - return; - } - const data: ISynthesisData = { - sourceLeft: schemaLeftID, - sourceRight: schemaRightID, - result: 1, - substitutions: substitutions.map(item => ({ - original: item.deleteRight ? item.rightCst.id : item.leftCst.id, - substitution: item.deleteRight ? item.leftCst.id : item.rightCst.id, - transfer_term: !item.deleteRight && item.takeLeftTerm - })) - }; - onSynthesis(data); - } - - useEffect(() => { - setSelected(sourceLeft.schema && sourceRight.schema ? sourceLeft.schema?.items.map(cst => cst.id) : []); - }, [sourceLeft.schema, sourceRight.schema]); - - const schemaPanel = useMemo( - () => ( - - ), [] - ) - - const substitutesPanel = useMemo( - () => ( - - - - ), - [sourceLeft.schema, sourceRight.schema, sourceLeft.loading, sourceRight.loading, selected, substitutions] - ); - - return ( - - - - - - - {schemaPanel} - {substitutesPanel} - - - - ); -} - -export default DlgSynthesis; diff --git a/rsconcept/frontend/src/hooks/useOssDetails.ts b/rsconcept/frontend/src/hooks/useOssDetails.ts index ae2184f8..5c4116e9 100644 --- a/rsconcept/frontend/src/hooks/useOssDetails.ts +++ b/rsconcept/frontend/src/hooks/useOssDetails.ts @@ -6,6 +6,7 @@ import { getOssDetails } from '@/app/backendAPI'; import { type ErrorData } from '@/components/info/InfoError'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { OssLoader } from '@/models/OssLoader'; +import { AccessPolicy, LibraryItemType } from '@/models/library.ts'; function useOssDetails({ target }: { target?: string }) { const [schema, setInner] = useState(undefined); @@ -27,6 +28,24 @@ function useOssDetails({ target }: { target?: string }) { if (!target) { return; } + + const staticData = { + id: Number(target), + comment: '123', + alias: 'oss1', + access_policy: AccessPolicy.PUBLIC, + editors: [], + owner: 1, + item_type: LibraryItemType.OSS, + location: '/U', + read_only: false, + subscribers: [], + time_create: '0', + time_update: '0', + title: 'TestOss', + visible: false + }; + getOssDetails(target, { showError: true, setLoading: setCustomLoading ?? setLoading, @@ -35,7 +54,11 @@ function useOssDetails({ target }: { target?: string }) { setError(error); }, onSuccess: schema => { - setSchema(schema); + const combinedData = { + ...staticData, + ...schema + } + setSchema(combinedData); if (callback) callback(); } }); diff --git a/rsconcept/frontend/src/models/OssLoader.ts b/rsconcept/frontend/src/models/OssLoader.ts index 2e3d0a09..e036a3f4 100644 --- a/rsconcept/frontend/src/models/OssLoader.ts +++ b/rsconcept/frontend/src/models/OssLoader.ts @@ -11,13 +11,14 @@ import { IOperationSchema, IOperationSchemaData } from './oss'; export class OssLoader { private schema: IOperationSchemaData; + constructor(input: IOperationSchemaData) { this.schema = input; } produceOSS(): IOperationSchema { const result = this.schema as IOperationSchema; - result.producedData = [1, 2, 3]; // TODO: put data processing here + //result.producedData = [1, 2, 3]; // TODO: put data processing here return result; } } diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index f1a706e1..34c60a81 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -4,12 +4,59 @@ import { ILibraryItemData } from './library'; import { UserID } from './user'; +import { IConstituenta, ISubstitution } from '@/models/rsform.ts'; /** * Represents backend data for Schema of Synthesis Operations. */ export interface IOperationSchemaData extends ILibraryItemData { - additional_data?: number[]; + input_nodes: IInputNode[]; + operation_nodes: ISynthesisNode[]; + edges: ISynthesisEdge[]; + substitutions: ISynthesisSubstitution[]; + graph: ISynthesisGraph; +} + +interface ISynthesisGraph { + id: number; + status: string; +} + +interface IInputNode { + id: number | null; + graph_id: number; + vertical_coordinate: number; + horizontal_coordinate: number; + rsform_id: number; +} + +interface ISynthesisNode extends IInputNode { + id: number | null; + name: string; + status: string; +} + +interface ISynthesisEdge { + id: number | null; + decoded_id: string; + source_handle: string; + graph_id: number; + node_from: string; + node_to: string; +} + +export interface ISynthesisSubstitution extends ISubstitution { + id: number | null; + graph_id: number; + operation_id: string; +} + +export interface IRunSynthesis { + operationId: number; +} + +export interface IRunSynthesisResponse { + rsformId: number; } /** @@ -19,5 +66,5 @@ export interface IOperationSchema extends IOperationSchemaData { subscribers: UserID[]; editors: UserID[]; - producedData: number[]; // TODO: modify this to store calculated state on load + //producedData: number[]; // TODO: modify this to store calculated state on load } diff --git a/rsconcept/frontend/src/models/synthesis.ts b/rsconcept/frontend/src/models/synthesis.ts deleted file mode 100644 index 73f863f0..00000000 --- a/rsconcept/frontend/src/models/synthesis.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {LibraryItemID} from "@/models/library.ts"; -import {ICstSubstitute} from "@/models/rsform.ts"; - -export interface ISynthesisData { - result: LibraryItemID; - sourceLeft: LibraryItemID; - sourceRight: LibraryItemID; - substitutions: ICstSubstitute[]; -} \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx index a42d4e6e..2d0f9022 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx @@ -1,13 +1,22 @@ 'use client'; import AnimateFade from '@/components/wrap/AnimateFade'; +import SynthesisToolbar from '@/pages/OssPage/SynthesisToolbar.tsx'; +import SynthesisFlow from '@/components/ui/Synthesis/SynthesisFlow.tsx'; +import { SynthesisState } from '@/pages/OssPage/SynthesisContext.tsx'; +import { ReactFlowProvider } from '@reactflow/core'; function EditorOssGraph() { // TODO: Implement OSS editing UI here return ( -
Реализация графического интерфейса
+ + + + + +
); } diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx new file mode 100644 index 00000000..7b5a96ce --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx @@ -0,0 +1,345 @@ +import { IRSFormData, ISubstitution } from '@/models/rsform.ts'; +import { DataCallback, runSingleSynthesis, postSynthesisGraph } from '@/app/backendAPI.ts'; +import { ISynthesisData } from '@/models/synthesis.ts'; +import { createContext, Dispatch, SetStateAction, useCallback, useContext, useEffect, useState } from 'react'; +import DlgSynthesis from '@/dialogs/DlgOssGraph/DlgSynthesis.tsx'; +import { + Node, + Edge, + useNodesState, + useEdgesState, + type Connection, + addEdge, + getIncomers, + getOutgoers, + getConnectedEdges +} from '@reactflow/core'; +import DlgSelectInputScheme from '@/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx'; +import { IOperationSchemaData, IRunSynthesis, ISynthesisSubstitution } from '@/models/oss.ts'; +import { useOSS } from '@/context/OssContext.tsx'; +import viewConstituents from '@/pages/RSFormPage/ViewConstituents'; + +interface ISynthesisContext { + synthesisSchemaID: string; + singleSynthesis: (nodeId: number) => void; + showSynthesis: () => void; + showSelectInput: () => void; + selectNode: (nodeId: string) => void; + getSelectedNode: () => string | undefined; + + addLibrarySchema: () => void; + addSynthesisOperation: () => void; + removeItem: () => void; + runSynthesisLayer: () => void; + + getNodes: () => Node[], + getEdges: () => Edge[] + + setNodes: (nodes: Node[]) => void; + setEdges: (nodes: Edge[]) => void; + + onNodesChange: any, + onEdgesChange: any, + onNodesDelete: any, + onConnect: any, + addBind: () => void, + + updateBounds: (nodeId: string, newRsform: number) => void, + getBind: (nodeId: string) => number + getNodeParentsRsform: (nodeId: string) => number[] + saveGraph: () => void; + substitutions: ISynthesisSubstitution[] + setSubstitutions: () => void, + getSubstitution: (id: string) => ISynthesisSubstitution[], + updateSubstitution: (id: string, substitution: ISynthesisSubstitution[], setSubstitutions: React.Dispatch>) => void, + +} + +interface IBoundMap { + nodeId: string, + rsformId: number +} + +const SynthesisContext = createContext(null); + +interface SynthesisStateProps { + synthesisSchemaID: string; + children: React.ReactNode; +} + +export const useSynthesis = () => { + const context = useContext(SynthesisContext); + if (context === null) { + throw new Error('useSynthesis has to be used within '); + } + return context; +}; + +export const SynthesisState = ({ synthesisSchemaID, children }: SynthesisStateProps) => { + const [showSynthesisModal, setShowSynthesisModal] = useState(false); + const [showSelectInputModal, setShowSelectInputModal] = useState(false); + const [selectedNode, setSelectedNode] = useState(null); + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const [bounds, setBounds] = useState([]); + const [substitutionBounds, setSubstitutionBounds] = useState([]); + const [substitutions, setSubstitutions] = useState([]); + const { schema, loading, errorLoading } = useOSS(); + const ossSchema = useOSS(); + + + const getSubstitution = (operation_id: string): ISynthesisSubstitution[] => { + return substitutions.filter(substitution => substitution.operation_id === operation_id); + }; + + const updateSubstitution = ( + operation_id: number, + newElements: ISubstitution[], + setSubstitutions: React.Dispatch> + ) => { + if (!Array.isArray(newElements)) { + console.error('newElements should be an array.'); + return; + } + + setSubstitutions((prevSubstitutions) => { + // Обновление существующих элементов + const updatedSubstitutions = prevSubstitutions.map((substitution) => { + const newElement = newElements.find((el) => el.id === substitution.id); + if (newElement) { + // Обновляем только соответствующие элементы + return { ...substitution, ...newElement, operation_id: substitution.operation_id }; + } + return substitution; + }); + + // Добавление новых элементов с присвоением operation_id + const newSubstitutions = newElements + .filter((newElement) => !prevSubstitutions.some((sub) => sub.id === newElement.id)) + .map((newElement) => ({ ...newElement, operation_id })); + + return [...updatedSubstitutions, ...newSubstitutions]; + }); + }; + + + const extractEdgeId = (edgeId: string) => { + const matches = edgeId.match(/\d+/g); + const combined = matches ? matches.join('') : ''; + return Number(combined); + }; + + + function saveGraph() { + const data: IOperationSchemaData = { + graph: { + id: schema?.id, + status: 'Draft' + }, + input_nodes: nodes.filter((node) => node.type == 'input').map(item => + ({ + id: item.id, + vertical_coordinate: item.position.y, + horizontal_coordinate: item.position.x, + rsform_id: getBind(item.id) + })), + operation_nodes: nodes.filter((node) => node.type == 'custom').map(item => + ({ + id: item.id, + vertical_coordinate: item.position.y, + horizontal_coordinate: item.position.x, + left_parent: getNodeParentsRsform(item.id)[0], + right_parent: getNodeParentsRsform(item.id)[1], + rsform_id: getBind(item.id), + name: 'name', + status: 'status' + }) + ), + edges: edges.map(item => ({ + id: extractEdgeId(item.id), + decoded_id: item.id, + source_handle: item.sourceHandle, + node_from: item.source, + node_to: item.target + }) + ), + substitutions: substitutions + }; + + postSynthesisGraph({ + data: data + }); + } + + useEffect(() => { + if (ossSchema.schema !== undefined) { + const initialNodes = [ + ...ossSchema.schema.input_nodes.map((node) => ({ + id: node.id?.toString() || 'null', + data: { label: '123' }, + position: { x: node.horizontal_coordinate, y: node.vertical_coordinate }, + type: 'input' + })), + ...ossSchema.schema.operation_nodes.map((node) => ({ + id: node.id?.toString() || 'null', + data: { label: '123' }, + position: { x: node.horizontal_coordinate, y: node.vertical_coordinate }, + type: 'custom' + })) + ]; + + const initialEdges = ossSchema.schema.edges.map((edge) => ({ + id: edge.decoded_id, + source: String(edge.node_from), + sourceHandle: edge.source_handle, + target: String(edge.node_to) + })); + //const initialEdges = [{ id: 'reactflow__edge-2a-0', source: '2', target: '0' }, { id: 'reactflow__edge-2b-1', sourceHandle: 'b',source: '2', target: '1' }]; + + + setNodes(initialNodes); + setEdges(initialEdges); + setSubstitutions(ossSchema.schema.substitutions); + [...ossSchema.schema.input_nodes, ...ossSchema.schema.operation_nodes].forEach((node) => { + const nodeId = node.id?.toString() || 'null'; + const rsformId = node.rsform_id; // Предполагаем, что rsform_id есть в данных нод + updateBounds(nodeId, rsformId); + }); + } + + }, [ossSchema]); + + const getBind = (nodeId: string) => { + const bound = bounds.find((item) => item.nodeId == nodeId); + return bound ? bound.rsformId : null; + + }; + const getBounds = (nodeId: string[]) => { + const parentBounds = bounds.filter((item) => item.nodeId in nodeId); + return parentBounds.map((item) => item.rsformId); + }; + + function getNodeParentsRsform(nodeId: string) { + const parentEdges = edges.filter((edge) => edge.source === nodeId); + const parentNodeIds = parentEdges.map((edge) => edge.target); + return getBounds(parentNodeIds); + } + + const updateBounds = (nodeId: string, newRsform: number) => { + setBounds((prevItems) => { + const existingItem = prevItems.find((item) => item.nodeId === nodeId); + + if (existingItem) { + return prevItems.map((item) => + item.nodeId === nodeId ? { ...item, rsformId: newRsform } : item + ); + } else { + return [...prevItems, { nodeId: nodeId, rsformId: newRsform }]; + } + }); + }; + + const onConnect = useCallback( + (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), + [setEdges] + ); + + const onNodeDelete = useCallback( + (nodeId: string) => { + setNodes((nodes) => nodes.filter((node) => node.id !== nodeId)); + setEdges((edges) => + edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId) + ); + }, + [] + ); + + const singleSynthesis = useCallback( + (operationId: number) => { + const data: IRunSynthesis = { + operationId: Number(operationId) + }; + runSingleSynthesis({ data: data }); + }, + + [] + ); + const newLibrarySchema = { + id: String(nodes.length > 0 ? 1 + Math.max(...nodes.map(item => Number(item.id))) : 0), + type: 'input', + position: { x: 250, y: 5 }, + data: { + label: 'Node 1', onDelete: onNodeDelete + } + }; + + const newOperation = { + id: String(nodes.length > 0 ? 1 + Math.max(...nodes.map(item => Number(item.id))) : 0), + type: 'custom', + position: { x: 350, y: 20 }, + data: { + label: 'Node 1', onDelete: onNodeDelete + } + }; + + const selectNode = useCallback( + (nodeId: string) => { + for (const node of nodes) { + if (node.id === nodeId) { + setSelectedNode(node); + return; + } + } + }, [nodes] + ); + return ( + setShowSynthesisModal(true), + showSelectInput: () => setShowSelectInputModal(true), + selectNode: (nodeId) => selectNode(nodeId), + getSelectedNode: () => selectedNode?.id, + addLibrarySchema: () => { + setNodes([...nodes, newLibrarySchema]); + }, + addSynthesisOperation: () => { + setNodes([...nodes, newOperation]); + }, + setNodes: (nodes: Node[]) => { + setNodes(nodes); + }, + setEdges: (edges: Edge[]) => { + setEdges(edges); + }, + getNodes: () => nodes, + getEdges: () => edges, + onNodesChange: onNodesChange, + onEdgesChange: onEdgesChange, + onConnect: onConnect, + updateBounds: updateBounds, + getNodeParentsRsform: getNodeParentsRsform, + getBind: getBind, + saveGraph: saveGraph, + setSubstitutions: setSubstitutions, + substitutions: substitutions, + updateSubstitution: updateSubstitution, + getSubstitution: getSubstitution + + }}> + {showSynthesisModal ? ( setShowSynthesisModal(false)} + nodeId={selectedNode?.id} + onSynthesis={() => singleSynthesis} + />) : null} + {showSelectInputModal ? ( setShowSelectInputModal(false)} + />) : null} + {children} + + + ); +}; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/SynthesisPage/SynthesisOperation.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisOperation.tsx similarity index 100% rename from rsconcept/frontend/src/pages/SynthesisPage/SynthesisOperation.tsx rename to rsconcept/frontend/src/pages/OssPage/SynthesisOperation.tsx diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx new file mode 100644 index 00000000..1cb6cc67 --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { ErrorData } from '@/components/info/InfoError.tsx'; +import DataLoader from '@/components/wrap/DataLoader.tsx'; +import { IRSForm, ISubstitution } from '@/models/rsform.ts'; +import { prefixes } from '@/utils/constants.ts'; +import PickSubstitutions from '../../components/select/PickSubstitutions'; + +interface SynthesisSubstitutionsTabProps { + receiver?: IRSForm; + source?: IRSForm; + + error?: ErrorData; + + substitutions: ISubstitution[]; + setSubstitutions: React.Dispatch>; +} + +function SynthesisSubstitutionsTab({ + source, + receiver, + error, + substitutions, + setSubstitutions + }: SynthesisSubstitutionsTabProps) { + return ( + + + + ); +} + +export default SynthesisSubstitutionsTab; diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx new file mode 100644 index 00000000..3ebb530f --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx @@ -0,0 +1,45 @@ +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; +import Button from '@/components/ui/Button.tsx'; +import { IoMdAdd, IoMdRemove } from 'react-icons/io'; +import { MdCallMerge } from 'react-icons/md'; +import Overlay from '@/components/ui/Overlay.tsx'; +import { MdLibraryAdd } from 'react-icons/md'; + +import { VscRunAll, VscSave } from 'react-icons/vsc'; + +function SynthesisToolbar() { + const controller = useSynthesis(); + + return ( + +