From bf7902258b670eaf0b263ca8b7ac1d14e43be016 Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 23 Jul 2024 23:04:21 +0300 Subject: [PATCH] Improve OSS frontend --- .../apps/rsform/serializers/io_files.py | 2 +- rsconcept/frontend/src/app/backendAPI.ts | 18 ++- rsconcept/frontend/src/context/OssContext.tsx | 45 +++++- rsconcept/frontend/src/models/oss.ts | 17 ++- .../OssPage/EditorOssGraph/EditorOssGraph.tsx | 9 +- .../OssPage/EditorOssGraph/InputNode.tsx | 26 ++-- .../OssPage/EditorOssGraph/OperationNode.tsx | 46 +++--- .../pages/OssPage/EditorOssGraph/OssFlow.tsx | 132 +++++++++++++----- .../EditorOssGraph/ToolbarOssGraph.tsx | 46 +++++- .../src/pages/OssPage/OssEditContext.tsx | 44 +++++- .../frontend/src/pages/OssPage/OssTabs.tsx | 8 +- .../EditorTermGraph/EditorTermGraph.tsx | 2 +- .../EditorTermGraph/ToolbarTermGraph.tsx | 6 +- rsconcept/frontend/src/styling/overrides.css | 49 ++++++- rsconcept/frontend/src/utils/constants.ts | 2 + rsconcept/frontend/src/utils/labels.ts | 1 + 16 files changed, 357 insertions(+), 96 deletions(-) diff --git a/rsconcept/backend/apps/rsform/serializers/io_files.py b/rsconcept/backend/apps/rsform/serializers/io_files.py index 45f05578..bfb68894 100644 --- a/rsconcept/backend/apps/rsform/serializers/io_files.py +++ b/rsconcept/backend/apps/rsform/serializers/io_files.py @@ -4,7 +4,7 @@ from rest_framework import serializers from shared import messages as msg -from ..models import Constituenta, LibraryItem, RSForm +from ..models import Constituenta, RSForm from ..utils import fix_old_references _CST_TYPE = 'constituenta' diff --git a/rsconcept/frontend/src/app/backendAPI.ts b/rsconcept/frontend/src/app/backendAPI.ts index c65ee65e..4dddb75e 100644 --- a/rsconcept/frontend/src/app/backendAPI.ts +++ b/rsconcept/frontend/src/app/backendAPI.ts @@ -13,7 +13,9 @@ import { ICstSubstituteData, IOperationCreateData, IOperationCreatedResponse, - IOperationSchemaData + IOperationSchemaData, + IPositionsData, + ITargetOperation } from '@/models/oss'; import { IConstituentaList, @@ -424,6 +426,13 @@ export function getOssDetails(target: string, request: FrontPull) { + AxiosPatch({ + endpoint: `/api/oss/${schema}/update-positions`, + request: request + }); +} + export function postCreateOperation( schema: string, request: FrontExchange @@ -434,6 +443,13 @@ export function postCreateOperation( }); } +export function patchDeleteOperation(schema: string, request: FrontExchange) { + AxiosPatch({ + endpoint: `/api/oss/${schema}/delete-operation`, + request: request + }); +} + export function postInflectText(request: FrontExchange) { AxiosPost({ endpoint: `/api/cctext/inflect`, diff --git a/rsconcept/frontend/src/context/OssContext.tsx b/rsconcept/frontend/src/context/OssContext.tsx index 07b6d83e..fc67fdce 100644 --- a/rsconcept/frontend/src/context/OssContext.tsx +++ b/rsconcept/frontend/src/context/OssContext.tsx @@ -5,11 +5,13 @@ import { createContext, useCallback, useContext, useMemo, useState } from 'react import { type DataCallback, deleteUnsubscribe, + patchDeleteOperation, patchEditorsSet as patchSetEditors, patchLibraryItem, patchSetAccessPolicy, patchSetLocation, patchSetOwner, + patchUpdatePositions, postCreateOperation, postSubscribe } from '@/app/backendAPI'; @@ -17,7 +19,7 @@ import { type ErrorData } from '@/components/info/InfoError'; import useOssDetails from '@/hooks/useOssDetails'; import { AccessPolicy, ILibraryItem } from '@/models/library'; import { ILibraryUpdateData } from '@/models/library'; -import { IOperation, IOperationCreateData, IOperationSchema } from '@/models/oss'; +import { IOperation, IOperationCreateData, IOperationSchema, IPositionsData, ITargetOperation } from '@/models/oss'; import { UserID } from '@/models/user'; import { contextOutsideScope } from '@/utils/labels'; @@ -45,7 +47,9 @@ interface IOssContext { setLocation: (newLocation: string, callback?: () => void) => void; setEditors: (newEditors: UserID[], callback?: () => void) => void; + savePositions: (data: IPositionsData, callback?: () => void) => void; createOperation: (data: IOperationCreateData, callback?: DataCallback) => void; + deleteOperation: (data: ITargetOperation, callback?: () => void) => void; } const OssContext = createContext(null); @@ -250,6 +254,23 @@ export const OssState = ({ itemID, children }: OssStateProps) => { [itemID, schema] ); + const savePositions = useCallback( + (data: IPositionsData, callback?: () => void) => { + setProcessingError(undefined); + patchUpdatePositions(itemID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: () => { + library.localUpdateTimestamp(Number(itemID)); + if (callback) callback(); + } + }); + }, + [itemID, library] + ); + const createOperation = useCallback( (data: IOperationCreateData, callback?: DataCallback) => { setProcessingError(undefined); @@ -268,6 +289,24 @@ export const OssState = ({ itemID, children }: OssStateProps) => { [itemID, library, setSchema] ); + const deleteOperation = useCallback( + (data: ITargetOperation, callback?: () => void) => { + setProcessingError(undefined); + patchDeleteOperation(itemID, { + data: data, + showError: true, + setLoading: setProcessing, + onError: setProcessingError, + onSuccess: newData => { + setSchema(newData); + library.localUpdateTimestamp(newData.id); + if (callback) callback(); + } + }); + }, + [itemID, library, setSchema] + ); + return ( { setAccessPolicy, setLocation, - createOperation + savePositions, + createOperation, + deleteOperation }} > {children} diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index 968ec063..8b3d655a 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -41,16 +41,29 @@ export interface IOperation { */ export interface IOperationPosition extends Pick {} +/** + * Represents all {@link IOperation} positions in {@link IOperationSchema}. + */ +export interface IPositionsData { + positions: IOperationPosition[]; +} + +/** + * Represents target {@link IOperation}. + */ +export interface ITargetOperation extends IPositionsData { + target: OperationID; +} + /** * Represents {@link IOperation} data, used in creation process. */ -export interface IOperationCreateData { +export interface IOperationCreateData extends IPositionsData { item_data: Pick< IOperation, 'alias' | 'operation_type' | 'title' | 'comment' | 'position_x' | 'position_y' | 'result' >; arguments: OperationID[] | undefined; - positions: IOperationPosition[]; } /** diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx index 5d38ce39..16f595be 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx @@ -4,10 +4,15 @@ import { ReactFlowProvider } from 'reactflow'; import OssFlow from './OssFlow'; -function EditorOssGraph() { +interface EditorOssGraphProps { + isModified: boolean; + setIsModified: React.Dispatch>; +} + +function EditorOssGraph({ isModified, setIsModified }: EditorOssGraphProps) { return ( - + ); } diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx index deb6f1a9..4317aa05 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/InputNode.tsx @@ -1,7 +1,6 @@ -import { CiSquareRemove } from 'react-icons/ci'; -import { PiPlugsConnected } from 'react-icons/pi'; import { Handle, Position } from 'reactflow'; +import { IconDestroy, IconEdit2 } from '@/components/Icons'; import MiniButton from '@/components/ui/MiniButton.tsx'; import { useOssEdit } from '../OssEditContext'; @@ -21,27 +20,30 @@ function InputNode({ id, data }: InputNodeProps) { console.log('delete node ' + id); }; - const handleClick = () => { - // controller.selectNode(id); - // controller.showSelectInput(); + const handleEditOperation = () => { + console.log('edit operation ' + id); + //controller.selectNode(id); + //controller.showSynthesis(); }; return ( <> - -
+ +
{data.label}
} - title='Привязать схему' + icon={} + noPadding + title='Редактировать' onClick={() => { - handleClick(); + handleEditOperation(); }} /> } - title='Удалить' + noPadding + icon={} + title='Удалить операцию' onClick={handleDelete} />
diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx index 34ff84fb..26e7dd73 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OperationNode.tsx @@ -1,16 +1,18 @@ -import { CiSquareRemove } from 'react-icons/ci'; -import { IoGitNetworkSharp } from 'react-icons/io5'; import { VscDebugStart } from 'react-icons/vsc'; import { Handle, Position } from 'reactflow'; +import { IconDestroy, IconEdit2 } from '@/components/Icons'; import MiniButton from '@/components/ui/MiniButton.tsx'; import { useOssEdit } from '../OssEditContext'; interface OperationNodeProps { id: string; + data: { + label: string; + }; } -function OperationNode({ id }: OperationNodeProps) { +function OperationNode({ id, data }: OperationNodeProps) { const controller = useOssEdit(); console.log(controller.isMutable); @@ -32,37 +34,33 @@ function OperationNode({ id }: OperationNodeProps) { return ( <> - -
- } - title='Удалить' - onClick={handleDelete} - color={'red'} - /> -
- Тип: Отождествление -
-
- Схема: + +
+
{data.label}
+
+ } + title='Редактировать' + onClick={() => { + handleEditOperation(); + }} + /> } + icon={} title='Синтез' onClick={() => handleRunOperation()} /> } - title='Отождествления' - onClick={() => handleEditOperation()} + icon={} + title='Удалить операцию' + onClick={handleDelete} />
- - + + ); } diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx index e2b96c7e..a57cc133 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/OssFlow.tsx @@ -1,58 +1,86 @@ 'use client'; -import { useCallback, useLayoutEffect, useMemo } from 'react'; +import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { - EdgeChange, + Node, NodeChange, NodeTypes, ProOptions, ReactFlow, useEdgesState, useNodesState, - useViewport + useOnSelectionChange, + useReactFlow } from 'reactflow'; import Overlay from '@/components/ui/Overlay'; import AnimateFade from '@/components/wrap/AnimateFade'; import { useConceptOptions } from '@/context/ConceptOptionsContext'; import { useOSS } from '@/context/OssContext'; +import { PARAMETER } from '@/utils/constants'; import { useOssEdit } from '../OssEditContext'; import InputNode from './InputNode'; import OperationNode from './OperationNode'; import ToolbarOssGraph from './ToolbarOssGraph'; -function OssFlow() { +interface OssFlowProps { + isModified: boolean; + setIsModified: React.Dispatch>; +} + +function OssFlow({ isModified, setIsModified }: OssFlowProps) { const { calculateHeight } = useConceptOptions(); const model = useOSS(); const controller = useOssEdit(); - const viewport = useViewport(); + const flow = useReactFlow(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const [toggleReset, setToggleReset] = useState(false); + + const onSelectionChange = useCallback( + ({ nodes }: { nodes: Node[] }) => { + controller.setSelected(nodes.map(node => Number(node.id))); + console.log(nodes); + }, + [controller] + ); + + useOnSelectionChange({ + onChange: onSelectionChange + }); useLayoutEffect(() => { if (!model.schema) { setNodes([]); setEdges([]); - return; + } else { + setNodes( + model.schema.items.map(operation => ({ + id: String(operation.id), + data: { label: operation.alias }, + position: { x: operation.position_x, y: operation.position_y }, + type: operation.operation_type.toString() + })) + ); + setEdges( + model.schema.arguments.map((argument, index) => ({ + id: String(index), + source: String(argument.argument), + target: String(argument.operation), + targetHandle: + model.schema!.operationByID.get(argument.argument)!.position_x > + model.schema!.operationByID.get(argument.operation)!.position_x + ? 'right' + : 'left' + })) + ); } - setNodes( - model.schema.items.map(operation => ({ - id: String(operation.id), - data: { label: operation.alias }, - position: { x: operation.position_x, y: operation.position_y }, - type: operation.operation_type.toString() - })) - ); - setEdges( - model.schema.arguments.map((argument, index) => ({ - id: String(index), - source: String(argument.argument), - target: String(argument.operation) - })) - ); - }, [model.schema, setNodes, setEdges]); + setTimeout(() => { + setIsModified(false); + }, PARAMETER.graphRefreshDelay); + }, [model.schema, setNodes, setEdges, setIsModified, toggleReset]); const getPositions = useCallback( () => @@ -66,22 +94,46 @@ function OssFlow() { const handleNodesChange = useCallback( (changes: NodeChange[]) => { + if (changes.some(change => change.type === 'position' && change.position)) { + setIsModified(true); + } onNodesChange(changes); }, - [onNodesChange] + [onNodesChange, setIsModified] ); - const handleEdgesChange = useCallback( - (changes: EdgeChange[]) => { - onEdgesChange(changes); - }, - [onEdgesChange] - ); + const handleSavePositions = useCallback(() => { + controller.savePositions(getPositions(), () => setIsModified(false)); + }, [controller, getPositions, setIsModified]); const handleCreateOperation = useCallback(() => { - // TODO: calculate insert location - controller.promptCreateOperation(viewport.x, viewport.y, getPositions()); - }, [controller, viewport, getPositions]); + const center = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); + console.log(center); + controller.promptCreateOperation(center.x, center.y, getPositions()); + }, [controller, getPositions, flow]); + + const handleDeleteOperation = useCallback(() => { + if (controller.selected.length !== 1) { + return; + } + controller.deleteOperation(controller.selected[0], getPositions()); + }, [controller, getPositions]); + + function handleKeyDown(event: React.KeyboardEvent) { + // Hotkeys implementation + if (controller.isProcessing) { + return; + } + if (!controller.isMutable) { + return; + } + if (event.key === 'Delete') { + event.preventDefault(); + event.stopPropagation(); + handleDeleteOperation(); + return; + } + } const proOptions: ProOptions = useMemo(() => ({ hideAttribution: true }), []); const canvasWidth = useMemo(() => 'calc(100vw - 1rem)', []); @@ -101,21 +153,29 @@ function OssFlow() { nodes={nodes} edges={edges} onNodesChange={handleNodesChange} - onEdgesChange={handleEdgesChange} + onEdgesChange={onEdgesChange} fitView proOptions={proOptions} nodeTypes={OssNodeTypes} maxZoom={2} minZoom={0.75} + nodesConnectable={false} /> ), - [nodes, edges, proOptions, handleNodesChange, handleEdgesChange, OssNodeTypes] + [nodes, edges, proOptions, handleNodesChange, onEdgesChange, OssNodeTypes] ); return ( - + - + flow.fitView({ duration: PARAMETER.zoomDuration })} + onCreate={handleCreateOperation} + onDelete={handleDeleteOperation} + onResetPositions={() => setToggleReset(prev => !prev)} + onSavePositions={handleSavePositions} + />
{graph} diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx index 571f4625..30aa14c0 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/ToolbarOssGraph.tsx @@ -1,22 +1,56 @@ import clsx from 'clsx'; -import { IconNewItem } from '@/components/Icons'; +import { IconDestroy, IconFitImage, IconNewItem, IconReset, IconSave } from '@/components/Icons'; import BadgeHelp from '@/components/info/BadgeHelp'; import MiniButton from '@/components/ui/MiniButton'; import { HelpTopic } from '@/models/miscellaneous'; import { PARAMETER } from '@/utils/constants'; +import { prepareTooltip } from '@/utils/labels'; import { useOssEdit } from '../OssEditContext'; interface ToolbarOssGraphProps { + isModified: boolean; onCreate: () => void; + onDelete: () => void; + onFitView: () => void; + onSavePositions: () => void; + onResetPositions: () => void; } -function ToolbarOssGraph({ onCreate }: ToolbarOssGraphProps) { +function ToolbarOssGraph({ + isModified, + onCreate, + onDelete, + onFitView, + onSavePositions, + onResetPositions +}: ToolbarOssGraphProps) { const controller = useOssEdit(); return (
+ {controller.isMutable ? ( + } + disabled={controller.isProcessing || !isModified} + onClick={onSavePositions} + /> + ) : null} + {controller.isMutable ? ( + } + disabled={!isModified} + onClick={onResetPositions} + /> + ) : null} + } + title='Сбросить вид' + onClick={onFitView} + /> {controller.isMutable ? ( ) : null} + {controller.isMutable ? ( + } + disabled={controller.selected.length !== 1 || controller.isProcessing} + onClick={onDelete} + /> + ) : null} void; toggleSubscribe: () => void; + setSelected: React.Dispatch>; + share: () => void; + savePositions: (positions: IOperationPosition[], callback?: () => void) => void; promptCreateOperation: (x: number, y: number, positions: IOperationPosition[]) => void; + deleteOperation: (target: OperationID, positions: IOperationPosition[]) => void; } const OssEditContext = createContext(null); @@ -45,10 +50,12 @@ export const useOssEdit = () => { interface OssEditStateProps { // isModified: boolean; + selected: OperationID[]; + setSelected: React.Dispatch>; children: React.ReactNode; } -export const OssEditState = ({ children }: OssEditStateProps) => { +export const OssEditState = ({ selected, setSelected, children }: OssEditStateProps) => { // const router = useConceptNavigation(); const { user } = useAuth(); const { adminMode } = useConceptOptions(); @@ -144,6 +151,23 @@ export const OssEditState = ({ children }: OssEditStateProps) => { [model] ); + const savePositions = useCallback( + (positions: IOperationPosition[], callback?: () => void) => { + model.savePositions({ positions: positions }, () => { + positions.forEach(item => { + const operation = model.schema?.operationByID.get(item.id); + if (operation) { + operation.position_x = item.position_x; + operation.position_y = item.position_y; + } + }); + toast.success(information.changesSaved); + if (callback) callback(); + }); + }, + [model] + ); + const promptCreateOperation = useCallback((x: number, y: number, positions: IOperationPosition[]) => { setInsertPosition({ x: x, y: y }); setPositions(positions); @@ -157,10 +181,21 @@ export const OssEditState = ({ children }: OssEditStateProps) => { [model] ); + const deleteOperation = useCallback( + (target: OperationID, positions: IOperationPosition[]) => { + model.deleteOperation({ target: target, positions: positions }, () => + toast.success(information.operationDestroyed) + ); + }, + [model] + ); + return ( { promptLocation, share, + setSelected, - promptCreateOperation + savePositions, + promptCreateOperation, + deleteOperation }} > {model.schema ? ( diff --git a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx index fe920fb5..e3ecb535 100644 --- a/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx +++ b/rsconcept/frontend/src/pages/OssPage/OssTabs.tsx @@ -17,6 +17,7 @@ import { useLibrary } from '@/context/LibraryContext'; import { useBlockNavigation, useConceptNavigation } from '@/context/NavigationContext'; import { useOSS } from '@/context/OssContext'; import useQueryStrings from '@/hooks/useQueryStrings'; +import { OperationID } from '@/models/oss'; import { information, prompts } from '@/utils/labels'; import EditorRSForm from './EditorOssCard'; @@ -39,6 +40,7 @@ function OssTabs() { const { destroyItem } = useLibrary(); const [isModified, setIsModified] = useState(false); + const [selected, setSelected] = useState([]); useBlockNavigation(isModified); useLayoutEffect(() => { @@ -112,14 +114,14 @@ function OssTabs() { const graphPanel = useMemo( () => ( - + ), - [] + [isModified] ); return ( - + {loading ? : null} {errorLoading ? : null} {schema && !loading ? ( diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx index c6cfbf8b..1cc7a8f4 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/EditorTermGraph.tsx @@ -311,7 +311,7 @@ function EditorTermGraph({ onOpenEdit }: EditorTermGraphProps) { showParamsDialog={() => setShowParamsDialog(true)} onCreate={handleCreateCst} onDelete={handleDeleteCst} - onResetViewpoint={() => setToggleResetView(prev => !prev)} + onFitView={() => setToggleResetView(prev => !prev)} onSaveImage={handleSaveImage} toggleOrbit={() => setOrbit(prev => !prev)} toggleFoldDerived={handleFoldDerived} diff --git a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ToolbarTermGraph.tsx b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ToolbarTermGraph.tsx index b923d4d2..120ea3a5 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ToolbarTermGraph.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/EditorTermGraph/ToolbarTermGraph.tsx @@ -29,7 +29,7 @@ interface ToolbarTermGraphProps { showParamsDialog: () => void; onCreate: () => void; onDelete: () => void; - onResetViewpoint: () => void; + onFitView: () => void; onSaveImage: () => void; toggleFoldDerived: () => void; @@ -48,7 +48,7 @@ function ToolbarTermGraph({ showParamsDialog, onCreate, onDelete, - onResetViewpoint, + onFitView, onSaveImage }: ToolbarTermGraphProps) { const controller = useRSEdit(); @@ -63,7 +63,7 @@ function ToolbarTermGraph({ } title='Граф целиком' - onClick={onResetViewpoint} + onClick={onFitView} /> `Конституенты удалены: ${aliases}` };