diff --git a/.vscode/settings.json b/.vscode/settings.json
index ad6cfec1..709589a5 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -128,6 +128,7 @@
"perfectivity",
"PNCT",
"ponomarev",
+ "popleft",
"PRCL",
"PRTF",
"PRTS",
diff --git a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py
index 9fd63189..08d57fd4 100644
--- a/rsconcept/backend/apps/oss/models/OperationSchemaCached.py
+++ b/rsconcept/backend/apps/oss/models/OperationSchemaCached.py
@@ -23,6 +23,7 @@ from apps.rsform.models import (
from .Argument import Argument
from .Inheritance import Inheritance
from .Operation import Operation, OperationType
+from .Reference import Reference
from .Substitution import Substitution
CstMapping = dict[str, Optional[Constituenta]]
@@ -36,13 +37,15 @@ class OperationSchemaCached:
self.model = model
self.cache = OssCache(self)
- def delete_reference(self, target: Operation, keep_connections: bool = False):
+ def delete_reference(self, target: int, keep_connections: bool = False, keep_constituents: bool = False):
''' Delete Reference Operation. '''
+ self.cache.ensure_loaded_subs()
+ operation = self.cache.operation_by_id[target]
if keep_connections:
- referred_operations = target.getQ_reference_target()
+ referred_operations = operation.getQ_reference_target()
if len(referred_operations) == 1:
referred_operation = referred_operations[0]
- for arg in target.getQ_as_argument():
+ for arg in operation.getQ_as_argument():
arg.pk = None
arg.argument = referred_operation
arg.save()
@@ -50,7 +53,9 @@ class OperationSchemaCached:
pass
# if target.result_id is not None:
# self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema
- target.delete()
+
+ self.cache.remove_operation(target)
+ operation.delete()
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete Operation. '''
@@ -707,17 +712,23 @@ class OssCache:
self._schemas: list[RSFormCached] = []
self._schema_by_id: dict[int, RSFormCached] = {}
- self.operations = list(Operation.objects.filter(oss=oss.model).only('result_id'))
+ self.operations = list(Operation.objects.filter(oss=oss.model).only('result_id', 'operation_type'))
self.operation_by_id = {operation.pk: operation for operation in self.operations}
self.graph = Graph[int]()
for operation in self.operations:
self.graph.add_node(operation.pk)
+
+ references = Reference.objects.filter(reference__oss=self._oss.model).only('reference_id', 'target_id')
+ self.reference_target = {ref.reference_id: ref.target_id for ref in references}
arguments = Argument.objects \
.filter(operation__oss=self._oss.model) \
.only('operation_id', 'argument_id') \
.order_by('order')
for argument in arguments:
self.graph.add_edge(argument.argument_id, argument.operation_id)
+ target = self.reference_target.get(argument.argument_id)
+ if target is not None:
+ self.graph.add_edge(target, argument.operation_id)
self.is_loaded_subs = False
self.substitutions: dict[int, list[Substitution]] = {}
@@ -785,18 +796,12 @@ class OssCache:
schema.cache.ensure_loaded()
self._insert_new(schema)
- def insert_operation(self, operation: Operation) -> None:
- ''' Insert new operation. '''
- self.operations.append(operation)
- self.operation_by_id[operation.pk] = operation
- self.graph.add_node(operation.pk)
- if self.is_loaded_subs:
- self.substitutions[operation.pk] = []
- self.inheritance[operation.pk] = []
-
def insert_argument(self, argument: Argument) -> None:
''' Insert new argument. '''
self.graph.add_edge(argument.argument_id, argument.operation_id)
+ target = self.reference_target.get(argument.argument_id)
+ if target is not None:
+ self.graph.add_edge(target, argument.operation_id)
def insert_inheritance(self, inheritance: Inheritance) -> None:
''' Insert new inheritance. '''
@@ -839,6 +844,10 @@ class OssCache:
def remove_argument(self, argument: Argument) -> None:
''' Remove argument from cache. '''
self.graph.remove_edge(argument.argument_id, argument.operation_id)
+ target = self.reference_target.get(argument.argument_id)
+ if target is not None:
+ if not Argument.objects.filter(argument_id=target, operation_id=argument.operation_id).exists():
+ self.graph.remove_edge(target, argument.operation_id)
def remove_substitution(self, target: Substitution) -> None:
''' Remove substitution from cache. '''
diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py
index 94fc2100..98ddfe21 100644
--- a/rsconcept/backend/apps/oss/serializers/__init__.py
+++ b/rsconcept/backend/apps/oss/serializers/__init__.py
@@ -16,6 +16,7 @@ from .data_access import (
MoveItemsSerializer,
OperationSchemaSerializer,
OperationSerializer,
+ ReferenceSerializer,
RelocateConstituentsSerializer,
SetOperationInputSerializer,
TargetOperationSerializer,
diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py
index c854b2fa..07893003 100644
--- a/rsconcept/backend/apps/oss/serializers/data_access.py
+++ b/rsconcept/backend/apps/oss/serializers/data_access.py
@@ -13,7 +13,16 @@ from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg
from shared.serializers import StrictModelSerializer, StrictSerializer
-from ..models import Argument, Block, Inheritance, Layout, Operation, OperationType, Substitution
+from ..models import (
+ Argument,
+ Block,
+ Inheritance,
+ Layout,
+ Operation,
+ OperationType,
+ Reference,
+ Substitution
+)
from .basics import NodeSerializer, PositionSerializer, SubstitutionExSerializer
@@ -45,6 +54,14 @@ class ArgumentSerializer(StrictModelSerializer):
fields = ('operation', 'argument')
+class ReferenceSerializer(StrictModelSerializer):
+ ''' Serializer: Reference data. '''
+ class Meta:
+ ''' serializer metadata. '''
+ model = Reference
+ fields = ('reference', 'target')
+
+
class CreateBlockSerializer(StrictSerializer):
''' Serializer: Block creation. '''
class BlockCreateData(StrictModelSerializer):
@@ -444,6 +461,7 @@ class DeleteReferenceSerializer(StrictSerializer):
)
target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'operation_type'))
keep_connections = serializers.BooleanField(default=False, required=False)
+ keep_constituents = serializers.BooleanField(default=False, required=False)
def validate(self, attrs):
oss = cast(LibraryItem, self.context['oss'])
@@ -517,6 +535,9 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitutions = serializers.ListField(
child=SubstitutionExSerializer()
)
+ references = serializers.ListField(
+ child=ReferenceSerializer()
+ )
layout = serializers.ListField(
child=NodeSerializer()
)
@@ -534,6 +555,7 @@ class OperationSchemaSerializer(StrictModelSerializer):
result['blocks'] = []
result['arguments'] = []
result['substitutions'] = []
+ result['references'] = []
for operation in Operation.objects.filter(oss=instance).order_by('pk'):
operation_data = OperationSerializer(operation).data
operation_result = operation.result
@@ -556,6 +578,9 @@ class OperationSchemaSerializer(StrictModelSerializer):
substitution_term=F('substitution__term_resolved'),
).order_by('pk'):
result['substitutions'].append(substitution)
+ for reference in Reference.objects.filter(target__oss=instance).order_by('pk'):
+ result['references'].append(ReferenceSerializer(reference).data)
+
return result
diff --git a/rsconcept/backend/apps/oss/tests/s_models/__init__.py b/rsconcept/backend/apps/oss/tests/s_models/__init__.py
index 95f5899a..ebfbdc83 100644
--- a/rsconcept/backend/apps/oss/tests/s_models/__init__.py
+++ b/rsconcept/backend/apps/oss/tests/s_models/__init__.py
@@ -1,5 +1,7 @@
''' Tests for Django Models. '''
from .t_Argument import *
from .t_Inheritance import *
+from .t_Layout import *
from .t_Operation import *
+from .t_Reference import *
from .t_Substitution import *
diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_Layout.py b/rsconcept/backend/apps/oss/tests/s_models/t_Layout.py
new file mode 100644
index 00000000..ab73eb3c
--- /dev/null
+++ b/rsconcept/backend/apps/oss/tests/s_models/t_Layout.py
@@ -0,0 +1,39 @@
+''' Testing models: Layout. '''
+from django.test import TestCase
+
+from apps.library.models import LibraryItem
+from apps.oss.models import Layout
+
+
+class TestLayout(TestCase):
+ ''' Testing Layout model. '''
+
+
+ def setUp(self):
+ self.library_item = LibraryItem.objects.create(alias='LIB1')
+ self.layout = Layout.objects.create(
+ oss=self.library_item,
+ data=[{'x': 1, 'y': 2}]
+ )
+
+
+ def test_str(self):
+ expected = f'Схема расположения {self.library_item.alias}'
+ self.assertEqual(str(self.layout), expected)
+
+
+ def test_update_data(self):
+ new_data = [{'x': 10, 'y': 20}]
+ Layout.update_data(self.library_item.id, new_data)
+ self.layout.refresh_from_db()
+ self.assertEqual(self.layout.data, new_data)
+
+
+ def test_default_data(self):
+ layout2 = Layout.objects.create(oss=self.library_item)
+ self.assertEqual(layout2.data, [])
+
+
+ def test_related_name_layout(self):
+ layouts = self.library_item.layout.all()
+ self.assertIn(self.layout, layouts)
diff --git a/rsconcept/backend/apps/oss/tests/s_models/t_Reference.py b/rsconcept/backend/apps/oss/tests/s_models/t_Reference.py
new file mode 100644
index 00000000..13a7a986
--- /dev/null
+++ b/rsconcept/backend/apps/oss/tests/s_models/t_Reference.py
@@ -0,0 +1,44 @@
+''' Testing models: Reference. '''
+from django.test import TestCase
+
+from apps.oss.models import Operation, OperationSchema, OperationType, Reference
+from apps.rsform.models import RSForm
+
+
+class TestReference(TestCase):
+ ''' Testing Reference model. '''
+
+
+ def setUp(self):
+ self.oss = OperationSchema.create(alias='T1')
+
+ self.operation1 = Operation.objects.create(
+ oss=self.oss.model,
+ alias='KS1',
+ operation_type=OperationType.INPUT,
+ )
+ self.operation2 = Operation.objects.create(
+ oss=self.oss.model,
+ operation_type=OperationType.REFERENCE,
+ )
+ self.reference = Reference.objects.create(
+ reference=self.operation2,
+ target=self.operation1
+ )
+
+
+ def test_str(self):
+ testStr = f'{self.operation2} -> {self.operation1}'
+ self.assertEqual(str(self.reference), testStr)
+
+
+ def test_cascade_delete_operation(self):
+ self.assertEqual(Reference.objects.count(), 1)
+ self.operation2.delete()
+ self.assertEqual(Reference.objects.count(), 0)
+
+
+ def test_cascade_delete_target(self):
+ self.assertEqual(Reference.objects.count(), 1)
+ self.operation1.delete()
+ self.assertEqual(Reference.objects.count(), 0)
diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py b/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py
index fd9d7fbb..57681282 100644
--- a/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py
+++ b/rsconcept/backend/apps/oss/tests/s_propagation/__init__.py
@@ -2,4 +2,5 @@
from .t_attributes import *
from .t_constituents import *
from .t_operations import *
+from .t_references import *
from .t_substitutions import *
diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py
new file mode 100644
index 00000000..e4992579
--- /dev/null
+++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py
@@ -0,0 +1,199 @@
+''' Testing API: Propagate changes through references in OSS. '''
+
+from apps.oss.models import OperationSchema, OperationType
+from apps.rsform.models import Constituenta, CstType, RSForm
+from shared.EndpointTester import EndpointTester, decl_endpoint
+
+
+class ReferencePropagationTestCase(EndpointTester):
+ ''' Test propagation through references in OSS. '''
+
+ def setUp(self):
+ super().setUp()
+
+ self.owned = OperationSchema.create(
+ title='Test',
+ alias='T1',
+ owner=self.user
+ )
+ self.owned_id = self.owned.model.pk
+
+ self.ks1 = RSForm.create(
+ alias='KS1',
+ title='Test1',
+ owner=self.user
+ )
+ self.ks1X1 = self.ks1.insert_last('X1', convention='KS1X1')
+ self.ks1X2 = self.ks1.insert_last('X2', convention='KS1X2')
+ self.ks1D1 = self.ks1.insert_last('D1', definition_formal='X1 X2', convention='KS1D1')
+
+ self.ks2 = RSForm.create(
+ alias='KS2',
+ title='Test2',
+ owner=self.user
+ )
+ self.ks2X1 = self.ks2.insert_last('X1', convention='KS2X1')
+ self.ks2X2 = self.ks2.insert_last('X2', convention='KS2X2')
+ self.ks2S1 = self.ks2.insert_last(
+ alias='S1',
+ definition_formal=r'ℬ(X1)',
+ convention='KS2S1'
+ )
+
+ self.operation1 = self.owned.create_operation(
+ alias='1',
+ operation_type=OperationType.INPUT,
+ result=self.ks1.model
+ )
+ self.operation2 = self.owned.create_operation(
+ alias='2',
+ operation_type=OperationType.INPUT,
+ result=self.ks2.model
+ )
+ self.operation3 = self.owned.create_reference(self.operation1)
+
+ self.operation4 = self.owned.create_operation(
+ alias='4',
+ operation_type=OperationType.SYNTHESIS
+ )
+ self.owned.set_arguments(self.operation4.pk, [self.operation1, self.operation2])
+ self.owned.set_substitutions(self.operation4.pk, [{
+ 'original': self.ks1X1,
+ 'substitution': self.ks2S1
+ }])
+ self.owned.execute_operation(self.operation4)
+ self.operation4.refresh_from_db()
+ self.ks4 = RSForm(self.operation4.result)
+ self.ks4X1 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk)
+ self.ks4S1 = Constituenta.objects.get(as_child__parent_id=self.ks2S1.pk)
+ self.ks4D1 = Constituenta.objects.get(as_child__parent_id=self.ks1D1.pk)
+ self.ks4D2 = self.ks4.insert_last(
+ alias='D2',
+ definition_formal=r'X1 X2 X3 S1 D1',
+ convention='KS4D2'
+ )
+
+ self.operation5 = self.owned.create_operation(
+ alias='5',
+ operation_type=OperationType.SYNTHESIS
+ )
+ self.owned.set_arguments(self.operation5.pk, [self.operation4, self.operation3])
+ self.owned.set_substitutions(self.operation5.pk, [{
+ 'original': self.ks4X1,
+ 'substitution': self.ks1X1
+ }])
+ self.owned.execute_operation(self.operation5)
+ self.operation5.refresh_from_db()
+ self.ks5 = RSForm(self.operation5.result)
+ self.ks5D4 = self.ks5.insert_last(
+ alias='D4',
+ definition_formal=r'X1 X2 X3 S1 D1 D2 D3',
+ convention='KS5D4'
+ )
+
+ self.layout_data = [
+ {'nodeID': 'o' + str(self.operation1.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
+ {'nodeID': 'o' + str(self.operation2.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
+ {'nodeID': 'o' + str(self.operation3.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
+ {'nodeID': 'o' + str(self.operation4.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40},
+ {'nodeID': 'o' + str(self.operation5.pk), 'x': 0, 'y': 0, 'width': 150, 'height': 40}
+ ]
+ layout = OperationSchema.layoutQ(self.owned_id)
+ layout.data = self.layout_data
+ layout.save()
+
+
+ def test_reference_creation(self):
+ ''' Test reference creation. '''
+ self.assertEqual(self.operation1.result, self.operation3.result)
+
+
+
+ # @decl_endpoint('/api/oss/{item}/delete-reference', method='patch')
+ # def test_delete_reference_propagation(self):
+ # ''' Test propagation when deleting a reference operation. '''
+ # schema_cached = OperationSchemaCached(self.schema2)
+ # # Ensure reference exists
+ # self.assertIn(self.reference.pk, schema_cached.cache.operation_by_id)
+ # # Delete the reference
+ # schema_cached.delete_reference(self.reference.pk)
+ # # Reference should be deleted
+ # with self.assertRaises(Reference.DoesNotExist):
+ # Reference.objects.get(pk=self.reference.pk)
+ # # Operation2 should still exist
+ # self.assertTrue(Operation.objects.filter(pk=self.op2.pk).exists())
+
+
+ # @decl_endpoint('/api/oss/{item}/set-arguments', method='patch')
+ # def test_set_arguments_propagation(self):
+ # ''' Test propagation when setting arguments for a reference. '''
+ # schema_cached = OperationSchemaCached(self.schema2)
+ # # Add op1 as argument to op2
+ # schema_cached.set_arguments(self.op2.pk, [self.op1])
+ # op2 = Operation.objects.get(pk=self.op2.pk)
+ # args = list(op2.getQ_arguments())
+ # self.assertEqual(len(args), 1)
+ # self.assertEqual(args[0].argument, self.op1)
+
+
+ # @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
+ # def test_delete_operation_with_reference(self):
+ # ''' Test propagation when deleting an operation that is referenced. '''
+ # schema_cached = OperationSchemaCached(self.schema1)
+ # # op1 is referenced by reference
+ # self.assertEqual(self.reference.getQ_reference_target(), self.op1)
+ # # Delete op1
+ # schema_cached.delete_operation(self.op1.pk)
+ # # op1 should be deleted
+ # with self.assertRaises(Operation.DoesNotExist):
+ # Operation.objects.get(pk=self.op1.pk)
+ # # Reference should be deleted as well
+ # self.assertFalse(Reference.objects.filter(pk=self.reference.pk).exists())
+
+
+ # @decl_endpoint('/api/oss/{item}/add-constituent', method='patch')
+ # def test_add_constituent_propagation(self):
+ # ''' Test propagation when adding a constituent to a referenced schema. '''
+ # # Add a new constituent to schema1 (referenced by op1, which is referenced by reference)
+ # new_cst = Constituenta.objects.create(
+ # schema=self.schema1, alias='cst_new', title='New Constituenta', cst_type=CstType.ATTRIBUTE
+ # )
+ # # Simulate propagation: after adding, the reference should still be valid and schema1 should have the new cst
+ # self.assertTrue(Constituenta.objects.filter(pk=new_cst.pk, schema=self.schema1).exists())
+ # # The reference's target operation's result should include the new constituent
+ # op1 = Operation.objects.get(pk=self.op1.pk)
+ # self.assertEqual(op1.result, self.schema1)
+ # self.assertIn(new_cst, Constituenta.objects.filter(schema=op1.result))
+
+
+ # @decl_endpoint('/api/oss/{item}/remove-constituent', method='patch')
+ # def test_remove_constituent_propagation(self):
+ # ''' Test propagation when removing a constituent from a referenced schema. '''
+ # # Remove cst2 from schema1
+ # self.cst2.delete()
+ # # The reference's target operation's result should not include cst2
+ # op1 = Operation.objects.get(pk=self.op1.pk)
+ # self.assertEqual(op1.result, self.schema1)
+ # self.assertNotIn(self.cst2, Constituenta.objects.filter(schema=op1.result))
+ # # Reference should still be valid
+ # self.assertEqual(self.reference.getQ_reference_target(), self.op1)
+
+
+ # @decl_endpoint('/api/oss/{item}/add-constituent-to-referenced-schema', method='patch')
+ # def test_propagation_to_multiple_references(self):
+ # ''' Test propagation when a schema is referenced by multiple references and constituents are added. '''
+ # # Create another reference to op1
+ # reference2 = Reference.objects.create(
+ # alias='ref2', title='Reference 2', type=OperationType.REFERENCE, result=self.schema2
+ # )
+ # reference2.setQ_reference_target(self.op1)
+ # reference2.save()
+ # # Add a new constituent to schema1
+ # new_cst = Constituenta.objects.create(
+ # schema=self.schema1, alias='cst_multi', title='Multi Constituenta', cst_type=CstType.ATTRIBUTE
+ # )
+ # # Both references should still be valid and op1's result should include the new constituent
+ # op1 = Operation.objects.get(pk=self.op1.pk)
+ # self.assertIn(new_cst, Constituenta.objects.filter(schema=op1.result))
+ # self.assertEqual(self.reference.getQ_reference_target(), self.op1)
+ # self.assertEqual(reference2.getQ_reference_target(), self.op1)
diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py
index c82eb99e..b5abd99e 100644
--- a/rsconcept/backend/apps/oss/views/oss.py
+++ b/rsconcept/backend/apps/oss/views/oss.py
@@ -683,7 +683,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
with transaction.atomic():
oss = m.OperationSchemaCached(item)
m.Layout.update_data(pk, layout)
- oss.delete_reference(operation, serializer.validated_data['keep_connections'])
+ oss.delete_reference(operation.pk, serializer.validated_data['keep_connections'])
item.save(update_fields=['time_update'])
return Response(
diff --git a/rsconcept/frontend/src/app/global-dialogs.tsx b/rsconcept/frontend/src/app/global-dialogs.tsx
index b61c4de6..366d6465 100644
--- a/rsconcept/frontend/src/app/global-dialogs.tsx
+++ b/rsconcept/frontend/src/app/global-dialogs.tsx
@@ -45,6 +45,11 @@ const DlgDeleteOperation = React.lazy(() =>
default: module.DlgDeleteOperation
}))
);
+const DlgDeleteReference = React.lazy(() =>
+ import('@/features/oss/dialogs/dlg-delete-reference').then(module => ({
+ default: module.DlgDeleteReference
+ }))
+);
const DlgEditEditors = React.lazy(() =>
import('@/features/library/dialogs/dlg-edit-editors').then(module => ({
default: module.DlgEditEditors
@@ -196,6 +201,8 @@ export const GlobalDialogs = () => {
return ;
case DialogType.DELETE_OPERATION:
return ;
+ case DialogType.DELETE_REFERENCE:
+ return ;
case DialogType.GRAPH_PARAMETERS:
return ;
case DialogType.RELOCATE_CONSTITUENTS:
diff --git a/rsconcept/frontend/src/components/icons.tsx b/rsconcept/frontend/src/components/icons.tsx
index 268ae8a7..58bd52f3 100644
--- a/rsconcept/frontend/src/components/icons.tsx
+++ b/rsconcept/frontend/src/components/icons.tsx
@@ -95,6 +95,8 @@ export { TbHexagonLetterD as IconCstTerm } from 'react-icons/tb';
export { TbHexagonLetterF as IconCstFunction } from 'react-icons/tb';
export { TbHexagonLetterP as IconCstPredicate } from 'react-icons/tb';
export { TbHexagonLetterT as IconCstTheorem } from 'react-icons/tb';
+export { PiArrowsMergeFill as IconSynthesis } from 'react-icons/pi';
+export { VscReferences as IconReference } from 'react-icons/vsc';
export { LuNewspaper as IconDefinition } from 'react-icons/lu';
export { LuDna as IconTerminology } from 'react-icons/lu';
export { FaRegHandshake as IconConvention } from 'react-icons/fa6';
@@ -129,7 +131,6 @@ export { BiStopCircle as IconStatusIncalculable } from 'react-icons/bi';
export { BiPauseCircle as IconStatusProperty } from 'react-icons/bi';
export { LuPower as IconKeepAliasOn } from 'react-icons/lu';
export { LuPowerOff as IconKeepAliasOff } from 'react-icons/lu';
-export { VscReferences as IconPhantom } from 'react-icons/vsc';
// ===== Domain actions =====
export { BiUpvote as IconMoveUp } from 'react-icons/bi';
@@ -144,7 +145,6 @@ export { BiDuplicate as IconClone } from 'react-icons/bi';
export { LuReplace as IconReplace } from 'react-icons/lu';
export { FaSortAmountDownAlt as IconSortList } from 'react-icons/fa';
export { LuNetwork as IconGenerateStructure } from 'react-icons/lu';
-export { LuCombine as IconSynthesis } from 'react-icons/lu';
export { LuBookCopy as IconInlineSynthesis } from 'react-icons/lu';
export { LuWandSparkles as IconGenerateNames } from 'react-icons/lu';
export { GrConnect as IconConnect } from 'react-icons/gr';
diff --git a/rsconcept/frontend/src/features/oss/backend/api.ts b/rsconcept/frontend/src/features/oss/backend/api.ts
index 3c58240e..3c7d31a7 100644
--- a/rsconcept/frontend/src/features/oss/backend/api.ts
+++ b/rsconcept/frontend/src/features/oss/backend/api.ts
@@ -9,10 +9,12 @@ import {
type ICloneSchemaDTO,
type IConstituentaReference,
type ICreateBlockDTO,
+ type ICreateReferenceDTO,
type ICreateSchemaDTO,
type ICreateSynthesisDTO,
type IDeleteBlockDTO,
type IDeleteOperationDTO,
+ type IDeleteReferenceDTO,
type IImportSchemaDTO,
type IInputCreatedResponse,
type IMoveItemsDTO,
@@ -87,6 +89,28 @@ export const ossApi = {
}
}),
+ createReference: ({ itemID, data }: { itemID: number; data: ICreateReferenceDTO }) =>
+ axiosPost({
+ schema: schemaOperationCreatedResponse,
+ endpoint: `/api/oss/${itemID}/create-reference`,
+ request: {
+ data: data,
+ successMessage: response => {
+ const alias = response.oss.operations.find(op => op.id === response.new_operation)?.alias;
+ return infoMsg.newOperation(alias ?? 'ОШИБКА');
+ }
+ }
+ }),
+ deleteReference: ({ itemID, data }: { itemID: number; data: IDeleteReferenceDTO }) =>
+ axiosPatch({
+ schema: schemaOperationSchema,
+ endpoint: `/api/oss/${itemID}/delete-reference`,
+ request: {
+ data: data,
+ successMessage: infoMsg.operationDestroyed
+ }
+ }),
+
createSchema: ({ itemID, data }: { itemID: number; data: ICreateSchemaDTO }) =>
axiosPost({
schema: schemaOperationCreatedResponse,
diff --git a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
index 59ca4dab..d0aad68c 100644
--- a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
+++ b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
@@ -29,7 +29,7 @@ export class OssLoader {
private schemaIDs: number[] = [];
constructor(input: RO) {
- this.oss = structuredClone(input) as IOperationSchema;
+ this.oss = structuredClone(input) as unknown as IOperationSchema;
}
produceOSS(): IOperationSchema {
@@ -83,16 +83,37 @@ export class OssLoader {
}
private inferOperationAttributes() {
+ const referenceCounts = new Map();
+
this.graph.topologicalOrder().forEach(operationID => {
const operation = this.operationByID.get(operationID)!;
const position = this.oss.layout.find(item => item.nodeID === operation.nodeID);
operation.x = position?.x ?? 0;
operation.y = position?.y ?? 0;
- operation.is_consolidation = this.inferConsolidation(operationID);
- operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
- operation.arguments = this.oss.arguments
- .filter(item => item.operation === operationID)
- .map(item => item.argument);
+ switch (operation.operation_type) {
+ case OperationType.INPUT:
+ break;
+ case OperationType.SYNTHESIS:
+ operation.is_consolidation = this.inferConsolidation(operationID);
+ operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID);
+ operation.arguments = this.oss.arguments
+ .filter(item => item.operation === operationID)
+ .map(item => item.argument);
+ break;
+ case OperationType.REFERENCE:
+ const ref = this.oss.references.find(item => item.reference === operationID);
+ const target = !!ref ? this.oss.operationByID.get(ref.target) : null;
+ if (!target || !ref) {
+ throw new Error(`Reference ${operationID} not found`);
+ }
+ const refCount = (referenceCounts.get(target.id) ?? 0) + 1;
+ referenceCounts.set(target.id, refCount);
+ operation.target = ref.target;
+ operation.alias = `[${refCount}] ${target.alias}`;
+ operation.title = target.title;
+ operation.description = target.description;
+ break;
+ }
});
}
@@ -126,8 +147,11 @@ export class OssLoader {
count_inputs: operations.filter(item => item.operation_type === OperationType.INPUT).length,
count_synthesis: operations.filter(item => item.operation_type === OperationType.SYNTHESIS).length,
count_schemas: this.schemaIDs.length,
- count_owned: operations.filter(item => !!item.result && !item.is_import).length,
- count_block: this.oss.blocks.length
+ count_owned: operations.filter(
+ item => !!item.result && (item.operation_type !== OperationType.INPUT || !item.is_import)
+ ).length,
+ count_block: this.oss.blocks.length,
+ count_references: this.oss.references.length
};
}
}
diff --git a/rsconcept/frontend/src/features/oss/backend/types.ts b/rsconcept/frontend/src/features/oss/backend/types.ts
index b8d373c5..72ca1755 100644
--- a/rsconcept/frontend/src/features/oss/backend/types.ts
+++ b/rsconcept/frontend/src/features/oss/backend/types.ts
@@ -9,7 +9,8 @@ import { errorMsg } from '@/utils/labels';
/** Represents {@link IOperation} type. */
export const OperationType = {
INPUT: 'input',
- SYNTHESIS: 'synthesis'
+ SYNTHESIS: 'synthesis',
+ REFERENCE: 'reference'
} as const;
export type OperationType = (typeof OperationType)[keyof typeof OperationType];
@@ -48,6 +49,7 @@ export type IMoveItemsDTO = z.infer;
/** Represents {@link IOperation} data, used in Create action. */
export type ICreateSchemaDTO = z.infer;
+export type ICreateReferenceDTO = z.infer;
export type ICreateSynthesisDTO = z.infer;
export type IImportSchemaDTO = z.infer;
export type ICloneSchemaDTO = z.infer;
@@ -61,6 +63,9 @@ export type IUpdateOperationDTO = z.infer;
/** Represents {@link IOperation} data, used in Delete action. */
export type IDeleteOperationDTO = z.infer;
+/** Represents {@link IOperation} reference type data, used in Delete action. */
+export type IDeleteReferenceDTO = z.infer;
+
/** Represents target {@link IOperation}. */
export interface ITargetOperation {
layout: IOssLayout;
@@ -119,6 +124,11 @@ export const schemaBlock = z.strictObject({
parent: z.number().nullable()
});
+const schemaReference = z.strictObject({
+ target: z.number(),
+ reference: z.number()
+});
+
export const schemaPosition = z.strictObject({
x: z.number(),
y: z.number(),
@@ -148,6 +158,7 @@ export const schemaOperationSchema = schemaLibraryItem.extend({
editors: z.number().array(),
operations: z.array(schemaOperation),
blocks: z.array(schemaBlock),
+ references: z.array(schemaReference),
layout: schemaOssLayout,
arguments: z
.object({
@@ -188,6 +199,12 @@ export const schemaCreateSchema = z.strictObject({
position: schemaPosition
});
+export const schemaCreateReference = z.strictObject({
+ target: z.number(),
+ layout: schemaOssLayout,
+ position: schemaPosition
+});
+
export const schemaCloneSchema = z.strictObject({
layout: schemaOssLayout,
source_operation: z.number(),
@@ -230,6 +247,13 @@ export const schemaDeleteOperation = z.strictObject({
delete_schema: z.boolean()
});
+export const schemaDeleteReference = z.strictObject({
+ target: z.number(),
+ layout: schemaOssLayout,
+ keep_constituents: z.boolean(),
+ keep_connections: z.boolean()
+});
+
export const schemaMoveItems = z.strictObject({
layout: schemaOssLayout,
operations: z.array(z.number()),
diff --git a/rsconcept/frontend/src/features/oss/backend/use-create-reference.ts b/rsconcept/frontend/src/features/oss/backend/use-create-reference.ts
new file mode 100644
index 00000000..52d894ac
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/backend/use-create-reference.ts
@@ -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 ICreateReferenceDTO } from './types';
+
+export const useCreateReference = () => {
+ const client = useQueryClient();
+ const { updateTimestamp } = useUpdateTimestamp();
+ const mutation = useMutation({
+ mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'create-reference'],
+ mutationFn: ossApi.createReference,
+ onSuccess: data => {
+ updateTimestamp(data.oss.id, data.oss.time_update);
+ client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.oss.id }).queryKey, data.oss);
+ },
+ onError: () => client.invalidateQueries()
+ });
+ return {
+ createReference: (data: { itemID: number; data: ICreateReferenceDTO }) => mutation.mutateAsync(data)
+ };
+};
diff --git a/rsconcept/frontend/src/features/oss/backend/use-delete-reference.ts b/rsconcept/frontend/src/features/oss/backend/use-delete-reference.ts
new file mode 100644
index 00000000..af0d3ee2
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/backend/use-delete-reference.ts
@@ -0,0 +1,29 @@
+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 IDeleteReferenceDTO } from './types';
+
+export const useDeleteReference = () => {
+ const client = useQueryClient();
+ const { updateTimestamp } = useUpdateTimestamp();
+ const mutation = useMutation({
+ mutationKey: [KEYS.global_mutation, ossApi.baseKey, 'delete-reference'],
+ mutationFn: ossApi.deleteReference,
+ onSuccess: async data => {
+ updateTimestamp(data.id, data.time_update);
+ client.setQueryData(ossApi.getOssQueryOptions({ itemID: data.id }).queryKey, data);
+ await Promise.allSettled([
+ client.invalidateQueries({ queryKey: KEYS.composite.libraryList }),
+ client.invalidateQueries({ queryKey: [KEYS.rsform] })
+ ]);
+ },
+ onError: () => client.invalidateQueries()
+ });
+ return {
+ deleteReference: (data: { itemID: number; data: IDeleteReferenceDTO }) => mutation.mutateAsync(data)
+ };
+};
diff --git a/rsconcept/frontend/src/features/oss/components/info-operation.tsx b/rsconcept/frontend/src/features/oss/components/info-operation.tsx
index 1928c89c..816e929d 100644
--- a/rsconcept/frontend/src/features/oss/components/info-operation.tsx
+++ b/rsconcept/frontend/src/features/oss/components/info-operation.tsx
@@ -47,12 +47,12 @@ export function InfoOperation({ operation }: InfoOperationProps) {
Тип: {labelOperationType(operation.operation_type)}
- {operation.is_import ? (
+ {operation.operation_type === OperationType.INPUT && operation.is_import ? (
КС не принадлежит ОСС
) : null}
- {operation.is_consolidation ? (
+ {operation.operation_type === OperationType.SYNTHESIS && operation.is_consolidation ? (
Ромбовидный синтез
@@ -69,7 +69,7 @@ export function InfoOperation({ operation }: InfoOperationProps) {
{operation.description}
) : null}
- {operation.substitutions.length > 0 ? (
+ {operation.operation_type === OperationType.SYNTHESIS && operation.substitutions.length > 0 ? (
+