From 9c51e368a5bf06362d95931b3e5889d8ff32a6f4 Mon Sep 17 00:00:00 2001
From: Ivan <8611739+IRBorisov@users.noreply.github.com>
Date: Mon, 4 Aug 2025 22:58:08 +0300
Subject: [PATCH] F: Implementing references
---
.vscode/settings.json | 1 +
.../apps/oss/models/OperationSchemaCached.py | 62 ++++----
.../backend/apps/oss/serializers/__init__.py | 1 +
.../apps/oss/serializers/data_access.py | 27 +++-
.../apps/oss/tests/s_models/__init__.py | 2 +
.../apps/oss/tests/s_models/t_Layout.py | 39 +++++
.../apps/oss/tests/s_models/t_Reference.py | 44 ++++++
.../apps/oss/tests/s_propagation/__init__.py | 1 +
.../oss/tests/s_propagation/t_references.py | 146 ++++++++++++++++++
rsconcept/backend/apps/oss/views/oss.py | 2 +-
rsconcept/frontend/src/app/global-dialogs.tsx | 7 +
rsconcept/frontend/src/components/icons.tsx | 4 +-
.../frontend/src/features/oss/backend/api.ts | 24 +++
.../src/features/oss/backend/oss-loader.ts | 55 +++++--
.../src/features/oss/backend/types.ts | 26 +++-
.../oss/backend/use-create-reference.ts | 25 +++
.../oss/backend/use-delete-reference.ts | 29 ++++
.../oss/components/info-operation.tsx | 6 +-
.../src/features/oss/components/oss-stats.tsx | 11 +-
.../dlg-create-synthesis/tab-arguments.tsx | 5 +-
.../oss/dialogs/dlg-delete-operation.tsx | 10 +-
.../oss/dialogs/dlg-delete-reference.tsx | 79 ++++++++++
.../dlg-edit-operation/dlg-edit-operation.tsx | 17 +-
.../dlg-edit-operation/tab-arguments.tsx | 9 +-
rsconcept/frontend/src/features/oss/labels.ts | 6 +-
.../src/features/oss/models/oss-layout-api.ts | 9 +-
.../frontend/src/features/oss/models/oss.ts | 49 ++++--
.../context-menu/context-menu.tsx | 6 +-
.../context-menu/menu-operation.tsx | 99 +++++++++---
.../editor-oss-graph/graph/node-core.tsx | 9 +-
.../editor-oss-graph/graph/oss-node-types.ts | 6 +-
.../editor-oss-graph/graph/reference-node.tsx | 14 ++
...{operation-node.tsx => synthesis-node.tsx} | 2 +-
.../oss-page/editor-oss-graph/oss-flow.tsx | 23 ++-
.../side-panel/block-stats.tsx | 7 +-
.../side-panel/side-panel.tsx | 8 +-
.../side-panel/toolbar-schema.tsx | 2 +-
.../oss/pages/oss-page/oss-edit-state.tsx | 2 +-
.../dialogs/dlg-edit-cst/form-edit-cst.tsx | 7 +-
rsconcept/frontend/src/stores/dialogs.ts | 46 +++---
rsconcept/frontend/src/styling/reactflow.css | 3 +-
41 files changed, 783 insertions(+), 147 deletions(-)
create mode 100644 rsconcept/backend/apps/oss/tests/s_models/t_Layout.py
create mode 100644 rsconcept/backend/apps/oss/tests/s_models/t_Reference.py
create mode 100644 rsconcept/backend/apps/oss/tests/s_propagation/t_references.py
create mode 100644 rsconcept/frontend/src/features/oss/backend/use-create-reference.ts
create mode 100644 rsconcept/frontend/src/features/oss/backend/use-delete-reference.ts
create mode 100644 rsconcept/frontend/src/features/oss/dialogs/dlg-delete-reference.tsx
create mode 100644 rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/reference-node.tsx
rename rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/{operation-node.tsx => synthesis-node.tsx} (89%)
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..b22752db 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,32 +37,31 @@ 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. '''
- if keep_connections:
- referred_operations = target.getQ_reference_target()
- if len(referred_operations) == 1:
- referred_operation = referred_operations[0]
- for arg in target.getQ_as_argument():
- arg.pk = None
- arg.argument = referred_operation
- arg.save()
- else:
- pass
- # if target.result_id is not None:
- # self.before_delete_cst(schema, schema.cache.constituents) # TODO: use operation instead of schema
- target.delete()
+ if not keep_connections:
+ self.delete_operation(target, keep_constituents)
+ return
+ self.cache.ensure_loaded_subs()
+ operation = self.cache.operation_by_id[target]
+ reference_target = self.cache.reference_target.get(target)
+ if reference_target:
+ for arg in operation.getQ_as_argument():
+ arg.argument_id = reference_target
+ arg.save()
+ self.cache.remove_operation(target)
+ operation.delete()
+
def delete_operation(self, target: int, keep_constituents: bool = False):
''' Delete Operation. '''
self.cache.ensure_loaded_subs()
operation = self.cache.operation_by_id[target]
- schema = self.cache.get_schema(operation)
children = self.cache.graph.outputs[target]
- if schema is not None and len(children) > 0:
- ids = [cst.pk for cst in schema.cache.constituents]
+ if operation.result is not None and len(children) > 0:
+ ids = list(Constituenta.objects.filter(schema=operation.result).values_list('pk', flat=True))
if not keep_constituents:
- self.before_delete_cst(schema.model.pk, ids)
+ self._cascade_delete_inherited(operation.pk, ids)
else:
inheritance_to_delete: list[Inheritance] = []
for child_id in children:
@@ -707,17 +707,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 +791,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. '''
@@ -832,6 +832,8 @@ class OssCache:
del self._schema_by_id[target.result_id]
self.operations.remove(self.operation_by_id[operation])
del self.operation_by_id[operation]
+ if operation in self.reference_target:
+ del self.reference_target[operation]
if self.is_loaded_subs:
del self.substitutions[operation]
del self.inheritance[operation]
@@ -839,6 +841,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..6bb37062
--- /dev/null
+++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_references.py
@@ -0,0 +1,146 @@
+''' 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.ks1X2
+ }])
+ 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 X4 S1 D1 D2 D3',
+ convention='KS5D4'
+ )
+
+ self.operation6 = self.owned.create_operation(
+ alias='6',
+ operation_type=OperationType.SYNTHESIS
+ )
+ self.owned.set_arguments(self.operation6.pk, [self.operation2, self.operation3])
+ self.owned.set_substitutions(self.operation6.pk, [{
+ 'original': self.ks2X1,
+ 'substitution': self.ks1X1
+ }])
+ self.owned.execute_operation(self.operation6)
+ self.operation6.refresh_from_db()
+ self.ks6 = RSForm(self.operation6.result)
+ self.ks6D2 = self.ks6.insert_last(
+ alias='D2',
+ definition_formal=r'X1 X2 X3 S1 D1',
+ convention='KS6D2'
+ )
+
+ 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},
+ {'nodeID': 'o' + str(self.operation6.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)
+ self.assertEqual(self.ks1.constituentsQ().count(), 3)
+ self.assertEqual(self.ks2.constituentsQ().count(), 3)
+ self.assertEqual(self.ks4.constituentsQ().count(), 6)
+ self.assertEqual(self.ks5.constituentsQ().count(), 9)
+ self.assertEqual(self.ks6.constituentsQ().count(), 6)
+
+
+ @decl_endpoint('/api/oss/{item}/delete-operation', method='patch')
+ def test_delete_target_propagation(self):
+ ''' Test propagation when deleting a target operation. '''
+ data = {
+ 'layout': self.layout_data,
+ 'target': self.operation1.pk
+ }
+ self.executeOK(data=data, item=self.owned_id)
+ self.assertEqual(self.ks6.constituentsQ().count(), 4)
+ # self.assertEqual(self.ks5.constituentsQ().count(), 5)
+
+# TODO: add more tests
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..d90a0fee 100644
--- a/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
+++ b/rsconcept/frontend/src/features/oss/backend/oss-loader.ts
@@ -27,9 +27,10 @@ export class OssLoader {
private itemByNodeID = new Map();
private blockByID = new Map();
private schemaIDs: number[] = [];
+ private extendedGraph = new Graph();
constructor(input: RO) {
- this.oss = structuredClone(input) as IOperationSchema;
+ this.oss = structuredClone(input) as unknown as IOperationSchema;
}
produceOSS(): IOperationSchema {
@@ -47,6 +48,7 @@ export class OssLoader {
result.hierarchy = this.hierarchy;
result.schemas = this.schemaIDs;
result.stats = this.calculateStats();
+ result.extendedGraph = this.extendedGraph;
return result;
}
@@ -57,6 +59,7 @@ export class OssLoader {
this.itemByNodeID.set(operation.nodeID, operation);
this.operationByID.set(operation.id, operation);
this.graph.addNode(operation.id);
+ this.extendedGraph.addNode(operation.id);
this.hierarchy.addNode(operation.nodeID);
if (operation.parent) {
this.hierarchy.addEdge(constructNodeID(NodeType.BLOCK, operation.parent), operation.nodeID);
@@ -75,7 +78,13 @@ export class OssLoader {
}
private createGraph() {
- this.oss.arguments.forEach(argument => this.graph.addEdge(argument.argument, argument.operation));
+ this.oss.arguments.forEach(argument => {
+ this.graph.addEdge(argument.argument, argument.operation);
+ this.extendedGraph.addEdge(argument.argument, argument.operation);
+ });
+ this.oss.references.forEach(reference => {
+ this.extendedGraph.addEdge(reference.target, reference.reference);
+ });
}
private extractSchemas() {
@@ -83,16 +92,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.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;
+ }
});
}
@@ -107,13 +137,13 @@ export class OssLoader {
}
private inferConsolidation(operationID: number): boolean {
- const inputs = this.graph.expandInputs([operationID]);
+ const inputs = this.extendedGraph.expandInputs([operationID]);
if (inputs.length === 0) {
return false;
}
const ancestors = [...inputs];
inputs.forEach(input => {
- ancestors.push(...this.graph.expandAllInputs([input]));
+ ancestors.push(...this.extendedGraph.expandAllInputs([input]));
});
const unique = new Set(ancestors);
return unique.size < ancestors.length;
@@ -126,8 +156,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 ? (
+