F: Improve OSS graph interactions

This commit is contained in:
Ivan 2025-02-26 12:54:51 +03:00
parent ba11c1f82b
commit cab9ae8efc
7 changed files with 213 additions and 261 deletions

View File

@ -1,6 +1,12 @@
'use client'; 'use client';
import { useRef } from 'react'; import { useRef } from 'react';
import { toast } from 'react-toastify';
import { urls, useConceptNavigation } from '@/app';
import { useLibrary } from '@/features/library/backend/useLibrary';
import { useInputCreate } from '@/features/oss/backend/useInputCreate';
import { useOperationExecute } from '@/features/oss/backend/useOperationExecute';
import { Dropdown, DropdownButton } from '@/components/Dropdown'; import { Dropdown, DropdownButton } from '@/components/Dropdown';
import { import {
@ -13,6 +19,8 @@ import {
IconRSForm IconRSForm
} from '@/components/Icons'; } from '@/components/Icons';
import { useClickedOutside } from '@/hooks/useClickedOutside'; import { useClickedOutside } from '@/hooks/useClickedOutside';
import { useDialogsStore } from '@/stores/dialogs';
import { errorMsg } from '@/utils/labels';
import { prepareTooltip } from '@/utils/utils'; import { prepareTooltip } from '@/utils/utils';
import { OperationType } from '../../../backend/types'; import { OperationType } from '../../../backend/types';
@ -20,12 +28,14 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
import { type IOperation } from '../../../models/oss'; import { type IOperation } from '../../../models/oss';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
import { useGetPositions } from './useGetPositions';
// pixels - size of OSS context menu // pixels - size of OSS context menu
const MENU_WIDTH = 200; const MENU_WIDTH = 200;
const MENU_HEIGHT = 200; const MENU_HEIGHT = 200;
export interface ContextMenuData { export interface ContextMenuData {
operation: IOperation; operation: IOperation | null;
cursorX: number; cursorX: number;
cursorY: number; cursorY: number;
} }
@ -33,33 +43,25 @@ export interface ContextMenuData {
interface NodeContextMenuProps extends ContextMenuData { interface NodeContextMenuProps extends ContextMenuData {
isOpen: boolean; isOpen: boolean;
onHide: () => void; onHide: () => void;
onDelete: (target: number) => void;
onCreateInput: (target: number) => void;
onEditSchema: (target: number) => void;
onEditOperation: (target: number) => void;
onExecuteOperation: (target: number) => void;
onRelocateConstituents: (target: number) => void;
} }
export function NodeContextMenu({ export function NodeContextMenu({ isOpen, operation, cursorX, cursorY, onHide }: NodeContextMenuProps) {
isOpen, const router = useConceptNavigation();
operation, const { items: libraryItems } = useLibrary();
cursorX, const { schema, navigateOperationSchema, isMutable, canDeleteOperation: canDelete } = useOssEdit();
cursorY,
onHide,
onDelete,
onCreateInput,
onEditSchema,
onEditOperation,
onExecuteOperation,
onRelocateConstituents
}: NodeContextMenuProps) {
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { schema, navigateOperationSchema, isMutable, canDelete } = useOssEdit(); const getPositions = useGetPositions();
const { inputCreate } = useInputCreate();
const { operationExecute } = useOperationExecute();
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 ref = useRef<HTMLDivElement>(null);
const readyForSynthesis = (() => { const readyForSynthesis = (() => {
if (operation.operation_type !== OperationType.SYNTHESIS) { if (operation?.operation_type !== OperationType.SYNTHESIS) {
return false; return false;
} }
if (operation.result) { if (operation.result) {
@ -79,40 +81,89 @@ export function NodeContextMenu({
return true; return true;
})(); })();
const ref = useRef<HTMLDivElement>(null);
useClickedOutside(isOpen, ref, onHide); useClickedOutside(isOpen, ref, onHide);
function handleOpenSchema() { function handleOpenSchema() {
if (!operation) {
return;
}
onHide();
navigateOperationSchema(operation.id); navigateOperationSchema(operation.id);
} }
function handleEditSchema() { function handleEditSchema() {
if (!operation) {
return;
}
onHide(); onHide();
onEditSchema(operation.id); showEditInput({
oss: schema,
target: operation,
positions: getPositions()
});
} }
function handleEditOperation() { function handleEditOperation() {
if (!operation) {
return;
}
onHide(); onHide();
onEditOperation(operation.id); showEditOperation({
oss: schema,
target: operation,
positions: getPositions()
});
} }
function handleDeleteOperation() { function handleDeleteOperation() {
if (!operation || !canDelete(operation)) {
return;
}
onHide(); onHide();
onDelete(operation.id); showDeleteOperation({
oss: schema,
target: operation,
positions: getPositions()
});
} }
function handleCreateSchema() { function handleOperationExecute() {
if (!operation) {
return;
}
onHide(); onHide();
onCreateInput(operation.id); void operationExecute({
itemID: schema.id, //
data: { target: operation.id, positions: getPositions() }
});
} }
function handleRunSynthesis() { function handleInputCreate() {
if (!operation) {
return;
}
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
toast.error(errorMsg.inputAlreadyExists);
return;
}
onHide(); onHide();
onExecuteOperation(operation.id); void inputCreate({
itemID: schema.id,
data: { target: operation.id, positions: getPositions() }
}).then(new_schema => router.push(urls.schema(new_schema.id)));
} }
function handleRelocateConstituents() { function handleRelocateConstituents() {
if (!operation) {
return;
}
onHide(); onHide();
onRelocateConstituents(operation.id); showRelocateConstituents({
oss: schema,
initialTarget: operation,
positions: getPositions()
});
} }
return ( return (
@ -145,7 +196,7 @@ export function NodeContextMenu({
title='Создать пустую схему для загрузки' title='Создать пустую схему для загрузки'
icon={<IconNewRSForm size='1rem' className='icon-green' />} icon={<IconNewRSForm size='1rem' className='icon-green' />}
disabled={isProcessing} disabled={isProcessing}
onClick={handleCreateSchema} onClick={handleInputCreate}
/> />
) : null} ) : null}
{isMutable && operation?.operation_type === OperationType.INPUT ? ( {isMutable && operation?.operation_type === OperationType.INPUT ? (
@ -167,7 +218,7 @@ export function NodeContextMenu({
} }
icon={<IconExecute size='1rem' className='icon-green' />} icon={<IconExecute size='1rem' className='icon-green' />}
disabled={isProcessing || !readyForSynthesis} disabled={isProcessing || !readyForSynthesis}
onClick={handleRunSynthesis} onClick={handleOperationExecute}
/> />
) : null} ) : null}
@ -184,7 +235,7 @@ export function NodeContextMenu({
<DropdownButton <DropdownButton
text='Удалить операцию' text='Удалить операцию'
icon={<IconDestroy size='1rem' className='icon-red' />} icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={!isMutable || isProcessing || !operation || !canDelete(operation.id)} disabled={!isMutable || isProcessing || !operation || !canDelete(operation)}
onClick={handleDeleteOperation} onClick={handleDeleteOperation}
/> />
</Dropdown> </Dropdown>

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { import {
Background, Background,
type Node, type Node,
@ -12,18 +11,13 @@ import {
useReactFlow useReactFlow
} from 'reactflow'; } from 'reactflow';
import { urls, useConceptNavigation } from '@/app';
import { useLibrary } from '@/features/library/backend/useLibrary';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs';
import { useTooltipsStore } from '@/stores/tooltips'; import { useTooltipsStore } from '@/stores/tooltips';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { useInputCreate } from '../../../backend/useInputCreate';
import { useMutatingOss } from '../../../backend/useMutatingOss'; import { useMutatingOss } from '../../../backend/useMutatingOss';
import { useOperationExecute } from '../../../backend/useOperationExecute';
import { useUpdatePositions } from '../../../backend/useUpdatePositions'; import { useUpdatePositions } from '../../../backend/useUpdatePositions';
import { GRID_SIZE } from '../../../models/ossAPI'; import { GRID_SIZE } from '../../../models/ossAPI';
import { type OssNode } from '../../../models/ossLayout'; import { type OssNode } from '../../../models/ossLayout';
@ -33,9 +27,11 @@ import { useOssEdit } from '../OssEditContext';
import { OssNodeTypes } from './graph/OssNodeTypes'; import { OssNodeTypes } from './graph/OssNodeTypes';
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu'; import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
import { ToolbarOssGraph } from './ToolbarOssGraph'; import { ToolbarOssGraph } from './ToolbarOssGraph';
import { useGetPositions } from './useGetPositions';
const ZOOM_MAX = 2; const ZOOM_MAX = 2;
const ZOOM_MIN = 0.5; const ZOOM_MIN = 0.5;
export const VIEW_PADDING = 0.2;
export function OssFlow() { export function OssFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
@ -45,16 +41,9 @@ export function OssFlow() {
setSelected, setSelected,
selected, selected,
isMutable, isMutable,
promptCreateOperation, canDeleteOperation: canDelete
canDelete,
promptDeleteOperation,
promptEditInput,
promptEditOperation,
promptRelocateConstituents
} = useOssEdit(); } = useOssEdit();
const router = useConceptNavigation(); const { fitView, project } = useReactFlow();
const { items: libraryItems } = useLibrary();
const flow = useReactFlow();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
@ -64,16 +53,18 @@ export function OssFlow() {
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
const edgeStraight = useOSSGraphStore(state => state.edgeStraight); const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
const { inputCreate } = useInputCreate(); const getPositions = useGetPositions();
const { operationExecute } = useOperationExecute();
const { updatePositions } = useUpdatePositions(); const { updatePositions } = useUpdatePositions();
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const [menuProps, setMenuProps] = useState<ContextMenuData | null>(null); const [menuProps, setMenuProps] = useState<ContextMenuData>({ operation: null, cursorX: 0, cursorY: 0 });
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id)); const ids = nodes.map(node => Number(node.id));
setSelected(prev => [ setSelected(prev => [
@ -109,15 +100,8 @@ export function OssFlow() {
: 'left' : 'left'
})) }))
); );
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate]); fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
function getPositions() {
return nodes.map(node => ({
id: Number(node.id),
position_x: node.position.x,
position_y: node.position.y
}));
}
function handleSavePositions() { function handleSavePositions() {
const positions = getPositions(); const positions = getPositions();
@ -132,73 +116,34 @@ export function OssFlow() {
}); });
} }
function handleCreateOperation(inputs: number[]) { function handleCreateOperation() {
const positions = getPositions(); const targetPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 }); showCreateOperation({
promptCreateOperation({ oss: schema,
defaultX: target.x, defaultX: targetPosition.x,
defaultY: target.y, defaultY: targetPosition.y,
inputs: inputs, positions: getPositions(),
positions: positions, initialInputs: selected,
callback: () => setTimeout(() => flow.fitView({ duration: PARAMETER.zoomDuration }), PARAMETER.refreshTimeout) onCreate: () =>
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
}); });
} }
function handleDeleteOperation(target: number) {
if (!canDelete(target)) {
return;
}
promptDeleteOperation(target, getPositions());
}
function handleDeleteSelected() { function handleDeleteSelected() {
if (selected.length !== 1) { if (selected.length !== 1) {
return; return;
} }
handleDeleteOperation(selected[0]); const operation = schema.operationByID.get(selected[0]);
} if (!operation || !canDelete(operation)) {
function handleInputCreate(target: number) {
const operation = schema.operationByID.get(target);
if (!operation) {
return; return;
} }
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) { showDeleteOperation({
toast.error(errorMsg.inputAlreadyExists); oss: schema,
return; target: operation,
} positions: getPositions()
void inputCreate({
itemID: schema.id,
data: { target: target, positions: getPositions() }
}).then(new_schema => router.push(urls.schema(new_schema.id)));
}
function handleEditSchema(target: number) {
promptEditInput(target, getPositions());
}
function handleEditOperation(target: number) {
promptEditOperation(target, getPositions());
}
function handleOperationExecute(target: number) {
void operationExecute({
itemID: schema.id, //
data: { target: target, positions: getPositions() }
}); });
} }
function handleExecuteSelected() {
if (selected.length !== 1) {
return;
}
handleOperationExecute(selected[0]);
}
function handleRelocateConstituents(target: number) {
promptRelocateConstituents(target, getPositions());
}
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) { function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -212,14 +157,6 @@ export function OssFlow() {
setHoverOperation(null); setHoverOperation(null);
} }
function handleContextMenuHide() {
setIsContextMenuOpen(false);
}
function handleCanvasClick() {
handleContextMenuHide();
}
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) { function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -229,10 +166,7 @@ export function OssFlow() {
} }
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (isProcessing) { if (isProcessing || !isMutable) {
return;
}
if (!isMutable) {
return; return;
} }
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') { if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
@ -244,7 +178,7 @@ export function OssFlow() {
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') { if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleCreateOperation(selected); handleCreateOperation();
return; return;
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {
@ -262,35 +196,20 @@ export function OssFlow() {
className='rounded-b-2xl cc-blur hover:bg-prim-100 hover:bg-opacity-50' className='rounded-b-2xl cc-blur hover:bg-prim-100 hover:bg-opacity-50'
> >
<ToolbarOssGraph <ToolbarOssGraph
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })} onCreate={handleCreateOperation}
onCreate={() => handleCreateOperation(selected)}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onEdit={() => handleEditOperation(selected[0])}
onExecute={handleExecuteSelected}
onResetPositions={() => setToggleReset(prev => !prev)} onResetPositions={() => setToggleReset(prev => !prev)}
onSavePositions={handleSavePositions}
/> />
</Overlay> </Overlay>
{menuProps ? (
<NodeContextMenu <NodeContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
isOpen={isContextMenuOpen}
onHide={handleContextMenuHide}
onDelete={handleDeleteOperation}
onCreateInput={handleInputCreate}
onEditSchema={handleEditSchema}
onEditOperation={handleEditOperation}
onExecuteOperation={handleOperationExecute}
onRelocateConstituents={handleRelocateConstituents}
{...menuProps}
/>
) : null}
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}> <div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
edges={edges} edges={edges}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
onNodeDoubleClick={handleNodeDoubleClick}
edgesFocusable={false} edgesFocusable={false}
nodesFocusable={false} nodesFocusable={false}
fitView fitView
@ -300,8 +219,10 @@ export function OssFlow() {
nodesConnectable={false} nodesConnectable={false}
snapToGrid={true} snapToGrid={true}
snapGrid={[GRID_SIZE, GRID_SIZE]} snapGrid={[GRID_SIZE, GRID_SIZE]}
onClick={() => setIsContextMenuOpen(false)}
onNodeDoubleClick={handleNodeDoubleClick}
onNodeContextMenu={handleContextMenu} onNodeContextMenu={handleContextMenu}
onClick={handleCanvasClick} onNodeDragStart={() => setIsContextMenuOpen(false)}
> >
{showGrid ? <Background gap={GRID_SIZE} /> : null} {showGrid ? <Background gap={GRID_SIZE} /> : null}
</ReactFlow> </ReactFlow>

View File

@ -1,9 +1,12 @@
'use client'; 'use client';
import { useReactFlow } from 'reactflow';
import clsx from 'clsx'; import clsx from 'clsx';
import { HelpTopic } from '@/features/help'; import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components'; import { BadgeHelp } from '@/features/help/components';
import { useOperationExecute } from '@/features/oss/backend/useOperationExecute';
import { useUpdatePositions } from '@/features/oss/backend/useUpdatePositions';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { import {
@ -20,6 +23,7 @@ import {
IconReset, IconReset,
IconSave IconSave
} from '@/components/Icons'; } from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils'; import { prepareTooltip } from '@/utils/utils';
@ -28,28 +32,21 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
import { useOSSGraphStore } from '../../../stores/ossGraph'; import { useOSSGraphStore } from '../../../stores/ossGraph';
import { useOssEdit } from '../OssEditContext'; import { useOssEdit } from '../OssEditContext';
import { VIEW_PADDING } from './OssFlow';
import { useGetPositions } from './useGetPositions';
interface ToolbarOssGraphProps { interface ToolbarOssGraphProps {
onCreate: () => void; onCreate: () => void;
onDelete: () => void; onDelete: () => void;
onEdit: () => void;
onExecute: () => void;
onFitView: () => void;
onSavePositions: () => void;
onResetPositions: () => void; onResetPositions: () => void;
} }
export function ToolbarOssGraph({ export function ToolbarOssGraph({ onCreate, onDelete, onResetPositions }: ToolbarOssGraphProps) {
onCreate, const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
onDelete,
onEdit,
onExecute,
onFitView,
onSavePositions,
onResetPositions
}: ToolbarOssGraphProps) {
const { schema, selected, isMutable, canDelete } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { fitView } = useReactFlow();
const selectedOperation = schema.operationByID.get(selected[0]); const selectedOperation = schema.operationByID.get(selected[0]);
const getPositions = useGetPositions();
const showGrid = useOSSGraphStore(state => state.showGrid); const showGrid = useOSSGraphStore(state => state.showGrid);
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate); const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
@ -58,6 +55,11 @@ export function ToolbarOssGraph({
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate); const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight); const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
const { updatePositions } = useUpdatePositions();
const { operationExecute } = useOperationExecute();
const showEditOperation = useDialogsStore(state => state.showEditOperation);
const readyForSynthesis = (() => { const readyForSynthesis = (() => {
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) { if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
return false; return false;
@ -79,6 +81,44 @@ export function ToolbarOssGraph({
return true; return true;
})(); })();
function handleFitView() {
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
}
function handleSavePositions() {
const positions = getPositions();
void updatePositions({ itemID: schema.id, positions: positions }).then(() => {
positions.forEach(item => {
const operation = schema.operationByID.get(item.id);
if (operation) {
operation.position_x = item.position_x;
operation.position_y = item.position_y;
}
});
});
}
function handleOperationExecute() {
if (selected.length !== 1 || !readyForSynthesis || !selectedOperation) {
return;
}
void operationExecute({
itemID: schema.id, //
data: { target: selectedOperation.id, positions: getPositions() }
});
}
function handleEditOperation() {
if (selected.length !== 1 || !selectedOperation) {
return;
}
showEditOperation({
oss: schema,
target: selectedOperation,
positions: getPositions()
});
}
return ( return (
<div className='flex flex-col items-center'> <div className='flex flex-col items-center'>
<div className='cc-icons'> <div className='cc-icons'>
@ -90,7 +130,7 @@ export function ToolbarOssGraph({
<MiniButton <MiniButton
icon={<IconFitImage size='1.25rem' className='icon-primary' />} icon={<IconFitImage size='1.25rem' className='icon-primary' />}
title='Сбросить вид' title='Сбросить вид'
onClick={onFitView} onClick={handleFitView}
/> />
<MiniButton <MiniButton
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'} title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
@ -137,7 +177,7 @@ export function ToolbarOssGraph({
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
icon={<IconSave size='1.25rem' className='icon-primary' />} icon={<IconSave size='1.25rem' className='icon-primary' />}
disabled={isProcessing} disabled={isProcessing}
onClick={onSavePositions} onClick={handleSavePositions}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')} titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
@ -149,18 +189,18 @@ export function ToolbarOssGraph({
title='Активировать операцию' title='Активировать операцию'
icon={<IconExecute size='1.25rem' className='icon-green' />} icon={<IconExecute size='1.25rem' className='icon-green' />}
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis} disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
onClick={onExecute} onClick={handleOperationExecute}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')} titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
icon={<IconEdit2 size='1.25rem' className='icon-primary' />} icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
disabled={selected.length !== 1 || isProcessing} disabled={selected.length !== 1 || isProcessing}
onClick={onEdit} onClick={handleEditOperation}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')} titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={selected.length !== 1 || isProcessing || !canDelete(selected[0])} disabled={selected.length !== 1 || isProcessing || !selectedOperation || !canDelete(selectedOperation)}
onClick={onDelete} onClick={onDelete}
/> />
</div> </div>

View File

@ -54,6 +54,7 @@ export function NodeCore({ node }: NodeCoreProps) {
<div <div
className='h-[34px] w-[144px] flex items-center justify-center' className='h-[34px] w-[144px] flex items-center justify-center'
data-tooltip-id={globalIDs.operation_tooltip} data-tooltip-id={globalIDs.operation_tooltip}
data-tooltip-hidden={node.dragging}
onMouseEnter={() => setHover(node.data.operation)} onMouseEnter={() => setHover(node.data.operation)}
> >
<div <div

View File

@ -0,0 +1,12 @@
import { useReactFlow } from 'reactflow';
export function useGetPositions() {
const { getNodes } = useReactFlow();
return function getPositions() {
return getNodes().map(node => ({
id: Number(node.id),
position_x: node.position.x,
position_y: node.position.y
}));
};
}

View File

@ -3,6 +3,7 @@ import { useAuthSuspense } from '@/features/auth';
import { Button } from '@/components/Control'; import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown'; import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconChild, IconEdit2 } from '@/components/Icons'; import { IconChild, IconEdit2 } from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs';
import { useMutatingOss } from '../../backend/useMutatingOss'; import { useMutatingOss } from '../../backend/useMutatingOss';
@ -11,12 +12,18 @@ import { useOssEdit } from './OssEditContext';
export function MenuEditOss() { export function MenuEditOss() {
const { isAnonymous } = useAuthSuspense(); const { isAnonymous } = useAuthSuspense();
const editMenu = useDropdown(); const editMenu = useDropdown();
const { promptRelocateConstituents, isMutable } = useOssEdit(); const { schema, isMutable } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
function handleRelocate() { function handleRelocate() {
editMenu.hide(); editMenu.hide();
promptRelocateConstituents(undefined, []); showRelocateConstituents({
oss: schema,
initialTarget: undefined,
positions: []
});
} }
if (isAnonymous) { if (isAnonymous) {

View File

@ -9,13 +9,12 @@ import { useDeleteItem } from '@/features/library/backend/useDeleteItem';
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext'; import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
import { useRoleStore, UserRole } from '@/features/users'; import { useRoleStore, UserRole } from '@/features/users';
import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { promptText } from '@/utils/labels'; import { promptText } from '@/utils/labels';
import { type IOperationPosition, OperationType } from '../../backend/types'; import { type IOperationPosition, OperationType } from '../../backend/types';
import { useOssSuspense } from '../../backend/useOSS'; import { useOssSuspense } from '../../backend/useOSS';
import { type IOperationSchema } from '../../models/oss'; import { type IOperation, type IOperationSchema } from '../../models/oss';
export enum OssTabID { export enum OssTabID {
CARD = 0, CARD = 0,
@ -40,15 +39,9 @@ export interface IOssEditContext {
navigateTab: (tab: OssTabID) => void; navigateTab: (tab: OssTabID) => void;
navigateOperationSchema: (target: number) => void; navigateOperationSchema: (target: number) => void;
canDeleteOperation: (target: IOperation) => boolean;
deleteSchema: () => void; deleteSchema: () => void;
setSelected: React.Dispatch<React.SetStateAction<number[]>>; setSelected: React.Dispatch<React.SetStateAction<number[]>>;
canDelete: (target: number) => boolean;
promptCreateOperation: (props: ICreateOperationPrompt) => void;
promptDeleteOperation: (target: number, positions: IOperationPosition[]) => void;
promptEditInput: (target: number, positions: IOperationPosition[]) => void;
promptEditOperation: (target: number, positions: IOperationPosition[]) => void;
promptRelocateConstituents: (target: number | undefined, positions: IOperationPosition[]) => void;
} }
const OssEditContext = createContext<IOssEditContext | null>(null); const OssEditContext = createContext<IOssEditContext | null>(null);
@ -81,12 +74,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const showEditInput = useDialogsStore(state => state.showChangeInputSchema);
const showEditOperation = useDialogsStore(state => state.showEditOperation);
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
const showCreateOperation = useDialogsStore(state => state.showCreateOperation);
const { deleteItem } = useDeleteItem(); const { deleteItem } = useDeleteItem();
useEffect( useEffect(
@ -128,71 +115,11 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
}); });
} }
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) { function canDeleteOperation(target: IOperation) {
showCreateOperation({ if (target.operation_type === OperationType.INPUT) {
oss: schema,
defaultX: defaultX,
defaultY: defaultY,
positions: positions,
initialInputs: inputs,
onCreate: callback
});
}
function canDelete(target: number) {
const operation = schema.operationByID.get(target);
if (!operation) {
return false;
}
if (operation.operation_type === OperationType.INPUT) {
return true; return true;
} }
return schema.graph.expandOutputs([target]).length === 0; return schema.graph.expandOutputs([target.id]).length === 0;
}
function promptEditOperation(target: number, positions: IOperationPosition[]) {
const operation = schema.operationByID.get(target);
if (!operation) {
return;
}
showEditOperation({
oss: schema,
target: operation,
positions: positions
});
}
function promptDeleteOperation(target: number, positions: IOperationPosition[]) {
const operation = schema.operationByID.get(target);
if (!operation) {
return;
}
showDeleteOperation({
oss: schema,
positions: positions,
target: operation
});
}
function promptEditInput(target: number, positions: IOperationPosition[]) {
const operation = schema.operationByID.get(target);
if (!operation) {
return;
}
showEditInput({
oss: schema,
target: operation,
positions: positions
});
}
function promptRelocateConstituents(target: number | undefined, positions: IOperationPosition[]) {
const operation = target ? schema.operationByID.get(target) : undefined;
showRelocateConstituents({
oss: schema,
initialTarget: operation,
positions: positions
});
} }
return ( return (
@ -201,22 +128,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
schema, schema,
selected, selected,
navigateTab,
deleteSchema,
isOwned, isOwned,
isMutable, isMutable,
setSelected, navigateTab,
navigateOperationSchema, navigateOperationSchema,
promptCreateOperation,
canDelete, canDeleteOperation,
promptDeleteOperation, deleteSchema,
promptEditInput, setSelected
promptEditOperation,
promptRelocateConstituents
}} }}
> >
{children} {children}