From 591d36d772bac60c4cdbc94898b24a0aec422b08 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Sun, 28 Jul 2024 21:30:10 +0300 Subject: [PATCH] F: Implement input schema change UI --- .../backend/apps/oss/serializers/__init__.py | 3 +- .../apps/oss/serializers/data_access.py | 34 +++++++- .../backend/apps/oss/tests/s_views/t_oss.py | 59 +++++++++++++- rsconcept/backend/apps/oss/views/oss.py | 48 ++++++++++- .../apps/rsform/serializers/data_access.py | 18 ++--- rsconcept/backend/shared/messages.py | 6 +- rsconcept/frontend/src/backend/oss.ts | 8 ++ rsconcept/frontend/src/context/OssContext.tsx | 34 +++++++- .../frontend/src/context/RSFormContext.tsx | 6 +- .../src/dialogs/DlgChangeInputSchema.tsx | 79 +++++++++++++++++++ .../DlgCreateOperation/TabInputOperation.tsx | 2 +- rsconcept/frontend/src/models/oss.ts | 8 ++ .../EditorOssGraph/NodeContextMenu.tsx | 17 +++- .../OssPage/EditorOssGraph/OperationNode.tsx | 4 +- .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 8 ++ .../src/pages/OssPage/OssEditContext.tsx | 51 +++++++++++- 16 files changed, 352 insertions(+), 33 deletions(-) create mode 100644 rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx diff --git a/rsconcept/backend/apps/oss/serializers/__init__.py b/rsconcept/backend/apps/oss/serializers/__init__.py index e32b2334..11b37019 100644 --- a/rsconcept/backend/apps/oss/serializers/__init__.py +++ b/rsconcept/backend/apps/oss/serializers/__init__.py @@ -6,6 +6,7 @@ from .data_access import ( OperationCreateSerializer, OperationSchemaSerializer, OperationSerializer, - OperationTargetSerializer + OperationTargetSerializer, + SetOperationInputSerializer ) from .responses import NewOperationResponse, NewSchemaResponse diff --git a/rsconcept/backend/apps/oss/serializers/data_access.py b/rsconcept/backend/apps/oss/serializers/data_access.py index 8d66c11a..d82949ab 100644 --- a/rsconcept/backend/apps/oss/serializers/data_access.py +++ b/rsconcept/backend/apps/oss/serializers/data_access.py @@ -5,7 +5,7 @@ from django.db.models import F from rest_framework import serializers from rest_framework.serializers import PrimaryKeyRelatedField as PKField -from apps.library.models import LibraryItem +from apps.library.models import LibraryItem, LibraryItemType from apps.library.serializers import LibraryItemDetailsSerializer from shared import messages as msg @@ -66,9 +66,37 @@ class OperationTargetSerializer(serializers.Serializer): operation = cast(Operation, attrs['target']) if oss and operation.oss != oss: raise serializers.ValidationError({ - f'{operation.id}': msg.operationNotOwned(oss.title) + '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()) + input = PKField( + many=False, + queryset=LibraryItem.objects.filter(item_type=LibraryItemType.RSFORM), + allow_null=True, + default=None + ) + sync_text = serializers.BooleanField(default=False, required=False) + positions = serializers.ListField( + child=OperationPositionSerializer(), + default=[] + ) + + def validate(self, attrs): + oss = cast(LibraryItem, self.context['oss']) + operation = cast(Operation, attrs['target']) + if oss and operation.oss != oss: + raise serializers.ValidationError({ + 'target': msg.operationNotInOSS(oss.title) + }) + if operation.operation_type != OperationType.INPUT: + raise serializers.ValidationError({ + 'target': msg.operationNotInput(operation.alias) }) - self.instance = operation return attrs 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 81bb564b..9c1054c6 100644 --- a/rsconcept/backend/apps/oss/tests/s_views/t_oss.py +++ b/rsconcept/backend/apps/oss/tests/s_views/t_oss.py @@ -2,7 +2,7 @@ from rest_framework import status -from apps.library.models import AccessPolicy, LibraryItem, LibraryItemType, LocationHead +from apps.library.models import AccessPolicy, Editor, LibraryItem, LibraryItemType, LocationHead from apps.oss.models import Operation, OperationSchema, OperationType from apps.rsform.models import RSForm from shared.EndpointTester import EndpointTester, decl_endpoint @@ -208,6 +208,7 @@ class TestOssViewset(EndpointTester): @decl_endpoint('/api/oss/{item}/create-operation', method='post') def test_create_operation_schema(self): self.populateData() + Editor.add(self.owned.model, self.user2) data = { 'item_data': { 'alias': 'Test4', @@ -228,6 +229,7 @@ class TestOssViewset(EndpointTester): self.assertEqual(schema.visible, False) self.assertEqual(schema.access_policy, self.owned.model.access_policy) self.assertEqual(schema.location, self.owned.model.location) + self.assertIn(self.user2, schema.editors()) @decl_endpoint('/api/oss/{item}/delete-operation', method='patch') def test_delete_operation(self): @@ -287,3 +289,58 @@ class TestOssViewset(EndpointTester): data['target'] = self.operation3.pk self.executeBadData(data=data) + + @decl_endpoint('/api/oss/{item}/set-input', method='patch') + def test_set_input_null(self): + self.populateData() + self.executeBadData(item=self.owned_id) + + data = { + 'sync_text': True, + 'positions': [] + } + self.executeBadData(data=data) + + data['target'] = self.operation1.pk + data['input'] = None + + data['target'] = self.operation1.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.operation1.refresh_from_db() + self.assertEqual(self.operation1.sync_text, True) + self.assertEqual(self.operation1.result, None) + + data['input'] = self.ks1.model.pk + self.ks1.model.alias = 'Test42' + self.ks1.model.title = 'Test421' + self.ks1.model.comment = 'TestComment42' + self.ks1.save() + response = self.executeOK(data=data) + self.operation1.refresh_from_db() + self.assertEqual(self.operation1.sync_text, True) + self.assertEqual(self.operation1.result, self.ks1.model) + self.assertEqual(self.operation1.alias, self.ks1.model.alias) + self.assertEqual(self.operation1.title, self.ks1.model.title) + self.assertEqual(self.operation1.comment, self.ks1.model.comment) + + @decl_endpoint('/api/oss/{item}/set-input', method='patch') + def test_set_input_change_schema(self): + self.populateData() + self.operation2.result = None + + data = { + 'sync_text': True, + 'positions': [], + 'target': self.operation1.pk, + 'input': self.ks2.model.pk + } + response = self.executeOK(data=data, item=self.owned_id) + self.operation2.refresh_from_db() + self.assertEqual(self.operation2.sync_text, True) + self.assertEqual(self.operation2.result, self.ks2.model) diff --git a/rsconcept/backend/apps/oss/views/oss.py b/rsconcept/backend/apps/oss/views/oss.py index 43710069..5dc5a263 100644 --- a/rsconcept/backend/apps/oss/views/oss.py +++ b/rsconcept/backend/apps/oss/views/oss.py @@ -10,7 +10,7 @@ from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response -from apps.library.models import LibraryItem, LibraryItemType +from apps.library.models import Editor, LibraryItem, LibraryItemType from apps.library.serializers import LibraryItemSerializer from shared import messages as msg from shared import permissions @@ -35,7 +35,8 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'create_operation', 'delete_operation', 'update_positions', - 'create_input' + 'create_input', + 'set_input' ]: permission_list = [permissions.ItemEditor] elif self.action in ['details']: @@ -112,6 +113,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev access_policy=oss.model.access_policy, location=oss.model.location ) + Editor.set(schema, oss.model.editors()) data['result'] = schema new_operation = oss.create_operation(**data) if new_operation.operation_type != m.OperationType.INPUT and 'arguments' in serializer.validated_data: @@ -201,6 +203,7 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev access_policy=oss.model.access_policy, location=oss.model.location ) + Editor.set(schema, oss.model.editors()) operation.result = schema operation.sync_text = True operation.save() @@ -213,3 +216,44 @@ class OssViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retriev 'oss': s.OperationSchemaSerializer(oss.model).data } ) + + @extend_schema( + summary='set input schema for target operation', + tags=['OSS'], + request=s.SetOperationInputSerializer(), + 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='set-input') + def set_input(self, request: Request, pk): + ''' Set input schema for target operation. ''' + serializer = s.SetOperationInputSerializer( + data=request.data, + context={'oss': self.get_object()} + ) + serializer.is_valid(raise_exception=True) + + operation: m.Operation = cast(m.Operation, serializer.validated_data['target']) + result = serializer.validated_data['input'] + oss = m.OperationSchema(self.get_object()) + with transaction.atomic(): + oss.update_positions(serializer.validated_data['positions']) + operation.result = result + operation.sync_text = serializer.validated_data['sync_text'] + if result is not None and operation.sync_text: + operation.title = result.title + operation.comment = result.comment + operation.alias = result.alias + operation.save() + + # update arguments + + oss.refresh_from_db() + return Response( + status=c.HTTP_200_OK, + data=s.OperationSchemaSerializer(oss.model).data + ) diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index aaecfd2f..2d596699 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -212,7 +212,7 @@ class CstTargetSerializer(serializers.Serializer): cst = cast(Constituenta, attrs['target']) if schema and cst.schema != schema: raise serializers.ValidationError({ - f'{cst.id}': msg.constituentaNotOwned(schema.title) + f'{cst.id}': msg.constituentaNotInRSform(schema.title) }) if cst.cst_type not in [CstType.FUNCTION, CstType.STRUCTURED, CstType.TERM]: raise serializers.ValidationError({ @@ -234,7 +234,7 @@ class CstRenameSerializer(serializers.Serializer): cst = cast(Constituenta, attrs['target']) if cst.schema != schema: raise serializers.ValidationError({ - f'{cst.id}': msg.constituentaNotOwned(schema.title) + f'{cst.id}': msg.constituentaNotInRSform(schema.title) }) new_alias = self.initial_data['alias'] if cst.alias == new_alias: @@ -260,7 +260,7 @@ class CstListSerializer(serializers.Serializer): for item in attrs['items']: if item.schema != schema: raise serializers.ValidationError({ - f'{item.id}': msg.constituentaNotOwned(schema.title) + f'{item.id}': msg.constituentaNotInRSform(schema.title) }) return attrs @@ -300,11 +300,11 @@ class CstSubstituteSerializer(serializers.Serializer): }) if original_cst.schema != schema: raise serializers.ValidationError({ - 'original': msg.constituentaNotOwned(schema.title) + 'original': msg.constituentaNotInRSform(schema.title) }) if substitution_cst.schema != schema: raise serializers.ValidationError({ - 'substitution': msg.constituentaNotOwned(schema.title) + 'substitution': msg.constituentaNotInRSform(schema.title) }) deleted.add(original_cst.pk) return attrs @@ -325,14 +325,14 @@ class InlineSynthesisSerializer(serializers.Serializer): schema_out = cast(LibraryItem, attrs['receiver']) if user.is_anonymous or (schema_out.owner != user and not user.is_staff): raise PermissionDenied({ - 'message': msg.schemaNotOwned(), + 'message': msg.schemaForbidden(), 'object_id': schema_in.id }) constituents = cast(list[Constituenta], attrs['items']) for cst in constituents: if cst.schema != schema_in: raise serializers.ValidationError({ - f'{cst.id}': msg.constituentaNotOwned(schema_in.title) + f'{cst.id}': msg.constituentaNotInRSform(schema_in.title) }) deleted = set() for item in attrs['substitutions']: @@ -345,7 +345,7 @@ class InlineSynthesisSerializer(serializers.Serializer): }) if substitution_cst.schema != schema_out: raise serializers.ValidationError({ - f'{substitution_cst.id}': msg.constituentaNotOwned(schema_out.title) + f'{substitution_cst.id}': msg.constituentaNotInRSform(schema_out.title) }) else: if substitution_cst not in constituents: @@ -354,7 +354,7 @@ class InlineSynthesisSerializer(serializers.Serializer): }) if original_cst.schema != schema_out: raise serializers.ValidationError({ - f'{original_cst.id}': msg.constituentaNotOwned(schema_out.title) + f'{original_cst.id}': msg.constituentaNotInRSform(schema_out.title) }) if original_cst.pk in deleted: raise serializers.ValidationError({ diff --git a/rsconcept/backend/shared/messages.py b/rsconcept/backend/shared/messages.py index 3ace8a71..7cf9d105 100644 --- a/rsconcept/backend/shared/messages.py +++ b/rsconcept/backend/shared/messages.py @@ -2,11 +2,11 @@ # pylint: skip-file -def constituentaNotOwned(title: str): +def constituentaNotInRSform(title: str): return f'Конституента не принадлежит схеме: {title}' -def operationNotOwned(title: str): +def operationNotInOSS(title: str): return f'Операция не принадлежит схеме: {title}' @@ -14,7 +14,7 @@ def substitutionNotInList(): return 'Отождествляемая конституента отсутствует в списке' -def schemaNotOwned(): +def schemaForbidden(): return 'Нет доступа к схеме' diff --git a/rsconcept/frontend/src/backend/oss.ts b/rsconcept/frontend/src/backend/oss.ts index f92cae12..c7e1fc4b 100644 --- a/rsconcept/frontend/src/backend/oss.ts +++ b/rsconcept/frontend/src/backend/oss.ts @@ -7,6 +7,7 @@ import { IOperationCreateData, IOperationCreatedResponse, IOperationSchemaData, + IOperationSetInputData, IPositionsData, ITargetOperation } from '@/models/oss'; @@ -50,3 +51,10 @@ export function patchCreateInput(oss: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/oss/${oss}/set-input`, + request: request + }); +} diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index c21ed575..d4d51441 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -12,7 +12,13 @@ import { patchSetOwner, postSubscribe } from '@/backend/library'; -import { patchCreateInput, patchDeleteOperation, patchUpdatePositions, postCreateOperation } from '@/backend/oss'; +import { + patchCreateInput, + patchDeleteOperation, + patchSetInput, + patchUpdatePositions, + postCreateOperation +} from '@/backend/oss'; import { type ErrorData } from '@/components/info/InfoError'; import { AccessPolicy, ILibraryItem } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library'; @@ -21,6 +27,7 @@ import { IOperationCreateData, IOperationSchema, IOperationSchemaData, + IOperationSetInputData, IPositionsData, ITargetOperation } from '@/models/oss'; @@ -55,6 +62,7 @@ interface IOssContext { createOperation: (data: IOperationCreateData, callback?: DataCallback) => void; deleteOperation: (data: ITargetOperation, callback?: () => void) => void; createInput: (data: ITargetOperation, callback?: DataCallback) => void; + setInput: (data: IOperationSetInputData, callback?: () => void) => void; } const OssContext = createContext(null); @@ -333,6 +341,27 @@ export const OssState = ({ itemID, children }: OssStateProps) => { [itemID, library] ); + const setInput = useCallback( + (data: IOperationSetInputData, callback?: () => void) => { + if (!schema) { + return; + } + setProcessingError(undefined); + patchSetInput(itemID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: newData => { + library.setGlobalOSS(newData); + library.localUpdateTimestamp(newData.id); + if (callback) callback(); + } + }); + }, + [itemID, schema, library] + ); + return ( { savePositions, createOperation, deleteOperation, - createInput + createInput, + setInput }} > {children} diff --git a/rsconcept/frontend/src/context/RSFormContext.tsx b/rsconcept/frontend/src/context/RSFormContext.tsx index a9a76a24..c13f54a1 100644 --- a/rsconcept/frontend/src/context/RSFormContext.tsx +++ b/rsconcept/frontend/src/context/RSFormContext.tsx @@ -157,7 +157,11 @@ export const RSFormState = ({ itemID, versionID, children }: RSFormStateProps) = onSuccess: newData => { setSchema(Object.assign(schema, newData)); library.localUpdateItem(newData); - if (callback) callback(newData); + if (library.globalOSS?.schemas.includes(newData.id)) { + library.reloadOSS(() => { + if (callback) callback(newData); + }); + } else if (callback) callback(newData); } }); }, diff --git a/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx b/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx new file mode 100644 index 00000000..44069114 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgChangeInputSchema.tsx @@ -0,0 +1,79 @@ +'use client'; + +import clsx from 'clsx'; +import { useCallback, useMemo, useState } from 'react'; + +import { IconReset } from '@/components/Icons'; +import PickSchema from '@/components/select/PickSchema'; +import Checkbox from '@/components/ui/Checkbox'; +import Label from '@/components/ui/Label'; +import MiniButton from '@/components/ui/MiniButton'; +import Modal, { ModalProps } from '@/components/ui/Modal'; +import { ILibraryItem, LibraryItemID } from '@/models/library'; +import { IOperation, IOperationSchema } from '@/models/oss'; + +interface DlgChangeInputSchemaProps extends Pick { + oss: IOperationSchema; + target: IOperation; + onSubmit: (newSchema: LibraryItemID | undefined, syncText: boolean) => void; +} + +function DlgChangeInputSchema({ oss, hideWindow, target, onSubmit }: DlgChangeInputSchemaProps) { + const [selected, setSelected] = useState(target.result ?? undefined); + const [syncText, setSyncText] = useState(target.sync_text); + + const baseFilter = useCallback( + (item: ILibraryItem) => !oss.schemas.includes(item.id) || item.id === selected || item.id === target.result, + [oss, selected, target] + ); + + const isValid = useMemo(() => target.result !== selected, [target, selected]); + + const handleSelectLocation = useCallback((newValue: LibraryItemID) => { + setSelected(newValue); + }, []); + + function handleSubmit() { + onSubmit(selected, syncText); + } + + return ( + +
+
+
+
+ + +
+ ); +} + +export default DlgChangeInputSchema; diff --git a/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabInputOperation.tsx b/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabInputOperation.tsx index 702c72dd..c23fd2b2 100644 --- a/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabInputOperation.tsx +++ b/rsconcept/frontend/src/dialogs/DlgCreateOperation/TabInputOperation.tsx @@ -80,7 +80,7 @@ function TabInputOperation({ value={syncText} setValue={setSyncText} label='Синхронизировать текст' - title='Брать текст из концептуальной схемы' + titleHtml='Загрузить текстовые поля
из концептуальной схемы' /> diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index a00edaa2..5ec06a05 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -69,6 +69,14 @@ export interface IOperationCreateData extends IPositionsData { create_schema: boolean; } +/** + * Represents {@link IOperation} data, used in setInput process. + */ +export interface IOperationSetInputData extends ITargetOperation { + sync_text: boolean; + input: LibraryItemID | null; +} + /** * Represents {@link IOperation} Argument. */ diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx index 348e3813..832a14c4 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/NodeContextMenu.tsx @@ -23,9 +23,18 @@ interface NodeContextMenuProps extends ContextMenuData { onHide: () => void; onDelete: (target: OperationID) => void; onCreateInput: (target: OperationID) => void; + onEditSchema: (target: OperationID) => void; } -function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCreateInput }: NodeContextMenuProps) { +function NodeContextMenu({ + operation, + cursorX, + cursorY, + onHide, + onDelete, + onCreateInput, + onEditSchema +}: NodeContextMenuProps) { const controller = useOssEdit(); const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); @@ -44,8 +53,8 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCrea }; const handleEditSchema = () => { - toast.error('Not implemented'); handleHide(); + onEditSchema(operation.id); }; const handleEditOperation = () => { @@ -97,9 +106,9 @@ function NodeContextMenu({ operation, cursorX, cursorY, onHide, onDelete, onCrea onClick={handleCreateSchema} /> ) : null} - {controller.isMutable && !operation.result && operation.operation_type === OperationType.INPUT ? ( + {controller.isMutable && operation.operation_type === OperationType.INPUT ? ( } disabled={controller.isProcessing} diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx index 642ecf3c..9aec428a 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx @@ -28,9 +28,7 @@ function OperationNode(node: OssNodeInternal) { noHover title='Связанная КС' hideTitle={!controller.showTooltip} - onClick={() => { - handleOpenSchema(); - }} + onClick={handleOpenSchema} disabled={!hasFile} /> diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index d2571b5d..088d6a07 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -148,6 +148,13 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { [controller, getPositions] ); + const handleEditSchema = useCallback( + (target: OperationID) => { + controller.promptEditInput(target, getPositions()); + }, + [controller, getPositions] + ); + const handleFitView = useCallback(() => { flow.fitView({ duration: PARAMETER.zoomDuration }); }, [flow]); @@ -293,6 +300,7 @@ function OssFlow({ isModified, setIsModified }: OssFlowProps) { onHide={handleContextMenuHide} onDelete={handleDeleteOperation} onCreateInput={handleCreateInput} + onEditSchema={handleEditSchema} {...menuProps} /> ) : null} diff --git a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx index a36e8270..0cf52520 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssEditContext.tsx @@ -10,12 +10,19 @@ import { useAuth } from '@/context/AuthContext'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useConceptNavigation } from '@/context/NavigationContext'; import { useOSS } from '@/context/OssContext'; +import DlgChangeInputSchema from '@/dialogs/DlgChangeInputSchema'; import DlgChangeLocation from '@/dialogs/DlgChangeLocation'; import DlgCreateOperation from '@/dialogs/DlgCreateOperation'; import DlgEditEditors from '@/dialogs/DlgEditEditors'; -import { AccessPolicy } from '@/models/library'; +import { AccessPolicy, LibraryItemID } from '@/models/library'; import { Position2D } from '@/models/miscellaneous'; -import { IOperationCreateData, IOperationPosition, IOperationSchema, OperationID } from '@/models/oss'; +import { + IOperationCreateData, + IOperationPosition, + IOperationSchema, + IOperationSetInputData, + OperationID +} from '@/models/oss'; import { UserID, UserLevel } from '@/models/user'; import { information } from '@/utils/labels'; @@ -45,6 +52,7 @@ export interface IOssEditContext { promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void; deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; createInput: (target: OperationID, positions: IOperationPosition[]) => void; + promptEditInput: (target: OperationID, positions: IOperationPosition[]) => void; } const OssEditContext = createContext(null); @@ -79,10 +87,16 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr const [showEditEditors, setShowEditEditors] = useState(false); const [showEditLocation, setShowEditLocation] = useState(false); + const [showEditInput, setShowEditInput] = useState(false); const [showCreateOperation, setShowCreateOperation] = useState(false); const [insertPosition, setInsertPosition] = useState({ x: 0, y: 0 }); const [positions, setPositions] = useState([]); + const [targetOperationID, setTargetOperationID] = useState(undefined); + const targetOperation = useMemo( + () => (targetOperationID ? model.schema?.operationByID.get(targetOperationID) : undefined), + [model, targetOperationID] + ); useLayoutEffect( () => @@ -221,6 +235,28 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr [model, router] ); + const promptEditInput = useCallback((target: OperationID, positions: IOperationPosition[]) => { + setPositions(positions); + setTargetOperationID(target); + setShowEditInput(true); + }, []); + + const setTargetInput = useCallback( + (newInput: LibraryItemID | undefined, syncText: boolean) => { + if (!targetOperationID) { + return; + } + const data: IOperationSetInputData = { + target: targetOperationID, + positions: positions, + sync_text: syncText, + input: newInput ?? null + }; + model.setInput(data, () => toast.success(information.changesSaved)); + }, + [model, targetOperationID, positions] + ); + return ( {model.schema ? ( @@ -274,6 +311,14 @@ export const OssEditState = ({ selected, setSelected, children }: OssEditStatePr onCreate={handleCreateOperation} /> ) : null} + {showEditInput ? ( + setShowEditInput(false)} + oss={model.schema} + target={targetOperation!} + onSubmit={setTargetInput} + /> + ) : null} ) : null}