diff --git a/rsconcept/backend/apps/library/tests/s_views/t_library.py b/rsconcept/backend/apps/library/tests/s_views/t_library.py index 9cff6e94..ac1cd0be 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_library.py @@ -9,6 +9,7 @@ from apps.library.models import ( LibraryTemplate, LocationHead ) +from apps.oss.models import OperationSchema from apps.rsform.models import RSForm from shared.EndpointTester import EndpointTester, decl_endpoint from shared.testing_utils import response_contains @@ -58,6 +59,8 @@ class TestLibraryViewset(EndpointTester): 'read_only': True } response = self.executeCreated(data=data) + oss = OperationSchema(LibraryItem.objects.get(pk=response.data['id'])) + self.assertEqual(oss.model.owner, self.user) self.assertEqual(response.data['owner'], self.user.pk) self.assertEqual(response.data['item_type'], data['item_type']) self.assertEqual(response.data['title'], data['title']) @@ -65,6 +68,8 @@ class TestLibraryViewset(EndpointTester): self.assertEqual(response.data['access_policy'], data['access_policy']) self.assertEqual(response.data['visible'], data['visible']) self.assertEqual(response.data['read_only'], data['read_only']) + self.assertEqual(oss.layout().data['operations'], []) + self.assertEqual(oss.layout().data['blocks'], []) self.logout() data = {'title': 'Title2'} diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 4bd0996e..95d36064 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -13,7 +13,7 @@ from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response -from apps.oss.models import Operation, OperationSchema, PropagationFacade +from apps.oss.models import Layout, Operation, OperationSchema, PropagationFacade from apps.rsform.models import RSForm from apps.rsform.serializers import RSFormParseSerializer from apps.users.models import User @@ -40,6 +40,8 @@ class LibraryViewSet(viewsets.ModelViewSet): serializer.save(owner=self.request.user) else: serializer.save() + if serializer.data.get('item_type') == m.LibraryItemType.OPERATION_SCHEMA: + Layout.objects.create(oss=serializer.instance, data={'operations': [], 'blocks': []}) def perform_update(self, serializer) -> None: instance = serializer.save() diff --git a/rsconcept/backend/apps/oss/admin.py b/rsconcept/backend/apps/oss/admin.py index 86f87aab..c88e118b 100644 --- a/rsconcept/backend/apps/oss/admin.py +++ b/rsconcept/backend/apps/oss/admin.py @@ -15,11 +15,24 @@ class OperationAdmin(admin.ModelAdmin): 'alias', 'title', 'description', - 'position_x', - 'position_y'] + 'parent'] search_fields = ['id', 'operation_type', 'title', 'alias'] +class BlockAdmin(admin.ModelAdmin): + ''' Admin model: Block. ''' + ordering = ['oss'] + list_display = ['id', 'oss', 'title', 'description', 'parent'] + search_fields = ['oss'] + + +class LayoutAdmin(admin.ModelAdmin): + ''' Admin model: Layout. ''' + ordering = ['oss'] + list_display = ['id', 'oss', 'data'] + search_fields = ['oss'] + + class ArgumentAdmin(admin.ModelAdmin): ''' Admin model: Operation arguments. ''' ordering = ['operation'] @@ -42,6 +55,8 @@ class InheritanceAdmin(admin.ModelAdmin): admin.site.register(models.Operation, OperationAdmin) +admin.site.register(models.Block, BlockAdmin) +admin.site.register(models.Layout, LayoutAdmin) admin.site.register(models.Argument, ArgumentAdmin) admin.site.register(models.Substitution, SynthesisSubstitutionAdmin) admin.site.register(models.Inheritance, InheritanceAdmin) diff --git a/rsconcept/backend/apps/oss/migrations/0011_remove_operation_position_x_and_more.py b/rsconcept/backend/apps/oss/migrations/0011_remove_operation_position_x_and_more.py new file mode 100644 index 00000000..31ff26ba --- /dev/null +++ b/rsconcept/backend/apps/oss/migrations/0011_remove_operation_position_x_and_more.py @@ -0,0 +1,84 @@ +# Generated by Django 5.1.7 on 2025-03-26 16:04 + +import django.db.models.deletion +from django.db import migrations, models + + +def migrate_layout(apps, schema_editor): + LibraryItem = apps.get_model('library', 'LibraryItem') + Operation = apps.get_model('oss', 'Operation') + Layout = apps.get_model('oss', 'Layout') + + for library_item in LibraryItem.objects.filter(item_type='oss'): + layout_data = {'operations': [], 'blocks': []} + + operations = Operation.objects.filter(oss=library_item) + for operation in operations: + layout_data['operations'].append({ + 'id': operation.id, + 'x': operation.position_x, + 'y': operation.position_y + }) + + Layout.objects.create(oss=library_item, data=layout_data) + + +class Migration(migrations.Migration): + + dependencies = [ + ('library', '0007_rename_libraryitem_comment_libraryitem_description'), + ('oss', '0010_rename_comment_operation_description'), + ] + + operations = [ + migrations.CreateModel( + name='Layout', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.JSONField(default=dict, verbose_name='Расположение')), + ('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='layout', to='library.libraryitem', verbose_name='Схема синтеза')), + ], + options={ + 'verbose_name': 'Схема расположения', + 'verbose_name_plural': 'Схемы расположения', + }, + ), + migrations.RunPython(migrate_layout), + migrations.RemoveField( + model_name='operation', + name='position_x', + ), + migrations.RemoveField( + model_name='operation', + name='position_y', + ), + + migrations.CreateModel( + name='Block', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField(blank=True, verbose_name='Название')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('oss', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, + related_name='blocks', to='library.libraryitem', verbose_name='Схема синтеза')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + related_name='as_child_block', to='oss.block', verbose_name='Содержащий блок')), + ], + options={ + 'verbose_name': 'Блок', + 'verbose_name_plural': 'Блоки', + }, + ), + migrations.AddField( + model_name='operation', + name='parent', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name='as_child_operation', + to='oss.block', + verbose_name='Содержащий блок'), + ), + ] diff --git a/rsconcept/backend/apps/oss/models/Block.py b/rsconcept/backend/apps/oss/models/Block.py new file mode 100644 index 00000000..12780d6d --- /dev/null +++ b/rsconcept/backend/apps/oss/models/Block.py @@ -0,0 +1,39 @@ +''' Models: Content Block in OSS. ''' +# pylint: disable=duplicate-code +from django.db.models import CASCADE, SET_NULL, ForeignKey, Model, TextField + + +class Block(Model): + ''' Block of content in OSS.''' + oss = ForeignKey( + verbose_name='Схема синтеза', + to='library.LibraryItem', + on_delete=CASCADE, + related_name='blocks' + ) + + title = TextField( + verbose_name='Название', + blank=True + ) + description = TextField( + verbose_name='Описание', + blank=True + ) + + parent = ForeignKey( + verbose_name='Содержащий блок', + to='oss.Block', + blank=True, + null=True, + on_delete=SET_NULL, + related_name='as_child_block' + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Блок' + verbose_name_plural = 'Блоки' + + def __str__(self) -> str: + return f'Блок {self.title}' diff --git a/rsconcept/backend/apps/oss/models/Layout.py b/rsconcept/backend/apps/oss/models/Layout.py new file mode 100644 index 00000000..1f6efdd1 --- /dev/null +++ b/rsconcept/backend/apps/oss/models/Layout.py @@ -0,0 +1,25 @@ +''' Models: Content Block in OSS. ''' +from django.db.models import CASCADE, ForeignKey, JSONField, Model + + +class Layout(Model): + ''' Node layout in OSS.''' + oss = ForeignKey( + verbose_name='Схема синтеза', + to='library.LibraryItem', + on_delete=CASCADE, + related_name='layout' + ) + + data = JSONField( + verbose_name='Расположение', + default=dict + ) + + class Meta: + ''' Model metadata. ''' + verbose_name = 'Схема расположения' + verbose_name_plural = 'Схемы расположения' + + def __str__(self) -> str: + return f'Схема расположения {self.oss.alias}' diff --git a/rsconcept/backend/apps/oss/models/Operation.py b/rsconcept/backend/apps/oss/models/Operation.py index 0c0a3e8b..ffc1e4fa 100644 --- a/rsconcept/backend/apps/oss/models/Operation.py +++ b/rsconcept/backend/apps/oss/models/Operation.py @@ -1,9 +1,9 @@ ''' Models: Operation in OSS. ''' +# pylint: disable=duplicate-code from django.db.models import ( CASCADE, SET_NULL, CharField, - FloatField, ForeignKey, Model, QuerySet, @@ -44,6 +44,15 @@ class Operation(Model): related_name='producer' ) + parent = ForeignKey( + verbose_name='Содержащий блок', + to='oss.Block', + blank=True, + null=True, + on_delete=SET_NULL, + related_name='as_child_operation' + ) + alias = CharField( verbose_name='Шифр', max_length=255, @@ -58,15 +67,6 @@ class Operation(Model): blank=True ) - position_x = FloatField( - verbose_name='Положение по горизонтали', - default=0 - ) - position_y = FloatField( - verbose_name='Положение по вертикали', - default=0 - ) - class Meta: ''' Model metadata. ''' verbose_name = 'Операция' diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 6cabf745..5796c4b3 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -20,6 +20,7 @@ from apps.rsform.models import ( from .Argument import Argument from .Inheritance import Inheritance +from .Layout import Layout from .Operation import Operation from .Substitution import Substitution @@ -38,6 +39,7 @@ class OperationSchema: def create(**kwargs) -> 'OperationSchema': ''' Create LibraryItem via OperationSchema. ''' model = LibraryItem.objects.create(item_type=LibraryItemType.OPERATION_SCHEMA, **kwargs) + Layout.objects.create(oss=model, data={'operations': [], 'blocks': []}) return OperationSchema(model) @staticmethod @@ -62,6 +64,12 @@ class OperationSchema: ''' Operation arguments. ''' return Argument.objects.filter(operation__oss=self.model) + def layout(self) -> Layout: + ''' OSS layout. ''' + result = Layout.objects.filter(oss=self.model).first() + assert result is not None + return result + def substitutions(self) -> QuerySet[Substitution]: ''' Operation substitutions. ''' return Substitution.objects.filter(operation__oss=self.model) @@ -78,15 +86,11 @@ class OperationSchema: location=self.model.location ) - def update_positions(self, data: list[dict]) -> None: + def update_layout(self, data: dict) -> None: ''' Update positions. ''' - lookup = {x['id']: x for x in data} - operations = self.operations() - for item in operations: - if item.pk in lookup: - item.position_x = lookup[item.pk]['position_x'] - item.position_y = lookup[item.pk]['position_y'] - Operation.objects.bulk_update(operations, ['position_x', 'position_y']) + layout = self.layout() + layout.data = data + layout.save() def create_operation(self, **kwargs) -> Operation: ''' Insert new operation. ''' diff --git a/rsconcept/backend/apps/oss/models/__init__.py b/rsconcept/backend/apps/oss/models/__init__.py index e21bc285..143c6a59 100644 --- a/rsconcept/backend/apps/oss/models/__init__.py +++ b/rsconcept/backend/apps/oss/models/__init__.py @@ -1,7 +1,9 @@ ''' Django: Models. ''' from .Argument import Argument +from .Block import Block from .Inheritance import Inheritance +from .Layout import Layout from .Operation import Operation, OperationType from .OperationSchema import OperationSchema from .PropagationFacade import PropagationFacade diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index 874298ce..468c770a 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -1,6 +1,6 @@ ''' REST API: Serializers. ''' -from .basics import OperationPositionSerializer, PositionsSerializer, SubstitutionExSerializer +from .basics import LayoutSerializer, SubstitutionExSerializer from .data_access import ( ArgumentSerializer, OperationCreateSerializer, diff --git a/rsconcept/backend/apps/oss/serializers/basics.py b/rsconcept/backend/apps/oss/serializers/basics.py index 9d2328be..11caab87 100644 --- a/rsconcept/backend/apps/oss/serializers/basics.py +++ b/rsconcept/backend/apps/oss/serializers/basics.py @@ -2,17 +2,29 @@ from rest_framework import serializers -class OperationPositionSerializer(serializers.Serializer): +class OperationNodeSerializer(serializers.Serializer): ''' Operation position. ''' id = serializers.IntegerField() - position_x = serializers.FloatField() - position_y = serializers.FloatField() + x = serializers.FloatField() + y = serializers.FloatField() -class PositionsSerializer(serializers.Serializer): - ''' Operations position for OperationSchema. ''' - positions = serializers.ListField( - child=OperationPositionSerializer() +class BlockNodeSerializer(serializers.Serializer): + ''' Block position. ''' + id = serializers.IntegerField() + x = serializers.FloatField() + y = serializers.FloatField() + width = serializers.FloatField() + height = serializers.FloatField() + + +class LayoutSerializer(serializers.Serializer): + ''' Layout for OperationSchema. ''' + blocks = serializers.ListField( + child=BlockNodeSerializer() + ) + operations = serializers.ListField( + child=OperationNodeSerializer() ) diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index b56f75ac..d5f7fe85 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -12,7 +12,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase from shared import messages as msg from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType -from .basics import OperationPositionSerializer, SubstitutionExSerializer +from .basics import LayoutSerializer, SubstitutionExSerializer class OperationSerializer(serializers.ModelSerializer): @@ -44,17 +44,16 @@ class OperationCreateSerializer(serializers.Serializer): model = Operation fields = \ 'alias', 'operation_type', 'title', \ - 'description', 'result', 'position_x', 'position_y' + 'description', 'result', 'parent' + + layout = LayoutSerializer() + position_x = serializers.FloatField() + position_y = serializers.FloatField() - create_schema = serializers.BooleanField(default=False, required=False) item_data = OperationCreateData() + create_schema = serializers.BooleanField(default=False, required=False) arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False) - positions = serializers.ListField( - child=OperationPositionSerializer(), - default=[] - ) - class OperationUpdateSerializer(serializers.Serializer): ''' Serializer: Operation update. ''' @@ -65,6 +64,7 @@ class OperationUpdateSerializer(serializers.Serializer): model = Operation fields = 'alias', 'title', 'description' + layout = LayoutSerializer() target = PKField(many=False, queryset=Operation.objects.all()) item_data = OperationUpdateData() arguments = PKField(many=True, queryset=Operation.objects.all().only('oss_id', 'result_id'), required=False) @@ -73,11 +73,6 @@ class OperationUpdateSerializer(serializers.Serializer): required=False ) - positions = serializers.ListField( - child=OperationPositionSerializer(), - default=[] - ) - def validate(self, attrs): if 'arguments' not in attrs: return attrs @@ -120,11 +115,8 @@ class OperationUpdateSerializer(serializers.Serializer): class OperationTargetSerializer(serializers.Serializer): ''' Serializer: Target single operation. ''' + layout = LayoutSerializer() target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id')) - positions = serializers.ListField( - child=OperationPositionSerializer(), - default=[] - ) def validate(self, attrs): oss = cast(LibraryItem, self.context['oss']) @@ -138,11 +130,8 @@ class OperationTargetSerializer(serializers.Serializer): class OperationDeleteSerializer(serializers.Serializer): ''' Serializer: Delete operation. ''' + layout = LayoutSerializer() target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result')) - positions = serializers.ListField( - child=OperationPositionSerializer(), - default=[] - ) keep_constituents = serializers.BooleanField(default=False, required=False) delete_schema = serializers.BooleanField(default=False, required=False) @@ -158,6 +147,7 @@ class OperationDeleteSerializer(serializers.Serializer): class SetOperationInputSerializer(serializers.Serializer): ''' Serializer: Set input schema for operation. ''' + layout = LayoutSerializer() target = PKField(many=False, queryset=Operation.objects.all()) input = PKField( many=False, @@ -165,10 +155,6 @@ class SetOperationInputSerializer(serializers.Serializer): allow_null=True, default=None ) - positions = serializers.ListField( - child=OperationPositionSerializer(), - default=[] - ) def validate(self, attrs): oss = cast(LibraryItem, self.context['oss']) @@ -186,7 +172,7 @@ class SetOperationInputSerializer(serializers.Serializer): class OperationSchemaSerializer(serializers.ModelSerializer): ''' Serializer: Detailed data for OSS. ''' - items = serializers.ListField( + operations = serializers.ListField( child=OperationSerializer() ) arguments = serializers.ListField( @@ -195,6 +181,7 @@ class OperationSchemaSerializer(serializers.ModelSerializer): substitutions = serializers.ListField( child=SubstitutionExSerializer() ) + layout = LayoutSerializer() class Meta: ''' serializer metadata. ''' @@ -205,9 +192,10 @@ class OperationSchemaSerializer(serializers.ModelSerializer): result = LibraryItemDetailsSerializer(instance).data del result['versions'] oss = OperationSchema(instance) - result['items'] = [] + result['layout'] = oss.layout().data + result['operations'] = [] for operation in oss.operations().order_by('pk'): - result['items'].append(OperationSerializer(operation).data) + result['operations'].append(OperationSerializer(operation).data) result['arguments'] = [] for argument in oss.arguments().order_by('order'): result['arguments'].append(ArgumentSerializer(argument).data) diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py b/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py index 02a6bbe5..efcd9d50 100644 --- a/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py +++ b/rsconcept/backend/apps/oss/tests/s_models/t_Operation.py @@ -25,9 +25,8 @@ class TestOperation(TestCase): def test_create_default(self): self.assertEqual(self.operation.oss, self.oss.model) self.assertEqual(self.operation.operation_type, OperationType.INPUT) + self.assertEqual(self.operation.parent, None) self.assertEqual(self.operation.result, None) self.assertEqual(self.operation.alias, 'KS1') self.assertEqual(self.operation.title, '') self.assertEqual(self.operation.description, '') - self.assertEqual(self.operation.position_x, 0) - self.assertEqual(self.operation.position_y, 0) diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py index 96dc309d..6a692417 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_attributes.py @@ -58,6 +58,18 @@ class TestChangeAttributes(EndpointTester): self.operation3.refresh_from_db() self.ks3 = RSForm(self.operation3.result) + self.layout_data = { + 'operations': [ + {'id': self.operation1.pk, 'x': 0, 'y': 0}, + {'id': self.operation2.pk, 'x': 0, 'y': 0}, + {'id': self.operation3.pk, 'x': 0, 'y': 0}, + ], + 'blocks': [] + } + layout = self.owned.layout() + layout.data = self.layout_data + layout.save() + @decl_endpoint('/api/library/{item}/set-owner', method='patch') def test_set_owner(self): data = {'user': self.user3.pk} @@ -142,7 +154,7 @@ class TestChangeAttributes(EndpointTester): 'title': 'Test title mod', 'description': 'Comment mod' }, - 'positions': [], + 'layout': self.layout_data } response = self.executeOK(data=data, item=self.owned_id) diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py index ae160773..4afd5a38 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_constituents.py @@ -57,6 +57,18 @@ class TestChangeConstituents(EndpointTester): self.ks3 = RSForm(self.operation3.result) self.assertEqual(self.ks3.constituents().count(), 4) + self.layout_data = { + 'operations': [ + {'id': self.operation1.pk, 'x': 0, 'y': 0}, + {'id': self.operation2.pk, 'x': 0, 'y': 0}, + {'id': self.operation3.pk, 'x': 0, 'y': 0}, + ], + 'blocks': [] + } + layout = self.owned.layout() + layout.data = self.layout_data + layout.save() + @decl_endpoint('/api/rsforms/{item}/details', method='get') def test_retrieve_inheritance(self): response = self.executeOK(item=self.ks3.model.pk) diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py index 6db78d94..4ee3e715 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -106,6 +106,20 @@ class TestChangeOperations(EndpointTester): convention='KS5D4' ) + self.layout_data = { + 'operations': [ + {'id': self.operation1.pk, 'x': 0, 'y': 0}, + {'id': self.operation2.pk, 'x': 0, 'y': 0}, + {'id': self.operation3.pk, 'x': 0, 'y': 0}, + {'id': self.operation4.pk, 'x': 0, 'y': 0}, + {'id': self.operation5.pk, 'x': 0, 'y': 0}, + ], + 'blocks': [] + } + layout = self.owned.layout() + layout.data = self.layout_data + layout.save() + def test_oss_setup(self): self.assertEqual(self.ks1.constituents().count(), 3) self.assertEqual(self.ks2.constituents().count(), 3) @@ -117,7 +131,7 @@ class TestChangeOperations(EndpointTester): @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') def test_delete_input_operation(self): data = { - 'positions': [], + 'layout': self.layout_data, 'target': self.operation2.pk } self.executeOK(data=data, item=self.owned_id) @@ -137,7 +151,7 @@ class TestChangeOperations(EndpointTester): @decl_endpoint('/api/oss/{item}/set-input', method='patch') def test_set_input_null(self): data = { - 'positions': [], + 'layout': self.layout_data, 'target': self.operation2.pk, 'input': None } @@ -169,7 +183,7 @@ class TestChangeOperations(EndpointTester): ks6D1 = ks6.insert_new('D1', definition_formal='X1 X2', convention='KS6D1') data = { - 'positions': [], + 'layout': self.layout_data, 'target': self.operation2.pk, 'input': ks6.model.pk } @@ -211,7 +225,7 @@ class TestChangeOperations(EndpointTester): @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') def test_delete_operation_and_constituents(self): data = { - 'positions': [], + 'layout': self.layout_data, 'target': self.operation1.pk, 'keep_constituents': False, 'delete_schema': True @@ -232,7 +246,7 @@ class TestChangeOperations(EndpointTester): @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') def test_delete_operation_keep_constituents(self): data = { - 'positions': [], + 'layout': self.layout_data, 'target': self.operation1.pk, 'keep_constituents': True, 'delete_schema': True @@ -253,7 +267,7 @@ class TestChangeOperations(EndpointTester): @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') def test_delete_operation_keep_schema(self): data = { - 'positions': [], + 'layout': self.layout_data, 'target': self.operation1.pk, 'keep_constituents': True, 'delete_schema': False @@ -283,7 +297,7 @@ class TestChangeOperations(EndpointTester): 'title': 'Test title mod', 'description': 'Comment mod' }, - 'positions': [], + 'layout': self.layout_data, 'substitutions': [ { 'original': self.ks1X1.pk, @@ -317,7 +331,7 @@ class TestChangeOperations(EndpointTester): 'title': 'Test title mod', 'description': 'Comment mod' }, - 'positions': [], + 'layout': self.layout_data, 'arguments': [self.operation1.pk], } @@ -356,7 +370,7 @@ class TestChangeOperations(EndpointTester): data = { 'target': self.operation4.pk, - 'positions': [] + 'layout': self.layout_data } self.executeOK(data=data, item=self.owned_id) self.operation4.refresh_from_db() diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py index 943e56a6..6d85bc73 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_substitutions.py @@ -106,6 +106,20 @@ class TestChangeSubstitutions(EndpointTester): convention='KS5D4' ) + self.layout_data = { + 'operations': [ + {'id': self.operation1.pk, 'x': 0, 'y': 0}, + {'id': self.operation2.pk, 'x': 0, 'y': 0}, + {'id': self.operation3.pk, 'x': 0, 'y': 0}, + {'id': self.operation4.pk, 'x': 0, 'y': 0}, + {'id': self.operation5.pk, 'x': 0, 'y': 0}, + ], + 'blocks': [] + } + layout = self.owned.layout() + layout.data = self.layout_data + layout.save() + def test_oss_setup(self): self.assertEqual(self.ks1.constituents().count(), 3) @@ -139,10 +153,12 @@ class TestChangeSubstitutions(EndpointTester): @decl_endpoint('/api/rsforms/{schema}/substitute', method='patch') def test_substitute_substitution(self): - data = {'substitutions': [{ - 'original': self.ks2S1.pk, - 'substitution': self.ks2X1.pk - }]} + data = { + 'substitutions': [{ + 'original': self.ks2S1.pk, + 'substitution': self.ks2X1.pk + }] + } self.executeOK(data=data, schema=self.ks2.model.pk) self.ks4D1.refresh_from_db() self.ks4D2.refresh_from_db() diff --git a/rsconcept/backend/apps/oss/tests/s_views/__init__.py b/rsconcept/backend/apps/oss/tests/s_views/__init__.py index 10c776a8..c6532902 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/__init__.py +++ b/rsconcept/backend/apps/oss/tests/s_views/__init__.py @@ -1,2 +1,3 @@ ''' Tests for REST API. ''' +from .t_operations import * from .t_oss import * diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py new file mode 100644 index 00000000..4509a4c4 --- /dev/null +++ b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py @@ -0,0 +1,447 @@ +''' Testing API: Operation Schema. ''' +from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType +from apps.oss.models import Operation, OperationSchema, OperationType +from apps.rsform.models import Constituenta, RSForm +from shared.EndpointTester import EndpointTester, decl_endpoint + + +class TestOssOperations(EndpointTester): + ''' Testing OSS view - operations. ''' + + def setUp(self): + super().setUp() + self.owned = OperationSchema.create(title='Test', alias='T1', owner=self.user) + self.owned_id = self.owned.model.pk + self.unowned = OperationSchema.create(title='Test2', alias='T2') + self.unowned_id = self.unowned.model.pk + self.private = OperationSchema.create(title='Test2', alias='T2', access_policy=AccessPolicy.PRIVATE) + self.private_id = self.private.model.pk + self.invalid_id = self.private.model.pk + 1337 + + def populateData(self): + self.ks1 = RSForm.create( + alias='KS1', + title='Test1', + owner=self.user + ) + self.ks1X1 = self.ks1.insert_new( + 'X1', + term_raw='X1_1', + term_resolved='X1_1' + ) + self.ks2 = RSForm.create( + alias='KS2', + title='Test2', + owner=self.user + ) + self.ks2X1 = self.ks2.insert_new( + 'X2', + term_raw='X1_2', + term_resolved='X1_2' + ) + + self.operation1 = self.owned.create_operation( + alias='1', + operation_type=OperationType.INPUT, + result=self.ks1.model + ) + self.operation2 = self.owned.create_operation( + alias='2', + operation_type=OperationType.INPUT, + result=self.ks2.model + ) + self.operation3 = self.owned.create_operation( + alias='3', + operation_type=OperationType.SYNTHESIS + ) + self.layout_data = { + 'operations': [ + {'id': self.operation1.pk, 'x': 0, 'y': 0}, + {'id': self.operation2.pk, 'x': 0, 'y': 0}, + {'id': self.operation3.pk, 'x': 0, 'y': 0}, + ], + 'blocks': [] + } + layout = self.owned.layout() + layout.data = self.layout_data + layout.save() + + self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2]) + self.owned.set_substitutions(self.operation3.pk, [{ + 'original': self.ks1X1, + 'substitution': self.ks2X1 + }]) + + @decl_endpoint('/api/oss/{item}/create-operation', method='post') + def test_create_operation(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'item_data': { + 'alias': 'Test3', + 'title': 'Test title', + 'description': 'Тест кириллицы', + + }, + 'layout': self.layout_data, + 'position_x': 1, + 'position_y': 1 + + } + self.executeBadData(data=data) + + data['item_data']['operation_type'] = 'invalid' + self.executeBadData(data=data) + + data['item_data']['operation_type'] = OperationType.INPUT + self.executeNotFound(data=data, item=self.invalid_id) + + response = self.executeCreated(data=data, item=self.owned_id) + self.assertEqual(len(response.data['oss']['operations']), 4) + new_operation = response.data['new_operation'] + layout = response.data['oss']['layout'] + item = [item for item in layout['operations'] if item['id'] == new_operation['id']][0] + self.assertEqual(new_operation['alias'], data['item_data']['alias']) + self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type']) + self.assertEqual(new_operation['title'], data['item_data']['title']) + self.assertEqual(new_operation['description'], data['item_data']['description']) + self.assertEqual(new_operation['result'], None) + self.assertEqual(new_operation['parent'], None) + self.assertEqual(item['x'], data['position_x']) + self.assertEqual(item['y'], data['position_y']) + self.operation1.refresh_from_db() + + self.executeForbidden(data=data, item=self.unowned_id) + self.toggle_admin(True) + self.executeCreated(data=data, item=self.unowned_id) + + @decl_endpoint('/api/oss/{item}/create-operation', method='post') + def test_create_operation_arguments(self): + self.populateData() + data = { + 'item_data': { + 'alias': 'Test4', + 'operation_type': OperationType.SYNTHESIS + }, + 'layout': self.layout_data, + 'position_x': 1, + 'position_y': 1, + 'arguments': [self.operation1.pk, self.operation3.pk] + } + response = self.executeCreated(data=data, item=self.owned_id) + self.owned.refresh_from_db() + new_operation = response.data['new_operation'] + arguments = self.owned.arguments() + self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1)) + self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3)) + + @decl_endpoint('/api/oss/{item}/create-operation', method='post') + def test_create_operation_result(self): + self.populateData() + + self.operation1.result = None + self.operation1.save() + + data = { + 'item_data': { + 'alias': 'Test4', + 'operation_type': OperationType.INPUT, + 'result': self.ks1.model.pk + }, + 'layout': self.layout_data, + 'position_x': 1, + 'position_y': 1 + } + response = self.executeCreated(data=data, item=self.owned_id) + self.owned.refresh_from_db() + new_operation = response.data['new_operation'] + self.assertEqual(new_operation['result'], self.ks1.model.pk) + + @decl_endpoint('/api/oss/{item}/create-operation', method='post') + def test_create_operation_schema(self): + self.populateData() + Editor.add(self.owned.model.pk, self.user2.pk) + data = { + 'item_data': { + 'alias': 'Test4', + 'title': 'Test title', + 'description': 'Comment', + 'operation_type': OperationType.INPUT, + 'result': self.ks1.model.pk + }, + 'create_schema': True, + 'layout': self.layout_data, + 'position_x': 1, + 'position_y': 1 + } + self.executeBadData(data=data, item=self.owned_id) + data['item_data']['result'] = None + response = self.executeCreated(data=data, item=self.owned_id) + self.owned.refresh_from_db() + new_operation = response.data['new_operation'] + schema = LibraryItem.objects.get(pk=new_operation['result']) + self.assertEqual(schema.alias, data['item_data']['alias']) + self.assertEqual(schema.title, data['item_data']['title']) + self.assertEqual(schema.description, data['item_data']['description']) + self.assertEqual(schema.visible, False) + self.assertEqual(schema.access_policy, self.owned.model.access_policy) + self.assertEqual(schema.location, self.owned.model.location) + self.assertIn(self.user2, schema.getQ_editors()) + + @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') + def test_delete_operation(self): + self.executeNotFound(item=self.invalid_id) + + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'layout': self.layout_data + } + self.executeBadData(data=data) + + data['target'] = self.operation1.pk + self.toggle_admin(True) + self.executeBadData(data=data, item=self.unowned_id) + self.logout() + self.executeForbidden(data=data, item=self.owned_id) + + self.login() + response = self.executeOK(data=data) + layout = response.data['layout'] + deleted_items = [item for item in layout['operations'] if item['id'] == data['target']] + self.assertEqual(len(response.data['operations']), 2) + self.assertEqual(len(deleted_items), 0) + + @decl_endpoint('/api/oss/{item}/create-input', method='patch') + def test_create_input(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'layout': self.layout_data + } + self.executeBadData(data=data) + + data['target'] = self.operation1.pk + self.toggle_admin(True) + self.executeBadData(data=data, item=self.unowned_id) + self.logout() + self.executeForbidden(data=data, item=self.owned_id) + + self.login() + self.executeBadData(data=data, item=self.owned_id) + + self.operation1.result = None + self.operation1.description = 'TestComment' + self.operation1.title = 'TestTitle' + self.operation1.save() + response = self.executeOK(data=data) + self.operation1.refresh_from_db() + + new_schema = response.data['new_schema'] + self.assertEqual(new_schema['id'], self.operation1.result.pk) + self.assertEqual(new_schema['alias'], self.operation1.alias) + self.assertEqual(new_schema['title'], self.operation1.title) + self.assertEqual(new_schema['description'], self.operation1.description) + + data['target'] = self.operation3.pk + self.executeBadData(data=data) + + @decl_endpoint('/api/oss/{item}/set-input', method='patch') + def test_set_input_null(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'layout': self.layout_data + } + self.executeBadData(data=data) + + data['target'] = self.operation1.pk + data['input'] = None + 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.operation1.refresh_from_db() + self.assertEqual(self.operation1.result, None) + + data['input'] = self.ks1.model.pk + self.ks1.model.alias = 'Test42' + self.ks1.model.title = 'Test421' + self.ks1.model.description = 'TestComment42' + self.ks1.save() + response = self.executeOK(data=data) + self.operation1.refresh_from_db() + self.assertEqual(self.operation1.result, self.ks1.model) + self.assertEqual(self.operation1.alias, self.ks1.model.alias) + self.assertEqual(self.operation1.title, self.ks1.model.title) + self.assertEqual(self.operation1.description, self.ks1.model.description) + + @decl_endpoint('/api/oss/{item}/set-input', method='patch') + def test_set_input_change_schema(self): + self.populateData() + self.operation2.result = None + + data = { + 'layout': self.layout_data, + 'target': self.operation1.pk, + 'input': self.ks2.model.pk + } + self.executeBadData(data=data, item=self.owned_id) + + self.ks2.model.visible = False + self.ks2.model.save(update_fields=['visible']) + data = { + 'layout': self.layout_data, + 'target': self.operation2.pk, + 'input': None + } + self.executeOK(data=data, item=self.owned_id) + self.operation2.refresh_from_db() + self.ks2.model.refresh_from_db() + self.assertEqual(self.operation2.result, None) + self.assertEqual(self.ks2.model.visible, True) + + data = { + 'layout': self.layout_data, + 'target': self.operation1.pk, + 'input': self.ks2.model.pk + } + self.executeOK(data=data, item=self.owned_id) + self.operation1.refresh_from_db() + self.assertEqual(self.operation1.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', + 'description': 'Comment mod' + }, + 'layout': self.layout_data, + 'arguments': [self.operation2.pk, self.operation1.pk], + 'substitutions': [ + { + 'original': self.ks1X1.pk, + 'substitution': ks3x1.pk + } + ] + } + 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.alias, data['item_data']['alias']) + self.assertEqual(self.operation3.title, data['item_data']['title']) + self.assertEqual(self.operation3.description, data['item_data']['description']) + args = self.operation3.getQ_arguments().order_by('order') + self.assertEqual(args[0].argument.pk, data['arguments'][0]) + self.assertEqual(args[0].order, 0) + self.assertEqual(args[1].argument.pk, data['arguments'][1]) + self.assertEqual(args[1].order, 1) + sub = self.operation3.getQ_substitutions()[0] + self.assertEqual(sub.original.pk, data['substitutions'][0]['original']) + self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution']) + + @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', + 'description': 'Comment mod' + }, + 'layout': self.layout_data + } + + response = self.executeOK(data=data) + self.operation1.refresh_from_db() + self.assertEqual(self.operation1.alias, data['item_data']['alias']) + self.assertEqual(self.operation1.title, data['item_data']['title']) + self.assertEqual(self.operation1.description, data['item_data']['description']) + 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.description, data['item_data']['description']) + + @decl_endpoint('/api/oss/{item}/update-operation', method='patch') + def test_update_operation_invalid_substitution(self): + self.populateData() + + self.ks1X2 = self.ks1.insert_new('X2') + + data = { + 'target': self.operation3.pk, + 'item_data': { + 'alias': 'Test3 mod', + 'title': 'Test title mod', + 'description': 'Comment mod' + }, + 'layout': self.layout_data, + 'arguments': [self.operation1.pk, self.operation2.pk], + 'substitutions': [ + { + 'original': self.ks1X1.pk, + 'substitution': self.ks2X1.pk + }, + { + 'original': self.ks2X1.pk, + 'substitution': self.ks1X2.pk + } + ] + } + self.executeBadData(data=data, item=self.owned_id) + + @decl_endpoint('/api/oss/{item}/execute-operation', method='post') + def test_execute_operation(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'layout': self.layout_data, + 'target': self.operation1.pk + } + self.executeBadData(data=data) + + data['target'] = self.operation3.pk + self.toggle_admin(True) + self.executeBadData(data=data, item=self.unowned_id) + self.logout() + self.executeForbidden(data=data, item=self.owned_id) + + self.login() + self.executeOK(data=data) + self.operation3.refresh_from_db() + schema = self.operation3.result + self.assertEqual(schema.alias, self.operation3.alias) + self.assertEqual(schema.description, self.operation3.description) + self.assertEqual(schema.title, self.operation3.title) + self.assertEqual(schema.visible, False) + items = list(RSForm(schema).constituents()) + self.assertEqual(len(items), 1) + self.assertEqual(items[0].alias, 'X1') + self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved) 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 a6d1fb18..a6c526dc 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -1,6 +1,6 @@ ''' Testing API: Operation Schema. ''' -from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType -from apps.oss.models import Operation, OperationSchema, OperationType +from apps.library.models import AccessPolicy, LibraryItemType +from apps.oss.models import OperationSchema, OperationType from apps.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -54,6 +54,14 @@ class TestOssViewset(EndpointTester): alias='3', operation_type=OperationType.SYNTHESIS ) + layout = self.owned.layout() + layout.data = {'operations': [ + {'id': self.operation1.pk, 'x': 0, 'y': 0}, + {'id': self.operation2.pk, 'x': 0, 'y': 0}, + {'id': self.operation3.pk, 'x': 0, 'y': 0}, + ], 'blocks': []} + layout.save() + self.owned.set_arguments(self.operation3.pk, [self.operation1, self.operation2]) self.owned.set_substitutions(self.operation3.pk, [{ 'original': self.ks1X1, @@ -74,9 +82,9 @@ class TestOssViewset(EndpointTester): self.assertEqual(response.data['item_type'], LibraryItemType.OPERATION_SCHEMA) - self.assertEqual(len(response.data['items']), 3) - self.assertEqual(response.data['items'][0]['id'], self.operation1.pk) - self.assertEqual(response.data['items'][0]['operation_type'], self.operation1.operation_type) + self.assertEqual(len(response.data['operations']), 3) + self.assertEqual(response.data['operations'][0]['id'], self.operation1.pk) + self.assertEqual(response.data['operations'][0]['operation_type'], self.operation1.operation_type) self.assertEqual(len(response.data['substitutions']), 1) sub = response.data['substitutions'][0] @@ -95,6 +103,12 @@ class TestOssViewset(EndpointTester): self.assertEqual(arguments[1]['operation'], self.operation3.pk) self.assertEqual(arguments[1]['argument'], self.operation2.pk) + layout = response.data['layout'] + self.assertEqual(layout['blocks'], []) + self.assertEqual(layout['operations'][0], {'id': self.operation1.pk, 'x': 0, 'y': 0}) + self.assertEqual(layout['operations'][1], {'id': self.operation2.pk, 'x': 0, 'y': 0}) + self.assertEqual(layout['operations'][2], {'id': self.operation3.pk, 'x': 0, 'y': 0}) + self.executeOK(item=self.unowned_id) self.executeForbidden(item=self.private_id) @@ -103,401 +117,32 @@ class TestOssViewset(EndpointTester): self.executeOK(item=self.unowned_id) self.executeForbidden(item=self.private_id) - @decl_endpoint('/api/oss/{item}/update-positions', method='patch') - def test_update_positions(self): + @decl_endpoint('/api/oss/{item}/update-layout', method='patch') + def test_update_layout(self): self.populateData() self.executeBadData(item=self.owned_id) - data = {'positions': []} + data = {'operations': [], 'blocks': []} self.executeOK(data=data) - data = {'positions': [ - {'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337}, - {'id': self.operation2.pk, 'position_x': 36.1, 'position_y': 1437}, - {'id': self.invalid_id, 'position_x': 31, 'position_y': 12}, - ]} + data = { + 'operations': [ + {'id': self.operation1.pk, 'x': 42.1, 'y': 1337}, + {'id': self.operation2.pk, 'x': 36.1, 'y': 1437}, + {'id': self.operation3.pk, 'x': 36.1, 'y': 1435} + ], 'blocks': [] + } self.toggle_admin(True) self.executeOK(data=data, item=self.unowned_id) - self.operation1.refresh_from_db() - self.assertNotEqual(self.operation1.position_x, data['positions'][0]['position_x']) - self.assertNotEqual(self.operation1.position_y, data['positions'][0]['position_y']) self.toggle_admin(False) self.executeOK(data=data, item=self.owned_id) - self.operation1.refresh_from_db() - self.operation2.refresh_from_db() - self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x']) - self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y']) - self.assertEqual(self.operation2.position_x, data['positions'][1]['position_x']) - self.assertEqual(self.operation2.position_y, data['positions'][1]['position_y']) + self.owned.refresh_from_db() + self.assertEqual(self.owned.layout().data, data) self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.private_id) - @decl_endpoint('/api/oss/{item}/create-operation', method='post') - def test_create_operation(self): - self.populateData() - self.executeBadData(item=self.owned_id) - - data = { - 'item_data': { - 'alias': 'Test3', - 'title': 'Test title', - 'description': 'Тест кириллицы', - 'position_x': 1, - 'position_y': 1, - }, - 'positions': [ - {'id': self.operation1.pk, 'position_x': 42.1, 'position_y': 1337} - ] - } - self.executeBadData(data=data) - - data['item_data']['operation_type'] = 'invalid' - self.executeBadData(data=data) - - data['item_data']['operation_type'] = OperationType.INPUT - self.executeNotFound(data=data, item=self.invalid_id) - - response = self.executeCreated(data=data, item=self.owned_id) - self.assertEqual(len(response.data['oss']['items']), 4) - new_operation = response.data['new_operation'] - self.assertEqual(new_operation['alias'], data['item_data']['alias']) - self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type']) - self.assertEqual(new_operation['title'], data['item_data']['title']) - self.assertEqual(new_operation['description'], data['item_data']['description']) - self.assertEqual(new_operation['position_x'], data['item_data']['position_x']) - self.assertEqual(new_operation['position_y'], data['item_data']['position_y']) - self.assertEqual(new_operation['result'], None) - self.operation1.refresh_from_db() - self.assertEqual(self.operation1.position_x, data['positions'][0]['position_x']) - self.assertEqual(self.operation1.position_y, data['positions'][0]['position_y']) - - self.executeForbidden(data=data, item=self.unowned_id) - self.toggle_admin(True) - self.executeCreated(data=data, item=self.unowned_id) - - @decl_endpoint('/api/oss/{item}/create-operation', method='post') - def test_create_operation_arguments(self): - self.populateData() - data = { - 'item_data': { - 'alias': 'Test4', - 'operation_type': OperationType.SYNTHESIS - }, - 'positions': [], - 'arguments': [self.operation1.pk, self.operation3.pk] - } - response = self.executeCreated(data=data, item=self.owned_id) - self.owned.refresh_from_db() - new_operation = response.data['new_operation'] - arguments = self.owned.arguments() - self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation1)) - self.assertTrue(arguments.filter(operation__id=new_operation['id'], argument=self.operation3)) - - @decl_endpoint('/api/oss/{item}/create-operation', method='post') - def test_create_operation_result(self): - self.populateData() - - self.operation1.result = None - self.operation1.save() - - data = { - 'item_data': { - 'alias': 'Test4', - 'operation_type': OperationType.INPUT, - 'result': self.ks1.model.pk - }, - 'positions': [], - } - response = self.executeCreated(data=data, item=self.owned_id) - self.owned.refresh_from_db() - new_operation = response.data['new_operation'] - self.assertEqual(new_operation['result'], self.ks1.model.pk) - - @decl_endpoint('/api/oss/{item}/create-operation', method='post') - def test_create_operation_schema(self): - self.populateData() - Editor.add(self.owned.model.pk, self.user2.pk) - data = { - 'item_data': { - 'alias': 'Test4', - 'title': 'Test title', - 'description': 'Comment', - 'operation_type': OperationType.INPUT, - 'result': self.ks1.model.pk - }, - 'create_schema': True, - 'positions': [], - } - self.executeBadData(data=data, item=self.owned_id) - data['item_data']['result'] = None - response = self.executeCreated(data=data, item=self.owned_id) - self.owned.refresh_from_db() - new_operation = response.data['new_operation'] - schema = LibraryItem.objects.get(pk=new_operation['result']) - self.assertEqual(schema.alias, data['item_data']['alias']) - self.assertEqual(schema.title, data['item_data']['title']) - self.assertEqual(schema.description, data['item_data']['description']) - self.assertEqual(schema.visible, False) - self.assertEqual(schema.access_policy, self.owned.model.access_policy) - self.assertEqual(schema.location, self.owned.model.location) - self.assertIn(self.user2, schema.getQ_editors()) - - @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') - def test_delete_operation(self): - self.executeNotFound(item=self.invalid_id) - - self.populateData() - self.executeBadData(item=self.owned_id) - - data = { - 'positions': [] - } - self.executeBadData(data=data) - - data['target'] = self.operation1.pk - self.toggle_admin(True) - self.executeBadData(data=data, item=self.unowned_id) - self.logout() - self.executeForbidden(data=data, item=self.owned_id) - - self.login() - response = self.executeOK(data=data) - self.assertEqual(len(response.data['items']), 2) - - @decl_endpoint('/api/oss/{item}/create-input', method='patch') - def test_create_input(self): - self.populateData() - self.executeBadData(item=self.owned_id) - - data = { - 'positions': [] - } - self.executeBadData(data=data) - - data['target'] = self.operation1.pk - self.toggle_admin(True) - self.executeBadData(data=data, item=self.unowned_id) - self.logout() - self.executeForbidden(data=data, item=self.owned_id) - - self.login() - self.executeBadData(data=data, item=self.owned_id) - - self.operation1.result = None - self.operation1.description = 'TestComment' - self.operation1.title = 'TestTitle' - self.operation1.save() - response = self.executeOK(data=data) - self.operation1.refresh_from_db() - - new_schema = response.data['new_schema'] - self.assertEqual(new_schema['id'], self.operation1.result.pk) - self.assertEqual(new_schema['alias'], self.operation1.alias) - self.assertEqual(new_schema['title'], self.operation1.title) - self.assertEqual(new_schema['description'], self.operation1.description) - - data['target'] = self.operation3.pk - self.executeBadData(data=data) - - @decl_endpoint('/api/oss/{item}/set-input', method='patch') - def test_set_input_null(self): - self.populateData() - self.executeBadData(item=self.owned_id) - - data = { - 'positions': [] - } - self.executeBadData(data=data) - - data['target'] = self.operation1.pk - data['input'] = None - 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.operation1.refresh_from_db() - self.assertEqual(self.operation1.result, None) - - data['input'] = self.ks1.model.pk - self.ks1.model.alias = 'Test42' - self.ks1.model.title = 'Test421' - self.ks1.model.description = 'TestComment42' - self.ks1.save() - response = self.executeOK(data=data) - self.operation1.refresh_from_db() - self.assertEqual(self.operation1.result, self.ks1.model) - self.assertEqual(self.operation1.alias, self.ks1.model.alias) - self.assertEqual(self.operation1.title, self.ks1.model.title) - self.assertEqual(self.operation1.description, self.ks1.model.description) - - @decl_endpoint('/api/oss/{item}/set-input', method='patch') - def test_set_input_change_schema(self): - self.populateData() - self.operation2.result = None - - data = { - 'positions': [], - 'target': self.operation1.pk, - 'input': self.ks2.model.pk - } - self.executeBadData(data=data, item=self.owned_id) - - self.ks2.model.visible = False - self.ks2.model.save(update_fields=['visible']) - data = { - 'positions': [], - 'target': self.operation2.pk, - 'input': None - } - self.executeOK(data=data, item=self.owned_id) - self.operation2.refresh_from_db() - self.ks2.model.refresh_from_db() - self.assertEqual(self.operation2.result, None) - self.assertEqual(self.ks2.model.visible, True) - - data = { - 'positions': [], - 'target': self.operation1.pk, - 'input': self.ks2.model.pk - } - self.executeOK(data=data, item=self.owned_id) - self.operation1.refresh_from_db() - self.assertEqual(self.operation1.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', - 'description': 'Comment mod' - }, - 'positions': [], - 'arguments': [self.operation2.pk, self.operation1.pk], - 'substitutions': [ - { - 'original': self.ks1X1.pk, - 'substitution': ks3x1.pk - } - ] - } - 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.alias, data['item_data']['alias']) - self.assertEqual(self.operation3.title, data['item_data']['title']) - self.assertEqual(self.operation3.description, data['item_data']['description']) - args = self.operation3.getQ_arguments().order_by('order') - self.assertEqual(args[0].argument.pk, data['arguments'][0]) - self.assertEqual(args[0].order, 0) - self.assertEqual(args[1].argument.pk, data['arguments'][1]) - self.assertEqual(args[1].order, 1) - sub = self.operation3.getQ_substitutions()[0] - self.assertEqual(sub.original.pk, data['substitutions'][0]['original']) - self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution']) - - @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', - 'description': 'Comment mod' - }, - 'positions': [], - } - - response = self.executeOK(data=data) - self.operation1.refresh_from_db() - self.assertEqual(self.operation1.alias, data['item_data']['alias']) - self.assertEqual(self.operation1.title, data['item_data']['title']) - self.assertEqual(self.operation1.description, data['item_data']['description']) - 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.description, data['item_data']['description']) - - @decl_endpoint('/api/oss/{item}/update-operation', method='patch') - def test_update_operation_invalid_substitution(self): - self.populateData() - - self.ks1X2 = self.ks1.insert_new('X2') - - data = { - 'target': self.operation3.pk, - 'item_data': { - 'alias': 'Test3 mod', - 'title': 'Test title mod', - 'description': 'Comment mod' - }, - 'positions': [], - 'arguments': [self.operation1.pk, self.operation2.pk], - 'substitutions': [ - { - 'original': self.ks1X1.pk, - 'substitution': self.ks2X1.pk - }, - { - 'original': self.ks2X1.pk, - 'substitution': self.ks1X2.pk - } - ] - } - self.executeBadData(data=data, item=self.owned_id) - - @decl_endpoint('/api/oss/{item}/execute-operation', method='post') - def test_execute_operation(self): - self.populateData() - self.executeBadData(item=self.owned_id) - - data = { - 'positions': [], - 'target': self.operation1.pk - } - self.executeBadData(data=data) - - data['target'] = self.operation3.pk - self.toggle_admin(True) - self.executeBadData(data=data, item=self.unowned_id) - self.logout() - self.executeForbidden(data=data, item=self.owned_id) - - self.login() - self.executeOK(data=data) - self.operation3.refresh_from_db() - schema = self.operation3.result - self.assertEqual(schema.alias, self.operation3.alias) - self.assertEqual(schema.description, self.operation3.description) - self.assertEqual(schema.title, self.operation3.title) - self.assertEqual(schema.visible, False) - items = list(RSForm(schema).constituents()) - self.assertEqual(len(items), 1) - self.assertEqual(items[0].alias, 'X1') - self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved) - @decl_endpoint('/api/oss/get-predecessor', method='post') def test_get_predecessor(self): self.populateData() diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 967ebf36..948d7c5c 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -36,9 +36,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev def get_permissions(self): ''' Determine permission class. ''' if self.action in [ + 'update_layout', 'create_operation', 'delete_operation', - 'update_positions', 'create_input', 'set_input', 'update_operation', @@ -73,21 +73,21 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev ) @extend_schema( - summary='update positions', + summary='update layout', tags=['OSS'], - request=s.PositionsSerializer, + request=s.LayoutSerializer, responses={ c.HTTP_200_OK: None, c.HTTP_403_FORBIDDEN: None, c.HTTP_404_NOT_FOUND: None } ) - @action(detail=True, methods=['patch'], url_path='update-positions') - def update_positions(self, request: Request, pk) -> HttpResponse: - ''' Endpoint: Update operations positions. ''' - serializer = s.PositionsSerializer(data=request.data) + @action(detail=True, methods=['patch'], url_path='update-layout') + def update_layout(self, request: Request, pk) -> HttpResponse: + ''' Endpoint: Update schema layout. ''' + serializer = s.LayoutSerializer(data=request.data) serializer.is_valid(raise_exception=True) - m.OperationSchema(self.get_object()).update_positions(serializer.validated_data['positions']) + m.OperationSchema(self.get_object()).update_layout(serializer.validated_data) return Response(status=c.HTTP_200_OK) @extend_schema( @@ -108,9 +108,16 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev serializer.is_valid(raise_exception=True) oss = m.OperationSchema(self.get_object()) + layout = serializer.validated_data['layout'] with transaction.atomic(): - oss.update_positions(serializer.validated_data['positions']) new_operation = oss.create_operation(**serializer.validated_data['item_data']) + layout['operations'].append({ + 'id': new_operation.pk, + 'x': serializer.validated_data['position_x'], + 'y': serializer.validated_data['position_y'] + }) + oss.update_layout(layout) + schema = new_operation.result if schema is not None: connected_operations = \ @@ -164,9 +171,11 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss = m.OperationSchema(self.get_object()) operation = cast(m.Operation, serializer.validated_data['target']) old_schema = operation.result + layout = serializer.validated_data['layout'] + layout['operations'] = [x for x in layout['operations'] if x['id'] != operation.pk] with transaction.atomic(): - oss.update_positions(serializer.validated_data['positions']) oss.delete_operation(operation.pk, serializer.validated_data['keep_constituents']) + oss.update_layout(layout) if old_schema is not None: if serializer.validated_data['delete_schema']: m.PropagationFacade.before_delete_schema(old_schema) @@ -211,7 +220,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss = m.OperationSchema(self.get_object()) with transaction.atomic(): - oss.update_positions(serializer.validated_data['positions']) + oss.update_layout(serializer.validated_data['layout']) schema = oss.create_input(operation) return Response( @@ -262,7 +271,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev if old_schema.is_synced(oss.model): old_schema.visible = True old_schema.save(update_fields=['visible']) - oss.update_positions(serializer.validated_data['positions']) + oss.update_layout(serializer.validated_data['layout']) oss.set_input(target_operation.pk, schema) return Response( status=c.HTTP_200_OK, @@ -292,7 +301,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 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']) + oss.update_layout(serializer.validated_data['layout']) operation.alias = serializer.validated_data['item_data']['alias'] operation.title = serializer.validated_data['item_data']['title'] operation.description = serializer.validated_data['item_data']['description'] @@ -346,7 +355,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss = m.OperationSchema(self.get_object()) with transaction.atomic(): - oss.update_positions(serializer.validated_data['positions']) + oss.update_layout(serializer.validated_data['layout']) oss.execute_operation(operation) return Response( diff --git a/rsconcept/frontend/src/features/library/backend/use-set-access-policy.tsx b/rsconcept/frontend/src/features/library/backend/use-set-access-policy.tsx index 8ca10b9c..16d4f83f 100644 --- a/rsconcept/frontend/src/features/library/backend/use-set-access-policy.tsx +++ b/rsconcept/frontend/src/features/library/backend/use-set-access-policy.tsx @@ -20,7 +20,7 @@ export const useSetAccessPolicy = () => { client.setQueryData(ossKey, { ...ossData, access_policy: variables.policy }); return Promise.allSettled([ client.invalidateQueries({ queryKey: KEYS.composite.libraryList }), - ...ossData.items + ...ossData.operations .map(item => { if (!item.result) { return; diff --git a/rsconcept/frontend/src/features/library/backend/use-set-editors.tsx b/rsconcept/frontend/src/features/library/backend/use-set-editors.tsx index 9a97e2c2..199bdc21 100644 --- a/rsconcept/frontend/src/features/library/backend/use-set-editors.tsx +++ b/rsconcept/frontend/src/features/library/backend/use-set-editors.tsx @@ -18,7 +18,7 @@ export const useSetEditors = () => { if (ossData) { client.setQueryData(ossKey, { ...ossData, editors: variables.editors }); return Promise.allSettled( - ossData.items + ossData.operations .map(item => { if (!item.result) { return; diff --git a/rsconcept/frontend/src/features/library/backend/use-set-location.tsx b/rsconcept/frontend/src/features/library/backend/use-set-location.tsx index c979170f..3f0a324b 100644 --- a/rsconcept/frontend/src/features/library/backend/use-set-location.tsx +++ b/rsconcept/frontend/src/features/library/backend/use-set-location.tsx @@ -20,7 +20,7 @@ export const useSetLocation = () => { client.setQueryData(ossKey, { ...ossData, location: variables.location }); return Promise.allSettled([ client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), - ...ossData.items + ...ossData.operations .map(item => { if (!item.result) { return; diff --git a/rsconcept/frontend/src/features/library/backend/use-set-owner.tsx b/rsconcept/frontend/src/features/library/backend/use-set-owner.tsx index 826a7788..7060c1e4 100644 --- a/rsconcept/frontend/src/features/library/backend/use-set-owner.tsx +++ b/rsconcept/frontend/src/features/library/backend/use-set-owner.tsx @@ -20,7 +20,7 @@ export const useSetOwner = () => { client.setQueryData(ossKey, { ...ossData, owner: variables.owner }); return Promise.allSettled([ client.invalidateQueries({ queryKey: libraryApi.libraryListKey }), - ...ossData.items + ...ossData.operations .map(item => { if (!item.result) { return; diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts index 2b45dfbf..8dbfac5e 100644 --- a/rsconcept/frontend/src/features/oss/backend/api.ts +++ b/rsconcept/frontend/src/features/oss/backend/api.ts @@ -12,9 +12,9 @@ import { type IOperationCreatedResponse, type IOperationCreateDTO, type IOperationDeleteDTO, - type IOperationPosition, type IOperationSchemaDTO, type IOperationUpdateDTO, + type IOssLayout, type ITargetOperation, schemaConstituentaReference, schemaOperationCreatedResponse, @@ -39,19 +39,11 @@ export const ossApi = { }); }, - updatePositions: ({ - itemID, - positions, - isSilent - }: { - itemID: number; - positions: IOperationPosition[]; - isSilent?: boolean; - }) => + updateLayout: ({ itemID, data, isSilent }: { itemID: number; data: IOssLayout; isSilent?: boolean }) => axiosPatch({ - endpoint: `/api/oss/${itemID}/update-positions`, + endpoint: `/api/oss/${itemID}/update-layout`, request: { - data: { positions: positions }, + data: data, successMessage: isSilent ? undefined : infoMsg.changesSaved } }), diff --git a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts index fba0565a..95d83e42 100644 --- a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts +++ b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts @@ -41,7 +41,7 @@ export class OssLoader { } private prepareLookups() { - this.oss.items.forEach(operation => { + this.oss.operations.forEach(operation => { this.operationByID.set(operation.id, operation); this.graph.addNode(operation.id); }); @@ -52,13 +52,16 @@ export class OssLoader { } private extractSchemas() { - this.schemaIDs = this.oss.items.map(operation => operation.result).filter(item => item !== null); + this.schemaIDs = this.oss.operations.map(operation => operation.result).filter(item => item !== null); } private inferOperationAttributes() { this.graph.topologicalOrder().forEach(operationID => { const operation = this.operationByID.get(operationID)!; const schema = this.items.find(item => item.id === operation.result); + const position = this.oss.layout.operations.find(item => item.id === operationID); + operation.x = position?.x ?? 0; + operation.y = position?.y ?? 0; operation.is_consolidation = this.inferConsolidation(operationID); operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location); operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); @@ -82,7 +85,7 @@ export class OssLoader { } private calculateStats(): IOperationSchemaStats { - const items = this.oss.items; + const items = this.oss.operations; return { count_operations: items.length, count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length, diff --git a/rsconcept/frontend/src/features/oss/backend/types.ts b/rsconcept/frontend/src/features/oss/backend/types.ts index b390c4b8..c38ee20a 100644 --- a/rsconcept/frontend/src/features/oss/backend/types.ts +++ b/rsconcept/frontend/src/features/oss/backend/types.ts @@ -23,8 +23,8 @@ export type IOperationDTO = z.infer; /** Represents backend data for {@link IOperationSchema}. */ export type IOperationSchemaDTO = z.infer; -/** Represents {@link IOperation} position. */ -export type IOperationPosition = z.infer; +/** Represents {@link schemaOperation} layout. */ +export type IOssLayout = z.infer; /** Represents {@link IOperation} data, used in creation process. */ export type IOperationCreateDTO = z.infer; @@ -35,7 +35,7 @@ export type IOperationCreatedResponse = z.infer { mutationFn: ossApi.operationUpdate, onSuccess: (data, variables) => { client.setQueryData(KEYS.composite.ossItem({ itemID: data.id }), data); - const schemaID = data.items.find(item => item.id === variables.data.target)?.result; + const schemaID = data.operations.find(item => item.id === variables.data.target)?.result; if (!schemaID) { return; } diff --git a/rsconcept/frontend/src/features/oss/backend/use-update-positions.tsx b/rsconcept/frontend/src/features/oss/backend/use-update-layout.tsx similarity index 50% rename from rsconcept/frontend/src/features/oss/backend/use-update-positions.tsx rename to rsconcept/frontend/src/features/oss/backend/use-update-layout.tsx index 269eac33..724865b9 100644 --- a/rsconcept/frontend/src/features/oss/backend/use-update-positions.tsx +++ b/rsconcept/frontend/src/features/oss/backend/use-update-layout.tsx @@ -5,21 +5,33 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest import { KEYS } from '@/backend/configuration'; import { ossApi } from './api'; -import { type IOperationPosition } from './types'; +import { type IOperationSchemaDTO, type IOssLayout } from './types'; -export const useUpdatePositions = () => { +export const useUpdateLayout = () => { const client = useQueryClient(); const { updateTimestamp } = useUpdateTimestamp(); const mutation = useMutation({ - mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-positions'], - mutationFn: ossApi.updatePositions, - onSuccess: (_, variables) => updateTimestamp(variables.itemID), + mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'update-layout'], + mutationFn: ossApi.updateLayout, + onSuccess: (_, variables) => { + updateTimestamp(variables.itemID); + client.setQueryData( + ossApi.getOssQueryOptions({ itemID: variables.itemID }).queryKey, + (prev: IOperationSchemaDTO | undefined) => + !prev + ? prev + : { + ...prev, + layout: variables.data + } + ); + }, onError: () => client.invalidateQueries() }); return { - updatePositions: (data: { + updateLayout: (data: { itemID: number; // - positions: IOperationPosition[]; + data: IOssLayout; isSilent?: boolean; }) => mutation.mutateAsync(data) }; diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-change-input-schema.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-change-input-schema.tsx index e50d30d6..dc80328b 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-change-input-schema.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-change-input-schema.tsx @@ -13,7 +13,7 @@ import { Label } from '@/components/input'; import { ModalForm } from '@/components/modal'; import { useDialogsStore } from '@/stores/dialogs'; -import { type IInputUpdateDTO, type IOperationPosition, schemaInputUpdate } from '../backend/types'; +import { type IInputUpdateDTO, type IOssLayout, schemaInputUpdate } from '../backend/types'; import { useInputUpdate } from '../backend/use-input-update'; import { type IOperation, type IOperationSchema } from '../models/oss'; import { sortItemsForOSS } from '../models/oss-api'; @@ -21,18 +21,18 @@ import { sortItemsForOSS } from '../models/oss-api'; export interface DlgChangeInputSchemaProps { oss: IOperationSchema; target: IOperation; - positions: IOperationPosition[]; + layout: IOssLayout; } export function DlgChangeInputSchema() { - const { oss, target, positions } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps); + const { oss, target, layout } = useDialogsStore(state => state.props as DlgChangeInputSchemaProps); const { inputUpdate } = useInputUpdate(); const { setValue, handleSubmit, control } = useForm({ resolver: zodResolver(schemaInputUpdate), defaultValues: { target: target.id, - positions: positions, + layout: layout, input: target.result } }); diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx index c22a9a1b..392a9088 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx @@ -10,12 +10,7 @@ import { ModalForm } from '@/components/modal'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { useDialogsStore } from '@/stores/dialogs'; -import { - type IOperationCreateDTO, - type IOperationPosition, - OperationType, - schemaOperationCreate -} from '../../backend/types'; +import { type IOperationCreateDTO, type IOssLayout, OperationType, schemaOperationCreate } from '../../backend/types'; import { useOperationCreate } from '../../backend/use-operation-create'; import { describeOperationType, labelOperationType } from '../../labels'; import { type IOperationSchema } from '../../models/oss'; @@ -26,7 +21,7 @@ import { TabSynthesisOperation } from './tab-synthesis-operation'; export interface DlgCreateOperationProps { oss: IOperationSchema; - positions: IOperationPosition[]; + layout: IOssLayout; initialInputs: number[]; defaultX: number; defaultY: number; @@ -42,7 +37,7 @@ export type TabID = (typeof TabID)[keyof typeof TabID]; export function DlgCreateOperation() { const { operationCreate } = useOperationCreate(); - const { oss, positions, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( + const { oss, layout, initialInputs, onCreate, defaultX, defaultY } = useDialogsStore( state => state.props as DlgCreateOperationProps ); @@ -51,30 +46,31 @@ export function DlgCreateOperation() { defaultValues: { item_data: { operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS, - result: null, - position_x: defaultX, - position_y: defaultY, alias: '', title: '', - description: '' + description: '', + result: null, + parent: null }, + position_x: defaultX, + position_y: defaultY, arguments: initialInputs, create_schema: false, - positions: positions + layout: layout }, mode: 'onChange' }); const alias = useWatch({ control: methods.control, name: 'item_data.alias' }); const [activeTab, setActiveTab] = useState(initialInputs.length === 0 ? TabID.INPUT : TabID.SYNTHESIS); - const isValid = !!alias && !oss.items.some(operation => operation.alias === alias); + const isValid = !!alias && !oss.operations.some(operation => operation.alias === alias); function onSubmit(data: IOperationCreateDTO) { - const target = calculateInsertPosition(oss, data.arguments, positions, { + const target = calculateInsertPosition(oss, data.arguments, layout, { x: defaultX, y: defaultY }); - data.item_data.position_x = target.x; - data.item_data.position_y = target.y; + data.position_x = target.x; + data.position_y = target.y; void operationCreate({ itemID: oss.id, data: data }).then(response => onCreate?.(response.new_operation.id)); } diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx index 809677c0..6cac05f6 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-synthesis-operation.tsx @@ -50,7 +50,7 @@ export function TabSynthesisOperation() { name='arguments' control={control} render={({ field }) => ( - + )} /> diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-operation.tsx index f7ef43a2..5a16e9b6 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-delete-operation.tsx @@ -9,25 +9,25 @@ import { Checkbox, TextInput } from '@/components/input'; import { ModalForm } from '@/components/modal'; import { useDialogsStore } from '@/stores/dialogs'; -import { type IOperationDeleteDTO, type IOperationPosition, schemaOperationDelete } from '../backend/types'; +import { type IOperationDeleteDTO, type IOssLayout, schemaOperationDelete } from '../backend/types'; import { useOperationDelete } from '../backend/use-operation-delete'; import { type IOperation, type IOperationSchema } from '../models/oss'; export interface DlgDeleteOperationProps { oss: IOperationSchema; target: IOperation; - positions: IOperationPosition[]; + layout: IOssLayout; } export function DlgDeleteOperation() { - const { oss, target, positions } = useDialogsStore(state => state.props as DlgDeleteOperationProps); + const { oss, target, layout } = useDialogsStore(state => state.props as DlgDeleteOperationProps); const { operationDelete } = useOperationDelete(); const { handleSubmit, control } = useForm({ resolver: zodResolver(schemaOperationDelete), defaultValues: { target: target.id, - positions: positions, + layout: layout, keep_constituents: false, delete_schema: false } diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx index 9cc52a6c..6bb35414 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation.tsx @@ -11,12 +11,7 @@ import { ModalForm } from '@/components/modal'; import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; import { useDialogsStore } from '@/stores/dialogs'; -import { - type IOperationPosition, - type IOperationUpdateDTO, - OperationType, - schemaOperationUpdate -} from '../../backend/types'; +import { type IOperationUpdateDTO, type IOssLayout, OperationType, schemaOperationUpdate } from '../../backend/types'; import { useOperationUpdate } from '../../backend/use-operation-update'; import { type IOperation, type IOperationSchema } from '../../models/oss'; @@ -27,7 +22,7 @@ import { TabSynthesis } from './tab-synthesis'; export interface DlgEditOperationProps { oss: IOperationSchema; target: IOperation; - positions: IOperationPosition[]; + layout: IOssLayout; } export const TabID = { @@ -38,7 +33,7 @@ export const TabID = { export type TabID = (typeof TabID)[keyof typeof TabID]; export function DlgEditOperation() { - const { oss, target, positions } = useDialogsStore(state => state.props as DlgEditOperationProps); + const { oss, target, layout } = useDialogsStore(state => state.props as DlgEditOperationProps); const { operationUpdate } = useOperationUpdate(); const methods = useForm({ @@ -55,7 +50,7 @@ export function DlgEditOperation() { original: sub.original, substitution: sub.substitution })), - positions: positions + layout: layout }, mode: 'onChange' }); diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx index 5d94f45a..b626a8ea 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-edit-operation/tab-arguments.tsx @@ -13,7 +13,7 @@ export function TabArguments() { const { control, setValue } = useFormContext(); const { oss, target } = useDialogsStore(state => state.props as DlgEditOperationProps); const potentialCycle = [target.id, ...oss.graph.expandAllOutputs([target.id])]; - const filtered = oss.items.filter(item => !potentialCycle.includes(item.id)); + const filtered = oss.operations.filter(item => !potentialCycle.includes(item.id)); function handleChangeArguments(prev: number[], newValue: number[]) { setValue('arguments', newValue, { shouldValidate: true }); diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx index d82117c3..5cf66ec3 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-relocate-constituents.tsx @@ -16,9 +16,9 @@ import { Loader } from '@/components/loader'; import { ModalForm } from '@/components/modal'; import { useDialogsStore } from '@/stores/dialogs'; -import { type ICstRelocateDTO, type IOperationPosition, schemaCstRelocate } from '../backend/types'; +import { type ICstRelocateDTO, type IOssLayout, schemaCstRelocate } from '../backend/types'; import { useRelocateConstituents } from '../backend/use-relocate-constituents'; -import { useUpdatePositions } from '../backend/use-update-positions'; +import { useUpdateLayout } from '../backend/use-update-layout'; import { IconRelocationUp } from '../components/icon-relocation-up'; import { type IOperation, type IOperationSchema } from '../models/oss'; import { getRelocateCandidates } from '../models/oss-api'; @@ -26,13 +26,13 @@ import { getRelocateCandidates } from '../models/oss-api'; export interface DlgRelocateConstituentsProps { oss: IOperationSchema; initialTarget?: IOperation; - positions: IOperationPosition[]; + layout?: IOssLayout; } export function DlgRelocateConstituents() { - const { oss, initialTarget, positions } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps); + const { oss, initialTarget, layout } = useDialogsStore(state => state.props as DlgRelocateConstituentsProps); const { items: libraryItems } = useLibrary(); - const { updatePositions } = useUpdatePositions(); + const { updateLayout: updatePositions } = useUpdateLayout(); const { relocateConstituents } = useRelocateConstituents(); const { @@ -55,7 +55,7 @@ export function DlgRelocateConstituents() { libraryItems.find(item => item.id === initialTarget?.result) ?? null ); - const operation = oss.items.find(item => item.result === source?.id); + const operation = oss.operations.find(item => item.result === source?.id); const sourceSchemas = libraryItems.filter(item => oss.schemas.includes(item.id)); const destinationSchemas = (() => { if (!operation) { @@ -73,7 +73,7 @@ export function DlgRelocateConstituents() { if (!sourceData.schema || !destinationItem || !operation) { return []; } - const destinationOperation = oss.items.find(item => item.result === destination); + const destinationOperation = oss.operations.find(item => item.result === destination); return getRelocateCandidates(operation.id, destinationOperation!.id, sourceData.schema, oss); })(); @@ -98,17 +98,13 @@ export function DlgRelocateConstituents() { } function onSubmit(data: ICstRelocateDTO) { - const positionsUnchanged = positions.every(item => { - const operation = oss.operationByID.get(item.id)!; - return operation.position_x === item.position_x && operation.position_y === item.position_y; - }); - if (positionsUnchanged) { + if (!layout || JSON.stringify(layout) === JSON.stringify(oss.layout)) { return relocateConstituents(data); } else { return updatePositions({ isSilent: true, itemID: oss.id, - positions: positions + data: layout }).then(() => relocateConstituents(data)); } } diff --git a/rsconcept/frontend/src/features/oss/models/oss-api.ts b/rsconcept/frontend/src/features/oss/models/oss-api.ts index 0700b9fd..4ee4cf40 100644 --- a/rsconcept/frontend/src/features/oss/models/oss-api.ts +++ b/rsconcept/frontend/src/features/oss/models/oss-api.ts @@ -23,7 +23,7 @@ import { infoMsg } from '@/utils/labels'; import { TextMatcher } from '@/utils/utils'; import { Graph } from '../../../models/graph'; -import { type IOperationPosition } from '../backend/types'; +import { type IOssLayout } from '../backend/types'; import { describeSubstitutionError } from '../labels'; import { type IOperation, type IOperationSchema, SubstitutionErrorType } from './oss'; @@ -494,40 +494,39 @@ export function getRelocateCandidates( export function calculateInsertPosition( oss: IOperationSchema, argumentsOps: number[], - positions: IOperationPosition[], + layout: IOssLayout, defaultPosition: Position2D ): Position2D { const result = defaultPosition; - if (positions.length === 0) { + const operations = layout.operations; + if (operations.length === 0) { return result; } if (argumentsOps.length === 0) { - let inputsPositions = positions.filter(pos => - oss.items.find(operation => operation.arguments.length === 0 && operation.id === pos.id) + let inputsPositions = operations.filter(pos => + oss.operations.find(operation => operation.arguments.length === 0 && operation.id === pos.id) ); if (inputsPositions.length === 0) { - inputsPositions = positions; + inputsPositions = operations; } - const maxX = Math.max(...inputsPositions.map(node => node.position_x)); - const minY = Math.min(...inputsPositions.map(node => node.position_y)); + const maxX = Math.max(...inputsPositions.map(node => node.x)); + const minY = Math.min(...inputsPositions.map(node => node.y)); result.x = maxX + DISTANCE_X; result.y = minY; } else { - const argNodes = positions.filter(pos => argumentsOps.includes(pos.id)); - const maxY = Math.max(...argNodes.map(node => node.position_y)); - const minX = Math.min(...argNodes.map(node => node.position_x)); - const maxX = Math.max(...argNodes.map(node => node.position_x)); + const argNodes = operations.filter(pos => argumentsOps.includes(pos.id)); + const maxY = Math.max(...argNodes.map(node => node.y)); + const minX = Math.min(...argNodes.map(node => node.x)); + const maxX = Math.max(...argNodes.map(node => node.x)); result.x = Math.ceil((maxX + minX) / 2 / GRID_SIZE) * GRID_SIZE; result.y = maxY + DISTANCE_Y; } let flagIntersect = false; do { - flagIntersect = positions.some( - position => - Math.abs(position.position_x - result.x) < MIN_DISTANCE && - Math.abs(position.position_y - result.y) < MIN_DISTANCE + flagIntersect = operations.some( + position => Math.abs(position.x - result.x) < MIN_DISTANCE && Math.abs(position.y - result.y) < MIN_DISTANCE ); if (flagIntersect) { result.x += MIN_DISTANCE; diff --git a/rsconcept/frontend/src/features/oss/models/oss.ts b/rsconcept/frontend/src/features/oss/models/oss.ts index b0651a70..cea0da7e 100644 --- a/rsconcept/frontend/src/features/oss/models/oss.ts +++ b/rsconcept/frontend/src/features/oss/models/oss.ts @@ -8,6 +8,8 @@ import { type ICstSubstituteInfo, type IOperationDTO, type IOperationSchemaDTO } /** Represents Operation. */ export interface IOperation extends IOperationDTO { + x: number; + y: number; is_owned: boolean; is_consolidation: boolean; // aka 'diamond synthesis' substitutions: ICstSubstituteInfo[]; @@ -25,7 +27,7 @@ export interface IOperationSchemaStats { /** Represents OperationSchema. */ export interface IOperationSchema extends IOperationSchemaDTO { - items: IOperation[]; + operations: IOperation[]; graph: Graph; schemas: number[]; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx index a7e3baf9..f29a9474 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx @@ -27,7 +27,7 @@ import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { type IOperation } from '../../../models/oss'; import { useOssEdit } from '../oss-edit-context'; -import { useGetPositions } from './use-get-positions'; +import { useGetLayout } from './use-get-layout'; // pixels - size of OSS context menu const MENU_WIDTH = 200; @@ -49,7 +49,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: const { items: libraryItems } = useLibrary(); const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit(); const isProcessing = useMutatingOss(); - const getPositions = useGetPositions(); + const getLayout = useGetLayout(); const { inputCreate } = useInputCreate(); const { operationExecute } = useOperationExecute(); @@ -104,7 +104,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: showEditInput({ oss: schema, target: operation, - positions: getPositions() + layout: getLayout() }); } @@ -116,7 +116,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: showEditOperation({ oss: schema, target: operation, - positions: getPositions() + layout: getLayout() }); } @@ -128,7 +128,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: showDeleteOperation({ oss: schema, target: operation, - positions: getPositions() + layout: getLayout() }); } @@ -139,7 +139,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: onHide(); void operationExecute({ itemID: schema.id, // - data: { target: operation.id, positions: getPositions() } + data: { target: operation.id, layout: getLayout() } }); } @@ -154,7 +154,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: onHide(); void inputCreate({ itemID: schema.id, - data: { target: operation.id, positions: getPositions() } + data: { target: operation.id, layout: getLayout() } }).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true })); } @@ -166,7 +166,7 @@ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: showRelocateConstituents({ oss: schema, initialTarget: operation, - positions: getPositions() + layout: getLayout() }); } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx index 43f85afd..f8a31132 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx @@ -16,7 +16,7 @@ import { useDialogsStore } from '@/stores/dialogs'; import { PARAMETER } from '@/utils/constants'; import { useMutatingOss } from '../../../backend/use-mutating-oss'; -import { useUpdatePositions } from '../../../backend/use-update-positions'; +import { useUpdateLayout } from '../../../backend/use-update-layout'; import { GRID_SIZE } from '../../../models/oss-api'; import { type OssNode } from '../../../models/oss-layout'; import { useOperationTooltipStore } from '../../../stores/operation-tooltip'; @@ -26,7 +26,7 @@ import { useOssEdit } from '../oss-edit-context'; import { OssNodeTypes } from './graph/oss-node-types'; import { type ContextMenuData, NodeContextMenu } from './node-context-menu'; import { ToolbarOssGraph } from './toolbar-oss-graph'; -import { useGetPositions } from './use-get-positions'; +import { useGetLayout } from './use-get-layout'; const ZOOM_MAX = 2; const ZOOM_MIN = 0.5; @@ -52,8 +52,8 @@ export function OssFlow() { const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeStraight = useOSSGraphStore(state => state.edgeStraight); - const getPositions = useGetPositions(); - const { updatePositions } = useUpdatePositions(); + const getLayout = useGetLayout(); + const { updateLayout: updatePositions } = useUpdateLayout(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -78,10 +78,10 @@ export function OssFlow() { useEffect(() => { setNodes( - schema.items.map(operation => ({ + schema.operations.map(operation => ({ id: String(operation.id), data: { label: operation.alias, operation: operation }, - position: { x: operation.position_x, y: operation.position_y }, + position: { x: operation.x, y: operation.y }, type: operation.operation_type.toString() })) ); @@ -93,8 +93,7 @@ export function OssFlow() { type: edgeStraight ? 'straight' : 'simplebezier', animated: edgeAnimate, targetHandle: - schema.operationByID.get(argument.argument)!.position_x > - schema.operationByID.get(argument.operation)!.position_x + schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x ? 'right' : 'left' })) @@ -103,16 +102,7 @@ export function OssFlow() { }, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]); function handleSavePositions() { - const positions = getPositions(); - void updatePositions({ itemID: schema.id, positions: positions }).then(() => { - positions.forEach(item => { - const operation = schema.operationByID.get(item.id); - if (operation) { - operation.position_x = item.position_x; - operation.position_y = item.position_y; - } - }); - }); + void updatePositions({ itemID: schema.id, data: getLayout() }); } function handleCreateOperation() { @@ -121,7 +111,7 @@ export function OssFlow() { oss: schema, defaultX: targetPosition.x, defaultY: targetPosition.y, - positions: getPositions(), + layout: getLayout(), initialInputs: selected, onCreate: () => setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout) @@ -139,7 +129,7 @@ export function OssFlow() { showDeleteOperation({ oss: schema, target: operation, - positions: getPositions() + layout: getLayout() }); } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx index 6dbee7ba..0124d4dd 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/toolbar-oss-graph.tsx @@ -6,7 +6,7 @@ import clsx from 'clsx'; import { HelpTopic } from '@/features/help'; import { BadgeHelp } from '@/features/help/components'; import { useOperationExecute } from '@/features/oss/backend/use-operation-execute'; -import { useUpdatePositions } from '@/features/oss/backend/use-update-positions'; +import { useUpdateLayout } from '@/features/oss/backend/use-update-layout'; import { MiniButton } from '@/components/control'; import { @@ -34,7 +34,7 @@ import { useOSSGraphStore } from '../../../stores/oss-graph'; import { useOssEdit } from '../oss-edit-context'; import { VIEW_PADDING } from './oss-flow'; -import { useGetPositions } from './use-get-positions'; +import { useGetLayout } from './use-get-layout'; interface ToolbarOssGraphProps extends Styling { onCreate: () => void; @@ -53,7 +53,7 @@ export function ToolbarOssGraph({ const isProcessing = useMutatingOss(); const { fitView } = useReactFlow(); const selectedOperation = schema.operationByID.get(selected[0]); - const getPositions = useGetPositions(); + const getLayout = useGetLayout(); const showGrid = useOSSGraphStore(state => state.showGrid); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); @@ -62,7 +62,7 @@ export function ToolbarOssGraph({ const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate); const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight); - const { updatePositions } = useUpdatePositions(); + const { updateLayout: updatePositions } = useUpdateLayout(); const { operationExecute } = useOperationExecute(); const showEditOperation = useDialogsStore(state => state.showEditOperation); @@ -93,16 +93,7 @@ export function ToolbarOssGraph({ } function handleSavePositions() { - const positions = getPositions(); - void updatePositions({ itemID: schema.id, positions: positions }).then(() => { - positions.forEach(item => { - const operation = schema.operationByID.get(item.id); - if (operation) { - operation.position_x = item.position_x; - operation.position_y = item.position_y; - } - }); - }); + void updatePositions({ itemID: schema.id, data: getLayout() }); } function handleOperationExecute() { @@ -111,7 +102,7 @@ export function ToolbarOssGraph({ } void operationExecute({ itemID: schema.id, // - data: { target: selectedOperation.id, positions: getPositions() } + data: { target: selectedOperation.id, layout: getLayout() } }); } @@ -122,7 +113,7 @@ export function ToolbarOssGraph({ showEditOperation({ oss: schema, target: selectedOperation, - positions: getPositions() + layout: getLayout() }); } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx new file mode 100644 index 00000000..05114890 --- /dev/null +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-layout.tsx @@ -0,0 +1,17 @@ +import { useReactFlow } from 'reactflow'; + +import { type IOssLayout } from '@/features/oss/backend/types'; + +export function useGetLayout() { + const { getNodes } = useReactFlow(); + return function getLayout(): IOssLayout { + return { + operations: getNodes().map(node => ({ + id: Number(node.id), + x: node.position.x, + y: node.position.y + })), + blocks: [] + }; + }; +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-positions.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-positions.tsx deleted file mode 100644 index 421885c1..00000000 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/use-get-positions.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useReactFlow } from 'reactflow'; - -export function useGetPositions() { - const { getNodes } = useReactFlow(); - return function getPositions() { - return getNodes().map(node => ({ - id: Number(node.id), - position_x: node.position.x, - position_y: node.position.y - })); - }; -} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/menu-edit-oss.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/menu-edit-oss.tsx index 8d941324..49d19c3c 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/menu-edit-oss.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/menu-edit-oss.tsx @@ -21,8 +21,7 @@ export function MenuEditOss() { menu.hide(); showRelocateConstituents({ oss: schema, - initialTarget: undefined, - positions: [] + initialTarget: undefined }); }