F: Improve OSS graph interactions
This commit is contained in:
parent
ba11c1f82b
commit
cab9ae8efc
|
@ -1,6 +1,12 @@
|
|||
'use client';
|
||||
|
||||
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 {
|
||||
|
@ -13,6 +19,8 @@ import {
|
|||
IconRSForm
|
||||
} from '@/components/Icons';
|
||||
import { useClickedOutside } from '@/hooks/useClickedOutside';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
import { OperationType } from '../../../backend/types';
|
||||
|
@ -20,12 +28,14 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
|||
import { type IOperation } from '../../../models/oss';
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
import { useGetPositions } from './useGetPositions';
|
||||
|
||||
// pixels - size of OSS context menu
|
||||
const MENU_WIDTH = 200;
|
||||
const MENU_HEIGHT = 200;
|
||||
|
||||
export interface ContextMenuData {
|
||||
operation: IOperation;
|
||||
operation: IOperation | null;
|
||||
cursorX: number;
|
||||
cursorY: number;
|
||||
}
|
||||
|
@ -33,33 +43,25 @@ export interface ContextMenuData {
|
|||
interface NodeContextMenuProps extends ContextMenuData {
|
||||
isOpen: boolean;
|
||||
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({
|
||||
isOpen,
|
||||
operation,
|
||||
cursorX,
|
||||
cursorY,
|
||||
onHide,
|
||||
onDelete,
|
||||
onCreateInput,
|
||||
onEditSchema,
|
||||
onEditOperation,
|
||||
onExecuteOperation,
|
||||
onRelocateConstituents
|
||||
}: NodeContextMenuProps) {
|
||||
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 { 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 = (() => {
|
||||
if (operation.operation_type !== OperationType.SYNTHESIS) {
|
||||
if (operation?.operation_type !== OperationType.SYNTHESIS) {
|
||||
return false;
|
||||
}
|
||||
if (operation.result) {
|
||||
|
@ -79,40 +81,89 @@ export function NodeContextMenu({
|
|||
return true;
|
||||
})();
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useClickedOutside(isOpen, ref, onHide);
|
||||
|
||||
function handleOpenSchema() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
navigateOperationSchema(operation.id);
|
||||
}
|
||||
|
||||
function handleEditSchema() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onEditSchema(operation.id);
|
||||
showEditInput({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditOperation() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onEditOperation(operation.id);
|
||||
showEditOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteOperation() {
|
||||
if (!operation || !canDelete(operation)) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onDelete(operation.id);
|
||||
showDeleteOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
function handleCreateSchema() {
|
||||
function handleOperationExecute() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
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() {
|
||||
if (!operation) {
|
||||
return;
|
||||
}
|
||||
onHide();
|
||||
onRelocateConstituents(operation.id);
|
||||
showRelocateConstituents({
|
||||
oss: schema,
|
||||
initialTarget: operation,
|
||||
positions: getPositions()
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -145,7 +196,7 @@ export function NodeContextMenu({
|
|||
title='Создать пустую схему для загрузки'
|
||||
icon={<IconNewRSForm size='1rem' className='icon-green' />}
|
||||
disabled={isProcessing}
|
||||
onClick={handleCreateSchema}
|
||||
onClick={handleInputCreate}
|
||||
/>
|
||||
) : null}
|
||||
{isMutable && operation?.operation_type === OperationType.INPUT ? (
|
||||
|
@ -167,7 +218,7 @@ export function NodeContextMenu({
|
|||
}
|
||||
icon={<IconExecute size='1rem' className='icon-green' />}
|
||||
disabled={isProcessing || !readyForSynthesis}
|
||||
onClick={handleRunSynthesis}
|
||||
onClick={handleOperationExecute}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
@ -184,7 +235,7 @@ export function NodeContextMenu({
|
|||
<DropdownButton
|
||||
text='Удалить операцию'
|
||||
icon={<IconDestroy size='1rem' className='icon-red' />}
|
||||
disabled={!isMutable || isProcessing || !operation || !canDelete(operation.id)}
|
||||
disabled={!isMutable || isProcessing || !operation || !canDelete(operation)}
|
||||
onClick={handleDeleteOperation}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Background,
|
||||
type Node,
|
||||
|
@ -12,18 +11,13 @@ import {
|
|||
useReactFlow
|
||||
} from 'reactflow';
|
||||
|
||||
import { urls, useConceptNavigation } from '@/app';
|
||||
import { useLibrary } from '@/features/library/backend/useLibrary';
|
||||
|
||||
import { Overlay } from '@/components/Container';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { useTooltipsStore } from '@/stores/tooltips';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { errorMsg } from '@/utils/labels';
|
||||
|
||||
import { useInputCreate } from '../../../backend/useInputCreate';
|
||||
import { useMutatingOss } from '../../../backend/useMutatingOss';
|
||||
import { useOperationExecute } from '../../../backend/useOperationExecute';
|
||||
import { useUpdatePositions } from '../../../backend/useUpdatePositions';
|
||||
import { GRID_SIZE } from '../../../models/ossAPI';
|
||||
import { type OssNode } from '../../../models/ossLayout';
|
||||
|
@ -33,9 +27,11 @@ import { useOssEdit } from '../OssEditContext';
|
|||
import { OssNodeTypes } from './graph/OssNodeTypes';
|
||||
import { type ContextMenuData, NodeContextMenu } from './NodeContextMenu';
|
||||
import { ToolbarOssGraph } from './ToolbarOssGraph';
|
||||
import { useGetPositions } from './useGetPositions';
|
||||
|
||||
const ZOOM_MAX = 2;
|
||||
const ZOOM_MIN = 0.5;
|
||||
export const VIEW_PADDING = 0.2;
|
||||
|
||||
export function OssFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
|
@ -45,16 +41,9 @@ export function OssFlow() {
|
|||
setSelected,
|
||||
selected,
|
||||
isMutable,
|
||||
promptCreateOperation,
|
||||
canDelete,
|
||||
promptDeleteOperation,
|
||||
promptEditInput,
|
||||
promptEditOperation,
|
||||
promptRelocateConstituents
|
||||
canDeleteOperation: canDelete
|
||||
} = useOssEdit();
|
||||
const router = useConceptNavigation();
|
||||
const { items: libraryItems } = useLibrary();
|
||||
const flow = useReactFlow();
|
||||
const { fitView, project } = useReactFlow();
|
||||
|
||||
const isProcessing = useMutatingOss();
|
||||
|
||||
|
@ -64,16 +53,18 @@ export function OssFlow() {
|
|||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||
|
||||
const { inputCreate } = useInputCreate();
|
||||
const { operationExecute } = useOperationExecute();
|
||||
const getPositions = useGetPositions();
|
||||
const { updatePositions } = useUpdatePositions();
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
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 showCreateOperation = useDialogsStore(state => state.showCreateOperation);
|
||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||
|
||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
setSelected(prev => [
|
||||
|
@ -109,15 +100,8 @@ export function OssFlow() {
|
|||
: 'left'
|
||||
}))
|
||||
);
|
||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate]);
|
||||
|
||||
function getPositions() {
|
||||
return nodes.map(node => ({
|
||||
id: Number(node.id),
|
||||
position_x: node.position.x,
|
||||
position_y: node.position.y
|
||||
}));
|
||||
}
|
||||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
||||
|
||||
function handleSavePositions() {
|
||||
const positions = getPositions();
|
||||
|
@ -132,73 +116,34 @@ export function OssFlow() {
|
|||
});
|
||||
}
|
||||
|
||||
function handleCreateOperation(inputs: number[]) {
|
||||
const positions = getPositions();
|
||||
const target = flow.project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
promptCreateOperation({
|
||||
defaultX: target.x,
|
||||
defaultY: target.y,
|
||||
inputs: inputs,
|
||||
positions: positions,
|
||||
callback: () => setTimeout(() => flow.fitView({ duration: PARAMETER.zoomDuration }), PARAMETER.refreshTimeout)
|
||||
function handleCreateOperation() {
|
||||
const targetPosition = project({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
|
||||
showCreateOperation({
|
||||
oss: schema,
|
||||
defaultX: targetPosition.x,
|
||||
defaultY: targetPosition.y,
|
||||
positions: getPositions(),
|
||||
initialInputs: selected,
|
||||
onCreate: () =>
|
||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteOperation(target: number) {
|
||||
if (!canDelete(target)) {
|
||||
return;
|
||||
}
|
||||
promptDeleteOperation(target, getPositions());
|
||||
}
|
||||
|
||||
function handleDeleteSelected() {
|
||||
if (selected.length !== 1) {
|
||||
return;
|
||||
}
|
||||
handleDeleteOperation(selected[0]);
|
||||
}
|
||||
|
||||
function handleInputCreate(target: number) {
|
||||
const operation = schema.operationByID.get(target);
|
||||
if (!operation) {
|
||||
const operation = schema.operationByID.get(selected[0]);
|
||||
if (!operation || !canDelete(operation)) {
|
||||
return;
|
||||
}
|
||||
if (libraryItems.find(item => item.alias === operation.alias && item.location === schema.location)) {
|
||||
toast.error(errorMsg.inputAlreadyExists);
|
||||
return;
|
||||
}
|
||||
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() }
|
||||
showDeleteOperation({
|
||||
oss: schema,
|
||||
target: operation,
|
||||
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) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -212,14 +157,6 @@ export function OssFlow() {
|
|||
setHoverOperation(null);
|
||||
}
|
||||
|
||||
function handleContextMenuHide() {
|
||||
setIsContextMenuOpen(false);
|
||||
}
|
||||
|
||||
function handleCanvasClick() {
|
||||
handleContextMenuHide();
|
||||
}
|
||||
|
||||
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
@ -229,10 +166,7 @@ export function OssFlow() {
|
|||
}
|
||||
|
||||
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
||||
if (isProcessing) {
|
||||
return;
|
||||
}
|
||||
if (!isMutable) {
|
||||
if (isProcessing || !isMutable) {
|
||||
return;
|
||||
}
|
||||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyS') {
|
||||
|
@ -244,7 +178,7 @@ export function OssFlow() {
|
|||
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleCreateOperation(selected);
|
||||
handleCreateOperation();
|
||||
return;
|
||||
}
|
||||
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'
|
||||
>
|
||||
<ToolbarOssGraph
|
||||
onFitView={() => flow.fitView({ duration: PARAMETER.zoomDuration })}
|
||||
onCreate={() => handleCreateOperation(selected)}
|
||||
onCreate={handleCreateOperation}
|
||||
onDelete={handleDeleteSelected}
|
||||
onEdit={() => handleEditOperation(selected[0])}
|
||||
onExecute={handleExecuteSelected}
|
||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
||||
onSavePositions={handleSavePositions}
|
||||
/>
|
||||
</Overlay>
|
||||
{menuProps ? (
|
||||
<NodeContextMenu
|
||||
isOpen={isContextMenuOpen}
|
||||
onHide={handleContextMenuHide}
|
||||
onDelete={handleDeleteOperation}
|
||||
onCreateInput={handleInputCreate}
|
||||
onEditSchema={handleEditSchema}
|
||||
onEditOperation={handleEditOperation}
|
||||
onExecuteOperation={handleOperationExecute}
|
||||
onRelocateConstituents={handleRelocateConstituents}
|
||||
{...menuProps}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<NodeContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
||||
|
||||
<div className='cc-fade-in relative w-[100vw]' style={{ height: mainHeight, fontFamily: 'Rubik' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
edgesFocusable={false}
|
||||
nodesFocusable={false}
|
||||
fitView
|
||||
|
@ -300,8 +219,10 @@ export function OssFlow() {
|
|||
nodesConnectable={false}
|
||||
snapToGrid={true}
|
||||
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
||||
onClick={() => setIsContextMenuOpen(false)}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onNodeContextMenu={handleContextMenu}
|
||||
onClick={handleCanvasClick}
|
||||
onNodeDragStart={() => setIsContextMenuOpen(false)}
|
||||
>
|
||||
{showGrid ? <Background gap={GRID_SIZE} /> : null}
|
||||
</ReactFlow>
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { HelpTopic } from '@/features/help';
|
||||
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 {
|
||||
|
@ -20,6 +23,7 @@ import {
|
|||
IconReset,
|
||||
IconSave
|
||||
} from '@/components/Icons';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
import { prepareTooltip } from '@/utils/utils';
|
||||
|
||||
|
@ -28,28 +32,21 @@ import { useMutatingOss } from '../../../backend/useMutatingOss';
|
|||
import { useOSSGraphStore } from '../../../stores/ossGraph';
|
||||
import { useOssEdit } from '../OssEditContext';
|
||||
|
||||
import { VIEW_PADDING } from './OssFlow';
|
||||
import { useGetPositions } from './useGetPositions';
|
||||
|
||||
interface ToolbarOssGraphProps {
|
||||
onCreate: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit: () => void;
|
||||
onExecute: () => void;
|
||||
onFitView: () => void;
|
||||
onSavePositions: () => void;
|
||||
onResetPositions: () => void;
|
||||
}
|
||||
|
||||
export function ToolbarOssGraph({
|
||||
onCreate,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onExecute,
|
||||
onFitView,
|
||||
onSavePositions,
|
||||
onResetPositions
|
||||
}: ToolbarOssGraphProps) {
|
||||
const { schema, selected, isMutable, canDelete } = useOssEdit();
|
||||
export function ToolbarOssGraph({ onCreate, onDelete, onResetPositions }: ToolbarOssGraphProps) {
|
||||
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
const { fitView } = useReactFlow();
|
||||
const selectedOperation = schema.operationByID.get(selected[0]);
|
||||
const getPositions = useGetPositions();
|
||||
|
||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||
|
@ -58,6 +55,11 @@ export function ToolbarOssGraph({
|
|||
const toggleEdgeAnimate = useOSSGraphStore(state => state.toggleEdgeAnimate);
|
||||
const toggleEdgeStraight = useOSSGraphStore(state => state.toggleEdgeStraight);
|
||||
|
||||
const { updatePositions } = useUpdatePositions();
|
||||
const { operationExecute } = useOperationExecute();
|
||||
|
||||
const showEditOperation = useDialogsStore(state => state.showEditOperation);
|
||||
|
||||
const readyForSynthesis = (() => {
|
||||
if (!selectedOperation || selectedOperation.operation_type !== OperationType.SYNTHESIS) {
|
||||
return false;
|
||||
|
@ -79,6 +81,44 @@ export function ToolbarOssGraph({
|
|||
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 (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className='cc-icons'>
|
||||
|
@ -90,7 +130,7 @@ export function ToolbarOssGraph({
|
|||
<MiniButton
|
||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||
title='Сбросить вид'
|
||||
onClick={onFitView}
|
||||
onClick={handleFitView}
|
||||
/>
|
||||
<MiniButton
|
||||
title={showGrid ? 'Скрыть сетку' : 'Отобразить сетку'}
|
||||
|
@ -137,7 +177,7 @@ export function ToolbarOssGraph({
|
|||
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
|
||||
icon={<IconSave size='1.25rem' className='icon-primary' />}
|
||||
disabled={isProcessing}
|
||||
onClick={onSavePositions}
|
||||
onClick={handleSavePositions}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')}
|
||||
|
@ -149,18 +189,18 @@ export function ToolbarOssGraph({
|
|||
title='Активировать операцию'
|
||||
icon={<IconExecute size='1.25rem' className='icon-green' />}
|
||||
disabled={isProcessing || selected.length !== 1 || !readyForSynthesis}
|
||||
onClick={onExecute}
|
||||
onClick={handleOperationExecute}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Редактировать выбранную', 'Двойной клик')}
|
||||
icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
|
||||
disabled={selected.length !== 1 || isProcessing}
|
||||
onClick={onEdit}
|
||||
onClick={handleEditOperation}
|
||||
/>
|
||||
<MiniButton
|
||||
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -54,6 +54,7 @@ export function NodeCore({ node }: NodeCoreProps) {
|
|||
<div
|
||||
className='h-[34px] w-[144px] flex items-center justify-center'
|
||||
data-tooltip-id={globalIDs.operation_tooltip}
|
||||
data-tooltip-hidden={node.dragging}
|
||||
onMouseEnter={() => setHover(node.data.operation)}
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -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
|
||||
}));
|
||||
};
|
||||
}
|
|
@ -3,6 +3,7 @@ import { useAuthSuspense } from '@/features/auth';
|
|||
import { Button } from '@/components/Control';
|
||||
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
|
||||
import { IconChild, IconEdit2 } from '@/components/Icons';
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
|
||||
import { useMutatingOss } from '../../backend/useMutatingOss';
|
||||
|
||||
|
@ -11,12 +12,18 @@ import { useOssEdit } from './OssEditContext';
|
|||
export function MenuEditOss() {
|
||||
const { isAnonymous } = useAuthSuspense();
|
||||
const editMenu = useDropdown();
|
||||
const { promptRelocateConstituents, isMutable } = useOssEdit();
|
||||
const { schema, isMutable } = useOssEdit();
|
||||
const isProcessing = useMutatingOss();
|
||||
|
||||
const showRelocateConstituents = useDialogsStore(state => state.showRelocateConstituents);
|
||||
|
||||
function handleRelocate() {
|
||||
editMenu.hide();
|
||||
promptRelocateConstituents(undefined, []);
|
||||
showRelocateConstituents({
|
||||
oss: schema,
|
||||
initialTarget: undefined,
|
||||
positions: []
|
||||
});
|
||||
}
|
||||
|
||||
if (isAnonymous) {
|
||||
|
|
|
@ -9,13 +9,12 @@ import { useDeleteItem } from '@/features/library/backend/useDeleteItem';
|
|||
import { RSTabID } from '@/features/rsform/pages/RSFormPage/RSEditContext';
|
||||
import { useRoleStore, UserRole } from '@/features/users';
|
||||
|
||||
import { useDialogsStore } from '@/stores/dialogs';
|
||||
import { usePreferencesStore } from '@/stores/preferences';
|
||||
import { promptText } from '@/utils/labels';
|
||||
|
||||
import { type IOperationPosition, OperationType } from '../../backend/types';
|
||||
import { useOssSuspense } from '../../backend/useOSS';
|
||||
import { type IOperationSchema } from '../../models/oss';
|
||||
import { type IOperation, type IOperationSchema } from '../../models/oss';
|
||||
|
||||
export enum OssTabID {
|
||||
CARD = 0,
|
||||
|
@ -40,15 +39,9 @@ export interface IOssEditContext {
|
|||
navigateTab: (tab: OssTabID) => void;
|
||||
navigateOperationSchema: (target: number) => void;
|
||||
|
||||
canDeleteOperation: (target: IOperation) => boolean;
|
||||
deleteSchema: () => void;
|
||||
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);
|
||||
|
@ -81,12 +74,6 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
|
||||
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();
|
||||
|
||||
useEffect(
|
||||
|
@ -128,71 +115,11 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
});
|
||||
}
|
||||
|
||||
function promptCreateOperation({ defaultX, defaultY, inputs, positions, callback }: ICreateOperationPrompt) {
|
||||
showCreateOperation({
|
||||
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) {
|
||||
function canDeleteOperation(target: IOperation) {
|
||||
if (target.operation_type === OperationType.INPUT) {
|
||||
return true;
|
||||
}
|
||||
return schema.graph.expandOutputs([target]).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 schema.graph.expandOutputs([target.id]).length === 0;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -201,22 +128,15 @@ export const OssEditState = ({ itemID, children }: React.PropsWithChildren<OssEd
|
|||
schema,
|
||||
selected,
|
||||
|
||||
navigateTab,
|
||||
|
||||
deleteSchema,
|
||||
|
||||
isOwned,
|
||||
isMutable,
|
||||
|
||||
setSelected,
|
||||
|
||||
navigateTab,
|
||||
navigateOperationSchema,
|
||||
promptCreateOperation,
|
||||
canDelete,
|
||||
promptDeleteOperation,
|
||||
promptEditInput,
|
||||
promptEditOperation,
|
||||
promptRelocateConstituents
|
||||
|
||||
canDeleteOperation,
|
||||
deleteSchema,
|
||||
setSelected
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
Loading…
Reference in New Issue
Block a user