From 2b312cebb17e5abbb1a51be8b99d84ce77e6f10b Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:53:41 +0300 Subject: [PATCH] F: Constituenta relocation pt2 --- .../apps/oss/models/OperationSchema.py | 62 +++++++++++-- .../apps/oss/models/PropagationFacade.py | 37 +++++--- .../backend/apps/oss/serializers/__init__.py | 1 + .../apps/oss/serializers/data_access.py | 63 ++++++++++++- .../oss/tests/s_propagation/t_operations.py | 62 +++++++++++++ .../backend/apps/oss/tests/s_views/t_oss.py | 88 ++++++++++++++++++- rsconcept/backend/apps/oss/views/oss.py | 38 +++++++- .../backend/apps/rsform/models/RSForm.py | 1 + rsconcept/backend/shared/messages.py | 12 +++ rsconcept/frontend/src/backend/oss.ts | 8 ++ rsconcept/frontend/src/context/OssContext.tsx | 30 ++++++- .../src/dialogs/DlgRelocateConstituents.tsx | 9 +- .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 4 +- .../src/pages/OssPage/OssEditContext.tsx | 18 ++-- 14 files changed, 393 insertions(+), 40 deletions(-) diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index a55a6923..d58b60aa 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -267,7 +267,51 @@ class OperationSchema: self.save(update_fields=['time_update']) return True - def after_create_cst(self, source: RSForm, cst_list: list[Constituenta]) -> None: + def relocate_down(self, source: RSForm, destination: RSForm, items: list[Constituenta]): + ''' Move list of constituents to specific schema inheritor. ''' + self.cache.ensure_loaded() + self.cache.insert_schema(source) + self.cache.insert_schema(destination) + operation = self.cache.get_operation(destination.model.pk) + + self._undo_substitutions_cst(items, operation, destination) + + inheritance_to_delete = [item for item in self.cache.inheritance[operation.pk] if item.parent_id in items] + for item in inheritance_to_delete: + self.cache.remove_inheritance(item) + Inheritance.objects.filter(operation_id=operation.pk, parent__in=items).delete() + + def relocate_up(self, source: RSForm, destination: RSForm, items: list[Constituenta]) -> list[Constituenta]: + ''' Move list of constituents to specific schema upstream. ''' + self.cache.ensure_loaded() + self.cache.insert_schema(source) + self.cache.insert_schema(destination) + + operation = self.cache.get_operation(source.model.pk) + alias_mapping: dict[str, str] = {} + for item in self.cache.inheritance[operation.pk]: + if item.parent_id in destination.cache.by_id: + source_cst = source.cache.by_id[item.child_id] + destination_cst = destination.cache.by_id[item.parent_id] + alias_mapping[source_cst.alias] = destination_cst.alias + + new_items = destination.insert_copy(items, initial_mapping=alias_mapping) + for index, cst in enumerate(new_items): + new_inheritance = Inheritance.objects.create( + operation=operation, + child=items[index], + parent=cst + ) + self.cache.insert_inheritance(new_inheritance) + self.after_create_cst(destination, new_items, exclude=[operation.pk]) + + return new_items + + def after_create_cst( + self, source: RSForm, + cst_list: list[Constituenta], + exclude: Optional[list[int]] = None + ) -> None: ''' Trigger cascade resolutions when new constituent is created. ''' self.cache.insert_schema(source) inserted_aliases = [cst.alias for cst in cst_list] @@ -281,7 +325,7 @@ class OperationSchema: if cst is not None: alias_mapping[alias] = cst operation = self.cache.get_operation(source.model.pk) - self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping) + self._cascade_inherit_cst(operation.pk, source, cst_list, alias_mapping, exclude) def after_change_cst_type(self, source: RSForm, target: Constituenta) -> None: ''' Trigger cascade resolutions when constituenta type is changed. ''' @@ -344,18 +388,20 @@ class OperationSchema: mapping={} ) + # pylint: disable=too-many-arguments, too-many-positional-arguments def _cascade_inherit_cst( - self, - target_operation: int, + self, target_operation: int, source: RSForm, items: list[Constituenta], - mapping: CstMapping + mapping: CstMapping, + exclude: Optional[list[int]] = None ) -> None: children = self.cache.graph.outputs[target_operation] if len(children) == 0: return for child_id in children: - self._execute_inherit_cst(child_id, source, items, mapping) + if not exclude or child_id not in exclude: + self._execute_inherit_cst(child_id, source, items, mapping) def _execute_inherit_cst( self, @@ -827,6 +873,10 @@ class OssCache: ''' Remove substitution from cache. ''' self.substitutions[target.operation_id].remove(target) + def remove_inheritance(self, target: Inheritance) -> None: + ''' Remove inheritance from cache. ''' + self.inheritance[target.operation_id].remove(target) + def unfold_sub(self, sub: Substitution) -> tuple[RSForm, RSForm, Constituenta, Constituenta]: operation = self.operation_by_id[sub.operation_id] parents = self.graph.inputs[operation.pk] diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index f919838d..635e88c5 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -1,4 +1,6 @@ ''' Models: Change propagation facade - managing all changes in OSS. ''' +from typing import Optional + from apps.library.models import LibraryItem, LibraryItemType from apps.rsform.models import Constituenta, RSForm @@ -14,42 +16,53 @@ class PropagationFacade: ''' Change propagation API. ''' @staticmethod - def after_create_cst(source: RSForm, new_cst: list[Constituenta]) -> None: + def after_create_cst(source: RSForm, new_cst: list[Constituenta], exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions when new constituent is created. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).after_create_cst(source, new_cst) + if exclude is None or host.pk not in exclude: + OperationSchema(host).after_create_cst(source, new_cst) @staticmethod - def after_change_cst_type(source: RSForm, target: Constituenta) -> None: + def after_change_cst_type(source: RSForm, target: Constituenta, exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions when constituenta type is changed. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).after_change_cst_type(source, target) + if exclude is None or host.pk not in exclude: + OperationSchema(host).after_change_cst_type(source, target) @staticmethod - def after_update_cst(source: RSForm, target: Constituenta, data: dict, old_data: dict) -> None: + def after_update_cst( + source: RSForm, + target: Constituenta, + data: dict, + old_data: dict, + exclude: Optional[list[int]] = None + ) -> None: ''' Trigger cascade resolutions when constituenta data is changed. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).after_update_cst(source, target, data, old_data) + if exclude is None or host.pk not in exclude: + OperationSchema(host).after_update_cst(source, target, data, old_data) @staticmethod - def before_delete_cst(source: RSForm, target: list[Constituenta]) -> None: + def before_delete_cst(source: RSForm, target: list[Constituenta], exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions before constituents are deleted. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).before_delete_cst(source, target) + if exclude is None or host.pk not in exclude: + OperationSchema(host).before_delete_cst(source, target) @staticmethod - def before_substitute(source: RSForm, substitutions: CstSubstitution) -> None: + def before_substitute(source: RSForm, substitutions: CstSubstitution, exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions before constituents are substituted. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).before_substitute(source, substitutions) + if exclude is None or host.pk not in exclude: + OperationSchema(host).before_substitute(source, substitutions) @staticmethod - def before_delete_schema(item: LibraryItem) -> None: + def before_delete_schema(item: LibraryItem, exclude: Optional[list[int]] = None) -> None: ''' Trigger cascade resolutions before schema is deleted. ''' if item.item_type != LibraryItemType.RSFORM: return @@ -58,4 +71,4 @@ class PropagationFacade: return schema = RSForm(item) - PropagationFacade.before_delete_cst(schema, list(schema.constituents().order_by('order'))) + PropagationFacade.before_delete_cst(schema, list(schema.constituents().order_by('order')), exclude) diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index 215fcfd1..874298ce 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -9,6 +9,7 @@ from .data_access import ( OperationSerializer, OperationTargetSerializer, OperationUpdateSerializer, + RelocateConstituentsSerializer, SetOperationInputSerializer ) from .responses import ConstituentaReferenceResponse, NewOperationResponse, NewSchemaResponse diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 3ffed473..5cb04e4f 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -11,7 +11,7 @@ from apps.rsform.models import Constituenta from apps.rsform.serializers import SubstitutionSerializerBase from shared import messages as msg -from ..models import Argument, Operation, OperationSchema, OperationType +from ..models import Argument, Inheritance, Operation, OperationSchema, OperationType from .basics import OperationPositionSerializer, SubstitutionExSerializer @@ -118,8 +118,6 @@ class OperationUpdateSerializer(serializers.Serializer): return attrs - - class OperationTargetSerializer(serializers.Serializer): ''' Serializer: Target single operation. ''' target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result_id')) @@ -224,3 +222,62 @@ class OperationSchemaSerializer(serializers.ModelSerializer): ).order_by('pk'): result['substitutions'].append(substitution) return result + + +class RelocateConstituentsSerializer(serializers.Serializer): + ''' Serializer: Relocate constituents. ''' + destination = PKField( + many=False, + queryset=LibraryItem.objects.all().only('id') + ) + items = PKField( + many=True, + allow_empty=False, + queryset=Constituenta.objects.all() + ) + + def validate(self, attrs): + attrs['destination'] = attrs['destination'].id + attrs['source'] = attrs['items'][0].schema_id + + # TODO: check permissions for editing source and destination + + if attrs['source'] == attrs['destination']: + raise serializers.ValidationError({ + 'destination': msg.sourceEqualDestination() + }) + for cst in attrs['items']: + if cst.schema_id != attrs['source']: + raise serializers.ValidationError({ + f'{cst.pk}': msg.constituentaNotInRSform(attrs['items'][0].schema.title) + }) + if Inheritance.objects.filter(child__in=attrs['items']).exists(): + raise serializers.ValidationError({ + 'items': msg.RelocatingInherited() + }) + + oss = LibraryItem.objects \ + .filter(operations__result_id=attrs['destination']) \ + .filter(operations__result_id=attrs['source']).only('id') + if not oss.exists(): + raise serializers.ValidationError({ + 'destination': msg.schemasNotConnected() + }) + attrs['oss'] = oss[0].pk + + if Argument.objects.filter( + operation__result_id=attrs['destination'], + argument__result_id=attrs['source'] + ).exists(): + attrs['move_down'] = True + elif Argument.objects.filter( + operation__result_id=attrs['source'], + argument__result_id=attrs['destination'] + ).exists(): + attrs['move_down'] = False + else: + raise serializers.ValidationError({ + 'destination': msg.schemasNotConnected() + }) + + return attrs diff --git a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py index ab1bf053..4c3290f2 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -339,3 +339,65 @@ class TestChangeOperations(EndpointTester): self.ks5.refresh_from_db() self.assertNotEqual(self.operation4.result, None) self.assertEqual(self.ks5.constituents().count(), 8) + + @decl_endpoint('/api/oss/relocate-constituents', method='post') + def test_relocate_constituents_up(self): + ks1_old_count = self.ks1.constituents().count() + ks4_old_count = self.ks4.constituents().count() + operation6 = self.owned.create_operation( + alias='6', + operation_type=OperationType.SYNTHESIS + ) + self.owned.set_arguments(operation6.pk, [self.operation1, self.operation2]) + self.owned.execute_operation(operation6) + operation6.refresh_from_db() + ks6 = RSForm(operation6.result) + ks6A1 = ks6.insert_new('A1') + ks6_old_count = ks6.constituents().count() + + data = { + 'destination': self.ks1.model.pk, + 'items': [ks6A1.pk] + } + + self.executeOK(data=data) + ks6.refresh_from_db() + self.ks1.refresh_from_db() + self.ks4.refresh_from_db() + + self.assertEqual(ks6.constituents().count(), ks6_old_count) + self.assertEqual(self.ks1.constituents().count(), ks1_old_count + 1) + self.assertEqual(self.ks4.constituents().count(), ks4_old_count + 1) + + @decl_endpoint('/api/oss/relocate-constituents', method='post') + def test_relocate_constituents_down(self): + ks1_old_count = self.ks1.constituents().count() + ks4_old_count = self.ks4.constituents().count() + + operation6 = self.owned.create_operation( + alias='6', + operation_type=OperationType.SYNTHESIS + ) + self.owned.set_arguments(operation6.pk, [self.operation1, self.operation2]) + self.owned.execute_operation(operation6) + operation6.refresh_from_db() + ks6 = RSForm(operation6.result) + ks6_old_count = ks6.constituents().count() + + data = { + 'destination': ks6.model.pk, + 'items': [self.ks1X2.pk] + } + + self.executeOK(data=data) + ks6.refresh_from_db() + self.ks1.refresh_from_db() + self.ks4.refresh_from_db() + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + + self.assertEqual(ks6.constituents().count(), ks6_old_count) + self.assertEqual(self.ks1.constituents().count(), ks1_old_count - 1) + self.assertEqual(self.ks4.constituents().count(), ks4_old_count - 1) + self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 D1') + self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 D2 D3') diff --git a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py index f019e752..472640d6 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -1,7 +1,7 @@ ''' Testing API: Operation Schema. ''' from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType from apps.oss.models import Operation, OperationSchema, OperationType -from apps.rsform.models import RSForm +from apps.rsform.models import Constituenta, RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -18,7 +18,6 @@ class TestOssViewset(EndpointTester): self.private_id = self.private.model.pk self.invalid_id = self.private.model.pk + 1337 - def populateData(self): self.ks1 = RSForm.create( alias='KS1', @@ -135,7 +134,6 @@ class TestOssViewset(EndpointTester): self.executeForbidden(data=data, item=self.unowned_id) self.executeForbidden(data=data, item=self.private_id) - @decl_endpoint('/api/oss/{item}/create-operation', method='post') def test_create_operation(self): self.populateData() @@ -499,3 +497,87 @@ class TestOssViewset(EndpointTester): self.assertEqual(len(items), 1) self.assertEqual(items[0].alias, 'X1') self.assertEqual(items[0].term_resolved, self.ks2X1.term_resolved) + + @decl_endpoint('/api/oss/get-predecessor', method='post') + def test_get_predecessor(self): + self.populateData() + self.ks1X2 = self.ks1.insert_new('X2') + + self.owned.execute_operation(self.operation3) + self.operation3.refresh_from_db() + self.ks3 = RSForm(self.operation3.result) + self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) + + self.executeBadData(data={'target': self.invalid_id}) + + response = self.executeOK(data={'target': self.ks1X1.pk}) + self.assertEqual(response.data['id'], self.ks1X1.pk) + self.assertEqual(response.data['schema'], self.ks1.model.pk) + + response = self.executeOK(data={'target': self.ks3X2.pk}) + self.assertEqual(response.data['id'], self.ks1X2.pk) + self.assertEqual(response.data['schema'], self.ks1.model.pk) + + @decl_endpoint('/api/oss/relocate-constituents', method='post') + def test_relocate_constituents(self): + self.populateData() + self.ks1X2 = self.ks1.insert_new('X2', convention='test') + + self.owned.execute_operation(self.operation3) + self.operation3.refresh_from_db() + self.ks3 = RSForm(self.operation3.result) + self.ks3X2 = Constituenta.objects.get(as_child__parent_id=self.ks1X2.pk) + self.ks3X10 = self.ks3.insert_new('X10', convention='test2') + + # invalid destination + data = { + 'destination': self.invalid_id, + 'items': [] + } + self.executeBadData(data=data) + + # empty items + data = { + 'destination': self.ks1.model.pk, + 'items': [] + } + self.executeBadData(data=data) + + # source == destination + data = { + 'destination': self.ks1.model.pk, + 'items': [self.ks1X1.pk] + } + self.executeBadData(data=data) + + # moving inherited + data = { + 'destination': self.ks1.model.pk, + 'items': [self.ks3X2.pk] + } + self.executeBadData(data=data) + + # source and destination are not connected + data = { + 'destination': self.ks2.model.pk, + 'items': [self.ks1X1.pk] + } + self.executeBadData(data=data) + + data = { + 'destination': self.ks3.model.pk, + 'items': [self.ks1X2.pk] + } + self.ks3X2.refresh_from_db() + self.assertEqual(self.ks3X2.convention, 'test') + self.executeOK(data=data) + self.assertFalse(Constituenta.objects.filter(as_child__parent_id=self.ks1X2.pk).exists()) + + data = { + 'destination': self.ks1.model.pk, + 'items': [self.ks3X10.pk] + } + self.executeOK(data=data) + self.assertTrue(Constituenta.objects.filter(as_parent__child_id=self.ks3X10.pk).exists()) + self.ks1X3 = Constituenta.objects.get(as_parent__child_id=self.ks3X10.pk) + self.assertEqual(self.ks1X3.convention, 'test2') diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 4c056540..7e55686d 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -14,7 +14,7 @@ from rest_framework.response import Response from apps.library.models import LibraryItem, LibraryItemType from apps.library.serializers import LibraryItemSerializer -from apps.rsform.models import Constituenta +from apps.rsform.models import Constituenta, RSForm from apps.rsform.serializers import CstTargetSerializer from shared import messages as msg from shared import permissions @@ -42,7 +42,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'create_input', 'set_input', 'update_operation', - 'execute_operation' + 'execute_operation', + 'relocate_constituents' ]: permission_list = [permissions.ItemEditor] elif self.action in ['details']: @@ -385,3 +386,36 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'schema': cst.schema_id } ) + + @extend_schema( + summary='relocate constituents from one schema to another', + tags=['OSS'], + request=s.RelocateConstituentsSerializer(), + responses={ + c.HTTP_200_OK: s.OperationSchemaSerializer, + c.HTTP_400_BAD_REQUEST: None, + c.HTTP_403_FORBIDDEN: None, + c.HTTP_404_NOT_FOUND: None + } + ) + @action(detail=False, methods=['post'], url_path='relocate-constituents') + def relocate_constituents(self, request: Request) -> Response: + ''' Relocate constituents from one schema to another. ''' + serializer = s.RelocateConstituentsSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + data = serializer.validated_data + oss = m.OperationSchema(LibraryItem.objects.get(pk=data['oss'])) + source = RSForm(LibraryItem.objects.get(pk=data['source'])) + destination = RSForm(LibraryItem.objects.get(pk=data['destination'])) + + with transaction.atomic(): + if data['move_down']: + oss.relocate_down(source, destination, data['items']) + m.PropagationFacade.before_delete_cst(source, data['items']) + source.delete_cst(data['items']) + else: + new_items = oss.relocate_up(source, destination, data['items']) + m.PropagationFacade.after_create_cst(destination, new_items, exclude=[oss.model.pk]) + + return Response(status=c.HTTP_200_OK) diff --git a/rsconcept/backend/apps/rsform/models/RSForm.py b/rsconcept/backend/apps/rsform/models/RSForm.py index bdc9171f..2fac40ff 100644 --- a/rsconcept/backend/apps/rsform/models/RSForm.py +++ b/rsconcept/backend/apps/rsform/models/RSForm.py @@ -64,6 +64,7 @@ class RSForm: def refresh_from_db(self) -> None: ''' Model wrapper. ''' self.model.refresh_from_db() + self.cache = RSFormCache(self) def constituents(self) -> QuerySet[Constituenta]: ''' Get QuerySet containing all constituents of current RSForm. ''' diff --git a/rsconcept/backend/shared/messages.py b/rsconcept/backend/shared/messages.py index 9729c7b2..6f0bb86e 100644 --- a/rsconcept/backend/shared/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -38,6 +38,18 @@ def operationResultFromAnotherOSS(): return 'Схема является результатом другой ОСС' +def schemasNotConnected(): + return 'Концептуальные схемы не связаны через ОСС' + + +def sourceEqualDestination(): + return 'Схема-источник и схема-получатель не могут быть одинаковыми' + + +def RelocatingInherited(): + return 'Невозможно переместить наследуемые конституенты' + + def operationInputAlreadyConnected(): return 'Схема уже подключена к другой операции' diff --git a/rsconcept/frontend/src/backend/oss.ts b/rsconcept/frontend/src/backend/oss.ts index 4ca33780..a9f63858 100644 --- a/rsconcept/frontend/src/backend/oss.ts +++ b/rsconcept/frontend/src/backend/oss.ts @@ -3,6 +3,7 @@ */ import { + ICstRelocateData, IInputCreatedResponse, IOperationCreateData, IOperationCreatedResponse, @@ -76,6 +77,13 @@ export function postExecuteOperation(oss: string, request: FrontExchange) { + AxiosPost({ + endpoint: `/api/oss/relocate-constituents`, + request: request + }); +} + export function postFindPredecessor(request: FrontExchange) { AxiosPost({ endpoint: `/api/oss/get-predecessor`, diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index ba2c547e..5c73c4a5 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -17,12 +17,14 @@ import { patchUpdateOperation, patchUpdatePositions, postCreateOperation, - postExecuteOperation + postExecuteOperation, + postRelocateConstituents } from '@/backend/oss'; import { type ErrorData } from '@/components/info/InfoError'; import { AccessPolicy, ILibraryItem } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library'; import { + ICstRelocateData, IOperationCreateData, IOperationData, IOperationDeleteData, @@ -65,6 +67,7 @@ interface IOssContext { setInput: (data: IOperationSetInputData, callback?: () => void) => void; updateOperation: (data: IOperationUpdateData, callback?: () => void) => void; executeOperation: (data: ITargetOperation, callback?: () => void) => void; + relocateConstituents: (data: ICstRelocateData, callback?: () => void) => void; } const OssContext = createContext(null); @@ -353,6 +356,28 @@ export const OssState = ({ itemID, children }: React.PropsWithChildren void) => { + if (!model) { + return; + } + setProcessingError(undefined); + postRelocateConstituents({ + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: () => { + oss.reload(); + library.reloadItems(() => { + if (callback) callback(); + }); + } + }); + }, + [model, library, oss] + ); + return ( {children} diff --git a/rsconcept/frontend/src/dialogs/DlgRelocateConstituents.tsx b/rsconcept/frontend/src/dialogs/DlgRelocateConstituents.tsx index 4800b66c..15a5df2d 100644 --- a/rsconcept/frontend/src/dialogs/DlgRelocateConstituents.tsx +++ b/rsconcept/frontend/src/dialogs/DlgRelocateConstituents.tsx @@ -55,12 +55,15 @@ function DlgRelocateConstituents({ oss, hideWindow, target, onSubmit }: DlgReloc }, []); const handleSubmit = useCallback(() => { + if (!destination) { + return; + } const data: ICstRelocateData = { - destination: target.result ?? 0, - items: [] + destination: destination.id, + items: selected }; onSubmit(data); - }, [target, onSubmit]); + }, [destination, onSubmit, selected]); return ( { - controller.promptRelocateConstituents(target); + controller.promptRelocateConstituents(target, getPositions()); }, - [controller] + [controller, getPositions] ); const handleFitView = useCallback(() => { diff --git a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx index ae1eb05d..2f23e672 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx @@ -76,7 +76,7 @@ export interface IOssEditContext extends ILibraryItemEditor { promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void; executeOperation: (target: OperationID, positions: IOperationPosition[]) => void; - promptRelocateConstituents: (target: OperationID) => void; + promptRelocateConstituents: (target: OperationID, positions: IOperationPosition[]) => void; } const OssEditContext = createContext(null); @@ -360,16 +360,20 @@ export const OssEditState = ({ selected, setSelected, children }: React.PropsWit [model] ); - const promptRelocateConstituents = useCallback((target: OperationID) => { + const promptRelocateConstituents = useCallback((target: OperationID, positions: IOperationPosition[]) => { + setPositions(positions); setTargetOperationID(target); setShowRelocateConstituents(true); }, []); - const handleRelocateConstituents = useCallback((data: ICstRelocateData) => { - // TODO: implement backed call - console.log(data); - toast.success('В разработке'); - }, []); + const handleRelocateConstituents = useCallback( + (data: ICstRelocateData) => { + model.savePositions({ positions: positions }, () => + model.relocateConstituents(data, () => toast.success(information.changesSaved)) + ); + }, + [model, positions] + ); return (