From 35883458f357c3b8c5ffc337b6edd1dde3d6abc8 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Thu, 15 Aug 2024 23:23:45 +0300 Subject: [PATCH] F: Implement operation and schema delete consequence for OSS --- .../apps/library/models/LibraryItem.py | 8 +++ .../apps/library/tests/s_views/t_library.py | 7 +- .../backend/apps/library/views/library.py | 6 +- .../apps/oss/models/OperationSchema.py | 6 +- .../apps/oss/models/PropagationFacade.py | 18 +++++- .../backend/apps/oss/serializers/__init__.py | 1 + .../apps/oss/serializers/data_access.py | 20 ++++++ .../oss/tests/s_propagation/t_operations.py | 58 +++++++++++++++++ .../backend/apps/oss/tests/s_views/t_oss.py | 4 ++ rsconcept/backend/apps/oss/views/oss.py | 25 +++++--- .../backend/apps/rsform/views/rsforms.py | 2 +- rsconcept/frontend/src/backend/oss.ts | 3 +- .../src/components/info/TooltipOperation.tsx | 5 ++ .../frontend/src/components/ui/Checkbox.tsx | 2 +- .../src/components/ui/CheckboxTristate.tsx | 2 +- .../frontend/src/context/LibraryContext.tsx | 2 +- rsconcept/frontend/src/context/OssContext.tsx | 5 +- .../src/dialogs/DlgChangeInputSchema.tsx | 6 +- .../src/dialogs/DlgChangeLocation.tsx | 6 +- .../src/dialogs/DlgDeleteOperation.tsx | 64 +++++++++++++++++++ .../frontend/src/dialogs/DlgGraphParams.tsx | 7 +- .../frontend/src/dialogs/DlgRenameCst.tsx | 4 +- rsconcept/frontend/src/hooks/useOssDetails.ts | 5 +- rsconcept/frontend/src/models/OssLoader.ts | 16 +++-- rsconcept/frontend/src/models/oss.ts | 9 +++ .../OssPage/EditorOssGraph/InputNode.tsx | 7 ++ .../EditorOssGraph/NodeContextMenu.tsx | 2 +- .../OssPage/EditorOssGraph/OperationNode.tsx | 6 ++ .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 7 +- .../EditorOssGraph/ToolbarOssGraph.tsx | 6 +- .../src/pages/OssPage/OssEditContext.tsx | 62 +++++++++++++++--- 31 files changed, 315 insertions(+), 66 deletions(-) create mode 100644 rsconcept/frontend/src/dialogs/DlgDeleteOperation.tsx diff --git a/rsconcept/backend/apps/library/models/LibraryItem.py b/rsconcept/backend/apps/library/models/LibraryItem.py index 3b1762a3..0885b3c1 100644 --- a/rsconcept/backend/apps/library/models/LibraryItem.py +++ b/rsconcept/backend/apps/library/models/LibraryItem.py @@ -123,3 +123,11 @@ class LibraryItem(Model): def versions(self) -> QuerySet[Version]: ''' Get all Versions of this item. ''' return Version.objects.filter(item=self.pk).order_by('-time_create') + + def is_synced(self, target: 'LibraryItem') -> bool: + ''' Check if item is synced with target. ''' + if self.owner != target.owner: + return False + if self.location != target.location: + return False + return True diff --git a/rsconcept/backend/apps/library/tests/s_views/t_library.py b/rsconcept/backend/apps/library/tests/s_views/t_library.py index a52d54e1..ef559ad6 100644 --- a/rsconcept/backend/apps/library/tests/s_views/t_library.py +++ b/rsconcept/backend/apps/library/tests/s_views/t_library.py @@ -217,13 +217,10 @@ class TestLibraryViewset(EndpointTester): @decl_endpoint('/api/library/{item}', method='delete') def test_destroy(self): - response = self.execute(item=self.owned.pk) - self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT]) - + self.executeNoContent(item=self.owned.pk) self.executeForbidden(item=self.unowned.pk) self.toggle_admin(True) - response = self.execute(item=self.unowned.pk) - self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT]) + self.executeNoContent(item=self.unowned.pk) @decl_endpoint('/api/library/active', method='get') diff --git a/rsconcept/backend/apps/library/views/library.py b/rsconcept/backend/apps/library/views/library.py index 7a77f9f2..5628a06f 100644 --- a/rsconcept/backend/apps/library/views/library.py +++ b/rsconcept/backend/apps/library/views/library.py @@ -13,7 +13,7 @@ from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response -from apps.oss.models import Operation, OperationSchema +from apps.oss.models import Operation, OperationSchema, PropagationFacade from apps.rsform.models import RSForm from apps.rsform.serializers import RSFormParseSerializer from apps.users.models import User @@ -67,6 +67,10 @@ class LibraryViewSet(viewsets.ModelViewSet): if update_list: Operation.objects.bulk_update(update_list, ['alias', 'title', 'comment']) + def perform_destroy(self, instance: m.LibraryItem) -> None: + PropagationFacade.before_delete_schema(instance) + return super().perform_destroy(instance) + def get_permissions(self): if self.action in ['update', 'partial_update']: access_level = permissions.ItemEditor diff --git a/rsconcept/backend/apps/oss/models/OperationSchema.py b/rsconcept/backend/apps/oss/models/OperationSchema.py index 9d7f4012..39eeae94 100644 --- a/rsconcept/backend/apps/oss/models/OperationSchema.py +++ b/rsconcept/backend/apps/oss/models/OperationSchema.py @@ -100,7 +100,7 @@ class OperationSchema: if not keep_constituents: schema = self.cache.get_schema(target) if schema is not None: - self.before_delete(schema.cache.constituents, schema) + self.before_delete_cst(schema.cache.constituents, schema) self.cache.remove_operation(target.pk) target.delete() self.save(update_fields=['time_update']) @@ -115,7 +115,7 @@ class OperationSchema: if old_schema is not None: if has_children: - self.before_delete(old_schema.cache.constituents, old_schema) + self.before_delete_cst(old_schema.cache.constituents, old_schema) self.cache.remove_schema(old_schema) operation.result = schema @@ -280,7 +280,7 @@ class OperationSchema: mapping=alias_mapping ) - def before_delete(self, target: list[Constituenta], source: RSForm) -> None: + def before_delete_cst(self, target: list[Constituenta], source: RSForm) -> None: ''' Trigger cascade resolutions before constituents are deleted. ''' self.cache.insert(source) operation = self.cache.get_operation(source.model.pk) diff --git a/rsconcept/backend/apps/oss/models/PropagationFacade.py b/rsconcept/backend/apps/oss/models/PropagationFacade.py index d43e743e..df3b9550 100644 --- a/rsconcept/backend/apps/oss/models/PropagationFacade.py +++ b/rsconcept/backend/apps/oss/models/PropagationFacade.py @@ -1,5 +1,5 @@ ''' Models: Change propagation facade - managing all changes in OSS. ''' -from apps.library.models import LibraryItem +from apps.library.models import LibraryItem, LibraryItemType from apps.rsform.models import Constituenta, RSForm from .OperationSchema import CstSubstitution, OperationSchema @@ -35,11 +35,11 @@ class PropagationFacade: OperationSchema(host).after_update_cst(target, data, old_data, source) @staticmethod - def before_delete(target: list[Constituenta], source: RSForm) -> None: + def before_delete_cst(target: list[Constituenta], source: RSForm) -> None: ''' Trigger cascade resolutions before constituents are deleted. ''' hosts = _get_oss_hosts(source.model) for host in hosts: - OperationSchema(host).before_delete(target, source) + OperationSchema(host).before_delete_cst(target, source) @staticmethod def before_substitute(substitutions: CstSubstitution, source: RSForm) -> None: @@ -47,3 +47,15 @@ class PropagationFacade: hosts = _get_oss_hosts(source.model) for host in hosts: OperationSchema(host).before_substitute(substitutions, source) + + @staticmethod + def before_delete_schema(item: LibraryItem) -> None: + ''' Trigger cascade resolutions before schema is deleted. ''' + if item.item_type != LibraryItemType.RSFORM: + return + hosts = _get_oss_hosts(item) + if len(hosts) == 0: + return + + schema = RSForm(item) + PropagationFacade.before_delete_cst(list(schema.constituents()), schema) diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index bb2d7e3d..215fcfd1 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -4,6 +4,7 @@ from .basics import OperationPositionSerializer, PositionsSerializer, Substituti from .data_access import ( ArgumentSerializer, OperationCreateSerializer, + OperationDeleteSerializer, OperationSchemaSerializer, OperationSerializer, OperationTargetSerializer, diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 3829b8d6..79d0e4be 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -138,6 +138,26 @@ class OperationTargetSerializer(serializers.Serializer): return attrs +class OperationDeleteSerializer(serializers.Serializer): + ''' Serializer: Delete operation. ''' + target = PKField(many=False, queryset=Operation.objects.all().only('oss_id', 'result')) + positions = serializers.ListField( + child=OperationPositionSerializer(), + default=[] + ) + keep_constituents = serializers.BooleanField(default=False, required=False) + delete_schema = serializers.BooleanField(default=False, required=False) + + def validate(self, attrs): + oss = cast(LibraryItem, self.context['oss']) + operation = cast(Operation, attrs['target']) + if oss and operation.oss_id != oss.pk: + raise serializers.ValidationError({ + 'target': msg.operationNotInOSS(oss.title) + }) + return attrs + + class SetOperationInputSerializer(serializers.Serializer): ''' Serializer: Set input schema for operation. ''' target = PKField(many=False, queryset=Operation.objects.all()) 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 76dec21a..987030b3 100644 --- a/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py +++ b/rsconcept/backend/apps/oss/tests/s_propagation/t_operations.py @@ -191,3 +191,61 @@ class TestChangeOperations(EndpointTester): self.assertEqual(self.ks4D1.definition_formal, r'X4 X1') self.assertEqual(self.ks4D2.definition_formal, r'X1 DEL DEL DEL D1') self.assertEqual(self.ks5D4.definition_formal, r'X1 DEL DEL DEL D1 D2 D3') + + @decl_endpoint('/api/library/{item}', method='delete') + def test_delete_schema(self): + self.executeNoContent(item=self.ks1.model.pk) + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + self.operation1.refresh_from_db() + self.assertEqual(self.operation1.result, None) + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 0) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 0) + self.assertEqual(self.ks4.constituents().count(), 4) + self.assertEqual(self.ks5.constituents().count(), 7) + self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL') + self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 DEL D3') + + @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') + def test_delete_operation_and_constituents(self): + data = { + 'positions': [], + 'target': self.operation1.pk, + 'keep_constituents': False, + 'delete_schema': True + } + + self.executeOK(data=data, item=self.owned_id) + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 0) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 0) + self.assertEqual(self.ks4.constituents().count(), 4) + self.assertEqual(self.ks5.constituents().count(), 7) + self.assertEqual(self.ks4D2.definition_formal, r'DEL X2 X3 S1 DEL') + self.assertEqual(self.ks5D4.definition_formal, r'X1 X2 X3 S1 D1 DEL D3') + + @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') + def test_delete_operation_keep_constituents(self): + data = { + 'positions': [], + 'target': self.operation1.pk, + 'keep_constituents': True, + 'delete_schema': True + } + + self.executeOK(data=data, item=self.owned_id) + self.ks4D2.refresh_from_db() + self.ks5D4.refresh_from_db() + subs1_2 = self.operation4.getSubstitutions() + self.assertEqual(subs1_2.count(), 0) + subs3_4 = self.operation5.getSubstitutions() + self.assertEqual(subs3_4.count(), 1) + self.assertEqual(self.ks4.constituents().count(), 6) + self.assertEqual(self.ks5.constituents().count(), 8) + self.assertEqual(self.ks4D2.definition_formal, r'X1 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 c4338ae3..6937d9bb 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -349,6 +349,8 @@ class TestOssViewset(EndpointTester): } self.executeBadData(data=data, item=self.owned_id) + self.ks2.model.visible = False + self.ks2.model.save(update_fields=['visible']) data = { 'positions': [], 'target': self.operation2.pk, @@ -356,7 +358,9 @@ class TestOssViewset(EndpointTester): } self.executeOK(data=data, item=self.owned_id) self.operation2.refresh_from_db() + self.ks2.model.refresh_from_db() self.assertEqual(self.operation2.result, None) + self.assertEqual(self.ks2.model.visible, True) data = { 'positions': [], diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 1e214911..6a4a4dc5 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -143,7 +143,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @extend_schema( summary='delete operation', tags=['OSS'], - request=s.OperationTargetSerializer, + request=s.OperationDeleteSerializer, responses={ c.HTTP_200_OK: s.OperationSchemaSerializer, c.HTTP_400_BAD_REQUEST: None, @@ -154,20 +154,25 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev @action(detail=True, methods=['patch'], url_path='delete-operation') def delete_operation(self, request: Request, pk) -> HttpResponse: ''' Endpoint: Delete operation. ''' - serializer = s.OperationTargetSerializer( + serializer = s.OperationDeleteSerializer( data=request.data, context={'oss': self.get_object()} ) serializer.is_valid(raise_exception=True) oss = m.OperationSchema(self.get_object()) + operation = cast(m.Operation, serializer.validated_data['target']) + old_schema: Optional[LibraryItem] = operation.result with transaction.atomic(): oss.update_positions(serializer.validated_data['positions']) - - # TODO: propagate changes to RSForms - - oss.delete_operation(serializer.validated_data['target']) - + oss.delete_operation(operation, serializer.validated_data['keep_constituents']) + if old_schema is not None: + if serializer.validated_data['delete_schema']: + m.PropagationFacade.before_delete_schema(old_schema) + old_schema.delete() + elif old_schema.is_synced(oss.model): + old_schema.visible = True + old_schema.save(update_fields=['visible']) return Response( status=c.HTTP_200_OK, data=s.OperationSchemaSerializer(oss.model).data @@ -249,9 +254,13 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev raise serializers.ValidationError({ 'input': msg.operationInputAlreadyConnected() }) - oss = m.OperationSchema(self.get_object()) + old_schema: Optional[LibraryItem] = target_operation.result with transaction.atomic(): + if old_schema is not None: + if old_schema.is_synced(oss.model): + old_schema.visible = True + old_schema.save(update_fields=['visible']) oss.update_positions(serializer.validated_data['positions']) oss.set_input(target_operation.pk, schema) return Response( diff --git a/rsconcept/backend/apps/rsform/views/rsforms.py b/rsconcept/backend/apps/rsform/views/rsforms.py index 46c87f34..4499e836 100644 --- a/rsconcept/backend/apps/rsform/views/rsforms.py +++ b/rsconcept/backend/apps/rsform/views/rsforms.py @@ -263,7 +263,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr cst_list: list[m.Constituenta] = serializer.validated_data['items'] schema = m.RSForm(model) with transaction.atomic(): - PropagationFacade.before_delete(cst_list, schema) + PropagationFacade.before_delete_cst(cst_list, schema) schema.delete_cst(cst_list) return Response( status=c.HTTP_200_OK, diff --git a/rsconcept/frontend/src/backend/oss.ts b/rsconcept/frontend/src/backend/oss.ts index da74dc14..4ca33780 100644 --- a/rsconcept/frontend/src/backend/oss.ts +++ b/rsconcept/frontend/src/backend/oss.ts @@ -6,6 +6,7 @@ import { IInputCreatedResponse, IOperationCreateData, IOperationCreatedResponse, + IOperationDeleteData, IOperationSchemaData, IOperationSetInputData, IOperationUpdateData, @@ -40,7 +41,7 @@ export function postCreateOperation( }); } -export function patchDeleteOperation(oss: string, request: FrontExchange) { +export function patchDeleteOperation(oss: string, request: FrontExchange) { AxiosPatch({ endpoint: `/api/oss/${oss}/delete-operation`, request: request diff --git a/rsconcept/frontend/src/components/info/TooltipOperation.tsx b/rsconcept/frontend/src/components/info/TooltipOperation.tsx index e53a6ffc..0877297e 100644 --- a/rsconcept/frontend/src/components/info/TooltipOperation.tsx +++ b/rsconcept/frontend/src/components/info/TooltipOperation.tsx @@ -67,6 +67,11 @@ function TooltipOperation({ node, anchor }: TooltipOperationProps) {

Тип: {labelOperationType(node.data.operation.operation_type)}

+ {!node.data.operation.is_owned ? ( +

+ КС не принадлежит ОСС +

+ ) : null} {node.data.operation.title ? (

Название: diff --git a/rsconcept/frontend/src/components/ui/Checkbox.tsx b/rsconcept/frontend/src/components/ui/Checkbox.tsx index 633e3ea8..e61f8034 100644 --- a/rsconcept/frontend/src/components/ui/Checkbox.tsx +++ b/rsconcept/frontend/src/components/ui/Checkbox.tsx @@ -27,7 +27,7 @@ function Checkbox({ }: CheckboxProps) { const cursor = useMemo(() => { if (disabled) { - return 'cursor-auto'; + return 'cursor-arrow'; } else if (setValue) { return 'cursor-pointer'; } else { diff --git a/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx b/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx index ee886cce..46ed3500 100644 --- a/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx +++ b/rsconcept/frontend/src/components/ui/CheckboxTristate.tsx @@ -25,7 +25,7 @@ function CheckboxTristate({ }: CheckboxTristateProps) { const cursor = useMemo(() => { if (disabled) { - return 'cursor-auto'; + return 'cursor-arrow'; } else if (setValue) { return 'cursor-pointer'; } else { diff --git a/rsconcept/frontend/src/context/LibraryContext.tsx b/rsconcept/frontend/src/context/LibraryContext.tsx index 618308d4..4851c18b 100644 --- a/rsconcept/frontend/src/context/LibraryContext.tsx +++ b/rsconcept/frontend/src/context/LibraryContext.tsx @@ -91,7 +91,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => { setSchema: setGlobalOSS, loading: ossLoading, reload: reloadOssInternal - } = useOssDetails({ target: ossID }); + } = useOssDetails({ target: ossID, items: items }); const reloadOSS = useCallback( (callback?: () => void) => { diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index f2417013..2bb28435 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -27,6 +27,7 @@ import { ILibraryUpdateData } from '@/models/library'; import { IOperationCreateData, IOperationData, + IOperationDeleteData, IOperationSchema, IOperationSchemaData, IOperationSetInputData, @@ -63,7 +64,7 @@ interface IOssContext { savePositions: (data: IPositionsData, callback?: () => void) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback) => void; - deleteOperation: (data: ITargetOperation, callback?: () => void) => void; + deleteOperation: (data: IOperationDeleteData, callback?: () => void) => void; createInput: (data: ITargetOperation, callback?: DataCallback) => void; setInput: (data: IOperationSetInputData, callback?: () => void) => void; updateOperation: (data: IOperationUpdateData, callback?: () => void) => void; @@ -309,7 +310,7 @@ export const OssState = ({ itemID, children }: OssStateProps) => { ); const deleteOperation = useCallback( - (data: ITargetOperation, callback?: () => void) => { + (data: IOperationDeleteData, callback?: () => void) => { setProcessingError(undefined); patchDeleteOperation(itemID, { data: data, diff --git a/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx b/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx index 62e7e407..c9a4e17b 100644 --- a/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx +++ b/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx @@ -31,10 +31,6 @@ function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeIn setSelected(newValue); }, []); - function handleSubmit() { - onSubmit(selected); - } - return ( onSubmit(selected)} className={clsx('w-[35rem]', 'pb-3 px-6 cc-column')} >

diff --git a/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx b/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx index 82ef4bf3..d42da1c1 100644 --- a/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgChangeLocation.tsx @@ -34,10 +34,6 @@ function DlgChangeLocation({ hideWindow, initial, onChangeLocation }: DlgChangeL setBody(newValue.length > 3 ? newValue.substring(3) : ''); }, []); - function handleSubmit() { - onChangeLocation(location); - } - return ( onChangeLocation(location)} className={clsx('w-[35rem]', 'pb-3 px-6 flex gap-3')} >
diff --git a/rsconcept/frontend/src/dialogs/DlgDeleteOperation.tsx b/rsconcept/frontend/src/dialogs/DlgDeleteOperation.tsx new file mode 100644 index 00000000..2f8f09de --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgDeleteOperation.tsx @@ -0,0 +1,64 @@ +'use client'; + +import clsx from 'clsx'; +import { useState } from 'react'; + +import Checkbox from '@/components/ui/Checkbox'; +import Modal, { ModalProps } from '@/components/ui/Modal'; +import TextInput from '@/components/ui/TextInput'; +import { IOperation } from '@/models/oss'; + +interface DlgDeleteOperationProps extends Pick { + target: IOperation; + onSubmit: (keepConstituents: boolean, deleteSchema: boolean) => void; +} + +function DlgDeleteOperation({ hideWindow, target, onSubmit }: DlgDeleteOperationProps) { + const [keepConstituents, setKeepConstituents] = useState(false); + const [deleteSchema, setDeleteSchema] = useState(false); + + function handleSubmit() { + onSubmit(keepConstituents, deleteSchema); + } + + return ( + + + + + + ); +} + +export default DlgDeleteOperation; diff --git a/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx b/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx index 67da136e..cd7616f0 100644 --- a/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx +++ b/rsconcept/frontend/src/dialogs/DlgGraphParams.tsx @@ -15,17 +15,12 @@ interface DlgGraphParamsProps extends Pick { function DlgGraphParams({ hideWindow, initial, onConfirm }: DlgGraphParamsProps) { const [params, updateParams] = usePartialUpdate(initial); - function handleSubmit() { - hideWindow(); - onConfirm(params); - } - return ( onConfirm(params)} submitText='Применить' className='flex gap-6 justify-between px-6 pb-3 w-[30rem]' > diff --git a/rsconcept/frontend/src/dialogs/DlgRenameCst.tsx b/rsconcept/frontend/src/dialogs/DlgRenameCst.tsx index 55a84acb..bc3b28d1 100644 --- a/rsconcept/frontend/src/dialogs/DlgRenameCst.tsx +++ b/rsconcept/frontend/src/dialogs/DlgRenameCst.tsx @@ -26,8 +26,6 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) { const [validated, setValidated] = useState(false); const [cstData, updateData] = usePartialUpdate(initial); - const handleSubmit = () => onRename(cstData); - useLayoutEffect(() => { if (schema && initial && cstData.cst_type !== initial.cst_type) { updateData({ alias: generateAlias(cstData.cst_type, schema) }); @@ -47,7 +45,7 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) { submitInvalidTooltip={'Введите незанятое имя, соответствующее типу'} hideWindow={hideWindow} canSubmit={validated} - onSubmit={handleSubmit} + onSubmit={() => onRename(cstData)} className={clsx('w-[30rem]', 'py-6 pr-3 pl-6 flex justify-center items-center')} > (undefined); const [loading, setLoading] = useState(target != undefined); @@ -19,7 +20,7 @@ function useOssDetails({ target }: { target?: string }) { setInner(undefined); return; } - const newSchema = new OssLoader(data).produceOSS(); + const newSchema = new OssLoader(data, items).produceOSS(); setInner(newSchema); } diff --git a/rsconcept/frontend/src/models/OssLoader.ts b/rsconcept/frontend/src/models/OssLoader.ts index 37c35463..dd00e791 100644 --- a/rsconcept/frontend/src/models/OssLoader.ts +++ b/rsconcept/frontend/src/models/OssLoader.ts @@ -3,7 +3,7 @@ */ import { Graph } from './Graph'; -import { LibraryItemID } from './library'; +import { ILibraryItem, LibraryItemID } from './library'; import { IOperation, IOperationSchema, @@ -21,10 +21,12 @@ export class OssLoader { private oss: IOperationSchemaData; private graph: Graph = new Graph(); private operationByID = new Map(); - private schemas: LibraryItemID[] = []; + private schemaIDs: LibraryItemID[] = []; + private items: ILibraryItem[]; - constructor(input: IOperationSchemaData) { + constructor(input: IOperationSchemaData, items: ILibraryItem[]) { this.oss = input; + this.items = items; } produceOSS(): IOperationSchema { @@ -36,7 +38,7 @@ export class OssLoader { result.operationByID = this.operationByID; result.graph = this.graph; - result.schemas = this.schemas; + result.schemas = this.schemaIDs; result.stats = this.calculateStats(); return result; } @@ -53,12 +55,14 @@ export class OssLoader { } private extractSchemas() { - this.schemas = this.oss.items.map(operation => operation.result).filter(item => item !== null); + this.schemaIDs = this.oss.items.map(operation => operation.result).filter(item => item !== null); } private inferOperationAttributes() { this.graph.topologicalOrder().forEach(operationID => { const operation = this.operationByID.get(operationID)!; + const schema = this.items.find(item => item.id === operation.result); + operation.is_owned = !schema || (schema.owner === this.oss.owner && schema.location === this.oss.location); operation.substitutions = this.oss.substitutions.filter(item => item.operation === operationID); operation.arguments = this.oss.arguments .filter(item => item.operation === operationID) @@ -72,7 +76,7 @@ export class OssLoader { count_operations: items.length, count_inputs: items.filter(item => item.operation_type === OperationType.INPUT).length, count_synthesis: items.filter(item => item.operation_type === OperationType.SYNTHESIS).length, - count_schemas: this.schemas.length + count_schemas: this.schemaIDs.length }; } } diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index fb31af81..ae0f7137 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -36,6 +36,7 @@ export interface IOperation { result: LibraryItemID | null; + is_owned: boolean; substitutions: ICstSubstituteEx[]; arguments: OperationID[]; } @@ -85,6 +86,14 @@ export interface IOperationUpdateData extends ITargetOperation { substitutions: ICstSubstitute[] | undefined; } +/** + * Represents {@link IOperation} data, used in destruction process. + */ +export interface IOperationDeleteData extends ITargetOperation { + keep_constituents: boolean; + delete_schema: boolean; +} + /** * Represents {@link IOperation} data, used in setInput process. */ diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx index 13e76f6c..4c019f35 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx @@ -33,6 +33,13 @@ function InputNode(node: OssNodeInternal) { disabled={!hasFile} /> + + {!node.data.operation.is_owned ? ( + +
+
+ ) : null} +
{node.data.label} {controller.showTooltip && !node.dragging ? ( diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx index 1b58cca5..af936100 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx @@ -155,7 +155,7 @@ function NodeContextMenu({ } - disabled={!controller.isMutable || controller.isProcessing} + disabled={!controller.isMutable || controller.isProcessing || !controller.canDelete(operation.id)} onClick={handleDeleteOperation} /> diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx index d93cb9ec..5b928104 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx @@ -33,6 +33,12 @@ function OperationNode(node: OssNodeInternal) { /> + {!node.data.operation.is_owned ? ( + +
+
+ ) : null} +
{node.data.label} {controller.showTooltip && !node.dragging ? ( diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index e80e3f04..c35ce23a 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -182,12 +182,15 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { if (controller.selected.length !== 1) { return; } - controller.deleteOperation(controller.selected[0], getPositions()); + handleDeleteOperation(controller.selected[0]); }, [controller, getPositions]); const handleDeleteOperation = useCallback( (target: OperationID) => { - controller.deleteOperation(target, getPositions()); + if (!controller.canDelete(target)) { + return; + } + controller.promptDeleteOperation(target, getPositions()); }, [controller, getPositions] ); diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx index fdf91dd8..90aa097e 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx @@ -175,7 +175,11 @@ function ToolbarOssGraph({ } - disabled={controller.selected.length !== 1 || controller.isProcessing} + disabled={ + controller.selected.length !== 1 || + controller.isProcessing || + !controller.canDelete(controller.selected[0]) + } onClick={onDelete} />
diff --git a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx index cac659c0..e81a2915 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx @@ -13,17 +13,20 @@ import { useOSS } from '@/context/OssContext'; import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; +import DlgDeleteOperation from '@/dialogs/DlgDeleteOperation'; import DlgEditEditors from '@/dialogs/DlgEditEditors'; import DlgEditOperation from '@/dialogs/DlgEditOperation'; import { AccessPolicy, ILibraryItemEditor, LibraryItemID } from '@/models/library'; import { Position2D } from '@/models/miscellaneous'; import { IOperationCreateData, + IOperationDeleteData, IOperationPosition, IOperationSchema, IOperationSetInputData, IOperationUpdateData, - OperationID + OperationID, + OperationType } from '@/models/oss'; import { UserID, UserLevel } from '@/models/user'; import { PARAMETER } from '@/utils/constants'; @@ -62,7 +65,8 @@ export interface IOssEditContext extends ILibraryItemEditor { savePositions: (positions: IOperationPosition[], callback?: () => void) => void; promptCreateOperation: (props: ICreateOperationPrompt) => void; - deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; + canDelete: (target: OperationID) => boolean; + promptDeleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; createInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void; @@ -103,6 +107,7 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr const [showEditLocation, setShowEditLocation] = useState(false); const [showEditInput, setShowEditInput] = useState(false); const [showEditOperation, setShowEditOperation] = useState(false); + const [showDeleteOperation, setShowDeleteOperation] = useState(false); const [showCreateOperation, setShowCreateOperation] = useState(false); const [insertPosition, setInsertPosition] = useState({ x: 0, y: 0 }); @@ -258,15 +263,48 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr [model, positions] ); - const deleteOperation = useCallback( - (target: OperationID, positions: IOperationPosition[]) => { - model.deleteOperation({ target: target, positions: positions }, () => - toast.success(information.operationDestroyed) - ); + const canDelete = useCallback( + (target: OperationID) => { + if (!model.schema) { + return false; + } + const operation = model.schema.operationByID.get(target); + if (!operation) { + return false; + } + if (operation.operation_type === OperationType.INPUT) { + return true; + } + return model.schema.graph.expandOutputs([target]).length === 0; }, [model] ); + const promptDeleteOperation = useCallback( + (target: OperationID, positions: IOperationPosition[]) => { + setPositions(positions); + setTargetOperationID(target); + setShowDeleteOperation(true); + }, + [model] + ); + + const deleteOperation = useCallback( + (keepConstituents: boolean, deleteSchema: boolean) => { + if (!targetOperationID) { + return; + } + const data: IOperationDeleteData = { + target: targetOperationID, + positions: positions, + keep_constituents: keepConstituents, + delete_schema: deleteSchema + }; + model.deleteOperation(data, () => toast.success(information.operationDestroyed)); + }, + [model, targetOperationID, positions] + ); + const createInput = useCallback( (target: OperationID, positions: IOperationPosition[]) => { model.createInput({ target: target, positions: positions }, new_schema => { @@ -334,7 +372,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr openOperationSchema, savePositions, promptCreateOperation, - deleteOperation, + canDelete, + promptDeleteOperation, createInput, promptEditInput, promptEditOperation, @@ -381,6 +420,13 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr onSubmit={handleEditOperation} /> ) : null} + {showDeleteOperation ? ( + setShowDeleteOperation(false)} + target={targetOperation!} + onSubmit={deleteOperation} + /> + ) : null} ) : null}