diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index 03bd35ae..31517346 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -5,9 +5,11 @@ from .data_access import ( ArgumentSerializer, BlockSerializer, CreateBlockSerializer, - CreateOperationSerializer, + CreateSchemaSerializer, + CreateSynthesisSerializer, DeleteBlockSerializer, DeleteOperationSerializer, + ImportSchemaSerializer, MoveItemsSerializer, OperationSchemaSerializer, OperationSerializer, diff --git a/rsconcept/backend/apps/oss/serializers/basics.py b/rsconcept/backend/apps/oss/serializers/basics.py index ec568260..04077c91 100644 --- a/rsconcept/backend/apps/oss/serializers/basics.py +++ b/rsconcept/backend/apps/oss/serializers/basics.py @@ -2,8 +2,16 @@ from rest_framework import serializers +class PositionSerializer(serializers.Serializer): + ''' Serializer: Position data. ''' + x = serializers.FloatField() + y = serializers.FloatField() + width = serializers.FloatField() + height = serializers.FloatField() + + class NodeSerializer(serializers.Serializer): - ''' Block position. ''' + ''' Oss node serializer. ''' nodeID = serializers.CharField() x = serializers.FloatField() y = serializers.FloatField() diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index fe5a644c..09ef44dd 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -13,7 +13,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase from shared import messages as msg from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType -from .basics import NodeSerializer, SubstitutionExSerializer +from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer class OperationSerializer(serializers.ModelSerializer): @@ -58,10 +58,7 @@ class CreateBlockSerializer(serializers.Serializer): child=NodeSerializer() ) item_data = BlockCreateData() - width = serializers.FloatField() - height = serializers.FloatField() - position_x = serializers.FloatField() - position_y = serializers.FloatField() + position = PositionSerializer() children_operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id')) children_blocks = PKField(many=True, queryset=Block.objects.all().only('oss_id')) @@ -193,30 +190,21 @@ class MoveItemsSerializer(serializers.Serializer): return attrs -class CreateOperationSerializer(serializers.Serializer): - ''' Serializer: Operation creation. ''' - class CreateOperationData(serializers.ModelSerializer): - ''' Serializer: Operation creation data. ''' - alias = serializers.CharField() - operation_type = serializers.ChoiceField(OperationType.choices) +class CreateOperationData(serializers.ModelSerializer): + ''' Serializer: Operation creation data. ''' + alias = serializers.CharField() - class Meta: - ''' serializer metadata. ''' - model = Operation - fields = \ - 'alias', 'operation_type', 'title', \ - 'description', 'result', 'parent' + class Meta: + ''' serializer metadata. ''' + model = Operation + fields = 'alias', 'title', 'description', 'parent' - layout = serializers.ListField( - child=NodeSerializer() - ) + +class CreateSchemaSerializer(serializers.Serializer): + ''' Serializer: Schema creation for new operation. ''' + layout = serializers.ListField(child=NodeSerializer()) item_data = CreateOperationData() - width = serializers.FloatField() - height = serializers.FloatField() - position_x = serializers.FloatField() - position_y = serializers.FloatField() - create_schema = serializers.BooleanField(default=False, required=False) - arguments = PKField(many=True, queryset=Operation.objects.all().only('pk'), required=False) + position = PositionSerializer() def validate(self, attrs): oss = cast(LibraryItem, self.context['oss']) @@ -225,14 +213,82 @@ class CreateOperationSerializer(serializers.Serializer): raise serializers.ValidationError({ 'parent': msg.parentNotInOSS() }) + return attrs - if 'arguments' not in attrs: - return attrs + +class ImportSchemaSerializer(serializers.Serializer): + ''' Serializer: Import schema to new operation. ''' + layout = serializers.ListField(child=NodeSerializer()) + item_data = CreateOperationData() + position = PositionSerializer() + + source = PKField( + many=False, + queryset=LibraryItem.objects.filter(item_type=LibraryItemType.RSFORM) + ) # type: ignore + clone_source = serializers.BooleanField() + + def validate(self, attrs): + oss = cast(LibraryItem, self.context['oss']) + parent = attrs['item_data'].get('parent') + if parent is not None and parent.oss_id != oss.pk: + raise serializers.ValidationError({ + 'parent': msg.parentNotInOSS() + }) + return attrs + + +class CreateSynthesisSerializer(serializers.Serializer): + ''' Serializer: Synthesis operation creation. ''' + layout = serializers.ListField(child=NodeSerializer()) + item_data = CreateOperationData() + position = PositionSerializer() + + arguments = PKField( + many=True, + queryset=Operation.objects.all().only('pk') + ) + substitutions = serializers.ListField( + child=SubstitutionSerializerBase(), + ) + + def validate(self, attrs): + oss = cast(LibraryItem, self.context['oss']) + parent = attrs['item_data'].get('parent') + if parent is not None and parent.oss_id != oss.pk: + raise serializers.ValidationError({ + 'parent': msg.parentNotInOSS() + }) for operation in attrs['arguments']: if operation.oss_id != oss.pk: raise serializers.ValidationError({ 'arguments': msg.operationNotInOSS() }) + + schemas = [arg.result_id for arg in attrs['arguments'] if arg.result is not None] + substitutions = attrs['substitutions'] + to_delete = {x['original'].pk for x in substitutions} + deleted = set() + for item in substitutions: + original_cst = cast(Constituenta, item['original']) + substitution_cst = cast(Constituenta, item['substitution']) + if original_cst.schema_id not in schemas: + raise serializers.ValidationError({ + f'{original_cst.pk}': msg.constituentaNotFromOperation() + }) + if substitution_cst.schema_id not in schemas: + raise serializers.ValidationError({ + f'{substitution_cst.pk}': msg.constituentaNotFromOperation() + }) + if original_cst.pk in deleted or substitution_cst.pk in to_delete: + raise serializers.ValidationError({ + f'{original_cst.pk}': msg.substituteDouble(original_cst.alias) + }) + if original_cst.schema_id == substitution_cst.schema_id: + raise serializers.ValidationError({ + 'alias': msg.substituteTrivial(original_cst.alias) + }) + deleted.add(original_cst.pk) return attrs diff --git a/rsconcept/backend/apps/oss/serializers/responses.py b/rsconcept/backend/apps/oss/serializers/responses.py index 539aa9c4..8f9c083f 100644 --- a/rsconcept/backend/apps/oss/serializers/responses.py +++ b/rsconcept/backend/apps/oss/serializers/responses.py @@ -3,18 +3,18 @@ from rest_framework import serializers from apps.library.serializers import LibraryItemSerializer -from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer +from .data_access import OperationSchemaSerializer class OperationCreatedResponse(serializers.Serializer): ''' Serializer: Create operation response. ''' - new_operation = OperationSerializer() + new_operation = serializers.IntegerField() oss = OperationSchemaSerializer() class BlockCreatedResponse(serializers.Serializer): ''' Serializer: Create block response. ''' - new_block = BlockSerializer() + new_block = serializers.IntegerField() oss = OperationSchemaSerializer() diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py index ac1c608a..e319c458 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_blocks.py @@ -73,10 +73,12 @@ class TestOssBlocks(EndpointTester): 'description': 'Тест кириллицы', }, 'layout': self.layout_data, - 'position_x': 1337, - 'position_y': 1337, - 'width': 0.42, - 'height': 0.42, + 'position': { + 'x': 1337, + 'y': 1337, + 'width': 0.42, + 'height': 0.42 + }, 'children_operations': [], 'children_blocks': [] } @@ -86,14 +88,11 @@ class TestOssBlocks(EndpointTester): self.assertEqual(len(response.data['oss']['blocks']), 3) new_block = response.data['new_block'] layout = response.data['oss']['layout'] - item = [item for item in layout if item['nodeID'] == 'b' + str(new_block['id'])][0] - self.assertEqual(new_block['title'], data['item_data']['title']) - self.assertEqual(new_block['description'], data['item_data']['description']) - self.assertEqual(new_block['parent'], None) - self.assertEqual(item['x'], data['position_x']) - self.assertEqual(item['y'], data['position_y']) - self.assertEqual(item['width'], data['width']) - self.assertEqual(item['height'], data['height']) + block_node = [item for item in layout if item['nodeID'] == 'b' + str(new_block)][0] + self.assertEqual(block_node['x'], data['position']['x']) + self.assertEqual(block_node['y'], data['position']['y']) + self.assertEqual(block_node['width'], data['position']['width']) + self.assertEqual(block_node['height'], data['position']['height']) self.operation1.refresh_from_db() self.executeForbidden(data=data, item=self.unowned_id) @@ -111,10 +110,12 @@ class TestOssBlocks(EndpointTester): 'parent': self.invalid_id }, 'layout': self.layout_data, - 'position_x': 1337, - 'position_y': 1337, - 'width': 0.42, - 'height': 0.42, + 'position': { + 'x': 1337, + 'y': 1337, + 'width': 0.42, + 'height': 0.42 + }, 'children_operations': [], 'children_blocks': [] } @@ -126,7 +127,8 @@ class TestOssBlocks(EndpointTester): data['item_data']['parent'] = self.block1.pk response = self.executeCreated(data=data) new_block = response.data['new_block'] - self.assertEqual(new_block['parent'], self.block1.pk) + block_data = next((block for block in response.data['oss']['blocks'] if block['id'] == new_block), None) + self.assertEqual(block_data['parent'], self.block1.pk) @decl_endpoint('/api/oss/{item}/create-block', method='post') @@ -138,10 +140,12 @@ class TestOssBlocks(EndpointTester): 'description': 'Тест кириллицы', }, 'layout': self.layout_data, - 'position_x': 1337, - 'position_y': 1337, - 'width': 0.42, - 'height': 0.42, + 'position': { + 'x': 1337, + 'y': 1337, + 'width': 0.42, + 'height': 0.42 + }, 'children_operations': [self.invalid_id], 'children_blocks': [] } @@ -162,8 +166,8 @@ class TestOssBlocks(EndpointTester): new_block = response.data['new_block'] self.operation1.refresh_from_db() self.block1.refresh_from_db() - self.assertEqual(self.operation1.parent.pk, new_block['id']) - self.assertEqual(self.block1.parent.pk, new_block['id']) + self.assertEqual(self.operation1.parent.pk, new_block) + self.assertEqual(self.block1.parent.pk, new_block) @decl_endpoint('/api/oss/{item}/create-block', method='post') @@ -176,10 +180,12 @@ class TestOssBlocks(EndpointTester): 'parent': self.block2.pk }, 'layout': self.layout_data, - 'position_x': 1337, - 'position_y': 1337, - 'width': 0.42, - 'height': 0.42, + 'position': { + 'x': 1337, + 'y': 1337, + 'width': 0.42, + 'height': 0.42 + }, 'children_operations': [], 'children_blocks': [self.block1.pk] } diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py index b1833f31..d1ad5c8f 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_operations.py @@ -70,9 +70,10 @@ class TestOssOperations(EndpointTester): }]) - @decl_endpoint('/api/oss/{item}/create-operation', method='post') - def test_create_operation(self): + @decl_endpoint('/api/oss/{item}/create-schema', method='post') + def test_create_schema(self): self.populateData() + Editor.add(self.owned.model.pk, self.user2.pk) self.executeBadData(item=self.owned_id) data = { @@ -80,47 +81,50 @@ class TestOssOperations(EndpointTester): 'alias': 'Test3', 'title': 'Test title', 'description': 'Тест кириллицы', - + 'parent': None }, 'layout': self.layout_data, - 'position_x': 1, - 'position_y': 1, - 'width': 500, - 'height': 50 - + 'position': { + 'x': 1, + 'y': 1, + 'width': 500, + 'height': 50 + } } - 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'] + new_operation_id = response.data['new_operation'] + new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) layout = response.data['oss']['layout'] - item = [item for item in layout if item['nodeID'] == 'o' + str(new_operation['id'])][0] + operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0] + schema = LibraryItem.objects.get(pk=new_operation['result']) self.assertEqual(new_operation['alias'], data['item_data']['alias']) - self.assertEqual(new_operation['operation_type'], data['item_data']['operation_type']) + self.assertEqual(new_operation['operation_type'], OperationType.INPUT) 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.assertEqual(item['width'], data['width']) - self.assertEqual(item['height'], data['height']) - self.operation1.refresh_from_db() + self.assertNotEqual(new_operation['result'], None) + self.assertEqual(operation_node['x'], data['position']['x']) + self.assertEqual(operation_node['y'], data['position']['y']) + self.assertEqual(operation_node['width'], data['position']['width']) + self.assertEqual(operation_node['height'], data['position']['height']) + 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()) 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_parent(self): + @decl_endpoint('/api/oss/{item}/create-schema', method='post') + def test_create_schema_parent(self): self.populateData() data = { 'item_data': { @@ -128,14 +132,15 @@ class TestOssOperations(EndpointTester): 'alias': 'Test3', 'title': 'Test title', 'description': '', - 'operation_type': OperationType.INPUT }, 'layout': self.layout_data, - 'position_x': 1, - 'position_y': 1, - 'width': 500, - 'height': 50 + 'position': { + 'x': 1, + 'y': 1, + 'width': 500, + 'height': 50 + } } self.executeBadData(data=data, item=self.owned_id) @@ -147,90 +152,40 @@ class TestOssOperations(EndpointTester): block_owned = self.owned.create_block(title='TestBlock2') data['item_data']['parent'] = block_owned.id response = self.executeCreated(data=data, item=self.owned_id) + new_operation_id = response.data['new_operation'] + new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) self.assertEqual(len(response.data['oss']['operations']), 4) - new_operation = response.data['new_operation'] self.assertEqual(new_operation['parent'], block_owned.id) - @decl_endpoint('/api/oss/{item}/create-operation', method='post') - def test_create_operation_arguments(self): + @decl_endpoint('/api/oss/{item}/create-synthesis', method='post') + def test_create_synthesis(self): self.populateData() - data = { - 'item_data': { - 'alias': 'Test4', - 'operation_type': OperationType.SYNTHESIS - }, - 'layout': self.layout_data, - 'position_x': 1, - 'position_y': 1, - 'width': 500, - 'height': 50, - '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, - 'width': 500, - 'height': 50 - } - response = self.executeCreated(data=data, item=self.owned_id) - 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 + 'description': '', + 'parent': None }, - 'create_schema': True, 'layout': self.layout_data, - 'position_x': 1, - 'position_y': 1, - 'width': 500, - 'height': 50 + 'position': { + 'x': 1, + 'y': 1, + 'width': 500, + 'height': 50 + }, + 'arguments': [self.operation1.pk, self.operation3.pk], + 'substitutions': [] } - 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()) + new_operation_id = response.data['new_operation'] + new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) + 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)) + self.assertNotEqual(new_operation['result'], None) @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') @@ -497,3 +452,141 @@ class TestOssOperations(EndpointTester): 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/{item}/import-schema', method='post') + def test_import_schema(self): + self.populateData() + target_ks = RSForm.create( + alias='KS_Target', + title='Target', + owner=self.user + ) + data = { + 'item_data': { + 'alias': 'ImportedAlias', + 'title': 'Imported Title', + 'description': 'Imported Description', + 'parent': None + }, + 'layout': self.layout_data, + 'position': { + 'x': 10, + 'y': 20, + 'width': 300, + 'height': 60 + }, + 'source': target_ks.model.pk, + 'clone_source': False + } + response = self.executeCreated(data=data, item=self.owned_id) + new_operation_id = response.data['new_operation'] + new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) + layout = response.data['oss']['layout'] + operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0] + schema = LibraryItem.objects.get(pk=new_operation['result']) + self.assertEqual(new_operation['alias'], data['item_data']['alias']) + self.assertEqual(new_operation['title'], data['item_data']['title']) + self.assertEqual(new_operation['description'], data['item_data']['description']) + self.assertEqual(new_operation['operation_type'], OperationType.INPUT) + self.assertEqual(schema.pk, target_ks.model.pk) # Not a clone + self.assertEqual(operation_node['x'], data['position']['x']) + self.assertEqual(operation_node['y'], data['position']['y']) + self.assertEqual(operation_node['width'], data['position']['width']) + self.assertEqual(operation_node['height'], data['position']['height']) + self.assertEqual(schema.visible, target_ks.model.visible) + self.assertEqual(schema.access_policy, target_ks.model.access_policy) + self.assertEqual(schema.location, target_ks.model.location) + + @decl_endpoint('/api/oss/{item}/import-schema', method='post') + def test_import_schema_clone(self): + self.populateData() + # Use ks2 as the source RSForm + data = { + 'item_data': { + 'alias': 'ClonedAlias', + 'title': 'Cloned Title', + 'description': 'Cloned Description', + 'parent': None + }, + 'layout': self.layout_data, + 'position': { + 'x': 42, + 'y': 1337, + 'width': 400, + 'height': 80 + }, + 'source': self.ks2.model.pk, + 'clone_source': True + } + response = self.executeCreated(data=data, item=self.owned_id) + new_operation_id = response.data['new_operation'] + new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id) + layout = response.data['oss']['layout'] + operation_node = [item for item in layout if item['nodeID'] == 'o' + str(new_operation_id)][0] + schema = LibraryItem.objects.get(pk=new_operation['result']) + self.assertEqual(new_operation['alias'], data['item_data']['alias']) + self.assertEqual(new_operation['title'], data['item_data']['title']) + self.assertEqual(new_operation['description'], data['item_data']['description']) + self.assertEqual(new_operation['operation_type'], OperationType.INPUT) + self.assertNotEqual(schema.pk, self.ks2.model.pk) # Should be a clone + 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(operation_node['x'], data['position']['x']) + self.assertEqual(operation_node['y'], data['position']['y']) + self.assertEqual(operation_node['width'], data['position']['width']) + self.assertEqual(operation_node['height'], data['position']['height']) + self.assertEqual(schema.visible, False) + self.assertEqual(schema.access_policy, self.owned.model.access_policy) + self.assertEqual(schema.location, self.owned.model.location) + + @decl_endpoint('/api/oss/{item}/import-schema', method='post') + def test_import_schema_bad_data(self): + self.populateData() + # Missing source + data = { + 'item_data': { + 'alias': 'Bad', + 'title': 'Bad', + 'description': 'Bad', + 'parent': None + }, + 'layout': self.layout_data, + 'position': { + 'x': 0, 'y': 0, 'width': 1, 'height': 1 + }, + # 'source' missing + 'clone_source': False + } + self.executeBadData(data=data, item=self.owned_id) + # Invalid source + data['source'] = self.invalid_id + self.executeBadData(data=data, item=self.owned_id) + # Invalid OSS + data['source'] = self.ks1.model.pk + self.executeNotFound(data=data, item=self.invalid_id) + + @decl_endpoint('/api/oss/{item}/import-schema', method='post') + def test_import_schema_permissions(self): + self.populateData() + data = { + 'item_data': { + 'alias': 'PermTest', + 'title': 'PermTest', + 'description': 'PermTest', + 'parent': None + }, + 'layout': self.layout_data, + 'position': { + 'x': 5, 'y': 5, 'width': 10, 'height': 10 + }, + 'source': self.ks1.model.pk, + 'clone_source': False + } + # Not an editor + self.logout() + self.executeForbidden(data=data, item=self.owned_id) + # As admin + self.login() + self.toggle_admin(True) + self.executeCreated(data=data, item=self.owned_id) diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index b796534d..6e3e363d 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -1,8 +1,8 @@ ''' Endpoints for OSS. ''' +from copy import deepcopy from typing import Optional, cast from django.db import transaction -from django.db.models import Q from django.http import HttpResponse from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import generics, serializers @@ -23,6 +23,28 @@ from .. import models as m from .. import serializers as s +def _create_clone(prototype: LibraryItem, operation: m.Operation, oss: LibraryItem) -> LibraryItem: + ''' Create clone of prototype schema for operation. ''' + prototype_schema = RSForm(prototype) + clone = deepcopy(prototype) + clone.pk = None + clone.owner = oss.owner + clone.title = operation.title + clone.alias = operation.alias + clone.description = operation.description + clone.visible = False + clone.read_only = False + clone.access_policy = oss.access_policy + clone.location = oss.location + clone.save() + for cst in prototype_schema.constituents(): + cst_copy = deepcopy(cst) + cst_copy.pk = None + cst_copy.schema = clone + cst_copy.save() + return clone + + @extend_schema(tags=['OSS']) @extend_schema_view() class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): @@ -41,7 +63,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'update_block', 'delete_block', 'move_items', - 'create_operation', + 'create_schema', + 'import_schema', + 'create_synthesis', 'update_operation', 'delete_operation', 'create_input', @@ -116,16 +140,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss = m.OperationSchema(self.get_object()) layout = serializer.validated_data['layout'] + position = serializer.validated_data['position'] children_blocks: list[m.Block] = serializer.validated_data['children_blocks'] children_operations: list[m.Operation] = serializer.validated_data['children_operations'] with transaction.atomic(): new_block = oss.create_block(**serializer.validated_data['item_data']) layout.append({ 'nodeID': 'b' + str(new_block.pk), - 'x': serializer.validated_data['position_x'], - 'y': serializer.validated_data['position_y'], - 'width': serializer.validated_data['width'], - 'height': serializer.validated_data['height'], + 'x': position['x'], + 'y': position['y'], + 'width': position['width'], + 'height': position['height'], }) oss.update_layout(layout) if len(children_blocks) > 0: @@ -140,7 +165,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev return Response( status=c.HTTP_201_CREATED, data={ - 'new_block': s.BlockSerializer(new_block).data, + 'new_block': new_block.pk, 'oss': s.OperationSchemaSerializer(oss.model).data } ) @@ -251,9 +276,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev ) @extend_schema( - summary='create operation', + summary='create empty conceptual schema', tags=['OSS'], - request=s.CreateOperationSerializer(), + request=s.CreateSchemaSerializer(), responses={ c.HTTP_201_CREATED: s.OperationCreatedResponse, c.HTTP_400_BAD_REQUEST: None, @@ -261,10 +286,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev c.HTTP_404_NOT_FOUND: None } ) - @action(detail=True, methods=['post'], url_path='create-operation') - def create_operation(self, request: Request, pk) -> HttpResponse: - ''' Create Operation. ''' - serializer = s.CreateOperationSerializer( + @action(detail=True, methods=['post'], url_path='create-schema') + def create_schema(self, request: Request, pk) -> HttpResponse: + ''' Create schema. ''' + serializer = s.CreateSchemaSerializer( data=request.data, context={'oss': self.get_object()} ) @@ -272,43 +297,124 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev oss = m.OperationSchema(self.get_object()) layout = serializer.validated_data['layout'] + position = serializer.validated_data['position'] + data = serializer.validated_data['item_data'] + data['operation_type'] = m.OperationType.INPUT with transaction.atomic(): new_operation = oss.create_operation(**serializer.validated_data['item_data']) layout.append({ 'nodeID': 'o' + str(new_operation.pk), - 'x': serializer.validated_data['position_x'], - 'y': serializer.validated_data['position_y'], - 'width': serializer.validated_data['width'], - 'height': serializer.validated_data['height'] + 'x': position['x'], + 'y': position['y'], + 'width': position['width'], + 'height': position['height'] }) oss.update_layout(layout) + oss.create_input(new_operation) - schema = new_operation.result - if schema is not None: - connected_operations = \ - m.Operation.objects \ - .filter(Q(result=schema) & ~Q(pk=new_operation.pk)) \ - .only('operation_type', 'oss_id') - for operation in connected_operations: - if operation.operation_type != m.OperationType.INPUT: - raise serializers.ValidationError({ - 'item_data': msg.operationResultFromAnotherOSS() - }) - if operation.oss_id == new_operation.oss_id: - raise serializers.ValidationError({ - 'item_data': msg.operationInputAlreadyConnected() - }) - if new_operation.operation_type == m.OperationType.INPUT and serializer.validated_data['create_schema']: - oss.create_input(new_operation) - if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: - oss.set_arguments( - target=new_operation.pk, - arguments=serializer.validated_data['arguments'] - ) return Response( status=c.HTTP_201_CREATED, data={ - 'new_operation': s.OperationSerializer(new_operation).data, + 'new_operation': new_operation.pk, + 'oss': s.OperationSchemaSerializer(oss.model).data + } + ) + + + @extend_schema( + summary='import conceptual schema to new OSS operation', + tags=['OSS'], + request=s.ImportSchemaSerializer(), + responses={ + c.HTTP_201_CREATED: s.OperationCreatedResponse, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['post'], url_path='import-schema') + def import_schema(self, request: Request, pk) -> HttpResponse: + ''' Create operation with existing schema. ''' + serializer = s.ImportSchemaSerializer( + data=request.data, + context={'oss': self.get_object()} + ) + serializer.is_valid(raise_exception=True) + + oss = m.OperationSchema(self.get_object()) + layout = serializer.validated_data['layout'] + position = serializer.validated_data['position'] + data = serializer.validated_data['item_data'] + data['operation_type'] = m.OperationType.INPUT + if not serializer.validated_data['clone_source']: + data['result'] = serializer.validated_data['source'] + with transaction.atomic(): + new_operation = oss.create_operation(**serializer.validated_data['item_data']) + layout.append({ + 'nodeID': 'o' + str(new_operation.pk), + 'x': position['x'], + 'y': position['y'], + 'width': position['width'], + 'height': position['height'] + }) + oss.update_layout(layout) + + if serializer.validated_data['clone_source']: + prototype: LibraryItem = serializer.validated_data['source'] + new_operation.result = _create_clone(prototype, new_operation, oss.model) + new_operation.save(update_fields=["result"]) + + return Response( + status=c.HTTP_201_CREATED, + data={ + 'new_operation': new_operation.pk, + 'oss': s.OperationSchemaSerializer(oss.model).data + } + ) + + @extend_schema( + summary='create synthesis operation', + tags=['OSS'], + request=s.CreateSynthesisSerializer(), + responses={ + c.HTTP_201_CREATED: s.OperationCreatedResponse, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=True, methods=['post'], url_path='create-synthesis') + def create_synthesis(self, request: Request, pk) -> HttpResponse: + ''' Create Synthesis operation from arguments. ''' + serializer = s.CreateSynthesisSerializer( + data=request.data, + context={'oss': self.get_object()} + ) + serializer.is_valid(raise_exception=True) + + oss = m.OperationSchema(self.get_object()) + layout = serializer.validated_data['layout'] + position = serializer.validated_data['position'] + data = serializer.validated_data['item_data'] + data['operation_type'] = m.OperationType.SYNTHESIS + with transaction.atomic(): + new_operation = oss.create_operation(**serializer.validated_data['item_data']) + layout.append({ + 'nodeID': 'o' + str(new_operation.pk), + 'x': position['x'], + 'y': position['y'], + 'width': position['width'], + 'height': position['height'] + }) + oss.set_arguments(new_operation.pk, serializer.validated_data['arguments']) + oss.set_substitutions(new_operation.pk, serializer.validated_data['substitutions']) + oss.execute_operation(new_operation) + oss.update_layout(layout) + + return Response( + status=c.HTTP_201_CREATED, + data={ + 'new_operation': new_operation.pk, 'oss': s.OperationSchemaSerializer(oss.model).data } ) diff --git a/rsconcept/frontend/src/app/global-dialogs.tsx b/rsconcept/frontend/src/app/global-dialogs.tsx index 6caf8d87..a6bbbff6 100644 --- a/rsconcept/frontend/src/app/global-dialogs.tsx +++ b/rsconcept/frontend/src/app/global-dialogs.tsx @@ -20,9 +20,9 @@ const DlgCloneLibraryItem = React.lazy(() => const DlgCreateCst = React.lazy(() => import('@/features/rsform/dialogs/dlg-create-cst').then(module => ({ default: module.DlgCreateCst })) ); -const DlgCreateOperation = React.lazy(() => - import('@/features/oss/dialogs/dlg-create-operation').then(module => ({ - default: module.DlgCreateOperation +const DlgCreateSynthesis = React.lazy(() => + import('@/features/oss/dialogs/dlg-create-synthesis').then(module => ({ + default: module.DlgCreateSynthesis })) ); const DlgCreateVersion = React.lazy(() => @@ -134,6 +134,12 @@ const DlgEditCst = React.lazy(() => const DlgShowTermGraph = React.lazy(() => import('@/features/oss/dialogs/dlg-show-term-graph').then(module => ({ default: module.DlgShowTermGraph })) ); +const DlgCreateSchema = React.lazy(() => + import('@/features/oss/dialogs/dlg-create-schema').then(module => ({ default: module.DlgCreateSchema })) +); +const DlgImportSchema = React.lazy(() => + import('@/features/oss/dialogs/dlg-import-schema').then(module => ({ default: module.DlgImportSchema })) +); export const GlobalDialogs = () => { const active = useDialogsStore(state => state.active); @@ -146,8 +152,8 @@ export const GlobalDialogs = () => { return ; case DialogType.CREATE_CONSTITUENTA: return ; - case DialogType.CREATE_OPERATION: - return ; + case DialogType.CREATE_SYNTHESIS: + return ; case DialogType.CREATE_BLOCK: return ; case DialogType.EDIT_BLOCK: @@ -198,5 +204,9 @@ export const GlobalDialogs = () => { return ; case DialogType.SHOW_TERM_GRAPH: return ; + case DialogType.CREATE_SCHEMA: + return ; + case DialogType.IMPORT_SCHEMA: + return ; } }; diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts index c27eede9..0b1df28f 100644 --- a/rsconcept/frontend/src/features/oss/backend/api.ts +++ b/rsconcept/frontend/src/features/oss/backend/api.ts @@ -8,9 +8,11 @@ import { type IBlockCreatedResponse, type IConstituentaReference, type ICreateBlockDTO, - type ICreateOperationDTO, + type ICreateSchemaDTO, + type ICreateSynthesisDTO, type IDeleteBlockDTO, type IDeleteOperationDTO, + type IImportSchemaDTO, type IInputCreatedResponse, type IMoveItemsDTO, type IOperationCreatedResponse, @@ -83,13 +85,40 @@ export const ossApi = { } }), - createOperation: ({ itemID, data }: { itemID: number; data: ICreateOperationDTO }) => - axiosPost({ + createSchema: ({ itemID, data }: { itemID: number; data: ICreateSchemaDTO }) => + axiosPost({ schema: schemaOperationCreatedResponse, - endpoint: `/api/oss/${itemID}/create-operation`, + endpoint: `/api/oss/${itemID}/create-schema`, request: { data: data, - successMessage: response => infoMsg.newOperation(response.new_operation.alias) + successMessage: response => { + const alias = response.oss.operations.find(op => op.id === response.new_operation)?.alias; + return infoMsg.newOperation(alias ?? 'ОШИБКА'); + } + } + }), + createSynthesis: ({ itemID, data }: { itemID: number; data: ICreateSynthesisDTO }) => + axiosPost({ + schema: schemaOperationCreatedResponse, + endpoint: `/api/oss/${itemID}/create-synthesis`, + request: { + data: data, + successMessage: response => { + const alias = response.oss.operations.find(op => op.id === response.new_operation)?.alias; + return infoMsg.newOperation(alias ?? 'ОШИБКА'); + } + } + }), + importSchema: ({ itemID, data }: { itemID: number; data: IImportSchemaDTO }) => + axiosPost({ + schema: schemaOperationCreatedResponse, + endpoint: `/api/oss/${itemID}/import-schema`, + request: { + data: data, + successMessage: response => { + const alias = response.oss.operations.find(op => op.id === response.new_operation)?.alias; + return infoMsg.newOperation(alias ?? 'ОШИБКА'); + } } }), updateOperation: ({ itemID, data }: { itemID: number; data: IUpdateOperationDTO }) => diff --git a/rsconcept/frontend/src/features/oss/backend/types.ts b/rsconcept/frontend/src/features/oss/backend/types.ts index 490f070c..1ad509e6 100644 --- a/rsconcept/frontend/src/features/oss/backend/types.ts +++ b/rsconcept/frontend/src/features/oss/backend/types.ts @@ -43,7 +43,9 @@ export type IDeleteBlockDTO = z.infer; export type IMoveItemsDTO = z.infer; /** Represents {@link IOperation} data, used in Create action. */ -export type ICreateOperationDTO = z.infer; +export type ICreateSchemaDTO = z.infer; +export type ICreateSynthesisDTO = z.infer; +export type IImportSchemaDTO = z.infer; /** Represents data response when creating {@link IOperation}. */ export type IOperationCreatedResponse = z.infer; @@ -90,6 +92,13 @@ export const schemaOperation = z.strictObject({ result: z.number().nullable() }); +export const schemaOperationData = schemaOperation.pick({ + alias: true, + title: true, + description: true, + parent: true +}); + export const schemaBlock = z.strictObject({ id: z.number(), oss: z.number(), @@ -98,6 +107,13 @@ export const schemaBlock = z.strictObject({ parent: z.number().nullable() }); +export const schemaPosition = z.strictObject({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number() +}); + export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({ operation: z.number(), original_alias: z.string(), @@ -106,12 +122,8 @@ export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({ substitution_term: z.string() }); -export const schemaNodePosition = z.strictObject({ - nodeID: z.string(), - x: z.number(), - y: z.number(), - width: z.number(), - height: z.number() +export const schemaNodePosition = schemaPosition.extend({ + nodeID: z.string() }); export const schemaOssLayout = z.array(schemaNodePosition); @@ -137,16 +149,13 @@ export const schemaCreateBlock = z.strictObject({ description: z.string(), parent: z.number().nullable() }), - position_x: z.number(), - position_y: z.number(), - width: z.number(), - height: z.number(), + position: schemaPosition, children_operations: z.array(z.number()), children_blocks: z.array(z.number()) }); export const schemaBlockCreatedResponse = z.strictObject({ - new_block: schemaBlock, + new_block: z.number(), oss: schemaOperationSchema }); @@ -165,26 +174,35 @@ export const schemaDeleteBlock = z.strictObject({ layout: schemaOssLayout }); -export const schemaCreateOperation = z.strictObject({ +export const schemaCreateSchema = z.strictObject({ layout: schemaOssLayout, - item_data: z.strictObject({ - alias: z.string().nonempty(), - operation_type: schemaOperationType, - title: z.string(), - description: z.string(), - parent: z.number().nullable(), - result: z.number().nullable() - }), - position_x: z.number(), - position_y: z.number(), - width: z.number(), - height: z.number(), + item_data: schemaOperationData, + position: schemaPosition +}); + +export const schemaCreateSynthesis = z.strictObject({ + layout: schemaOssLayout, + item_data: schemaOperationData, + position: schemaPosition, arguments: z.array(z.number()), - create_schema: z.boolean() + substitutions: z.array(schemaSubstituteConstituents) +}); + +export const schemaImportSchema = z.strictObject({ + layout: schemaOssLayout, + item_data: schemaOperationData, + position: z.strictObject({ + x: z.number(), + y: z.number(), + width: z.number(), + height: z.number() + }), + source: z.number(), + clone_source: z.boolean() }); export const schemaOperationCreatedResponse = z.strictObject({ - new_operation: schemaOperation, + new_operation: z.number(), oss: schemaOperationSchema }); diff --git a/rsconcept/frontend/src/features/oss/backend/use-create-operation.tsx b/rsconcept/frontend/src/features/oss/backend/use-create-schema.tsx similarity index 72% rename from rsconcept/frontend/src/features/oss/backend/use-create-operation.tsx rename to rsconcept/frontend/src/features/oss/backend/use-create-schema.tsx index cec0fb2a..64b2874d 100644 --- a/rsconcept/frontend/src/features/oss/backend/use-create-operation.tsx +++ b/rsconcept/frontend/src/features/oss/backend/use-create-schema.tsx @@ -5,14 +5,14 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest import { KEYS } from '@/backend/configuration'; import { ossApi } from './api'; -import { type ICreateOperationDTO } from './types'; +import { type ICreateSchemaDTO } from './types'; -export const useCreateOperation = () => { +export const useCreateSchema = () => { const client = useQueryClient(); const { updateTimestamp } = useUpdateTimestamp(); const mutation = useMutation({ - mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-operation'], - mutationFn: ossApi.createOperation, + mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-schema'], + mutationFn: ossApi.createSchema, onSuccess: response => { client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss); updateTimestamp(response.oss.id); @@ -20,6 +20,6 @@ export const useCreateOperation = () => { onError: () => client.invalidateQueries() }); return { - createOperation: (data: { itemID: number; data: ICreateOperationDTO }) => mutation.mutateAsync(data) + createSchema: (data: { itemID: number; data: ICreateSchemaDTO }) => mutation.mutateAsync(data) }; }; diff --git a/rsconcept/frontend/src/features/oss/backend/use-create-synthesis.tsx b/rsconcept/frontend/src/features/oss/backend/use-create-synthesis.tsx new file mode 100644 index 00000000..522cd96e --- /dev/null +++ b/rsconcept/frontend/src/features/oss/backend/use-create-synthesis.tsx @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp'; + +import { KEYS } from '@/backend/configuration'; + +import { ossApi } from './api'; +import { type ICreateSynthesisDTO } from './types'; + +export const useCreateSynthesis = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-synthesis'], + mutationFn: ossApi.createSynthesis, + onSuccess: response => { + client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss); + updateTimestamp(response.oss.id); + }, + onError: () => client.invalidateQueries() + }); + return { + createSynthesis: (data: { itemID: number; data: ICreateSynthesisDTO }) => mutation.mutateAsync(data) + }; +}; diff --git a/rsconcept/frontend/src/features/oss/backend/use-import-schema.tsx b/rsconcept/frontend/src/features/oss/backend/use-import-schema.tsx new file mode 100644 index 00000000..385cfaea --- /dev/null +++ b/rsconcept/frontend/src/features/oss/backend/use-import-schema.tsx @@ -0,0 +1,25 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useUpdateTimestamp } from '@/features/library/backend/use-update-timestamp'; + +import { KEYS } from '@/backend/configuration'; + +import { ossApi } from './api'; +import { type IImportSchemaDTO } from './types'; + +export const useImportSchema = () => { + const client = useQueryClient(); + const { updateTimestamp } = useUpdateTimestamp(); + const mutation = useMutation({ + mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'import-schema'], + mutationFn: ossApi.importSchema, + onSuccess: response => { + client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss); + updateTimestamp(response.oss.id); + }, + onError: () => client.invalidateQueries() + }); + return { + importSchema: (data: { itemID: number; data: IImportSchemaDTO }) => mutation.mutateAsync(data) + }; +}; diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx index 253efc41..5124065b 100644 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx +++ b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-block/dlg-create-block.tsx @@ -49,10 +49,12 @@ export function DlgCreateBlock() { description: '', parent: initialParent }, - position_x: defaultX, - position_y: defaultY, - width: BLOCK_NODE_MIN_WIDTH, - height: BLOCK_NODE_MIN_HEIGHT, + position: { + x: defaultX, + y: defaultY, + width: BLOCK_NODE_MIN_WIDTH, + height: BLOCK_NODE_MIN_HEIGHT + }, children_blocks: initialChildren.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id), children_operations: initialChildren.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id), layout: manager.layout @@ -66,13 +68,9 @@ export function DlgCreateBlock() { const isValid = !!title && !manager.oss.blocks.some(block => block.title === title); function onSubmit(data: ICreateBlockDTO) { - const rectangle = manager.newBlockPosition(data); - data.position_x = rectangle.x; - data.position_y = rectangle.y; - data.width = rectangle.width; - data.height = rectangle.height; + data.position = manager.newBlockPosition(data); data.layout = manager.layout; - void createBlock({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_block.id)); + void createBlock({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_block)); } return ( 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 deleted file mode 100644 index 993eeda4..00000000 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/dlg-create-operation.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { FormProvider, useForm, useWatch } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; - -import { HelpTopic } from '@/features/help'; - -import { ModalForm } from '@/components/modal'; -import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs'; -import { useDialogsStore } from '@/stores/dialogs'; - -import { type ICreateOperationDTO, OperationType, schemaCreateOperation } from '../../backend/types'; -import { useCreateOperation } from '../../backend/use-create-operation'; -import { describeOperationType, labelOperationType } from '../../labels'; -import { type LayoutManager, OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../../models/oss-layout-api'; - -import { TabInputOperation } from './tab-input-operation'; -import { TabSynthesisOperation } from './tab-synthesis-operation'; - -export interface DlgCreateOperationProps { - manager: LayoutManager; - initialParent: number | null; - initialInputs: number[]; - defaultX: number; - defaultY: number; - onCreate?: (newID: number) => void; -} - -export const TabID = { - INPUT: 0, - SYNTHESIS: 1 -} as const; -export type TabID = (typeof TabID)[keyof typeof TabID]; - -export function DlgCreateOperation() { - const { createOperation } = useCreateOperation(); - - const { manager, initialInputs, initialParent, onCreate, defaultX, defaultY } = useDialogsStore( - state => state.props as DlgCreateOperationProps - ); - - const methods = useForm({ - resolver: zodResolver(schemaCreateOperation), - defaultValues: { - item_data: { - operation_type: initialInputs.length === 0 ? OperationType.INPUT : OperationType.SYNTHESIS, - alias: '', - title: '', - description: '', - result: null, - parent: initialParent - }, - position_x: defaultX, - position_y: defaultY, - arguments: initialInputs, - width: OPERATION_NODE_WIDTH, - height: OPERATION_NODE_HEIGHT, - create_schema: false, - layout: manager.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 && !manager.oss.operations.some(operation => operation.alias === alias); - - function onSubmit(data: ICreateOperationDTO) { - const target = manager.newOperationPosition(data); - data.position_x = target.x; - data.position_y = target.y; - data.layout = manager.layout; - void createOperation({ itemID: manager.oss.id, data: data }).then(response => - onCreate?.(response.new_operation.id) - ); - } - - function handleSelectTab(newTab: TabID, last: TabID) { - if (last === newTab) { - return; - } - if (newTab === TabID.INPUT) { - methods.setValue('item_data.operation_type', OperationType.INPUT); - methods.setValue('item_data.result', null); - methods.setValue('arguments', []); - } else { - methods.setValue('item_data.operation_type', OperationType.SYNTHESIS); - methods.setValue('arguments', initialInputs); - } - setActiveTab(newTab); - } - - return ( - void methods.handleSubmit(onSubmit)(event)} - className='w-180 px-6 h-128' - helpTopic={HelpTopic.CC_OSS} - > - handleSelectTab(index as TabID, last as TabID)} - > - - - - - - - - - - - - - - - - ); -} diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/index.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/index.tsx deleted file mode 100644 index 70de4bab..00000000 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { DlgCreateOperation } from './dlg-create-operation'; diff --git a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx b/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx deleted file mode 100644 index 3361a61f..00000000 --- a/rsconcept/frontend/src/features/oss/dialogs/dlg-create-operation/tab-input-operation.tsx +++ /dev/null @@ -1,129 +0,0 @@ -'use client'; - -import { Controller, useFormContext, useWatch } from 'react-hook-form'; - -import { type ILibraryItem, LibraryItemType } from '@/features/library'; -import { useLibrary } from '@/features/library/backend/use-library'; -import { PickSchema } from '@/features/library/components/pick-schema'; - -import { MiniButton } from '@/components/control'; -import { IconReset } from '@/components/icons'; -import { Checkbox, Label, TextArea, TextInput } from '@/components/input'; -import { useDialogsStore } from '@/stores/dialogs'; - -import { type ICreateOperationDTO } from '../../backend/types'; -import { SelectParent } from '../../components/select-parent'; -import { sortItemsForOSS } from '../../models/oss-api'; - -import { type DlgCreateOperationProps } from './dlg-create-operation'; - -export function TabInputOperation() { - const { manager } = useDialogsStore(state => state.props as DlgCreateOperationProps); - const { items: libraryItems } = useLibrary(); - const sortedItems = sortItemsForOSS(manager.oss, libraryItems); - - const { - register, - control, - setValue, - formState: { errors } - } = useFormContext(); - const createSchema = useWatch({ control, name: 'create_schema' }); - - function baseFilter(item: ILibraryItem) { - return !manager.oss.schemas.includes(item.id); - } - - function handleChangeCreateSchema(value: boolean) { - if (value) { - setValue('item_data.result', null); - } - setValue('create_schema', value); - } - - function handleSetInput(inputID: number) { - const schema = libraryItems.find(item => item.id === inputID); - if (!schema) { - return; - } - setValue('item_data.result', inputID); - setValue('create_schema', false); - setValue('item_data.alias', schema.alias); - setValue('item_data.title', schema.title); - setValue('item_data.description', schema.description, { shouldValidate: true }); - } - - return ( -
- -
-
- - ( - field.onChange(value ? value.id : null)} - /> - )} - /> -
-