From 0b8d66a17270f045378f95aecf5ecf8f93053cac Mon Sep 17 00:00:00 2001 From: Ivan <8611739+IRBorisov@users.noreply.github.com> Date: Tue, 22 Apr 2025 23:42:13 +0300 Subject: [PATCH] F: Improve node UI context menu --- TODO.txt | 1 + .../features/oss/components/info-block.tsx | 26 ++ .../oss/components/operation-tooltip.tsx | 21 -- .../oss/components/tooltip-oss-item.tsx | 27 ++ .../src/features/oss/models/oss-api.ts | 32 ++- .../src/features/oss/models/oss-layout.ts | 3 +- .../context-menu/context-menu.tsx | 62 +++++ .../editor-oss-graph/context-menu/index.tsx | 1 + .../context-menu/menu-block.tsx | 61 +++++ .../context-menu/menu-operation.tsx | 224 +++++++++++++++ .../editor-oss-graph/graph/block-node.tsx | 9 +- .../editor-oss-graph/graph/node-core.tsx | 2 +- .../editor-oss-graph/node-context-menu.tsx | 257 ------------------ .../oss-page/editor-oss-graph/oss-flow.tsx | 21 +- .../features/oss/pages/oss-page/oss-page.tsx | 2 +- .../features/oss/stores/operation-tooltip.ts | 10 +- 16 files changed, 447 insertions(+), 312 deletions(-) create mode 100644 rsconcept/frontend/src/features/oss/components/info-block.tsx delete mode 100644 rsconcept/frontend/src/features/oss/components/operation-tooltip.tsx create mode 100644 rsconcept/frontend/src/features/oss/components/tooltip-oss-item.tsx create mode 100644 rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx create mode 100644 rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/index.tsx create mode 100644 rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx create mode 100644 rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx delete mode 100644 rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx diff --git a/TODO.txt b/TODO.txt index 1f9e13a6..1d558539 100644 --- a/TODO.txt +++ b/TODO.txt @@ -15,6 +15,7 @@ User profile: - Profile pictures - Custom LibraryItem lists - Custom user filters and sharing filters +- Personal prompt templates - Static analyzer for RSForm as a whole: check term duplication and empty conventions - OSS clone and versioning diff --git a/rsconcept/frontend/src/features/oss/components/info-block.tsx b/rsconcept/frontend/src/features/oss/components/info-block.tsx new file mode 100644 index 00000000..3dfddc4d --- /dev/null +++ b/rsconcept/frontend/src/features/oss/components/info-block.tsx @@ -0,0 +1,26 @@ +'use client'; + +import { type IBlock } from '../models/oss'; + +interface InfoOperationProps { + block: IBlock; +} + +export function InfoBlock({ block }: InfoOperationProps) { + return ( + <> + {block.title ? ( +

+ Название: + {block.title} +

+ ) : null} + {block.description ? ( +

+ Описание: + {block.description} +

+ ) : null} + + ); +} diff --git a/rsconcept/frontend/src/features/oss/components/operation-tooltip.tsx b/rsconcept/frontend/src/features/oss/components/operation-tooltip.tsx deleted file mode 100644 index fa19caec..00000000 --- a/rsconcept/frontend/src/features/oss/components/operation-tooltip.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Tooltip } from '@/components/container'; -import { globalIDs } from '@/utils/constants'; - -import { useOperationTooltipStore } from '../stores/operation-tooltip'; - -import { InfoOperation } from './info-operation'; - -export function OperationTooltip() { - const hoverOperation = useOperationTooltipStore(state => state.activeOperation); - return ( - - ); -} diff --git a/rsconcept/frontend/src/features/oss/components/tooltip-oss-item.tsx b/rsconcept/frontend/src/features/oss/components/tooltip-oss-item.tsx new file mode 100644 index 00000000..bed24b3a --- /dev/null +++ b/rsconcept/frontend/src/features/oss/components/tooltip-oss-item.tsx @@ -0,0 +1,27 @@ +import { Tooltip } from '@/components/container'; +import { globalIDs } from '@/utils/constants'; + +import { type IBlock, type IOperation } from '../models/oss'; +import { isOperation } from '../models/oss-api'; +import { useOperationTooltipStore } from '../stores/operation-tooltip'; + +import { InfoBlock } from './info-block'; +import { InfoOperation } from './info-operation'; + +export function OperationTooltip() { + const hoverItem = useOperationTooltipStore(state => state.hoverItem); + const isOperationNode = isOperation(hoverItem); + + return ( + + ); +} diff --git a/rsconcept/frontend/src/features/oss/models/oss-api.ts b/rsconcept/frontend/src/features/oss/models/oss-api.ts index af8364e0..5387c67a 100644 --- a/rsconcept/frontend/src/features/oss/models/oss-api.ts +++ b/rsconcept/frontend/src/features/oss/models/oss-api.ts @@ -37,8 +37,8 @@ const DISTANCE_Y = 100; // pixels - insert y-distance between node centers const STARTING_SUB_INDEX = 900; // max semantic index for starting substitution /** Checks if element is {@link IOperation} or {@link IBlock}. */ -export function isOperation(item: IOssItem): boolean { - return 'arguments' in item; +export function isOperation(item: IOssItem | null): boolean { + return !!item && 'arguments' in item; } /** Sorts library items relevant for the specified {@link IOperationSchema}. */ @@ -528,7 +528,7 @@ export function calculateNewOperationPosition( /** Calculate insert position for a new {@link IBlock} */ export function calculateNewBlockPosition(data: ICreateBlockDTO, layout: IOssLayout): Rectangle2D { const block_nodes = data.children_blocks - .map(id => layout.blocks.find(block => block.id === -id)) + .map(id => layout.blocks.find(block => block.id === id)) .filter(node => !!node); const operation_nodes = data.children_operations .map(id => layout.operations.find(operation => operation.id === id)) @@ -544,25 +544,27 @@ export function calculateNewBlockPosition(data: ICreateBlockDTO, layout: IOssLay let bottom = undefined; for (const block of block_nodes) { - left = !left ? block.x - GRID_SIZE : Math.min(left, block.x - GRID_SIZE); - top = !top ? block.y - GRID_SIZE : Math.min(top, block.y - GRID_SIZE); + left = !left ? block.x - MIN_DISTANCE : Math.min(left, block.x - MIN_DISTANCE); + top = !top ? block.y - MIN_DISTANCE : Math.min(top, block.y - MIN_DISTANCE); right = !right - ? Math.max(left + data.width, block.x + block.width + GRID_SIZE) - : Math.max(right, block.x + block.width + GRID_SIZE); + ? Math.max(left + data.width, block.x + block.width + MIN_DISTANCE) + : Math.max(right, block.x + block.width + MIN_DISTANCE); bottom = !bottom - ? Math.max(top + data.height, block.y + block.height + GRID_SIZE) - : Math.max(bottom, block.y + block.height + GRID_SIZE); + ? Math.max(top + data.height, block.y + block.height + MIN_DISTANCE) + : Math.max(bottom, block.y + block.height + MIN_DISTANCE); } + console.log('left, top, right, bottom', left, top, right, bottom); + for (const operation of operation_nodes) { - left = !left ? operation.x - GRID_SIZE : Math.min(left, operation.x - GRID_SIZE); - top = !top ? operation.y - GRID_SIZE : Math.min(top, operation.y - GRID_SIZE); + left = !left ? operation.x - MIN_DISTANCE : Math.min(left, operation.x - MIN_DISTANCE); + top = !top ? operation.y - MIN_DISTANCE : Math.min(top, operation.y - MIN_DISTANCE); right = !right - ? Math.max(left + data.width, operation.x + OPERATION_NODE_WIDTH + GRID_SIZE) - : Math.max(right, operation.x + OPERATION_NODE_WIDTH + GRID_SIZE); + ? Math.max(left + data.width, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE) + : Math.max(right, operation.x + OPERATION_NODE_WIDTH + MIN_DISTANCE); bottom = !bottom - ? Math.max(top + data.height, operation.y + OPERATION_NODE_HEIGHT + GRID_SIZE) - : Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + GRID_SIZE); + ? Math.max(top + data.height, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE) + : Math.max(bottom, operation.y + OPERATION_NODE_HEIGHT + MIN_DISTANCE); } return { diff --git a/rsconcept/frontend/src/features/oss/models/oss-layout.ts b/rsconcept/frontend/src/features/oss/models/oss-layout.ts index 9844e6eb..be1e1d95 100644 --- a/rsconcept/frontend/src/features/oss/models/oss-layout.ts +++ b/rsconcept/frontend/src/features/oss/models/oss-layout.ts @@ -22,7 +22,8 @@ export interface OssNode extends Node { id: string; data: { label: string; - operation: IOperation; + operation?: IOperation; + block?: IBlock; }; position: { x: number; y: number }; } diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx new file mode 100644 index 00000000..f03c769f --- /dev/null +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/context-menu.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { useRef } from 'react'; + +import { isOperation } from '@/features/oss/models/oss-api'; + +import { Dropdown } from '@/components/dropdown'; + +import { type IBlock, type IOperation, type IOssItem } from '../../../../models/oss'; + +import { MenuBlock } from './menu-block'; +import { MenuOperation } from './menu-operation'; + +// pixels - size of OSS context menu +const MENU_WIDTH = 200; +const MENU_HEIGHT = 200; + +export interface ContextMenuData { + item: IOssItem | null; + cursorX: number; + cursorY: number; +} + +interface ContextMenuProps extends ContextMenuData { + isOpen: boolean; + onHide: () => void; +} + +export function ContextMenu({ isOpen, item, cursorX, cursorY, onHide }: ContextMenuProps) { + const ref = useRef(null); + const isOperationNode = isOperation(item); + + function handleBlur(event: React.FocusEvent) { + if (!ref.current?.contains(event.relatedTarget as Node)) { + onHide(); + } + } + + return ( +
+ = window.innerWidth - MENU_WIDTH} + stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT} + margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'} + > + {!!item ? ( + isOperationNode ? ( + + ) : ( + + ) + ) : null} + +
+ ); +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/index.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/index.tsx new file mode 100644 index 00000000..f628d09c --- /dev/null +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/index.tsx @@ -0,0 +1 @@ +export { ContextMenu } from './context-menu'; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx new file mode 100644 index 00000000..9185a55c --- /dev/null +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-block.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useDeleteBlock } from '@/features/oss/backend/use-delete-block'; + +import { DropdownButton } from '@/components/dropdown'; +import { IconDestroy, IconEdit2 } from '@/components/icons'; +import { useDialogsStore } from '@/stores/dialogs'; + +import { useMutatingOss } from '../../../../backend/use-mutating-oss'; +import { type IBlock } from '../../../../models/oss'; +import { useOssEdit } from '../../oss-edit-context'; +import { useGetLayout } from '../use-get-layout'; + +interface MenuBlockProps { + block: IBlock; + onHide: () => void; +} + +export function MenuBlock({ block, onHide }: MenuBlockProps) { + const { schema, isMutable } = useOssEdit(); + const isProcessing = useMutatingOss(); + const getLayout = useGetLayout(); + + const showEditBlock = useDialogsStore(state => state.showEditBlock); + const { deleteBlock } = useDeleteBlock(); + + function handleEditOperation() { + if (!block) { + return; + } + onHide(); + showEditBlock({ + oss: schema, + target: block, + layout: getLayout() + }); + } + + function handleDeleteBlock() { + onHide(); + void deleteBlock({ itemID: schema.id, data: { target: block.id, layout: getLayout() } }); + } + + return ( + <> + } + onClick={handleEditOperation} + disabled={!isMutable || isProcessing} + /> + } + onClick={handleDeleteBlock} + disabled={!isMutable || isProcessing} + /> + + ); +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx new file mode 100644 index 00000000..706d186d --- /dev/null +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/context-menu/menu-operation.tsx @@ -0,0 +1,224 @@ +'use client'; +import { toast } from 'react-toastify'; + +import { urls, useConceptNavigation } from '@/app'; +import { useLibrary } from '@/features/library/backend/use-library'; +import { useCreateInput } from '@/features/oss/backend/use-create-input'; +import { useExecuteOperation } from '@/features/oss/backend/use-execute-operation'; + +import { DropdownButton } from '@/components/dropdown'; +import { + IconChild, + IconConnect, + IconDestroy, + IconEdit2, + IconExecute, + IconNewRSForm, + IconRSForm +} from '@/components/icons'; +import { useDialogsStore } from '@/stores/dialogs'; +import { errorMsg } from '@/utils/labels'; +import { prepareTooltip } from '@/utils/utils'; + +import { OperationType } from '../../../../backend/types'; +import { useMutatingOss } from '../../../../backend/use-mutating-oss'; +import { type IOperation } from '../../../../models/oss'; +import { useOssEdit } from '../../oss-edit-context'; +import { useGetLayout } from '../use-get-layout'; + +interface MenuOperationProps { + operation: IOperation; + onHide: () => void; +} + +export function MenuOperation({ operation, onHide }: MenuOperationProps) { + const router = useConceptNavigation(); + const { items: libraryItems } = useLibrary(); + const { schema, navigateOperationSchema, isMutable, canDeleteOperation } = useOssEdit(); + const isProcessing = useMutatingOss(); + const getLayout = useGetLayout(); + + const { createInput: inputCreate } = useCreateInput(); + const { executeOperation: operationExecute } = useExecuteOperation(); + + const showEditInput = useDialogsStore(state => state.showChangeInputSchema); + const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents); + const showEditOperation = useDialogsStore(state => state.showEditOperation); + const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); + + const readyForSynthesis = (() => { + if (operation?.operation_type !== OperationType.SYNTHESIS) { + return false; + } + if (operation.result) { + return false; + } + + const argumentIDs = schema.graph.expandInputs([operation.id]); + if (!argumentIDs || argumentIDs.length < 1) { + return false; + } + + const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!); + if (argumentOperations.some(item => item.result === null)) { + return false; + } + + return true; + })(); + + function handleOpenSchema() { + if (!operation) { + return; + } + onHide(); + navigateOperationSchema(operation.id); + } + + function handleEditSchema() { + if (!operation) { + return; + } + onHide(); + showEditInput({ + oss: schema, + target: operation, + layout: getLayout() + }); + } + + function handleEditOperation() { + if (!operation) { + return; + } + onHide(); + showEditOperation({ + oss: schema, + target: operation, + layout: getLayout() + }); + } + + function handleDeleteOperation() { + if (!operation || !canDeleteOperation(operation)) { + return; + } + onHide(); + showDeleteOperation({ + oss: schema, + target: operation, + layout: getLayout() + }); + } + + function handleOperationExecute() { + if (!operation) { + return; + } + onHide(); + void operationExecute({ + itemID: schema.id, // + data: { target: operation.id, layout: getLayout() } + }); + } + + function handleInputCreate() { + if (!operation) { + return; + } + if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) { + toast.error(errorMsg.inputAlreadyExists); + return; + } + onHide(); + void inputCreate({ + itemID: schema.id, + data: { target: operation.id, layout: getLayout() } + }).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true })); + } + + function handleRelocateConstituents() { + if (!operation) { + return; + } + onHide(); + showRelocateConstituents({ + oss: schema, + initialTarget: operation, + layout: getLayout() + }); + } + + return ( + <> + } + onClick={handleEditOperation} + disabled={!isMutable || isProcessing} + /> + + {operation?.result ? ( + } + onClick={handleOpenSchema} + disabled={isProcessing} + /> + ) : null} + {isMutable && !operation?.result && operation?.arguments.length === 0 ? ( + } + onClick={handleInputCreate} + disabled={isProcessing} + /> + ) : null} + {isMutable && operation?.operation_type === OperationType.INPUT ? ( + } + onClick={handleEditSchema} + disabled={isProcessing} + /> + ) : null} + {isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? ( + и получить синтезированную КС' + : 'Необходимо предоставить все аргументы' + } + aria-label='Активировать операцию и получить синтезированную КС' + icon={} + onClick={handleOperationExecute} + disabled={isProcessing || !readyForSynthesis} + /> + ) : null} + + {isMutable && operation?.result ? ( + } + onClick={handleRelocateConstituents} + disabled={isProcessing} + /> + ) : null} + + } + onClick={handleDeleteOperation} + disabled={!isMutable || isProcessing || !operation || !canDeleteOperation(operation)} + /> + + ); +} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx index 5ed22755..3ddf347a 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx @@ -3,9 +3,11 @@ import { NodeResizeControl } from 'reactflow'; import clsx from 'clsx'; +import { useOperationTooltipStore } from '@/features/oss/stores/operation-tooltip'; import { useOSSGraphStore } from '@/features/oss/stores/oss-graph'; import { IconResize } from '@/components/icons'; +import { globalIDs } from '@/utils/constants'; import { type BlockInternalNode } from '../../../../models/oss-layout'; import { useOssEdit } from '../../oss-edit-context'; @@ -14,8 +16,10 @@ export const BLOCK_NODE_MIN_WIDTH = 160; export const BLOCK_NODE_MIN_HEIGHT = 100; export function BlockNode(node: BlockInternalNode) { - const showCoordinates = useOSSGraphStore(state => state.showCoordinates); const { selected, schema } = useOssEdit(); + const showCoordinates = useOSSGraphStore(state => state.showCoordinates); + const setHover = useOperationTooltipStore(state => state.setHoverItem); + const focus = selected.length === 1 ? selected[0] : null; const isParent = (!!focus && schema.hierarchy.at(focus)?.inputs.includes(-node.data.block.id)) ?? false; const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(-node.data.block.id)) ?? false; @@ -50,6 +54,9 @@ export function BlockNode(node: BlockInternalNode) { 'text-[18px]/[20px] line-clamp-2 text-center text-ellipsis', 'pointer-events-auto' )} + data-tooltip-id={globalIDs.operation_tooltip} + data-tooltip-hidden={node.dragging} + onMouseEnter={() => setHover(node.data.block)} > {node.data.label} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx index 47ff412e..699e2afb 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/node-core.tsx @@ -29,7 +29,7 @@ export function NodeCore({ node }: NodeCoreProps) { const focus = selected.length === 1 ? selected[0] : null; const isChild = (!!focus && schema.hierarchy.at(focus)?.outputs.includes(node.data.operation.id)) ?? false; - const setHover = useOperationTooltipStore(state => state.setActiveOperation); + const setHover = useOperationTooltipStore(state => state.setHoverItem); const showCoordinates = useOSSGraphStore(state => state.showCoordinates); const hasFile = !!node.data.operation.result; diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx deleted file mode 100644 index 76b15c87..00000000 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/node-context-menu.tsx +++ /dev/null @@ -1,257 +0,0 @@ -'use client'; - -import { useRef } from 'react'; -import { toast } from 'react-toastify'; - -import { urls, useConceptNavigation } from '@/app'; -import { useLibrary } from '@/features/library/backend/use-library'; -import { useCreateInput } from '@/features/oss/backend/use-create-input'; -import { useExecuteOperation } from '@/features/oss/backend/use-execute-operation'; - -import { Dropdown, DropdownButton } from '@/components/dropdown'; -import { - IconChild, - IconConnect, - IconDestroy, - IconEdit2, - IconExecute, - IconNewRSForm, - IconRSForm -} from '@/components/icons'; -import { useDialogsStore } from '@/stores/dialogs'; -import { errorMsg } from '@/utils/labels'; -import { prepareTooltip } from '@/utils/utils'; - -import { OperationType } from '../../../backend/types'; -import { useMutatingOss } from '../../../backend/use-mutating-oss'; -import { type IOperation } from '../../../models/oss'; -import { useOssEdit } from '../oss-edit-context'; - -import { useGetLayout } from './use-get-layout'; - -// pixels - size of OSS context menu -const MENU_WIDTH = 200; -const MENU_HEIGHT = 200; - -export interface ContextMenuData { - operation: IOperation | null; - cursorX: number; - cursorY: number; -} - -interface NodeContextMenuProps extends ContextMenuData { - isOpen: boolean; - onHide: () => void; -} - -export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: NodeContextMenuProps) { - const router = useConceptNavigation(); - const { items: libraryItems } = useLibrary(); - const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit(); - const isProcessing = useMutatingOss(); - const getLayout = useGetLayout(); - - const { createInput: inputCreate } = useCreateInput(); - const { executeOperation: operationExecute } = useExecuteOperation(); - - const showEditInput = useDialogsStore(state => state.showChangeInputSchema); - const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents); - const showEditOperation = useDialogsStore(state => state.showEditOperation); - const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation); - - const readyForSynthesis = (() => { - if (operation?.operation_type !== OperationType.SYNTHESIS) { - return false; - } - if (operation.result) { - return false; - } - - const argumentIDs = schema.graph.expandInputs([operation.id]); - if (!argumentIDs || argumentIDs.length < 1) { - return false; - } - - const argumentOperations = argumentIDs.map(id => schema.operationByID.get(id)!); - if (argumentOperations.some(item => item.result === null)) { - return false; - } - - return true; - })(); - - const ref = useRef(null); - - function handleBlur(event: React.FocusEvent) { - if (!ref.current?.contains(event.relatedTarget as Node)) { - onHide(); - } - } - - function handleOpenSchema() { - if (!operation) { - return; - } - onHide(); - navigateOperationSchema(operation.id); - } - - function handleEditSchema() { - if (!operation) { - return; - } - onHide(); - showEditInput({ - oss: schema, - target: operation, - layout: getLayout() - }); - } - - function handleEditOperation() { - if (!operation) { - return; - } - onHide(); - showEditOperation({ - oss: schema, - target: operation, - layout: getLayout() - }); - } - - function handleDeleteOperation() { - if (!operation || !canDelete(operation)) { - return; - } - onHide(); - showDeleteOperation({ - oss: schema, - target: operation, - layout: getLayout() - }); - } - - function handleOperationExecute() { - if (!operation) { - return; - } - onHide(); - void operationExecute({ - itemID: schema.id, // - data: { target: operation.id, layout: getLayout() } - }); - } - - function handleInputCreate() { - if (!operation) { - return; - } - if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) { - toast.error(errorMsg.inputAlreadyExists); - return; - } - onHide(); - void inputCreate({ - itemID: schema.id, - data: { target: operation.id, layout: getLayout() } - }).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true })); - } - - function handleRelocateConstituents() { - if (!operation) { - return; - } - onHide(); - showRelocateConstituents({ - oss: schema, - initialTarget: operation, - layout: getLayout() - }); - } - - return ( -
- = window.innerWidth - MENU_WIDTH} - stretchTop={cursorY >= window.innerHeight - MENU_HEIGHT} - margin={cursorY >= window.innerHeight - MENU_HEIGHT ? 'mb-3' : 'mt-3'} - > - } - onClick={handleEditOperation} - disabled={!isMutable || isProcessing} - /> - - {operation?.result ? ( - } - onClick={handleOpenSchema} - disabled={isProcessing} - /> - ) : null} - {isMutable && !operation?.result && operation?.arguments.length === 0 ? ( - } - onClick={handleInputCreate} - disabled={isProcessing} - /> - ) : null} - {isMutable && operation?.operation_type === OperationType.INPUT ? ( - } - onClick={handleEditSchema} - disabled={isProcessing} - /> - ) : null} - {isMutable && !operation?.result && operation?.operation_type === OperationType.SYNTHESIS ? ( - и получить синтезированную КС' - : 'Необходимо предоставить все аргументы' - } - aria-label='Активировать операцию и получить синтезированную КС' - icon={} - onClick={handleOperationExecute} - disabled={isProcessing || !readyForSynthesis} - /> - ) : null} - - {isMutable && operation?.result ? ( - } - onClick={handleRelocateConstituents} - disabled={isProcessing} - /> - ) : null} - - } - onClick={handleDeleteOperation} - disabled={!isMutable || isProcessing || !operation || !canDelete(operation)} - /> - -
- ); -} diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx index 84c043c0..29953dd3 100644 --- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx +++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx @@ -28,8 +28,8 @@ import { useOperationTooltipStore } from '../../../stores/operation-tooltip'; import { useOSSGraphStore } from '../../../stores/oss-graph'; import { useOssEdit } from '../oss-edit-context'; +import { ContextMenu, type ContextMenuData } from './context-menu/context-menu'; import { OssNodeTypes } from './graph/oss-node-types'; -import { type ContextMenuData, NodeContextMenu } from './node-context-menu'; import { ToolbarOssGraph } from './toolbar-oss-graph'; import { useGetLayout } from './use-get-layout'; @@ -53,11 +53,11 @@ export function OssFlow() { } = useOssEdit(); const { fitView, screenToFlowPosition } = useReactFlow(); const store = useStoreApi(); - const { resetSelectedElements } = store.getState(); + const { resetSelectedElements, addSelectedNodes } = store.getState(); const isProcessing = useMutatingOss(); - const setHoverOperation = useOperationTooltipStore(state => state.setActiveOperation); + const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem); const showGrid = useOSSGraphStore(state => state.showGrid); const showCoordinates = useOSSGraphStore(state => state.showCoordinates); @@ -71,7 +71,7 @@ export function OssFlow() { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [toggleReset, setToggleReset] = useState(false); - const [menuProps, setMenuProps] = useState({ operation: null, cursorX: 0, cursorY: 0 }); + const [menuProps, setMenuProps] = useState({ item: null, cursorX: 0, cursorY: 0 }); const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [mouseCoords, setMouseCoords] = useState({ x: 0, y: 0 }); @@ -202,12 +202,10 @@ export function OssFlow() { event.preventDefault(); event.stopPropagation(); - if (node.type === 'block') { - return; - } + addSelectedNodes([node.id]); setMenuProps({ - operation: node.data.operation, + item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null, cursorX: event.clientX, cursorY: event.clientY }); @@ -216,9 +214,12 @@ export function OssFlow() { } function handleNodeDoubleClick(event: React.MouseEvent, node: OssNode) { + if (node.type === 'block') { + return; + } event.preventDefault(); event.stopPropagation(); - if (node.data.operation.result) { + if (node.data.operation?.result) { navigateOperationSchema(Number(node.id)); } } @@ -285,7 +286,7 @@ export function OssFlow() { onResetPositions={() => setToggleReset(prev => !prev)} /> - setIsContextMenuOpen(false)} {...menuProps} /> + setIsContextMenuOpen(false)} {...menuProps} />
void; + hoverItem: IOssItem | null; + setHoverItem: (value: IOssItem | null) => void; } export const useOperationTooltipStore = create()(set => ({ - activeOperation: null, - setActiveOperation: value => set({ activeOperation: value }) + hoverItem: null, + setHoverItem: value => set({ hoverItem: value }) }));