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 ? ( +