F: Implement different dialogs for operation creation

This commit is contained in:
Ivan 2025-07-10 16:32:31 +03:00
parent 4f47c736ce
commit 073cd5412f
29 changed files with 1125 additions and 544 deletions

View File

@ -5,9 +5,11 @@ from .data_access import (
ArgumentSerializer, ArgumentSerializer,
BlockSerializer, BlockSerializer,
CreateBlockSerializer, CreateBlockSerializer,
CreateOperationSerializer, CreateSchemaSerializer,
CreateSynthesisSerializer,
DeleteBlockSerializer, DeleteBlockSerializer,
DeleteOperationSerializer, DeleteOperationSerializer,
ImportSchemaSerializer,
MoveItemsSerializer, MoveItemsSerializer,
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer, OperationSerializer,

View File

@ -2,8 +2,16 @@
from rest_framework import serializers 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): class NodeSerializer(serializers.Serializer):
''' Block position. ''' ''' Oss node serializer. '''
nodeID = serializers.CharField() nodeID = serializers.CharField()
x = serializers.FloatField() x = serializers.FloatField()
y = serializers.FloatField() y = serializers.FloatField()

View File

@ -13,7 +13,7 @@ from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType from ..models import Argument, Block, Inheritance, Operation, OperationSchema, OperationType
from .basics import NodeSerializer, SubstitutionExSerializer from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
class OperationSerializer(serializers.ModelSerializer): class OperationSerializer(serializers.ModelSerializer):
@ -58,10 +58,7 @@ class CreateBlockSerializer(serializers.Serializer):
child=NodeSerializer() child=NodeSerializer()
) )
item_data = BlockCreateData() item_data = BlockCreateData()
width = serializers.FloatField() position = PositionSerializer()
height = serializers.FloatField()
position_x = serializers.FloatField()
position_y = serializers.FloatField()
children_operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id')) children_operations = PKField(many=True, queryset=Operation.objects.all().only('oss_id'))
children_blocks = PKField(many=True, queryset=Block.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 return attrs
class CreateOperationSerializer(serializers.Serializer): class CreateOperationData(serializers.ModelSerializer):
''' Serializer: Operation creation. ''' ''' Serializer: Operation creation data. '''
class CreateOperationData(serializers.ModelSerializer): alias = serializers.CharField()
''' Serializer: Operation creation data. '''
alias = serializers.CharField()
operation_type = serializers.ChoiceField(OperationType.choices)
class Meta: class Meta:
''' serializer metadata. ''' ''' serializer metadata. '''
model = Operation model = Operation
fields = \ fields = 'alias', 'title', 'description', 'parent'
'alias', 'operation_type', 'title', \
'description', 'result', 'parent'
layout = serializers.ListField(
child=NodeSerializer() class CreateSchemaSerializer(serializers.Serializer):
) ''' Serializer: Schema creation for new operation. '''
layout = serializers.ListField(child=NodeSerializer())
item_data = CreateOperationData() item_data = CreateOperationData()
width = serializers.FloatField() position = PositionSerializer()
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)
def validate(self, attrs): def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss']) oss = cast(LibraryItem, self.context['oss'])
@ -225,14 +213,82 @@ class CreateOperationSerializer(serializers.Serializer):
raise serializers.ValidationError({ raise serializers.ValidationError({
'parent': msg.parentNotInOSS() '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']: for operation in attrs['arguments']:
if operation.oss_id != oss.pk: if operation.oss_id != oss.pk:
raise serializers.ValidationError({ raise serializers.ValidationError({
'arguments': msg.operationNotInOSS() '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 return attrs

View File

@ -3,18 +3,18 @@ from rest_framework import serializers
from apps.library.serializers import LibraryItemSerializer from apps.library.serializers import LibraryItemSerializer
from .data_access import BlockSerializer, OperationSchemaSerializer, OperationSerializer from .data_access import OperationSchemaSerializer
class OperationCreatedResponse(serializers.Serializer): class OperationCreatedResponse(serializers.Serializer):
''' Serializer: Create operation response. ''' ''' Serializer: Create operation response. '''
new_operation = OperationSerializer() new_operation = serializers.IntegerField()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()
class BlockCreatedResponse(serializers.Serializer): class BlockCreatedResponse(serializers.Serializer):
''' Serializer: Create block response. ''' ''' Serializer: Create block response. '''
new_block = BlockSerializer() new_block = serializers.IntegerField()
oss = OperationSchemaSerializer() oss = OperationSchemaSerializer()

View File

@ -73,10 +73,12 @@ class TestOssBlocks(EndpointTester):
'description': 'Тест кириллицы', 'description': 'Тест кириллицы',
}, },
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1337, 'position': {
'position_y': 1337, 'x': 1337,
'width': 0.42, 'y': 1337,
'height': 0.42, 'width': 0.42,
'height': 0.42
},
'children_operations': [], 'children_operations': [],
'children_blocks': [] 'children_blocks': []
} }
@ -86,14 +88,11 @@ class TestOssBlocks(EndpointTester):
self.assertEqual(len(response.data['oss']['blocks']), 3) self.assertEqual(len(response.data['oss']['blocks']), 3)
new_block = response.data['new_block'] new_block = response.data['new_block']
layout = response.data['oss']['layout'] layout = response.data['oss']['layout']
item = [item for item in layout if item['nodeID'] == 'b' + str(new_block['id'])][0] block_node = [item for item in layout if item['nodeID'] == 'b' + str(new_block)][0]
self.assertEqual(new_block['title'], data['item_data']['title']) self.assertEqual(block_node['x'], data['position']['x'])
self.assertEqual(new_block['description'], data['item_data']['description']) self.assertEqual(block_node['y'], data['position']['y'])
self.assertEqual(new_block['parent'], None) self.assertEqual(block_node['width'], data['position']['width'])
self.assertEqual(item['x'], data['position_x']) self.assertEqual(block_node['height'], data['position']['height'])
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.operation1.refresh_from_db()
self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.unowned_id)
@ -111,10 +110,12 @@ class TestOssBlocks(EndpointTester):
'parent': self.invalid_id 'parent': self.invalid_id
}, },
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1337, 'position': {
'position_y': 1337, 'x': 1337,
'width': 0.42, 'y': 1337,
'height': 0.42, 'width': 0.42,
'height': 0.42
},
'children_operations': [], 'children_operations': [],
'children_blocks': [] 'children_blocks': []
} }
@ -126,7 +127,8 @@ class TestOssBlocks(EndpointTester):
data['item_data']['parent'] = self.block1.pk data['item_data']['parent'] = self.block1.pk
response = self.executeCreated(data=data) response = self.executeCreated(data=data)
new_block = response.data['new_block'] 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') @decl_endpoint('/api/oss/{item}/create-block', method='post')
@ -138,10 +140,12 @@ class TestOssBlocks(EndpointTester):
'description': 'Тест кириллицы', 'description': 'Тест кириллицы',
}, },
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1337, 'position': {
'position_y': 1337, 'x': 1337,
'width': 0.42, 'y': 1337,
'height': 0.42, 'width': 0.42,
'height': 0.42
},
'children_operations': [self.invalid_id], 'children_operations': [self.invalid_id],
'children_blocks': [] 'children_blocks': []
} }
@ -162,8 +166,8 @@ class TestOssBlocks(EndpointTester):
new_block = response.data['new_block'] new_block = response.data['new_block']
self.operation1.refresh_from_db() self.operation1.refresh_from_db()
self.block1.refresh_from_db() self.block1.refresh_from_db()
self.assertEqual(self.operation1.parent.pk, new_block['id']) self.assertEqual(self.operation1.parent.pk, new_block)
self.assertEqual(self.block1.parent.pk, new_block['id']) self.assertEqual(self.block1.parent.pk, new_block)
@decl_endpoint('/api/oss/{item}/create-block', method='post') @decl_endpoint('/api/oss/{item}/create-block', method='post')
@ -176,10 +180,12 @@ class TestOssBlocks(EndpointTester):
'parent': self.block2.pk 'parent': self.block2.pk
}, },
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1337, 'position': {
'position_y': 1337, 'x': 1337,
'width': 0.42, 'y': 1337,
'height': 0.42, 'width': 0.42,
'height': 0.42
},
'children_operations': [], 'children_operations': [],
'children_blocks': [self.block1.pk] 'children_blocks': [self.block1.pk]
} }

View File

@ -70,9 +70,10 @@ class TestOssOperations(EndpointTester):
}]) }])
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-schema', method='post')
def test_create_operation(self): def test_create_schema(self):
self.populateData() self.populateData()
Editor.add(self.owned.model.pk, self.user2.pk)
self.executeBadData(item=self.owned_id) self.executeBadData(item=self.owned_id)
data = { data = {
@ -80,47 +81,50 @@ class TestOssOperations(EndpointTester):
'alias': 'Test3', 'alias': 'Test3',
'title': 'Test title', 'title': 'Test title',
'description': 'Тест кириллицы', 'description': 'Тест кириллицы',
'parent': None
}, },
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1, 'position': {
'position_y': 1, 'x': 1,
'width': 500, 'y': 1,
'height': 50 '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) self.executeNotFound(data=data, item=self.invalid_id)
response = self.executeCreated(data=data, item=self.owned_id) response = self.executeCreated(data=data, item=self.owned_id)
self.assertEqual(len(response.data['oss']['operations']), 4) 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'] 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['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['title'], data['item_data']['title'])
self.assertEqual(new_operation['description'], data['item_data']['description']) self.assertEqual(new_operation['description'], data['item_data']['description'])
self.assertEqual(new_operation['result'], None)
self.assertEqual(new_operation['parent'], None) self.assertEqual(new_operation['parent'], None)
self.assertEqual(item['x'], data['position_x']) self.assertNotEqual(new_operation['result'], None)
self.assertEqual(item['y'], data['position_y']) self.assertEqual(operation_node['x'], data['position']['x'])
self.assertEqual(item['width'], data['width']) self.assertEqual(operation_node['y'], data['position']['y'])
self.assertEqual(item['height'], data['height']) self.assertEqual(operation_node['width'], data['position']['width'])
self.operation1.refresh_from_db() 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.executeForbidden(data=data, item=self.unowned_id)
self.toggle_admin(True) self.toggle_admin(True)
self.executeCreated(data=data, item=self.unowned_id) self.executeCreated(data=data, item=self.unowned_id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-schema', method='post')
def test_create_operation_parent(self): def test_create_schema_parent(self):
self.populateData() self.populateData()
data = { data = {
'item_data': { 'item_data': {
@ -128,14 +132,15 @@ class TestOssOperations(EndpointTester):
'alias': 'Test3', 'alias': 'Test3',
'title': 'Test title', 'title': 'Test title',
'description': '', 'description': '',
'operation_type': OperationType.INPUT
}, },
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1, 'position': {
'position_y': 1, 'x': 1,
'width': 500, 'y': 1,
'height': 50 'width': 500,
'height': 50
}
} }
self.executeBadData(data=data, item=self.owned_id) self.executeBadData(data=data, item=self.owned_id)
@ -147,90 +152,40 @@ class TestOssOperations(EndpointTester):
block_owned = self.owned.create_block(title='TestBlock2') block_owned = self.owned.create_block(title='TestBlock2')
data['item_data']['parent'] = block_owned.id data['item_data']['parent'] = block_owned.id
response = self.executeCreated(data=data, item=self.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) self.assertEqual(len(response.data['oss']['operations']), 4)
new_operation = response.data['new_operation']
self.assertEqual(new_operation['parent'], block_owned.id) self.assertEqual(new_operation['parent'], block_owned.id)
@decl_endpoint('/api/oss/{item}/create-operation', method='post') @decl_endpoint('/api/oss/{item}/create-synthesis', method='post')
def test_create_operation_arguments(self): def test_create_synthesis(self):
self.populateData() 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 = { data = {
'item_data': { 'item_data': {
'alias': 'Test4', 'alias': 'Test4',
'title': 'Test title', 'title': 'Test title',
'description': 'Comment', 'description': '',
'operation_type': OperationType.INPUT, 'parent': None
'result': self.ks1.model.pk
}, },
'create_schema': True,
'layout': self.layout_data, 'layout': self.layout_data,
'position_x': 1, 'position': {
'position_y': 1, 'x': 1,
'width': 500, 'y': 1,
'height': 50 '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) response = self.executeCreated(data=data, item=self.owned_id)
self.owned.refresh_from_db() self.owned.refresh_from_db()
new_operation = response.data['new_operation'] new_operation_id = response.data['new_operation']
schema = LibraryItem.objects.get(pk=new_operation['result']) new_operation = next(op for op in response.data['oss']['operations'] if op['id'] == new_operation_id)
self.assertEqual(schema.alias, data['item_data']['alias']) arguments = self.owned.arguments()
self.assertEqual(schema.title, data['item_data']['title']) self.assertTrue(arguments.filter(operation__id=new_operation_id, argument=self.operation1))
self.assertEqual(schema.description, data['item_data']['description']) self.assertTrue(arguments.filter(operation__id=new_operation_id, argument=self.operation3))
self.assertEqual(schema.visible, False) self.assertNotEqual(new_operation['result'], None)
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') @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
@ -497,3 +452,141 @@ class TestOssOperations(EndpointTester):
self.assertEqual(len(items), 1) self.assertEqual(len(items), 1)
self.assertEqual(items[0].alias, 'X1') self.assertEqual(items[0].alias, 'X1')
self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved) 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)

View File

@ -1,8 +1,8 @@
''' Endpoints for OSS. ''' ''' Endpoints for OSS. '''
from copy import deepcopy
from typing import Optional, cast from typing import Optional, cast
from django.db import transaction from django.db import transaction
from django.db.models import Q
from django.http import HttpResponse from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics, serializers from rest_framework import generics, serializers
@ -23,6 +23,28 @@ from .. import models as m
from .. import serializers as s 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(tags=['OSS'])
@extend_schema_view() @extend_schema_view()
class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView): class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.RetrieveAPIView):
@ -41,7 +63,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'update_block', 'update_block',
'delete_block', 'delete_block',
'move_items', 'move_items',
'create_operation', 'create_schema',
'import_schema',
'create_synthesis',
'update_operation', 'update_operation',
'delete_operation', 'delete_operation',
'create_input', 'create_input',
@ -116,16 +140,17 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout'] layout = serializer.validated_data['layout']
position = serializer.validated_data['position']
children_blocks: list[m.Block] = serializer.validated_data['children_blocks'] children_blocks: list[m.Block] = serializer.validated_data['children_blocks']
children_operations: list[m.Operation] = serializer.validated_data['children_operations'] children_operations: list[m.Operation] = serializer.validated_data['children_operations']
with transaction.atomic(): with transaction.atomic():
new_block = oss.create_block(**serializer.validated_data['item_data']) new_block = oss.create_block(**serializer.validated_data['item_data'])
layout.append({ layout.append({
'nodeID': 'b' + str(new_block.pk), 'nodeID': 'b' + str(new_block.pk),
'x': serializer.validated_data['position_x'], 'x': position['x'],
'y': serializer.validated_data['position_y'], 'y': position['y'],
'width': serializer.validated_data['width'], 'width': position['width'],
'height': serializer.validated_data['height'], 'height': position['height'],
}) })
oss.update_layout(layout) oss.update_layout(layout)
if len(children_blocks) > 0: if len(children_blocks) > 0:
@ -140,7 +165,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
return Response( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ data={
'new_block': s.BlockSerializer(new_block).data, 'new_block': new_block.pk,
'oss': s.OperationSchemaSerializer(oss.model).data 'oss': s.OperationSchemaSerializer(oss.model).data
} }
) )
@ -251,9 +276,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
) )
@extend_schema( @extend_schema(
summary='create operation', summary='create empty conceptual schema',
tags=['OSS'], tags=['OSS'],
request=s.CreateOperationSerializer(), request=s.CreateSchemaSerializer(),
responses={ responses={
c.HTTP_201_CREATED: s.OperationCreatedResponse, c.HTTP_201_CREATED: s.OperationCreatedResponse,
c.HTTP_400_BAD_REQUEST: None, c.HTTP_400_BAD_REQUEST: None,
@ -261,10 +286,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
c.HTTP_404_NOT_FOUND: None c.HTTP_404_NOT_FOUND: None
} }
) )
@action(detail=True, methods=['post'], url_path='create-operation') @action(detail=True, methods=['post'], url_path='create-schema')
def create_operation(self, request: Request, pk) -> HttpResponse: def create_schema(self, request: Request, pk) -> HttpResponse:
''' Create Operation. ''' ''' Create schema. '''
serializer = s.CreateOperationSerializer( serializer = s.CreateSchemaSerializer(
data=request.data, data=request.data,
context={'oss': self.get_object()} context={'oss': self.get_object()}
) )
@ -272,43 +297,124 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
oss = m.OperationSchema(self.get_object()) oss = m.OperationSchema(self.get_object())
layout = serializer.validated_data['layout'] 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(): with transaction.atomic():
new_operation = oss.create_operation(**serializer.validated_data['item_data']) new_operation = oss.create_operation(**serializer.validated_data['item_data'])
layout.append({ layout.append({
'nodeID': 'o' + str(new_operation.pk), 'nodeID': 'o' + str(new_operation.pk),
'x': serializer.validated_data['position_x'], 'x': position['x'],
'y': serializer.validated_data['position_y'], 'y': position['y'],
'width': serializer.validated_data['width'], 'width': position['width'],
'height': serializer.validated_data['height'] 'height': position['height']
}) })
oss.update_layout(layout) 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( return Response(
status=c.HTTP_201_CREATED, status=c.HTTP_201_CREATED,
data={ 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 'oss': s.OperationSchemaSerializer(oss.model).data
} }
) )

View File

@ -20,9 +20,9 @@ const DlgCloneLibraryItem = React.lazy(() =>
const DlgCreateCst = React.lazy(() => const DlgCreateCst = React.lazy(() =>
import('@/features/rsform/dialogs/dlg-create-cst').then(module => ({ default: module.DlgCreateCst })) import('@/features/rsform/dialogs/dlg-create-cst').then(module => ({ default: module.DlgCreateCst }))
); );
const DlgCreateOperation = React.lazy(() => const DlgCreateSynthesis = React.lazy(() =>
import('@/features/oss/dialogs/dlg-create-operation').then(module => ({ import('@/features/oss/dialogs/dlg-create-synthesis').then(module => ({
default: module.DlgCreateOperation default: module.DlgCreateSynthesis
})) }))
); );
const DlgCreateVersion = React.lazy(() => const DlgCreateVersion = React.lazy(() =>
@ -134,6 +134,12 @@ const DlgEditCst = React.lazy(() =>
const DlgShowTermGraph = React.lazy(() => const DlgShowTermGraph = React.lazy(() =>
import('@/features/oss/dialogs/dlg-show-term-graph').then(module => ({ default: module.DlgShowTermGraph })) 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 = () => { export const GlobalDialogs = () => {
const active = useDialogsStore(state => state.active); const active = useDialogsStore(state => state.active);
@ -146,8 +152,8 @@ export const GlobalDialogs = () => {
return <DlgCstTemplate />; return <DlgCstTemplate />;
case DialogType.CREATE_CONSTITUENTA: case DialogType.CREATE_CONSTITUENTA:
return <DlgCreateCst />; return <DlgCreateCst />;
case DialogType.CREATE_OPERATION: case DialogType.CREATE_SYNTHESIS:
return <DlgCreateOperation />; return <DlgCreateSynthesis />;
case DialogType.CREATE_BLOCK: case DialogType.CREATE_BLOCK:
return <DlgCreateBlock />; return <DlgCreateBlock />;
case DialogType.EDIT_BLOCK: case DialogType.EDIT_BLOCK:
@ -198,5 +204,9 @@ export const GlobalDialogs = () => {
return <DlgEditCst />; return <DlgEditCst />;
case DialogType.SHOW_TERM_GRAPH: case DialogType.SHOW_TERM_GRAPH:
return <DlgShowTermGraph />; return <DlgShowTermGraph />;
case DialogType.CREATE_SCHEMA:
return <DlgCreateSchema />;
case DialogType.IMPORT_SCHEMA:
return <DlgImportSchema />;
} }
}; };

View File

@ -8,9 +8,11 @@ import {
type IBlockCreatedResponse, type IBlockCreatedResponse,
type IConstituentaReference, type IConstituentaReference,
type ICreateBlockDTO, type ICreateBlockDTO,
type ICreateOperationDTO, type ICreateSchemaDTO,
type ICreateSynthesisDTO,
type IDeleteBlockDTO, type IDeleteBlockDTO,
type IDeleteOperationDTO, type IDeleteOperationDTO,
type IImportSchemaDTO,
type IInputCreatedResponse, type IInputCreatedResponse,
type IMoveItemsDTO, type IMoveItemsDTO,
type IOperationCreatedResponse, type IOperationCreatedResponse,
@ -83,13 +85,40 @@ export const ossApi = {
} }
}), }),
createOperation: ({ itemID, data }: { itemID: number; data: ICreateOperationDTO }) => createSchema: ({ itemID, data }: { itemID: number; data: ICreateSchemaDTO }) =>
axiosPost<ICreateOperationDTO, IOperationCreatedResponse>({ axiosPost<ICreateSchemaDTO, IOperationCreatedResponse>({
schema: schemaOperationCreatedResponse, schema: schemaOperationCreatedResponse,
endpoint: `/api/oss/${itemID}/create-operation`, endpoint: `/api/oss/${itemID}/create-schema`,
request: { request: {
data: data, 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<ICreateSynthesisDTO, IOperationCreatedResponse>({
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<IImportSchemaDTO, IOperationCreatedResponse>({
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 }) => updateOperation: ({ itemID, data }: { itemID: number; data: IUpdateOperationDTO }) =>

View File

@ -43,7 +43,9 @@ export type IDeleteBlockDTO = z.infer<typeof schemaDeleteBlock>;
export type IMoveItemsDTO = z.infer<typeof schemaMoveItems>; export type IMoveItemsDTO = z.infer<typeof schemaMoveItems>;
/** Represents {@link IOperation} data, used in Create action. */ /** Represents {@link IOperation} data, used in Create action. */
export type ICreateOperationDTO = z.infer<typeof schemaCreateOperation>; export type ICreateSchemaDTO = z.infer<typeof schemaCreateSchema>;
export type ICreateSynthesisDTO = z.infer<typeof schemaCreateSynthesis>;
export type IImportSchemaDTO = z.infer<typeof schemaImportSchema>;
/** Represents data response when creating {@link IOperation}. */ /** Represents data response when creating {@link IOperation}. */
export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedResponse>; export type IOperationCreatedResponse = z.infer<typeof schemaOperationCreatedResponse>;
@ -90,6 +92,13 @@ export const schemaOperation = z.strictObject({
result: z.number().nullable() result: z.number().nullable()
}); });
export const schemaOperationData = schemaOperation.pick({
alias: true,
title: true,
description: true,
parent: true
});
export const schemaBlock = z.strictObject({ export const schemaBlock = z.strictObject({
id: z.number(), id: z.number(),
oss: z.number(), oss: z.number(),
@ -98,6 +107,13 @@ export const schemaBlock = z.strictObject({
parent: z.number().nullable() 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({ export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({
operation: z.number(), operation: z.number(),
original_alias: z.string(), original_alias: z.string(),
@ -106,12 +122,8 @@ export const schemaCstSubstituteInfo = schemaSubstituteConstituents.extend({
substitution_term: z.string() substitution_term: z.string()
}); });
export const schemaNodePosition = z.strictObject({ export const schemaNodePosition = schemaPosition.extend({
nodeID: z.string(), nodeID: z.string()
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number()
}); });
export const schemaOssLayout = z.array(schemaNodePosition); export const schemaOssLayout = z.array(schemaNodePosition);
@ -137,16 +149,13 @@ export const schemaCreateBlock = z.strictObject({
description: z.string(), description: z.string(),
parent: z.number().nullable() parent: z.number().nullable()
}), }),
position_x: z.number(), position: schemaPosition,
position_y: z.number(),
width: z.number(),
height: z.number(),
children_operations: z.array(z.number()), children_operations: z.array(z.number()),
children_blocks: z.array(z.number()) children_blocks: z.array(z.number())
}); });
export const schemaBlockCreatedResponse = z.strictObject({ export const schemaBlockCreatedResponse = z.strictObject({
new_block: schemaBlock, new_block: z.number(),
oss: schemaOperationSchema oss: schemaOperationSchema
}); });
@ -165,26 +174,35 @@ export const schemaDeleteBlock = z.strictObject({
layout: schemaOssLayout layout: schemaOssLayout
}); });
export const schemaCreateOperation = z.strictObject({ export const schemaCreateSchema = z.strictObject({
layout: schemaOssLayout, layout: schemaOssLayout,
item_data: z.strictObject({ item_data: schemaOperationData,
alias: z.string().nonempty(), position: schemaPosition
operation_type: schemaOperationType, });
title: z.string(),
description: z.string(), export const schemaCreateSynthesis = z.strictObject({
parent: z.number().nullable(), layout: schemaOssLayout,
result: z.number().nullable() item_data: schemaOperationData,
}), position: schemaPosition,
position_x: z.number(),
position_y: z.number(),
width: z.number(),
height: z.number(),
arguments: z.array(z.number()), 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({ export const schemaOperationCreatedResponse = z.strictObject({
new_operation: schemaOperation, new_operation: z.number(),
oss: schemaOperationSchema oss: schemaOperationSchema
}); });

View File

@ -5,14 +5,14 @@ import { useUpdateTimestamp } from '@/features/library/backend/use-update-timest
import { KEYS } from '@/backend/configuration'; import { KEYS } from '@/backend/configuration';
import { ossApi } from './api'; import { ossApi } from './api';
import { type ICreateOperationDTO } from './types'; import { type ICreateSchemaDTO } from './types';
export const useCreateOperation = () => { export const useCreateSchema = () => {
const client = useQueryClient(); const client = useQueryClient();
const { updateTimestamp } = useUpdateTimestamp(); const { updateTimestamp } = useUpdateTimestamp();
const mutation = useMutation({ const mutation = useMutation({
mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-operation'], mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-schema'],
mutationFn: ossApi.createOperation, mutationFn: ossApi.createSchema,
onSuccess: response => { onSuccess: response => {
client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss); client.setQueryData(ossApi.getOssQueryOptions({ itemID: response.oss.id }).queryKey, response.oss);
updateTimestamp(response.oss.id); updateTimestamp(response.oss.id);
@ -20,6 +20,6 @@ export const useCreateOperation = () => {
onError: () => client.invalidateQueries() onError: () => client.invalidateQueries()
}); });
return { return {
createOperation: (data: { itemID: number; data: ICreateOperationDTO }) => mutation.mutateAsync(data) createSchema: (data: { itemID: number; data: ICreateSchemaDTO }) => mutation.mutateAsync(data)
}; };
}; };

View File

@ -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)
};
};

View File

@ -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)
};
};

View File

@ -49,10 +49,12 @@ export function DlgCreateBlock() {
description: '', description: '',
parent: initialParent parent: initialParent
}, },
position_x: defaultX, position: {
position_y: defaultY, x: defaultX,
width: BLOCK_NODE_MIN_WIDTH, y: defaultY,
height: BLOCK_NODE_MIN_HEIGHT, width: BLOCK_NODE_MIN_WIDTH,
height: BLOCK_NODE_MIN_HEIGHT
},
children_blocks: initialChildren.filter(item => item.nodeType === NodeType.BLOCK).map(item => item.id), 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), children_operations: initialChildren.filter(item => item.nodeType === NodeType.OPERATION).map(item => item.id),
layout: manager.layout layout: manager.layout
@ -66,13 +68,9 @@ export function DlgCreateBlock() {
const isValid = !!title && !manager.oss.blocks.some(block => block.title === title); const isValid = !!title && !manager.oss.blocks.some(block => block.title === title);
function onSubmit(data: ICreateBlockDTO) { function onSubmit(data: ICreateBlockDTO) {
const rectangle = manager.newBlockPosition(data); data.position = manager.newBlockPosition(data);
data.position_x = rectangle.x;
data.position_y = rectangle.y;
data.width = rectangle.width;
data.height = rectangle.height;
data.layout = manager.layout; 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 ( return (

View File

@ -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<ICreateOperationDTO>({
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 (
<ModalForm
header='Создание операции'
submitText='Создать'
canSubmit={isValid}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
className='w-180 px-6 h-128'
helpTopic={HelpTopic.CC_OSS}
>
<Tabs
className='grid'
selectedIndex={activeTab}
onSelect={(index, last) => handleSelectTab(index as TabID, last as TabID)}
>
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none'>
<TabLabel
title={describeOperationType(OperationType.INPUT)}
label={labelOperationType(OperationType.INPUT)}
/>
<TabLabel
title={describeOperationType(OperationType.SYNTHESIS)}
label={labelOperationType(OperationType.SYNTHESIS)}
/>
</TabList>
<FormProvider {...methods}>
<TabPanel>
<TabInputOperation />
</TabPanel>
<TabPanel>
<TabSynthesisOperation />
</TabPanel>
</FormProvider>
</Tabs>
</ModalForm>
);
}

View File

@ -1 +0,0 @@
export { DlgCreateOperation } from './dlg-create-operation';

View File

@ -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<ICreateOperationDTO>();
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 (
<div className='cc-fade-in cc-column'>
<TextInput
id='operation_title' //
label='Название'
{...register('item_data.title')}
error={errors.item_data?.title}
/>
<div className='flex gap-6'>
<div className='grid gap-1'>
<TextInput
id='operation_alias' //
label='Сокращение'
className='w-80'
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
<Controller
name='item_data.parent'
control={control}
render={({ field }) => (
<SelectParent
items={manager.oss.blocks}
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
placeholder='Родительский блок'
onChange={value => field.onChange(value ? value.id : null)}
/>
)}
/>
</div>
<TextArea
id='operation_comment' //
label='Описание'
noResize
rows={3}
{...register('item_data.description')}
/>
</div>
<div className='flex justify-between gap-3 items-center'>
<div className='flex gap-3'>
<Label text='Загружаемая концептуальная схема' />
<MiniButton
title='Сбросить выбор схемы'
noPadding
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={() => setValue('item_data.result', null)}
/>
</div>
<Checkbox
value={createSchema} //
onChange={handleChangeCreateSchema}
label='Создать новую схему'
/>
</div>
{!createSchema ? (
<Controller
control={control}
name='item_data.result'
render={({ field }) => (
<PickSchema
items={sortedItems}
value={field.value}
itemType={LibraryItemType.RSFORM}
onChange={handleSetInput}
rows={8}
baseFilter={baseFilter}
/>
)}
/>
) : null}
</div>
);
}

View File

@ -0,0 +1,112 @@
'use client';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { HelpTopic } from '@/features/help';
import { TextArea, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateSchemaDTO, schemaCreateSchema } from '../backend/types';
import { useCreateSchema } from '../backend/use-create-schema';
import { SelectParent } from '../components/select-parent';
import { type LayoutManager, OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../models/oss-layout-api';
export interface DlgCreateSchemaProps {
manager: LayoutManager;
defaultX: number;
defaultY: number;
initialParent: number | null;
onCreate?: (newID: number) => void;
}
export function DlgCreateSchema() {
const { createSchema } = useCreateSchema();
const { manager, initialParent, onCreate, defaultX, defaultY } = useDialogsStore(
state => state.props as DlgCreateSchemaProps
);
const {
control,
register,
handleSubmit,
formState: { errors }
} = useForm<ICreateSchemaDTO>({
resolver: zodResolver(schemaCreateSchema),
defaultValues: {
item_data: {
alias: '',
title: '',
description: '',
parent: initialParent
},
position: {
x: defaultX,
y: defaultY,
width: OPERATION_NODE_WIDTH,
height: OPERATION_NODE_HEIGHT
},
layout: manager.layout
},
mode: 'onChange'
});
const alias = useWatch({ control: control, name: 'item_data.alias' });
const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: ICreateSchemaDTO) {
data.position = manager.newOperationPosition(data);
data.layout = manager.layout;
void createSchema({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_operation));
}
return (
<ModalForm
header='Создание операции: Пустая КС'
submitText='Создать'
canSubmit={isValid}
onSubmit={event => void handleSubmit(onSubmit)(event)}
className='w-180 px-6 cc-column'
helpTopic={HelpTopic.CC_OSS}
>
<TextInput
id='operation_title' //
label='Название'
{...register('item_data.title')}
error={errors.item_data?.title}
/>
<div className='flex gap-6'>
<div className='grid gap-1'>
<TextInput
id='operation_alias' //
label='Сокращение'
className='w-80'
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
<Controller
name='item_data.parent'
control={control}
render={({ field }) => (
<SelectParent
items={manager.oss.blocks}
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
placeholder='Родительский блок'
onChange={value => field.onChange(value ? value.id : null)}
/>
)}
/>
</div>
<TextArea
id='operation_comment' //
label='Описание'
noResize
rows={3}
{...register('item_data.description')}
/>
</div>
</ModalForm>
);
}

View File

@ -0,0 +1,107 @@
'use client';
import { Suspense, useState } from 'react';
import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { HelpTopic } from '@/features/help';
import { Loader } from '@/components/loader';
import { ModalForm } from '@/components/modal';
import { TabLabel, TabList, TabPanel, Tabs } from '@/components/tabs';
import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateSynthesisDTO, schemaCreateSynthesis } from '../../backend/types';
import { useCreateSynthesis } from '../../backend/use-create-synthesis';
import { type LayoutManager, OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../../models/oss-layout-api';
import { TabArguments } from './tab-arguments';
import { TabSubstitutions } from './tab-substitutions';
export interface DlgCreateSynthesisProps {
manager: LayoutManager;
initialParent: number | null;
initialInputs: number[];
defaultX: number;
defaultY: number;
onCreate?: (newID: number) => void;
}
export const TabID = {
ARGUMENTS: 0,
SUBSTITUTIONS: 1
} as const;
export type TabID = (typeof TabID)[keyof typeof TabID];
export function DlgCreateSynthesis() {
const { createSynthesis } = useCreateSynthesis();
const { manager, initialInputs, initialParent, onCreate, defaultX, defaultY } = useDialogsStore(
state => state.props as DlgCreateSynthesisProps
);
const methods = useForm<ICreateSynthesisDTO>({
resolver: zodResolver(schemaCreateSynthesis),
defaultValues: {
item_data: {
alias: '',
title: '',
description: '',
parent: initialParent
},
position: {
x: defaultX,
y: defaultY,
width: OPERATION_NODE_WIDTH,
height: OPERATION_NODE_HEIGHT
},
arguments: initialInputs,
substitutions: [],
layout: manager.layout
},
mode: 'onChange'
});
const alias = useWatch({ control: methods.control, name: 'item_data.alias' });
const [activeTab, setActiveTab] = useState<TabID>(TabID.ARGUMENTS);
const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: ICreateSynthesisDTO) {
data.position = manager.newOperationPosition(data);
data.layout = manager.layout;
void createSynthesis({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_operation));
}
return (
<ModalForm
header='Создание операции'
submitText='Создать'
canSubmit={isValid}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
className='w-180 px-6 h-128'
helpTopic={HelpTopic.CC_OSS}
>
<Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
<TabList className='z-pop mx-auto -mb-5 flex border divide-x rounded-none'>
<TabLabel title='Выбор аргументов операции' label='Аргументы' className='w-32' />
<TabLabel titleHtml='Таблица отождествлений' label='Отождествления' className='w-32' />
</TabList>
<FormProvider {...methods}>
<TabPanel>
<TabArguments />
</TabPanel>
<TabPanel>
<Suspense
fallback={
<div className='w-full h-full flex items-center justify-center'>
<Loader circular />
</div>
}
>
<TabSubstitutions />
</Suspense>
</TabPanel>
</FormProvider>
</Tabs>
</ModalForm>
);
}

View File

@ -0,0 +1 @@
export { DlgCreateSynthesis } from './dlg-create-synthesis';

View File

@ -3,19 +3,19 @@ import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { Label, TextArea, TextInput } from '@/components/input'; import { Label, TextArea, TextInput } from '@/components/input';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateOperationDTO } from '../../backend/types'; import { type ICreateSynthesisDTO } from '../../backend/types';
import { PickMultiOperation } from '../../components/pick-multi-operation'; import { PickMultiOperation } from '../../components/pick-multi-operation';
import { SelectParent } from '../../components/select-parent'; import { SelectParent } from '../../components/select-parent';
import { type DlgCreateOperationProps } from './dlg-create-operation'; import { type DlgCreateSynthesisProps } from './dlg-create-synthesis';
export function TabSynthesisOperation() { export function TabArguments() {
const { manager } = useDialogsStore(state => state.props as DlgCreateOperationProps); const { manager } = useDialogsStore(state => state.props as DlgCreateSynthesisProps);
const { const {
register, register,
control, control,
formState: { errors } formState: { errors }
} = useFormContext<ICreateOperationDTO>(); } = useFormContext<ICreateSynthesisDTO>();
const inputs = useWatch({ control, name: 'arguments' }); const inputs = useWatch({ control, name: 'arguments' });
return ( return (

View File

@ -0,0 +1,50 @@
'use client';
import { Controller, useFormContext, useWatch } from 'react-hook-form';
import { useRSForms } from '@/features/rsform/backend/use-rsforms';
import { PickSubstitutions } from '@/features/rsform/components/pick-substitutions';
import { TextArea } from '@/components/input';
import { useDialogsStore } from '@/stores/dialogs';
import { type ICreateSynthesisDTO } from '../../backend/types';
import { SubstitutionValidator } from '../../models/oss-api';
import { type DlgCreateSynthesisProps } from './dlg-create-synthesis';
export function TabSubstitutions() {
const { manager } = useDialogsStore(state => state.props as DlgCreateSynthesisProps);
const { control } = useFormContext<ICreateSynthesisDTO>();
const inputs = useWatch({ control, name: 'arguments' });
const substitutions = useWatch({ control, name: 'substitutions' });
const schemasIDs = inputs
.map(id => manager.oss.operationByID.get(id)!)
.map(operation => operation.result)
.filter(id => id !== null);
const schemas = useRSForms(schemasIDs);
const validator = new SubstitutionValidator(schemas, substitutions);
const isCorrect = validator.validate();
return (
<div className='cc-fade-in cc-column mt-9'>
<Controller
name='substitutions'
control={control}
render={({ field }) => (
<PickSubstitutions
schemas={schemas}
rows={8}
value={field.value}
onChange={field.onChange}
suggestions={validator.suggestions}
/>
)}
/>
<TextArea disabled value={validator.msg} rows={4} className={isCorrect ? '' : 'border-(--acc-fg-red) border-2'} />
</div>
);
}

View File

@ -18,7 +18,7 @@ import { type LayoutManager } from '../../models/oss-layout-api';
import { TabArguments } from './tab-arguments'; import { TabArguments } from './tab-arguments';
import { TabOperation } from './tab-operation'; import { TabOperation } from './tab-operation';
import { TabSynthesis } from './tab-synthesis'; import { TabSubstitutions } from './tab-substitutions';
export interface DlgEditOperationProps { export interface DlgEditOperationProps {
manager: LayoutManager; manager: LayoutManager;
@ -28,7 +28,7 @@ export interface DlgEditOperationProps {
export const TabID = { export const TabID = {
CARD: 0, CARD: 0,
ARGUMENTS: 1, ARGUMENTS: 1,
SUBSTITUTION: 2 SUBSTITUTIONS: 2
} as const; } as const;
export type TabID = (typeof TabID)[keyof typeof TabID]; export type TabID = (typeof TabID)[keyof typeof TabID];
@ -73,7 +73,7 @@ export function DlgEditOperation() {
onSubmit={event => void methods.handleSubmit(onSubmit)(event)} onSubmit={event => void methods.handleSubmit(onSubmit)(event)}
className='w-160 px-6 h-128' className='w-160 px-6 h-128'
helpTopic={HelpTopic.UI_SUBSTITUTIONS} helpTopic={HelpTopic.UI_SUBSTITUTIONS}
hideHelpWhen={() => activeTab !== TabID.SUBSTITUTION} hideHelpWhen={() => activeTab !== TabID.SUBSTITUTIONS}
> >
<Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}> <Tabs className='grid' selectedIndex={activeTab} onSelect={index => setActiveTab(index as TabID)}>
<TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'> <TabList className='mb-3 mx-auto w-fit flex border divide-x rounded-none'>
@ -109,7 +109,7 @@ export function DlgEditOperation() {
{target.operation_type === OperationType.SYNTHESIS ? ( {target.operation_type === OperationType.SYNTHESIS ? (
<TabPanel> <TabPanel>
<Suspense fallback={<Loader />}> <Suspense fallback={<Loader />}>
<TabSynthesis /> <TabSubstitutions />
</Suspense> </Suspense>
</TabPanel> </TabPanel>
) : null} ) : null}

View File

@ -13,7 +13,7 @@ import { SubstitutionValidator } from '../../models/oss-api';
import { type DlgEditOperationProps } from './dlg-edit-operation'; import { type DlgEditOperationProps } from './dlg-edit-operation';
export function TabSynthesis() { export function TabSubstitutions() {
const { manager } = useDialogsStore(state => state.props as DlgEditOperationProps); const { manager } = useDialogsStore(state => state.props as DlgEditOperationProps);
const { control } = useFormContext<IUpdateOperationDTO>(); const { control } = useFormContext<IUpdateOperationDTO>();
const inputs = useWatch({ control, name: 'arguments' }); const inputs = useWatch({ control, name: 'arguments' });

View File

@ -0,0 +1,158 @@
'use client';
import { Controller, useForm, useWatch } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { HelpTopic } from '@/features/help';
import { type ILibraryItem, LibraryItemType } from '@/features/library';
import { useLibrary } from '@/features/library/backend/use-library';
import { PickSchema } from '@/features/library/components/pick-schema';
import { Checkbox, TextArea, TextInput } from '@/components/input';
import { ModalForm } from '@/components/modal';
import { useDialogsStore } from '@/stores/dialogs';
import { type IImportSchemaDTO, schemaImportSchema } from '../backend/types';
import { useImportSchema } from '../backend/use-import-schema';
import { SelectParent } from '../components/select-parent';
import { sortItemsForOSS } from '../models/oss-api';
import { type LayoutManager, OPERATION_NODE_HEIGHT, OPERATION_NODE_WIDTH } from '../models/oss-layout-api';
export interface DlgImportSchemaProps {
manager: LayoutManager;
defaultX: number;
defaultY: number;
initialParent: number | null;
onCreate?: (newID: number) => void;
}
export function DlgImportSchema() {
const { importSchema } = useImportSchema();
const { manager, initialParent, onCreate, defaultX, defaultY } = useDialogsStore(
state => state.props as DlgImportSchemaProps
);
const { items: libraryItems } = useLibrary();
const sortedItems = sortItemsForOSS(manager.oss, libraryItems);
const {
control,
register,
handleSubmit,
setValue,
formState: { errors }
} = useForm<IImportSchemaDTO>({
resolver: zodResolver(schemaImportSchema),
defaultValues: {
item_data: {
alias: '',
title: '',
description: '',
parent: initialParent
},
position: {
x: defaultX,
y: defaultY,
width: OPERATION_NODE_WIDTH,
height: OPERATION_NODE_HEIGHT
},
layout: manager.layout,
source: 0,
clone_source: false
},
mode: 'onChange'
});
const alias = useWatch({ control: control, name: 'item_data.alias' });
const clone_source = useWatch({ control: control, name: 'clone_source' });
const isValid = !!alias && !manager.oss.operations.some(operation => operation.alias === alias);
function onSubmit(data: IImportSchemaDTO) {
data.position = manager.newOperationPosition(data);
data.layout = manager.layout;
void importSchema({ itemID: manager.oss.id, data: data }).then(response => onCreate?.(response.new_operation));
}
function baseFilter(item: ILibraryItem) {
return !manager.oss.schemas.includes(item.id);
}
function handleSetInput(inputID: number) {
const schema = libraryItems.find(item => item.id === inputID);
if (!schema) {
return;
}
setValue('source', inputID);
setValue('item_data.alias', schema.alias);
setValue('item_data.title', schema.title);
setValue('item_data.description', schema.description, { shouldValidate: true });
}
return (
<ModalForm
header='Создание операции: Загрузка'
submitText='Создать'
canSubmit={isValid}
onSubmit={event => void handleSubmit(onSubmit)(event)}
className='w-180 px-6 cc-column'
helpTopic={HelpTopic.CC_OSS}
>
<Controller
control={control}
name='source'
render={({ field }) => (
<PickSchema
items={sortedItems}
value={field.value}
itemType={LibraryItemType.RSFORM}
onChange={handleSetInput}
rows={8}
baseFilter={baseFilter}
/>
)}
/>
<Controller
control={control}
name='clone_source'
render={({ field }) => <Checkbox label='Клонировать схему' value={field.value} onChange={field.onChange} />}
/>
<TextInput
id='operation_title' //
label='Название'
disabled={!clone_source}
{...register('item_data.title')}
error={errors.item_data?.title}
/>
<div className='flex gap-6'>
<div className='grid gap-1'>
<TextInput
id='operation_alias' //
label='Сокращение'
className='w-80'
disabled={!clone_source}
{...register('item_data.alias')}
error={errors.item_data?.alias}
/>
<Controller
name='item_data.parent'
control={control}
render={({ field }) => (
<SelectParent
items={manager.oss.blocks}
value={field.value ? manager.oss.blockByID.get(field.value) ?? null : null}
placeholder='Родительский блок'
onChange={value => field.onChange(value ? value.id : null)}
/>
)}
/>
</div>
<TextArea
id='operation_comment' //
label='Описание'
rows={3}
disabled={!clone_source}
{...register('item_data.description')}
/>
</div>
</ModalForm>
);
}

View File

@ -1,4 +1,11 @@
import { type ICreateBlockDTO, type ICreateOperationDTO, type INodePosition, type IOssLayout } from '../backend/types'; import {
type ICreateBlockDTO,
type ICreateSchemaDTO,
type ICreateSynthesisDTO,
type IImportSchemaDTO,
type INodePosition,
type IOssLayout
} from '../backend/types';
import { type IOperationSchema } from './oss'; import { type IOperationSchema } from './oss';
import { type Position2D, type Rectangle2D } from './oss-layout'; import { type Position2D, type Rectangle2D } from './oss-layout';
@ -24,11 +31,11 @@ export class LayoutManager {
} }
/** Calculate insert position for a new {@link IOperation} */ /** Calculate insert position for a new {@link IOperation} */
newOperationPosition(data: ICreateOperationDTO): Rectangle2D { newOperationPosition(data: ICreateSchemaDTO | ICreateSynthesisDTO | IImportSchemaDTO): Rectangle2D {
const result = { x: data.position_x, y: data.position_y, width: data.width, height: data.height }; const result = { ...data.position };
const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null; const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null;
const operations = this.layout.filter(pos => pos.nodeID.startsWith('o')); const operations = this.layout.filter(pos => pos.nodeID.startsWith('o'));
if (data.arguments.length !== 0) { if ('arguments' in data && data.arguments.length !== 0) {
const pos = calculatePositionFromArgs( const pos = calculatePositionFromArgs(
operations.filter(node => data.arguments.includes(Number(node.nodeID.slice(1)))) operations.filter(node => data.arguments.includes(Number(node.nodeID.slice(1))))
); );
@ -59,20 +66,16 @@ export class LayoutManager {
.filter(node => !!node); .filter(node => !!node);
const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null; const parentNode = this.layout.find(pos => pos.nodeID === `b${data.item_data.parent}`) ?? null;
let result: Rectangle2D = { x: data.position_x, y: data.position_y, width: data.width, height: data.height }; let result: Rectangle2D = { ...data.position };
if (block_nodes.length !== 0 || operation_nodes.length !== 0) { if (block_nodes.length !== 0 || operation_nodes.length !== 0) {
result = calculatePositionFromChildren( result = calculatePositionFromChildren(data.position, operation_nodes, block_nodes);
{ x: data.position_x, y: data.position_y, width: data.width, height: data.height },
operation_nodes,
block_nodes
);
} else if (parentNode) { } else if (parentNode) {
result = { result = {
x: parentNode.x + MIN_DISTANCE, x: parentNode.x + MIN_DISTANCE,
y: parentNode.y + MIN_DISTANCE, y: parentNode.y + MIN_DISTANCE,
width: data.width, width: data.position.width,
height: data.height height: data.position.height
}; };
} else { } else {
result = this.calculatePositionForFreeBlock(result); result = this.calculatePositionForFreeBlock(result);

View File

@ -65,8 +65,10 @@ export function OssFlow() {
const showCreateOperation = useDialogsStore(state => state.showCreateOperation); const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
const showCreateBlock = useDialogsStore(state => state.showCreateBlock); const showCreateBlock = useDialogsStore(state => state.showCreateBlock);
const showCreateSchema = useDialogsStore(state => state.showCreateSchema);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showEditBlock = useDialogsStore(state => state.showEditBlock); const showEditBlock = useDialogsStore(state => state.showEditBlock);
const showImportSchema = useDialogsStore(state => state.showImportSchema);
const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu(); const { isOpen: isContextMenuOpen, menuProps, openContextMenu, hideContextMenu } = useContextMenu();
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu }); const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
@ -101,6 +103,28 @@ export function OssFlow() {
}); });
} }
function handleCreateSchema() {
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
showCreateSchema({
manager: new LayoutManager(schema, getLayout()),
defaultX: targetPosition.x,
defaultY: targetPosition.y,
initialParent: extractBlockParent(selectedItems),
onCreate: resetView
});
}
function handleImportSchema() {
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
showImportSchema({
manager: new LayoutManager(schema, getLayout()),
defaultX: targetPosition.x,
defaultY: targetPosition.y,
initialParent: extractBlockParent(selectedItems),
onCreate: resetView
});
}
function handleDeleteSelected() { function handleDeleteSelected() {
if (selected.length !== 1) { if (selected.length !== 1) {
return; return;
@ -194,8 +218,8 @@ export function OssFlow() {
<ToolbarOssGraph <ToolbarOssGraph
className='absolute z-pop top-8 right-1/2 translate-x-1/2' className='absolute z-pop top-8 right-1/2 translate-x-1/2'
onCreateBlock={handleCreateBlock} onCreateBlock={handleCreateBlock}
onCreateSchema={handleCreateOperation} onCreateSchema={handleCreateSchema}
onImportSchema={handleCreateOperation} onImportSchema={handleImportSchema}
onCreateSynthesis={handleCreateOperation} onCreateSynthesis={handleCreateOperation}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onResetPositions={resetGraph} onResetPositions={resetGraph}

View File

@ -21,7 +21,8 @@ import {
IconNewItem, IconNewItem,
IconReset, IconReset,
IconSave, IconSave,
IconSettings IconSettings,
IconSynthesis
} from '@/components/icons'; } from '@/components/icons';
import { type Styling } from '@/components/props'; import { type Styling } from '@/components/props';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
@ -184,13 +185,13 @@ export function ToolbarOssGraph({
<DropdownButton <DropdownButton
text='Импорт КС' text='Импорт КС'
titleHtml={prepareTooltip('Импорт концептуальной схемы', 'Alt + 3')} titleHtml={prepareTooltip('Импорт концептуальной схемы', 'Alt + 3')}
icon={<IconDownload size='1.25rem' className='text-constructive' />} icon={<IconDownload size='1.25rem' className='text-primary' />}
onClick={onImportSchema} onClick={onImportSchema}
/> />
<DropdownButton <DropdownButton
text='Синтез' text='Синтез'
titleHtml={prepareTooltip('Синтез концептуальных схем', 'Alt + 4')} titleHtml={prepareTooltip('Синтез концептуальных схем', 'Alt + 4')}
icon={<IconConceptBlock size='1.25rem' className='text-primary' />} icon={<IconSynthesis size='1.25rem' className='text-primary' />}
onClick={onCreateSynthesis} onClick={onCreateSynthesis}
/> />
{user.is_staff ? ( {user.is_staff ? (

View File

@ -7,10 +7,12 @@ import { type DlgEditEditorsProps } from '@/features/library/dialogs/dlg-edit-ed
import { type DlgEditVersionsProps } from '@/features/library/dialogs/dlg-edit-versions/dlg-edit-versions'; import { type DlgEditVersionsProps } from '@/features/library/dialogs/dlg-edit-versions/dlg-edit-versions';
import { type DlgChangeInputSchemaProps } from '@/features/oss/dialogs/dlg-change-input-schema'; import { type DlgChangeInputSchemaProps } from '@/features/oss/dialogs/dlg-change-input-schema';
import { type DlgCreateBlockProps } from '@/features/oss/dialogs/dlg-create-block/dlg-create-block'; import { type DlgCreateBlockProps } from '@/features/oss/dialogs/dlg-create-block/dlg-create-block';
import { type DlgCreateOperationProps } from '@/features/oss/dialogs/dlg-create-operation/dlg-create-operation'; import { type DlgCreateSchemaProps } from '@/features/oss/dialogs/dlg-create-schema';
import { type DlgCreateSynthesisProps } from '@/features/oss/dialogs/dlg-create-synthesis/dlg-create-synthesis';
import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-operation'; import { type DlgDeleteOperationProps } from '@/features/oss/dialogs/dlg-delete-operation';
import { type DlgEditBlockProps } from '@/features/oss/dialogs/dlg-edit-block'; import { type DlgEditBlockProps } from '@/features/oss/dialogs/dlg-edit-block';
import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation'; import { type DlgEditOperationProps } from '@/features/oss/dialogs/dlg-edit-operation/dlg-edit-operation';
import { type DlgImportSchemaProps } from '@/features/oss/dialogs/dlg-import-schema';
import { type DlgRelocateConstituentsProps } from '@/features/oss/dialogs/dlg-relocate-constituents'; import { type DlgRelocateConstituentsProps } from '@/features/oss/dialogs/dlg-relocate-constituents';
import { type DlgShowTermGraphProps } from '@/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph'; import { type DlgShowTermGraphProps } from '@/features/oss/dialogs/dlg-show-term-graph/dlg-show-term-graph';
import { type DlgCreateCstProps } from '@/features/rsform/dialogs/dlg-create-cst/dlg-create-cst'; import { type DlgCreateCstProps } from '@/features/rsform/dialogs/dlg-create-cst/dlg-create-cst';
@ -41,7 +43,7 @@ export const DialogType = {
CREATE_BLOCK: 7, CREATE_BLOCK: 7,
EDIT_BLOCK: 8, EDIT_BLOCK: 8,
CREATE_OPERATION: 9, CREATE_SYNTHESIS: 9,
EDIT_OPERATION: 10, EDIT_OPERATION: 10,
DELETE_OPERATION: 11, DELETE_OPERATION: 11,
CHANGE_INPUT_SCHEMA: 12, CHANGE_INPUT_SCHEMA: 12,
@ -63,7 +65,9 @@ export const DialogType = {
SHOW_AST: 25, SHOW_AST: 25,
SHOW_TYPE_GRAPH: 26, SHOW_TYPE_GRAPH: 26,
GRAPH_PARAMETERS: 27, GRAPH_PARAMETERS: 27,
SHOW_TERM_GRAPH: 28 SHOW_TERM_GRAPH: 28,
CREATE_SCHEMA: 29,
IMPORT_SCHEMA: 30
} as const; } as const;
export type DialogType = (typeof DialogType)[keyof typeof DialogType]; export type DialogType = (typeof DialogType)[keyof typeof DialogType];
@ -79,7 +83,7 @@ interface DialogsStore {
showCstTemplate: (props: DlgCstTemplateProps) => void; showCstTemplate: (props: DlgCstTemplateProps) => void;
showCreateCst: (props: DlgCreateCstProps) => void; showCreateCst: (props: DlgCreateCstProps) => void;
showCreateBlock: (props: DlgCreateBlockProps) => void; showCreateBlock: (props: DlgCreateBlockProps) => void;
showCreateOperation: (props: DlgCreateOperationProps) => void; showCreateOperation: (props: DlgCreateSynthesisProps) => void;
showDeleteCst: (props: DlgDeleteCstProps) => void; showDeleteCst: (props: DlgDeleteCstProps) => void;
showEditEditors: (props: DlgEditEditorsProps) => void; showEditEditors: (props: DlgEditEditorsProps) => void;
showEditOperation: (props: DlgEditOperationProps) => void; showEditOperation: (props: DlgEditOperationProps) => void;
@ -104,6 +108,8 @@ interface DialogsStore {
showSubstituteCst: (props: DlgSubstituteCstProps) => void; showSubstituteCst: (props: DlgSubstituteCstProps) => void;
showUploadRSForm: (props: DlgUploadRSFormProps) => void; showUploadRSForm: (props: DlgUploadRSFormProps) => void;
showEditCst: (props: DlgEditCstProps) => void; showEditCst: (props: DlgEditCstProps) => void;
showCreateSchema: (props: DlgCreateSchemaProps) => void;
showImportSchema: (props: DlgImportSchemaProps) => void;
} }
export const useDialogsStore = create<DialogsStore>()(set => ({ export const useDialogsStore = create<DialogsStore>()(set => ({
@ -118,7 +124,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
showCstTemplate: props => set({ active: DialogType.CONSTITUENTA_TEMPLATE, props: props }), showCstTemplate: props => set({ active: DialogType.CONSTITUENTA_TEMPLATE, props: props }),
showCreateCst: props => set({ active: DialogType.CREATE_CONSTITUENTA, props: props }), showCreateCst: props => set({ active: DialogType.CREATE_CONSTITUENTA, props: props }),
showCreateOperation: props => set({ active: DialogType.CREATE_OPERATION, props: props }), showCreateOperation: props => set({ active: DialogType.CREATE_SYNTHESIS, props: props }),
showCreateBlock: props => set({ active: DialogType.CREATE_BLOCK, props: props }), showCreateBlock: props => set({ active: DialogType.CREATE_BLOCK, props: props }),
showDeleteCst: props => set({ active: DialogType.DELETE_CONSTITUENTA, props: props }), showDeleteCst: props => set({ active: DialogType.DELETE_CONSTITUENTA, props: props }),
showEditEditors: props => set({ active: DialogType.EDIT_EDITORS, props: props }), showEditEditors: props => set({ active: DialogType.EDIT_EDITORS, props: props }),
@ -143,5 +149,7 @@ export const useDialogsStore = create<DialogsStore>()(set => ({
showQR: props => set({ active: DialogType.SHOW_QR_CODE, props: props }), showQR: props => set({ active: DialogType.SHOW_QR_CODE, props: props }),
showSubstituteCst: props => set({ active: DialogType.SUBSTITUTE_CONSTITUENTS, props: props }), showSubstituteCst: props => set({ active: DialogType.SUBSTITUTE_CONSTITUENTS, props: props }),
showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props }), showUploadRSForm: props => set({ active: DialogType.UPLOAD_RSFORM, props: props }),
showEditCst: props => set({ active: DialogType.EDIT_CONSTITUENTA, props: props }) showEditCst: props => set({ active: DialogType.EDIT_CONSTITUENTA, props: props }),
showCreateSchema: props => set({ active: DialogType.CREATE_SCHEMA, props: props }),
showImportSchema: props => set({ active: DialogType.IMPORT_SCHEMA, props: props })
})); }));