F: Implement Operation edit

This commit is contained in:
Ivan 2024-07-29 22:30:24 +03:00
parent d3213211b5
commit 0a7cfa1375
17 changed files with 477 additions and 71 deletions

View File

@ -7,10 +7,14 @@ from django.db.models import (
FloatField, FloatField,
ForeignKey, ForeignKey,
Model, Model,
QuerySet,
TextChoices, TextChoices,
TextField TextField
) )
from .Argument import Argument
from .Substitution import Substitution
class OperationType(TextChoices): class OperationType(TextChoices):
''' Type of operation. ''' ''' Type of operation. '''
@ -74,3 +78,11 @@ class Operation(Model):
def __str__(self) -> str: def __str__(self) -> str:
return f'Операция {self.alias}' return f'Операция {self.alias}'
def getArguments(self) -> QuerySet[Argument]:
''' Operation arguments. '''
return Argument.objects.filter(operation=self)
def getSubstitutions(self) -> QuerySet[Substitution]:
''' Operation substitutions. '''
return Substitution.objects.filter(operation=self)

View File

@ -100,39 +100,59 @@ class OperationSchema:
self.save() self.save()
@transaction.atomic @transaction.atomic
def add_argument(self, operation: Operation, argument: Operation) -> Optional[Argument]: def set_arguments(self, operation: Operation, arguments: list[Operation]):
''' Add Argument to operation. ''' ''' Set arguments to operation. '''
if Argument.objects.filter(operation=operation, argument=argument).exists(): processed: list[Operation] = []
return None changed = False
result = Argument.objects.create(operation=operation, argument=argument) for current in operation.getArguments():
self.save() if current.argument not in arguments:
return result changed = True
current.delete()
@transaction.atomic else:
def clear_arguments(self, target: Operation): processed.append(current.argument)
''' Clear all arguments for operation. ''' for arg in arguments:
if not Argument.objects.filter(operation=target).exists(): if arg not in processed:
changed = True
processed.append(arg)
Argument.objects.create(operation=operation, argument=arg)
if not changed:
return return
Argument.objects.filter(operation=target).delete()
Substitution.objects.filter(operation=target).delete()
# trigger on_change effects # trigger on_change effects
self.save() self.save()
@transaction.atomic @transaction.atomic
def set_substitutions(self, target: Operation, substitutes: list[dict]): def set_substitutions(self, target: Operation, substitutes: list[dict]):
''' Clear all arguments for operation. ''' ''' Clear all arguments for operation. '''
Substitution.objects.filter(operation=target).delete() processed: list[dict] = []
for sub in substitutes: changed = False
Substitution.objects.create(
operation=target,
original=sub['original'],
substitution=sub['substitution'],
transfer_term=sub['transfer_term']
)
for current in target.getSubstitutions():
subs = [
x for x in substitutes
if x['original'] == current.original and x['substitution'] == current.substitution
]
if len(subs) == 0:
changed = True
current.delete()
continue
if current.transfer_term != subs[0]['transfer_term']:
current.transfer_term = subs[0]['transfer_term']
current.save()
continue
processed.append(subs[0])
for sub in substitutes:
if sub not in processed:
changed = True
Substitution.objects.create(
operation=target,
original=sub['original'],
substitution=sub['substitution'],
transfer_term=sub['transfer_term']
)
if not changed:
return
# trigger on_change effects # trigger on_change effects
self.save() self.save()

View File

@ -7,6 +7,7 @@ from .data_access import (
OperationSchemaSerializer, OperationSchemaSerializer,
OperationSerializer, OperationSerializer,
OperationTargetSerializer, OperationTargetSerializer,
OperationUpdateSerializer,
SetOperationInputSerializer SetOperationInputSerializer
) )
from .responses import NewOperationResponse, NewSchemaResponse from .responses import NewOperationResponse, NewSchemaResponse

View File

@ -1,4 +1,5 @@
''' Serializers for persistent data manipulation. ''' ''' Serializers for persistent data manipulation. '''
import re
from typing import cast from typing import cast
from django.db.models import F from django.db.models import F
@ -7,6 +8,8 @@ from rest_framework.serializers import PrimaryKeyRelatedField as PKField
from apps.library.models import LibraryItem, LibraryItemType from apps.library.models import LibraryItem, LibraryItemType
from apps.library.serializers import LibraryItemDetailsSerializer from apps.library.serializers import LibraryItemDetailsSerializer
from apps.rsform.models import Constituenta
from apps.rsform.serializers import SubstitutionSerializerBase
from shared import messages as msg from shared import messages as msg
from ..models import Argument, Operation, OperationSchema, OperationType from ..models import Argument, Operation, OperationSchema, OperationType
@ -47,12 +50,75 @@ class OperationCreateSerializer(serializers.Serializer):
create_schema = serializers.BooleanField(default=False, required=False) create_schema = serializers.BooleanField(default=False, required=False)
item_data = OperationData() item_data = OperationData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False) arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
positions = serializers.ListField( positions = serializers.ListField(
child=OperationPositionSerializer(), child=OperationPositionSerializer(),
default=[] default=[]
) )
class OperationUpdateSerializer(serializers.Serializer):
''' Serializer: Operation creation. '''
class OperationData(serializers.ModelSerializer):
''' Serializer: Operation creation data. '''
class Meta:
''' serializer metadata. '''
model = Operation
fields = 'alias', 'title', 'sync_text', 'comment'
target = PKField(many=False, queryset=Operation.objects.all())
item_data = OperationData()
arguments = PKField(many=True, queryset=Operation.objects.all(), required=False)
substitutions = serializers.ListField(
child=SubstitutionSerializerBase(),
required=False
)
positions = serializers.ListField(
child=OperationPositionSerializer(),
default=[]
)
def validate(self, attrs):
if 'arguments' not in attrs:
return attrs
oss = cast(LibraryItem, self.context['oss'])
for operation in attrs['arguments']:
if operation.oss != oss:
raise serializers.ValidationError({
'arguments': msg.operationNotInOSS(oss.title)
})
if 'substitutions' not in attrs:
return attrs
schemas = [arg.result.pk for arg in attrs['arguments'] if arg.result is not None]
deleted = set()
for item in attrs['substitutions']:
original_cst = cast(Constituenta, item['original'])
substitution_cst = cast(Constituenta, item['substitution'])
if original_cst.schema.pk not in schemas:
raise serializers.ValidationError({
f'{original_cst.id}': msg.constituentaNotFromOperation()
})
if substitution_cst.schema.pk not in schemas:
raise serializers.ValidationError({
f'{substitution_cst.id}': msg.constituentaNotFromOperation()
})
if original_cst.pk in deleted:
raise serializers.ValidationError({
f'{original_cst.id}': msg.substituteDouble(original_cst.alias)
})
if original_cst.schema == substitution_cst.schema:
raise serializers.ValidationError({
'alias': msg.substituteTrivial(original_cst.alias)
})
deleted.add(original_cst.pk)
return attrs
class OperationTargetSerializer(serializers.Serializer): class OperationTargetSerializer(serializers.Serializer):
''' Serializer: Delete operation. ''' ''' Serializer: Delete operation. '''
target = PKField(many=False, queryset=Operation.objects.all()) target = PKField(many=False, queryset=Operation.objects.all())

View File

@ -41,8 +41,7 @@ class TestOssViewset(EndpointTester):
alias='3', alias='3',
operation_type=OperationType.SYNTHESIS operation_type=OperationType.SYNTHESIS
) )
self.owned.add_argument(self.operation3, self.operation1) self.owned.set_arguments(self.operation3, [self.operation1, self.operation2])
self.owned.add_argument(self.operation3, self.operation2)
self.owned.set_substitutions(self.operation3, [{ self.owned.set_substitutions(self.operation3, [{
'original': self.ks1x1, 'original': self.ks1x1,
'substitution': self.ks2x1, 'substitution': self.ks2x1,
@ -344,3 +343,76 @@ class TestOssViewset(EndpointTester):
self.operation2.refresh_from_db() self.operation2.refresh_from_db()
self.assertEqual(self.operation2.sync_text, True) self.assertEqual(self.operation2.sync_text, True)
self.assertEqual(self.operation2.result, self.ks2.model) self.assertEqual(self.operation2.result, self.ks2.model)
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation(self):
self.populateData()
self.executeBadData(item=self.owned_id)
ks3 = RSForm.create(alias='KS3', title='Test3', owner=self.user)
ks3x1 = ks3.insert_new('X1', term_resolved='X1_1')
data = {
'target': self.operation3.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod',
'sync_text': True
},
'positions': [],
'arguments': [self.operation1.pk, self.operation2.pk],
'substitutions': [
{
'original': self.ks1x1.pk,
'substitution': ks3x1.pk,
'transfer_term': False
}
]
}
self.executeBadData(data=data)
data['substitutions'][0]['substitution'] = self.ks2x1.pk
self.toggle_admin(True)
self.executeBadData(data=data, item=self.unowned_id)
self.logout()
self.executeForbidden(data=data, item=self.owned_id)
self.login()
response = self.executeOK(data=data)
self.operation3.refresh_from_db()
self.assertEqual(self.operation3.sync_text, data['item_data']['sync_text'])
self.assertEqual(self.operation3.alias, data['item_data']['alias'])
self.assertEqual(self.operation3.title, data['item_data']['title'])
self.assertEqual(self.operation3.comment, data['item_data']['comment'])
self.assertEqual(set([argument.pk for argument in self.operation3.getArguments()]), set(data['arguments']))
sub = self.operation3.getSubstitutions()[0]
self.assertEqual(sub.original.pk, data['substitutions'][0]['original'])
self.assertEqual(sub.substitution.pk, data['substitutions'][0]['substitution'])
self.assertEqual(sub.transfer_term, data['substitutions'][0]['transfer_term'])
@decl_endpoint('/api/oss/{item}/update-operation', method='patch')
def test_update_operation_sync(self):
self.populateData()
self.executeBadData(item=self.owned_id)
data = {
'target': self.operation1.pk,
'item_data': {
'alias': 'Test3 mod',
'title': 'Test title mod',
'comment': 'Comment mod',
'sync_text': True
},
'positions': [],
}
response = self.executeOK(data=data)
self.operation1.refresh_from_db()
self.assertEqual(self.operation1.sync_text, data['item_data']['sync_text'])
self.assertEqual(self.operation1.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.title, data['item_data']['title'])
self.assertEqual(self.operation1.comment, data['item_data']['comment'])
self.assertEqual(self.operation1.result.alias, data['item_data']['alias'])
self.assertEqual(self.operation1.result.title, data['item_data']['title'])
self.assertEqual(self.operation1.result.comment, data['item_data']['comment'])

View File

@ -36,7 +36,9 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
'delete_operation', 'delete_operation',
'update_positions', 'update_positions',
'create_input', 'create_input',
'set_input' 'set_input',
'update_operation',
'execute_operation',
]: ]:
permission_list = [permissions.ItemEditor] permission_list = [permissions.ItemEditor]
elif self.action in ['details']: elif self.action in ['details']:
@ -117,8 +119,10 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
data['result'] = schema data['result'] = schema
new_operation = oss.create_operation(**data) new_operation = oss.create_operation(**data)
if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data:
for argument in serializer.validated_data['arguments']: oss.set_arguments(
oss.add_argument(operation=new_operation, argument=argument) operation=new_operation,
arguments=serializer.validated_data['arguments']
)
oss.refresh_from_db() oss.refresh_from_db()
return Response( return Response(
@ -257,3 +261,96 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev
status=c.HTTP_200_OK, status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data data=s.OperationSchemaSerializer(oss.model).data
) )
@extend_schema(
summary='update operation',
tags=['OSS'],
request=s.OperationUpdateSerializer(),
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=True, methods=['patch'], url_path='update-operation')
def update_operation(self, request: Request, pk):
''' Update operation arguments and parameters. '''
serializer = s.OperationUpdateSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
oss = m.OperationSchema(self.get_object())
with transaction.atomic():
oss.update_positions(serializer.validated_data['positions'])
operation.alias = serializer.validated_data['item_data']['alias']
operation.title = serializer.validated_data['item_data']['title']
operation.comment = serializer.validated_data['item_data']['comment']
operation.sync_text = serializer.validated_data['item_data']['sync_text']
operation.save()
if operation.sync_text and operation.result is not None:
can_edit = permissions.can_edit_item(request.user, operation.result)
if can_edit:
operation.result.alias = operation.alias
operation.result.title = operation.title
operation.result.comment = operation.comment
operation.result.save()
if 'arguments' in serializer.validated_data:
oss.set_arguments(operation, serializer.validated_data['arguments'])
if 'substitutions' in serializer.validated_data:
oss.set_substitutions(operation, serializer.validated_data['substitutions'])
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)
@extend_schema(
summary='execute operation',
tags=['OSS'],
request=s.OperationTargetSerializer(),
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=True, methods=['post'], url_path='execute-operation')
def execute_operation(self, request: Request, pk):
''' Execute operation. '''
serializer = s.OperationTargetSerializer(
data=request.data,
context={'oss': self.get_object()}
)
serializer.is_valid(raise_exception=True)
operation: m.Operation = cast(m.Operation, serializer.validated_data['target'])
if operation.operation_type != m.OperationType.SYNTHESIS:
raise serializers.ValidationError({
'target': msg.operationNotSynthesis(operation.alias)
})
if operation.result is not None:
raise serializers.ValidationError({
'target': msg.operationResultNotEmpty(operation.alias)
})
oss = m.OperationSchema(self.get_object())
# with transaction.atomic():
# oss.update_positions(serializer.validated_data['positions'])
# operation.result.refresh_from_db()
# operation.result.title = operation.title
# operation.result.comment = operation.comment
# operation.result.alias = operation.alias
# operation.result.save()
# update arguments
oss.refresh_from_db()
return Response(
status=c.HTTP_200_OK,
data=s.OperationSchemaSerializer(oss.model).data
)

View File

@ -19,7 +19,8 @@ from .data_access import (
CstTargetSerializer, CstTargetSerializer,
InlineSynthesisSerializer, InlineSynthesisSerializer,
RSFormParseSerializer, RSFormParseSerializer,
RSFormSerializer RSFormSerializer,
SubstitutionSerializerBase
) )
from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer from .io_files import FileSerializer, RSFormTRSSerializer, RSFormUploadSerializer
from .io_pyconcept import PyConceptAdapter from .io_pyconcept import PyConceptAdapter

View File

@ -270,7 +270,7 @@ class CstMoveSerializer(CstListSerializer):
move_to = serializers.IntegerField() move_to = serializers.IntegerField()
class CstSubstituteSerializerBase(serializers.Serializer): class SubstitutionSerializerBase(serializers.Serializer):
''' Serializer: Basic substitution. ''' ''' Serializer: Basic substitution. '''
original = PKField(many=False, queryset=Constituenta.objects.all()) original = PKField(many=False, queryset=Constituenta.objects.all())
substitution = PKField(many=False, queryset=Constituenta.objects.all()) substitution = PKField(many=False, queryset=Constituenta.objects.all())
@ -280,7 +280,7 @@ class CstSubstituteSerializerBase(serializers.Serializer):
class CstSubstituteSerializer(serializers.Serializer): class CstSubstituteSerializer(serializers.Serializer):
''' Serializer: Constituenta substitution. ''' ''' Serializer: Constituenta substitution. '''
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=CstSubstituteSerializerBase(), child=SubstitutionSerializerBase(),
min_length=1 min_length=1
) )
@ -316,7 +316,7 @@ class InlineSynthesisSerializer(serializers.Serializer):
source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore source = PKField(many=False, queryset=LibraryItem.objects.all()) # type: ignore
items = PKField(many=True, queryset=Constituenta.objects.all()) items = PKField(many=True, queryset=Constituenta.objects.all())
substitutions = serializers.ListField( substitutions = serializers.ListField(
child=CstSubstituteSerializerBase() child=SubstitutionSerializerBase()
) )
def validate(self, attrs): def validate(self, attrs):

View File

@ -6,8 +6,12 @@ def constituentaNotInRSform(title: str):
return f'Конституента не принадлежит схеме: {title}' return f'Конституента не принадлежит схеме: {title}'
def constituentaNotFromOperation():
return f'Конституента не соответствую аргументам операции'
def operationNotInOSS(title: str): def operationNotInOSS(title: str):
return f'Операция не принадлежит схеме: {title}' return f'Операция не принадлежит ОСС: {title}'
def substitutionNotInList(): def substitutionNotInList():
@ -22,6 +26,10 @@ def operationNotInput(title: str):
return f'Операция не является Загрузкой: {title}' return f'Операция не является Загрузкой: {title}'
def operationNotSynthesis(title: str):
return f'Операция не является Синтезом: {title}'
def operationResultNotEmpty(title: str): def operationResultNotEmpty(title: str):
return f'Результат операции не пуст: {title}' return f'Результат операции не пуст: {title}'

View File

@ -32,6 +32,24 @@ def _extract_item(obj: Any) -> LibraryItem:
}) })
def can_edit_item(user, obj: Any) -> bool:
if user.is_anonymous:
return False
if hasattr(user, 'is_staff') and user.is_staff:
return True
item = _extract_item(obj)
if item.owner == user:
return True
if Editor.objects.filter(
item=item,
editor=cast(User, user)
).exists() and item.access_policy != AccessPolicy.PRIVATE:
return True
return False
class GlobalAdmin(_Base): class GlobalAdmin(_Base):
''' Item permission: Admin or higher. ''' ''' Item permission: Admin or higher. '''

View File

@ -8,6 +8,7 @@ import {
IOperationCreatedResponse, IOperationCreatedResponse,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData, IOperationSetInputData,
IOperationUpdateData,
IPositionsData, IPositionsData,
ITargetOperation ITargetOperation
} from '@/models/oss'; } from '@/models/oss';
@ -58,3 +59,17 @@ export function patchSetInput(oss: string, request: FrontExchange<IOperationSetI
request: request request: request
}); });
} }
export function patchUpdateOperation(oss: string, request: FrontExchange<IOperationUpdateData, IOperationSchemaData>) {
AxiosPatch({
endpoint: `/api/oss/${oss}/update-operation`,
request: request
});
}
export function postExecuteOperation(oss: string, request: FrontExchange<ITargetOperation, IOperationSchemaData>) {
AxiosPost({
endpoint: `/api/oss/${oss}/execute-operation`,
request: request
});
}

View File

@ -16,8 +16,10 @@ import {
patchCreateInput, patchCreateInput,
patchDeleteOperation, patchDeleteOperation,
patchSetInput, patchSetInput,
patchUpdateOperation,
patchUpdatePositions, patchUpdatePositions,
postCreateOperation postCreateOperation,
postExecuteOperation
} from '@/backend/oss'; } from '@/backend/oss';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { AccessPolicy, ILibraryItem } from '@/models/library'; import { AccessPolicy, ILibraryItem } from '@/models/library';
@ -28,6 +30,7 @@ import {
IOperationSchema, IOperationSchema,
IOperationSchemaData, IOperationSchemaData,
IOperationSetInputData, IOperationSetInputData,
IOperationUpdateData,
IPositionsData, IPositionsData,
ITargetOperation ITargetOperation
} from '@/models/oss'; } from '@/models/oss';
@ -63,6 +66,8 @@ interface IOssContext {
deleteOperation: (data: ITargetOperation, callback?: () => void) => void; deleteOperation: (data: ITargetOperation, callback?: () => void) => void;
createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void; createInput: (data: ITargetOperation, callback?: DataCallback<ILibraryItem>) => void;
setInput: (data: IOperationSetInputData, callback?: () => void) => void; setInput: (data: IOperationSetInputData, callback?: () => void) => void;
updateOperation: (data: IOperationUpdateData, callback?: () => void) => void;
executeOperation: (data: ITargetOperation, callback?: () => void) => void;
} }
const OssContext = createContext<IOssContext | null>(null); const OssContext = createContext<IOssContext | null>(null);
@ -362,6 +367,50 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
[itemID, schema, library] [itemID, schema, library]
); );
const updateOperation = useCallback(
(data: IOperationUpdateData, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
patchUpdateOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData);
library.reloadItems(() => {
if (callback) callback();
});
}
});
},
[itemID, schema, library]
);
const executeOperation = useCallback(
(data: ITargetOperation, callback?: () => void) => {
if (!schema) {
return;
}
setProcessingError(undefined);
postExecuteOperation(itemID, {
data: data,
showError: true,
setLoading: setProcessing,
onError: setProcessingError,
onSuccess: newData => {
library.setGlobalOSS(newData);
library.reloadItems(() => {
if (callback) callback();
});
}
});
},
[itemID, schema, library]
);
return ( return (
<OssContext.Provider <OssContext.Provider
value={{ value={{
@ -386,7 +435,9 @@ export const OssState = ({ itemID, children }: OssStateProps) => {
createOperation, createOperation,
deleteOperation, deleteOperation,
createInput, createInput,
setInput setInput,
updateOperation,
executeOperation
}} }}
> >
{children} {children}

View File

@ -10,7 +10,14 @@ import Overlay from '@/components/ui/Overlay';
import TabLabel from '@/components/ui/TabLabel'; import TabLabel from '@/components/ui/TabLabel';
import useRSFormCache from '@/hooks/useRSFormCache'; import useRSFormCache from '@/hooks/useRSFormCache';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
import { ICstSubstitute, IOperation, IOperationSchema, OperationID, OperationType } from '@/models/oss'; import {
ICstSubstitute,
IOperation,
IOperationSchema,
IOperationUpdateData,
OperationID,
OperationType
} from '@/models/oss';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import TabArguments from './TabArguments'; import TabArguments from './TabArguments';
@ -21,8 +28,7 @@ interface DlgEditOperationProps {
hideWindow: () => void; hideWindow: () => void;
oss: IOperationSchema; oss: IOperationSchema;
target: IOperation; target: IOperation;
// onSubmit: (data: IOperationEditData) => void; onSubmit: (data: IOperationUpdateData) => void;
onSubmit: () => void;
} }
export enum TabID { export enum TabID {
@ -56,22 +62,19 @@ function DlgEditOperation({ hideWindow, oss, target, onSubmit }: DlgEditOperatio
}, [schemasIDs]); }, [schemasIDs]);
const handleSubmit = () => { const handleSubmit = () => {
// const data: IOperationCreateData = { const data: IOperationUpdateData = {
// item_data: { target: target.id,
// position_x: insertPosition.x, item_data: {
// position_y: insertPosition.y, alias: alias,
// alias: alias, title: title,
// title: title, comment: comment,
// comment: comment, sync_text: syncText
// sync_text: activeTab === TabID.INPUT ? syncText : true, },
// operation_type: activeTab === TabID.INPUT ? OperationType.INPUT : OperationType.SYNTHESIS, positions: [],
// result: activeTab === TabID.INPUT ? attachedID ?? null : null arguments: target.operation_type !== OperationType.SYNTHESIS ? undefined : inputs,
// }, substitutions: target.operation_type !== OperationType.SYNTHESIS ? undefined : substitutions
// positions: positions, };
// arguments: activeTab === TabID.INPUT ? undefined : inputs.length > 0 ? inputs : undefined, onSubmit(data);
// create_schema: createSchema
// };
onSubmit();
}; };
const cardPanel = useMemo( const cardPanel = useMemo(

View File

@ -69,6 +69,15 @@ export interface IOperationCreateData extends IPositionsData {
create_schema: boolean; create_schema: boolean;
} }
/**
* Represents {@link IOperation} data, used in update process.
*/
export interface IOperationUpdateData extends ITargetOperation {
item_data: Pick<IOperation, 'alias' | 'title' | 'comment' | 'sync_text'>;
arguments: OperationID[] | undefined;
substitutions: ICstSubstitute[] | undefined;
}
/** /**
* Represents {@link IOperation} data, used in setInput process. * Represents {@link IOperation} data, used in setInput process.
*/ */

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify';
import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons'; import { IconConnect, IconDestroy, IconEdit2, IconExecute, IconNewItem, IconRSForm } from '@/components/Icons';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
@ -25,6 +24,7 @@ interface NodeContextMenuProps extends ContextMenuData {
onCreateInput: (target: OperationID) => void; onCreateInput: (target: OperationID) => void;
onEditSchema: (target: OperationID) => void; onEditSchema: (target: OperationID) => void;
onEditOperation: (target: OperationID) => void; onEditOperation: (target: OperationID) => void;
onRunOperation: (target: OperationID) => void;
} }
function NodeContextMenu({ function NodeContextMenu({
@ -35,7 +35,8 @@ function NodeContextMenu({
onDelete, onDelete,
onCreateInput, onCreateInput,
onEditSchema, onEditSchema,
onEditOperation onEditOperation,
onRunOperation
}: NodeContextMenuProps) { }: NodeContextMenuProps) {
const controller = useOssEdit(); const controller = useOssEdit();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -95,8 +96,8 @@ function NodeContextMenu({
}; };
const handleRunSynthesis = () => { const handleRunSynthesis = () => {
toast.error('Not implemented');
handleHide(); handleHide();
onRunOperation(operation.id);
}; };
return ( return (

View File

@ -162,6 +162,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
[controller, getPositions] [controller, getPositions]
); );
const handleRunOperation = useCallback(
(target: OperationID) => {
controller.runOperation(target, getPositions());
},
[controller, getPositions]
);
const handleFitView = useCallback(() => { const handleFitView = useCallback(() => {
flow.fitView({ duration: PARAMETER.zoomDuration }); flow.fitView({ duration: PARAMETER.zoomDuration });
}, [flow]); }, [flow]);
@ -227,6 +234,17 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
handleContextMenuHide(); handleContextMenuHide();
}, [handleContextMenuHide]); }, [handleContextMenuHide]);
const handleNodeClick = useCallback(
(event: CProps.EventMouse, node: OssNode) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
event.stopPropagation();
handleEditOperation(Number(node.id));
}
},
[handleEditOperation]
);
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (controller.isProcessing) { if (controller.isProcessing) {
return; return;
@ -266,6 +284,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
edges={edges} edges={edges}
onNodesChange={handleNodesChange} onNodesChange={handleNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onNodeClick={handleNodeClick}
fitView fitView
proOptions={{ hideAttribution: true }} proOptions={{ hideAttribution: true }}
nodeTypes={OssNodeTypes} nodeTypes={OssNodeTypes}
@ -309,6 +328,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) {
onCreateInput={handleCreateInput} onCreateInput={handleCreateInput}
onEditSchema={handleEditSchema} onEditSchema={handleEditSchema}
onEditOperation={handleEditOperation} onEditOperation={handleEditOperation}
onRunOperation={handleRunOperation}
{...menuProps} {...menuProps}
/> />
) : null} ) : null}

View File

@ -22,6 +22,7 @@ import {
IOperationPosition, IOperationPosition,
IOperationSchema, IOperationSchema,
IOperationSetInputData, IOperationSetInputData,
IOperationUpdateData,
OperationID OperationID
} from '@/models/oss'; } from '@/models/oss';
import { UserID, UserLevel } from '@/models/user'; import { UserID, UserLevel } from '@/models/user';
@ -55,6 +56,7 @@ export interface IOssEditContext {
createInput: (target: OperationID, positions: IOperationPosition[]) => void; createInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void;
promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void; promptEditOperation: (target: OperationID, positions: IOperationPosition[]) => void;
runOperation: (target: OperationID, positions: IOperationPosition[]) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -228,17 +230,13 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
setShowEditOperation(true); setShowEditOperation(true);
}, []); }, []);
const handleEditOperation = useCallback(() => { const handleEditOperation = useCallback(
// TODO: проверить наличие всех аргументов (data: IOperationUpdateData) => {
// TODO: проверить наличие синтеза data.positions = positions;
// TODO: проверить полноту синтеза model.updateOperation(data, () => toast.success(information.changesSaved));
// TODO: проверить правильность синтеза },
// TODO: сохранить позиции [model, positions]
// TODO: обновить схему );
// model.setInput(data, () => toast.success(information.changesSaved));
toast.error('Not implemented');
}, []);
const deleteOperation = useCallback( const deleteOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => { (target: OperationID, positions: IOperationPosition[]) => {
@ -281,6 +279,19 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
[model, targetOperationID, positions] [model, targetOperationID, positions]
); );
const runOperation = useCallback(
(target: OperationID, positions: IOperationPosition[]) => {
model.executeOperation(
{
target: target,
positions: positions
},
() => toast.success(information.changesSaved)
);
},
[model]
);
return ( return (
<OssEditContext.Provider <OssEditContext.Provider
value={{ value={{
@ -308,7 +319,8 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr
deleteOperation, deleteOperation,
createInput, createInput,
promptEditInput, promptEditInput,
promptEditOperation promptEditOperation,
runOperation
}} }}
> >
{model.schema ? ( {model.schema ? (