R: Improve ossFlow structure
This commit is contained in:
parent
12028471bd
commit
f820d84cb0
|
@ -1 +1,2 @@
|
||||||
export { ContextMenu } from './context-menu';
|
export { ContextMenu } from './context-menu';
|
||||||
|
export { useContextMenu } from './use-context-menu';
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useStoreApi } from 'reactflow';
|
||||||
|
|
||||||
|
import { type OssNode } from '../../../../models/oss-layout';
|
||||||
|
import { useOperationTooltipStore } from '../../../../stores/operation-tooltip';
|
||||||
|
|
||||||
|
import { type ContextMenuData } from './context-menu';
|
||||||
|
|
||||||
|
export function useContextMenu() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [menuProps, setMenuProps] = useState<ContextMenuData>({
|
||||||
|
item: null,
|
||||||
|
cursorX: 0,
|
||||||
|
cursorY: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
|
||||||
|
const { addSelectedNodes } = useStoreApi().getState();
|
||||||
|
|
||||||
|
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
addSelectedNodes([node.id]);
|
||||||
|
|
||||||
|
setMenuProps({
|
||||||
|
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
|
||||||
|
cursorX: event.clientX,
|
||||||
|
cursorY: event.clientY
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsOpen(true);
|
||||||
|
setHoverOperation(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideContextMenu() {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
menuProps,
|
||||||
|
handleContextMenu,
|
||||||
|
hideContextMenu
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { type Position2D } from '@/features/oss/models/oss-layout';
|
||||||
|
|
||||||
|
import { cn } from '@/components/utils';
|
||||||
|
|
||||||
|
interface CoordinateDisplayProps {
|
||||||
|
mouseCoords: Position2D;
|
||||||
|
className: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CoordinateDisplay({ mouseCoords, className }: CoordinateDisplayProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn('hover:bg-background backdrop-blur-xs text-sm font-math', className)}>
|
||||||
|
{`X: ${mouseCoords.x.toFixed(0)} Y: ${mouseCoords.y.toFixed(0)}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,20 +4,21 @@ import { NodeResizeControl } from 'reactflow';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { IconResize } from '@/components/icons';
|
import { IconResize } from '@/components/icons';
|
||||||
|
import { useDraggingStore } from '@/stores/dragging';
|
||||||
import { globalIDs } from '@/utils/constants';
|
import { globalIDs } from '@/utils/constants';
|
||||||
|
|
||||||
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
import { type BlockInternalNode } from '../../../../models/oss-layout';
|
||||||
import { useOperationTooltipStore } from '../../../../stores/operation-tooltip';
|
import { useOperationTooltipStore } from '../../../../stores/operation-tooltip';
|
||||||
import { useOSSGraphStore } from '../../../../stores/oss-graph';
|
import { useOSSGraphStore } from '../../../../stores/oss-graph';
|
||||||
import { useOssEdit } from '../../oss-edit-context';
|
import { useOssEdit } from '../../oss-edit-context';
|
||||||
import { useOssFlow } from '../oss-flow-context';
|
|
||||||
|
|
||||||
export const BLOCK_NODE_MIN_WIDTH = 160;
|
export const BLOCK_NODE_MIN_WIDTH = 160;
|
||||||
export const BLOCK_NODE_MIN_HEIGHT = 100;
|
export const BLOCK_NODE_MIN_HEIGHT = 100;
|
||||||
|
|
||||||
export function BlockNode(node: BlockInternalNode) {
|
export function BlockNode(node: BlockInternalNode) {
|
||||||
const { selected, schema } = useOssEdit();
|
const { selected, schema } = useOssEdit();
|
||||||
const { dropTarget, isDragging } = useOssFlow();
|
const dropTarget = useDraggingStore(state => state.dropTarget);
|
||||||
|
const isDragging = useDraggingStore(state => state.isDragging);
|
||||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const setHover = useOperationTooltipStore(state => state.setHoverItem);
|
const setHover = useOperationTooltipStore(state => state.setHoverItem);
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, use } from 'react';
|
import { createContext, use } from 'react';
|
||||||
|
import { type Edge, type EdgeChange, type Node, type NodeChange } from 'reactflow';
|
||||||
|
|
||||||
interface IOssFlowContext {
|
interface IOssFlowContext {
|
||||||
isDragging: boolean;
|
|
||||||
setIsDragging: React.Dispatch<React.SetStateAction<boolean>>;
|
|
||||||
dropTarget: number | null;
|
|
||||||
setDropTarget: React.Dispatch<React.SetStateAction<number | null>>;
|
|
||||||
containMovement: boolean;
|
containMovement: boolean;
|
||||||
setContainMovement: React.Dispatch<React.SetStateAction<boolean>>;
|
setContainMovement: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
|
||||||
|
nodes: Node[];
|
||||||
|
setNodes: React.Dispatch<React.SetStateAction<Node[]>>;
|
||||||
|
onNodesChange: (changes: NodeChange[]) => void;
|
||||||
|
edges: Edge[];
|
||||||
|
setEdges: React.Dispatch<React.SetStateAction<Edge[]>>;
|
||||||
|
onEdgesChange: (changes: EdgeChange[]) => void;
|
||||||
|
resetGraph: () => void;
|
||||||
|
resetView: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OssFlowContext = createContext<IOssFlowContext | null>(null);
|
export const OssFlowContext = createContext<IOssFlowContext | null>(null);
|
||||||
|
|
|
@ -1,28 +1,137 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { type Edge, type Node, useEdgesState, useNodesState, useOnSelectionChange, useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
|
import { type IOperationSchema } from '@/features/oss/models/oss';
|
||||||
|
import { type Position2D } from '@/features/oss/models/oss-layout';
|
||||||
|
import { useOSSGraphStore } from '@/features/oss/stores/oss-graph';
|
||||||
|
|
||||||
|
import { PARAMETER } from '@/utils/constants';
|
||||||
|
|
||||||
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { OssFlowContext } from './oss-flow-context';
|
import { OssFlowContext } from './oss-flow-context';
|
||||||
|
|
||||||
|
const VIEW_PADDING = 0.2;
|
||||||
|
|
||||||
|
const Z_BLOCK = 1;
|
||||||
|
const Z_SCHEMA = 10;
|
||||||
|
|
||||||
|
// TODO: decouple nodes and edges from controller callbacks
|
||||||
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
export const OssFlowState = ({ children }: React.PropsWithChildren) => {
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const { schema, setSelected } = useOssEdit();
|
||||||
const [dropTarget, setDropTarget] = useState<number | null>(null);
|
const { fitView } = useReactFlow();
|
||||||
|
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
||||||
|
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
||||||
|
|
||||||
const [containMovement, setContainMovement] = useState(false);
|
const [containMovement, setContainMovement] = useState(false);
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
|
||||||
|
|
||||||
|
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||||
|
const ids = nodes.map(node => Number(node.id));
|
||||||
|
setSelected(prev => [
|
||||||
|
...prev.filter(nodeID => ids.includes(nodeID)),
|
||||||
|
...ids.filter(nodeID => !prev.includes(Number(nodeID)))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
useOnSelectionChange({
|
||||||
|
onChange: onSelectionChange
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetGraph = useCallback(() => {
|
||||||
|
const newNodes: Node[] = [
|
||||||
|
...schema.hierarchy
|
||||||
|
.topologicalOrder()
|
||||||
|
.filter(id => id < 0)
|
||||||
|
.map(id => {
|
||||||
|
const block = schema.blockByID.get(-id)!;
|
||||||
|
return {
|
||||||
|
id: String(id),
|
||||||
|
type: 'block',
|
||||||
|
data: { label: block.title, block: block },
|
||||||
|
position: computeRelativePosition(schema, { x: block.x, y: block.y }, block.parent),
|
||||||
|
style: {
|
||||||
|
width: block.width,
|
||||||
|
height: block.height
|
||||||
|
},
|
||||||
|
parentId: block.parent ? `-${block.parent}` : undefined,
|
||||||
|
zIndex: Z_BLOCK
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...schema.operations.map(operation => ({
|
||||||
|
id: String(operation.id),
|
||||||
|
type: operation.operation_type.toString(),
|
||||||
|
data: { label: operation.alias, operation: operation },
|
||||||
|
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
|
||||||
|
parentId: operation.parent ? `-${operation.parent}` : undefined,
|
||||||
|
zIndex: Z_SCHEMA
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
const newEdges: Edge[] = schema.arguments.map((argument, index) => ({
|
||||||
|
id: String(index),
|
||||||
|
source: String(argument.argument),
|
||||||
|
target: String(argument.operation),
|
||||||
|
type: edgeStraight ? 'straight' : 'simplebezier',
|
||||||
|
animated: edgeAnimate,
|
||||||
|
targetHandle:
|
||||||
|
schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x
|
||||||
|
? 'right'
|
||||||
|
: 'left'
|
||||||
|
}));
|
||||||
|
|
||||||
|
setNodes(newNodes);
|
||||||
|
setEdges(newEdges);
|
||||||
|
|
||||||
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
||||||
|
}, [schema, setNodes, setEdges, edgeAnimate, edgeStraight, fitView]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetGraph();
|
||||||
|
}, [schema, edgeAnimate, edgeStraight, resetGraph]);
|
||||||
|
|
||||||
|
function resetView() {
|
||||||
|
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OssFlowContext
|
<OssFlowContext
|
||||||
value={{
|
value={{
|
||||||
isDragging,
|
|
||||||
setIsDragging,
|
|
||||||
|
|
||||||
dropTarget,
|
|
||||||
setDropTarget,
|
|
||||||
|
|
||||||
containMovement,
|
containMovement,
|
||||||
setContainMovement
|
setContainMovement,
|
||||||
|
|
||||||
|
nodes,
|
||||||
|
setNodes,
|
||||||
|
onNodesChange,
|
||||||
|
edges,
|
||||||
|
setEdges,
|
||||||
|
onEdgesChange,
|
||||||
|
|
||||||
|
resetView,
|
||||||
|
resetGraph
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</OssFlowContext>
|
</OssFlowContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ====== Internals =========
|
||||||
|
function computeRelativePosition(schema: IOperationSchema, position: Position2D, parent: number | null): Position2D {
|
||||||
|
if (!parent) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentBlock = schema.blockByID.get(parent);
|
||||||
|
if (!parentBlock) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: position.x - parentBlock.x,
|
||||||
|
y: position.y - parentBlock.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,85 +1,49 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import { Background, ReactFlow, useReactFlow, useStoreApi } from 'reactflow';
|
||||||
Background,
|
|
||||||
type Node,
|
|
||||||
ReactFlow,
|
|
||||||
useEdgesState,
|
|
||||||
useNodesState,
|
|
||||||
useOnSelectionChange,
|
|
||||||
useReactFlow,
|
|
||||||
useStoreApi
|
|
||||||
} from 'reactflow';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useThrottleCallback } from '@/hooks/use-throttle-callback';
|
|
||||||
import { useMainHeight } from '@/stores/app-layout';
|
import { useMainHeight } from '@/stores/app-layout';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
import { promptText } from '@/utils/labels';
|
import { promptText } from '@/utils/labels';
|
||||||
|
|
||||||
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
import { useDeleteBlock } from '../../../backend/use-delete-block';
|
||||||
import { useMoveItems } from '../../../backend/use-move-items';
|
|
||||||
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
import { useMutatingOss } from '../../../backend/use-mutating-oss';
|
||||||
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||||
import { type IOperationSchema } from '../../../models/oss';
|
|
||||||
import { type OssNode, type Position2D } from '../../../models/oss-layout';
|
import { type OssNode, type Position2D } from '../../../models/oss-layout';
|
||||||
import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api';
|
import { GRID_SIZE, LayoutManager } from '../../../models/oss-layout-api';
|
||||||
import { useOperationTooltipStore } from '../../../stores/operation-tooltip';
|
|
||||||
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
import { useOSSGraphStore } from '../../../stores/oss-graph';
|
||||||
import { useOssEdit } from '../oss-edit-context';
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { ContextMenu, type ContextMenuData } from './context-menu/context-menu';
|
import { ContextMenu } from './context-menu/context-menu';
|
||||||
|
import { useContextMenu } from './context-menu/use-context-menu';
|
||||||
import { OssNodeTypes } from './graph/oss-node-types';
|
import { OssNodeTypes } from './graph/oss-node-types';
|
||||||
|
import { CoordinateDisplay } from './coordinate-display';
|
||||||
import { useOssFlow } from './oss-flow-context';
|
import { useOssFlow } from './oss-flow-context';
|
||||||
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
import { ToolbarOssGraph } from './toolbar-oss-graph';
|
||||||
|
import { useDragging } from './use-dragging';
|
||||||
import { useGetLayout } from './use-get-layout';
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
|
||||||
const ZOOM_MAX = 2;
|
const ZOOM_MAX = 2;
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
|
|
||||||
const Z_BLOCK = 1;
|
|
||||||
const Z_SCHEMA = 10;
|
|
||||||
|
|
||||||
const DRAG_THROTTLE_DELAY = 50; // ms
|
|
||||||
|
|
||||||
export const VIEW_PADDING = 0.2;
|
|
||||||
|
|
||||||
export function OssFlow() {
|
export function OssFlow() {
|
||||||
const mainHeight = useMainHeight();
|
const mainHeight = useMainHeight();
|
||||||
const {
|
const { navigateOperationSchema, schema, selected, isMutable, canDeleteOperation } = useOssEdit();
|
||||||
navigateOperationSchema,
|
const { screenToFlowPosition } = useReactFlow();
|
||||||
schema,
|
const { containMovement, nodes, onNodesChange, edges, onEdgesChange, resetGraph, resetView } = useOssFlow();
|
||||||
setSelected,
|
|
||||||
selected,
|
|
||||||
isMutable,
|
|
||||||
canDeleteOperation: canDelete
|
|
||||||
} = useOssEdit();
|
|
||||||
const { fitView, screenToFlowPosition, getIntersectingNodes } = useReactFlow();
|
|
||||||
const { setDropTarget, setContainMovement, containMovement, setIsDragging } = useOssFlow();
|
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { resetSelectedElements, addSelectedNodes } = store.getState();
|
const { resetSelectedElements } = store.getState();
|
||||||
|
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
|
|
||||||
const setHoverOperation = useOperationTooltipStore(state => state.setHoverItem);
|
|
||||||
|
|
||||||
const showGrid = useOSSGraphStore(state => state.showGrid);
|
const showGrid = useOSSGraphStore(state => state.showGrid);
|
||||||
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
|
||||||
const edgeAnimate = useOSSGraphStore(state => state.edgeAnimate);
|
|
||||||
const edgeStraight = useOSSGraphStore(state => state.edgeStraight);
|
|
||||||
|
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
const { updateLayout } = useUpdateLayout();
|
const { updateLayout } = useUpdateLayout();
|
||||||
const { deleteBlock } = useDeleteBlock();
|
const { deleteBlock } = useDeleteBlock();
|
||||||
const { moveItems } = useMoveItems();
|
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
||||||
const [toggleReset, setToggleReset] = useState(false);
|
|
||||||
const [menuProps, setMenuProps] = useState<ContextMenuData>({ item: null, cursorX: 0, cursorY: 0 });
|
|
||||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
|
const [mouseCoords, setMouseCoords] = useState<Position2D>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
@ -88,62 +52,8 @@ export function OssFlow() {
|
||||||
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
const showDeleteOperation = useDialogsStore(state => state.showDeleteOperation);
|
||||||
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
const showEditBlock = useDialogsStore(state => state.showEditBlock);
|
||||||
|
|
||||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
const { isOpen: isContextMenuOpen, menuProps, handleContextMenu, hideContextMenu } = useContextMenu();
|
||||||
const ids = nodes.map(node => Number(node.id));
|
const { handleDragStart, handleDrag, handleDragStop } = useDragging({ hideContextMenu });
|
||||||
setSelected(prev => [
|
|
||||||
...prev.filter(nodeID => ids.includes(nodeID)),
|
|
||||||
...ids.filter(nodeID => !prev.includes(Number(nodeID)))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
useOnSelectionChange({
|
|
||||||
onChange: onSelectionChange
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNodes([
|
|
||||||
...schema.hierarchy
|
|
||||||
.topologicalOrder()
|
|
||||||
.filter(id => id < 0)
|
|
||||||
.map(id => {
|
|
||||||
const block = schema.blockByID.get(-id)!;
|
|
||||||
return {
|
|
||||||
id: String(id),
|
|
||||||
type: 'block',
|
|
||||||
data: { label: block.title, block: block },
|
|
||||||
position: computeRelativePosition(schema, { x: block.x, y: block.y }, block.parent),
|
|
||||||
style: {
|
|
||||||
width: block.width,
|
|
||||||
height: block.height
|
|
||||||
},
|
|
||||||
parentId: block.parent ? `-${block.parent}` : undefined,
|
|
||||||
zIndex: Z_BLOCK
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
...schema.operations.map(operation => ({
|
|
||||||
id: String(operation.id),
|
|
||||||
type: operation.operation_type.toString(),
|
|
||||||
data: { label: operation.alias, operation: operation },
|
|
||||||
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
|
|
||||||
parentId: operation.parent ? `-${operation.parent}` : undefined,
|
|
||||||
zIndex: Z_SCHEMA
|
|
||||||
}))
|
|
||||||
]);
|
|
||||||
setEdges(
|
|
||||||
schema.arguments.map((argument, index) => ({
|
|
||||||
id: String(index),
|
|
||||||
source: String(argument.argument),
|
|
||||||
target: String(argument.operation),
|
|
||||||
type: edgeStraight ? 'straight' : 'simplebezier',
|
|
||||||
animated: edgeAnimate,
|
|
||||||
targetHandle:
|
|
||||||
schema.operationByID.get(argument.argument)!.x > schema.operationByID.get(argument.operation)!.x
|
|
||||||
? 'right'
|
|
||||||
: 'left'
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout);
|
|
||||||
}, [schema, setNodes, setEdges, toggleReset, edgeStraight, edgeAnimate, fitView]);
|
|
||||||
|
|
||||||
function handleSavePositions() {
|
function handleSavePositions() {
|
||||||
void updateLayout({ itemID: schema.id, data: getLayout() });
|
void updateLayout({ itemID: schema.id, data: getLayout() });
|
||||||
|
@ -157,8 +67,7 @@ export function OssFlow() {
|
||||||
defaultY: targetPosition.y,
|
defaultY: targetPosition.y,
|
||||||
initialInputs: selected.filter(id => id > 0),
|
initialInputs: selected.filter(id => id > 0),
|
||||||
initialParent: extractSingleBlock(selected),
|
initialParent: extractSingleBlock(selected),
|
||||||
onCreate: () =>
|
onCreate: resetView
|
||||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,8 +78,7 @@ export function OssFlow() {
|
||||||
defaultX: targetPosition.x,
|
defaultX: targetPosition.x,
|
||||||
defaultY: targetPosition.y,
|
defaultY: targetPosition.y,
|
||||||
initialInputs: selected,
|
initialInputs: selected,
|
||||||
onCreate: () =>
|
onCreate: resetView
|
||||||
setTimeout(() => fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING }), PARAMETER.refreshTimeout)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +88,7 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
if (selected[0] > 0) {
|
if (selected[0] > 0) {
|
||||||
const operation = schema.operationByID.get(selected[0]);
|
const operation = schema.operationByID.get(selected[0]);
|
||||||
if (!operation || !canDelete(operation)) {
|
if (!operation || !canDeleteOperation(operation)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showDeleteOperation({
|
showDeleteOperation({
|
||||||
|
@ -200,21 +108,6 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContextMenu(event: React.MouseEvent<Element>, node: OssNode) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
addSelectedNodes([node.id]);
|
|
||||||
|
|
||||||
setMenuProps({
|
|
||||||
item: node.type === 'block' ? node.data.block ?? null : node.data.operation ?? null,
|
|
||||||
cursorX: event.clientX,
|
|
||||||
cursorY: event.clientY
|
|
||||||
});
|
|
||||||
setIsContextMenuOpen(true);
|
|
||||||
setHoverOperation(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
function handleNodeDoubleClick(event: React.MouseEvent<Element>, node: OssNode) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -276,114 +169,6 @@ export function OssFlow() {
|
||||||
setMouseCoords(targetPosition);
|
setMouseCoords(targetPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
function determineDropTarget(event: React.MouseEvent): number | null {
|
|
||||||
const mousePosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
|
||||||
let blocks = getIntersectingNodes({
|
|
||||||
x: mousePosition.x,
|
|
||||||
y: mousePosition.y,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
})
|
|
||||||
.map(node => Number(node.id))
|
|
||||||
.filter(id => id < 0 && !selected.includes(id))
|
|
||||||
.map(id => schema.blockByID.get(-id))
|
|
||||||
.filter(block => !!block);
|
|
||||||
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const successors = schema.hierarchy.expandAllOutputs([...selected]).filter(id => id < 0);
|
|
||||||
blocks = blocks.filter(block => !successors.includes(-block.id));
|
|
||||||
if (blocks.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (blocks.length === 1) {
|
|
||||||
return blocks[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parents = blocks.map(block => block.parent).filter(id => !!id);
|
|
||||||
const potentialTargets = blocks.map(block => block.id).filter(id => !parents.includes(id));
|
|
||||||
if (potentialTargets.length === 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return potentialTargets[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragStart(event: React.MouseEvent, target: Node) {
|
|
||||||
if (event.shiftKey) {
|
|
||||||
setContainMovement(true);
|
|
||||||
setNodes(prev =>
|
|
||||||
prev.map(node =>
|
|
||||||
node.id === target.id || selected.includes(Number(node.id))
|
|
||||||
? {
|
|
||||||
...node,
|
|
||||||
extent: 'parent',
|
|
||||||
expandParent: true
|
|
||||||
}
|
|
||||||
: node
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setContainMovement(false);
|
|
||||||
setDropTarget(determineDropTarget(event));
|
|
||||||
}
|
|
||||||
setIsContextMenuOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDrag = useThrottleCallback((event: React.MouseEvent) => {
|
|
||||||
if (containMovement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsDragging(true);
|
|
||||||
setDropTarget(determineDropTarget(event));
|
|
||||||
}, DRAG_THROTTLE_DELAY);
|
|
||||||
|
|
||||||
function handleDragStop(event: React.MouseEvent, target: Node) {
|
|
||||||
if (containMovement) {
|
|
||||||
setNodes(prev =>
|
|
||||||
prev.map(node =>
|
|
||||||
node.id === target.id || selected.includes(Number(node.id))
|
|
||||||
? {
|
|
||||||
...node,
|
|
||||||
extent: undefined,
|
|
||||||
expandParent: undefined
|
|
||||||
}
|
|
||||||
: node
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const new_parent = determineDropTarget(event);
|
|
||||||
const allSelected = [...selected.filter(id => id != Number(target.id)), Number(target.id)];
|
|
||||||
const operations = allSelected
|
|
||||||
.filter(id => id > 0)
|
|
||||||
.map(id => schema.operationByID.get(id))
|
|
||||||
.filter(operation => !!operation);
|
|
||||||
const blocks = allSelected
|
|
||||||
.filter(id => id < 0)
|
|
||||||
.map(id => schema.blockByID.get(-id))
|
|
||||||
.filter(operation => !!operation);
|
|
||||||
const parents = [...blocks.map(block => block.parent), ...operations.map(operation => operation.parent)].filter(
|
|
||||||
id => !!id
|
|
||||||
);
|
|
||||||
if ((parents.length !== 1 || parents[0] !== new_parent) && (parents.length !== 0 || new_parent !== null)) {
|
|
||||||
void moveItems({
|
|
||||||
itemID: schema.id,
|
|
||||||
data: {
|
|
||||||
layout: getLayout(),
|
|
||||||
operations: operations.map(operation => operation.id),
|
|
||||||
blocks: blocks.map(block => block.id),
|
|
||||||
destination: new_parent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setIsDragging(false);
|
|
||||||
setContainMovement(false);
|
|
||||||
setDropTarget(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
@ -391,20 +176,16 @@ export function OssFlow() {
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onMouseMove={showCoordinates ? handleMouseMove : undefined}
|
onMouseMove={showCoordinates ? handleMouseMove : undefined}
|
||||||
>
|
>
|
||||||
{showCoordinates ? (
|
{showCoordinates ? <CoordinateDisplay mouseCoords={mouseCoords} className='absolute top-1 right-2' /> : null}
|
||||||
<div className='absolute top-1 right-2 hover:bg-background backdrop-blur-xs text-sm font-math'>
|
|
||||||
{`X: ${mouseCoords.x.toFixed(0)} Y: ${mouseCoords.y.toFixed(0)}`}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<ToolbarOssGraph
|
<ToolbarOssGraph
|
||||||
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
className='absolute z-pop top-8 right-1/2 translate-x-1/2'
|
||||||
onCreateOperation={handleCreateOperation}
|
onCreateOperation={handleCreateOperation}
|
||||||
onCreateBlock={handleCreateBlock}
|
onCreateBlock={handleCreateBlock}
|
||||||
onDelete={handleDeleteSelected}
|
onDelete={handleDeleteSelected}
|
||||||
onResetPositions={() => setToggleReset(prev => !prev)}
|
onResetPositions={resetGraph}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContextMenu isOpen={isContextMenuOpen} onHide={() => setIsContextMenuOpen(false)} {...menuProps} />
|
<ContextMenu isOpen={isContextMenuOpen} onHide={hideContextMenu} {...menuProps} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx('relative w-[100vw] cc-mask-sides', !containMovement && 'cursor-relocate')}
|
className={clsx('relative w-[100vw] cc-mask-sides', !containMovement && 'cursor-relocate')}
|
||||||
|
@ -424,10 +205,13 @@ export function OssFlow() {
|
||||||
nodesConnectable={false}
|
nodesConnectable={false}
|
||||||
snapToGrid={true}
|
snapToGrid={true}
|
||||||
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
snapGrid={[GRID_SIZE, GRID_SIZE]}
|
||||||
onClick={() => setIsContextMenuOpen(false)}
|
onClick={hideContextMenu}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeContextMenu={handleContextMenu}
|
onNodeContextMenu={handleContextMenu}
|
||||||
onContextMenu={event => event.preventDefault()}
|
onContextMenu={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
hideContextMenu();
|
||||||
|
}}
|
||||||
onNodeDragStart={handleDragStart}
|
onNodeDragStart={handleDragStart}
|
||||||
onNodeDrag={handleDrag}
|
onNodeDrag={handleDrag}
|
||||||
onNodeDragStop={handleDragStop}
|
onNodeDragStop={handleDragStop}
|
||||||
|
@ -440,22 +224,6 @@ export function OssFlow() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Internals --------
|
// -------- Internals --------
|
||||||
function computeRelativePosition(schema: IOperationSchema, position: Position2D, parent: number | null): Position2D {
|
|
||||||
if (!parent) {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentBlock = schema.blockByID.get(parent);
|
|
||||||
if (!parentBlock) {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: position.x - parentBlock.x,
|
|
||||||
y: position.y - parentBlock.y
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractSingleBlock(selected: number[]): number | null {
|
function extractSingleBlock(selected: number[]): number | null {
|
||||||
const blocks = selected.filter(id => id < 0);
|
const blocks = selected.filter(id => id < 0);
|
||||||
return blocks.length === 1 ? -blocks[0] : null;
|
return blocks.length === 1 ? -blocks[0] : null;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useReactFlow } from 'reactflow';
|
|
||||||
|
|
||||||
import { HelpTopic } from '@/features/help';
|
import { HelpTopic } from '@/features/help';
|
||||||
import { BadgeHelp } from '@/features/help/components';
|
import { BadgeHelp } from '@/features/help/components';
|
||||||
|
@ -22,7 +21,6 @@ import {
|
||||||
import { type Styling } from '@/components/props';
|
import { type Styling } from '@/components/props';
|
||||||
import { cn } from '@/components/utils';
|
import { cn } from '@/components/utils';
|
||||||
import { useDialogsStore } from '@/stores/dialogs';
|
import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
import { OperationType } from '../../../backend/types';
|
import { OperationType } from '../../../backend/types';
|
||||||
|
@ -32,7 +30,7 @@ import { useUpdateLayout } from '../../../backend/use-update-layout';
|
||||||
import { LayoutManager } from '../../../models/oss-layout-api';
|
import { LayoutManager } from '../../../models/oss-layout-api';
|
||||||
import { useOssEdit } from '../oss-edit-context';
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
import { VIEW_PADDING } from './oss-flow';
|
import { useOssFlow } from './oss-flow-context';
|
||||||
import { useGetLayout } from './use-get-layout';
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
|
||||||
interface ToolbarOssGraphProps extends Styling {
|
interface ToolbarOssGraphProps extends Styling {
|
||||||
|
@ -52,7 +50,7 @@ export function ToolbarOssGraph({
|
||||||
}: ToolbarOssGraphProps) {
|
}: ToolbarOssGraphProps) {
|
||||||
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
const { schema, selected, isMutable, canDeleteOperation: canDelete } = useOssEdit();
|
||||||
const isProcessing = useMutatingOss();
|
const isProcessing = useMutatingOss();
|
||||||
const { fitView } = useReactFlow();
|
const { resetView } = useOssFlow();
|
||||||
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null;
|
const selectedOperation = selected.length !== 1 ? null : schema.operationByID.get(selected[0]) ?? null;
|
||||||
const selectedBlock = selected.length !== 1 ? null : schema.blockByID.get(-selected[0]) ?? null;
|
const selectedBlock = selected.length !== 1 ? null : schema.blockByID.get(-selected[0]) ?? null;
|
||||||
const getLayout = useGetLayout();
|
const getLayout = useGetLayout();
|
||||||
|
@ -85,10 +83,6 @@ export function ToolbarOssGraph({
|
||||||
return true;
|
return true;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function handleFitView() {
|
|
||||||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFixLayout() {
|
function handleFixLayout() {
|
||||||
// TODO: implement layout algorithm
|
// TODO: implement layout algorithm
|
||||||
toast.info('Еще не реализовано');
|
toast.info('Еще не реализовано');
|
||||||
|
@ -145,7 +139,7 @@ export function ToolbarOssGraph({
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Сбросить вид'
|
title='Сбросить вид'
|
||||||
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
|
||||||
onClick={handleFitView}
|
onClick={resetView}
|
||||||
/>
|
/>
|
||||||
<MiniButton
|
<MiniButton
|
||||||
title='Исправить позиции узлов'
|
title='Исправить позиции узлов'
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { type Node } from 'reactflow';
|
||||||
|
|
||||||
|
import { useMoveItems } from '@/features/oss/backend/use-move-items';
|
||||||
|
|
||||||
|
import { useThrottleCallback } from '@/hooks/use-throttle-callback';
|
||||||
|
import { useDraggingStore } from '@/stores/dragging';
|
||||||
|
|
||||||
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
|
import { useOssFlow } from './oss-flow-context';
|
||||||
|
import { useDropTarget } from './use-drop-target';
|
||||||
|
import { useGetLayout } from './use-get-layout';
|
||||||
|
|
||||||
|
const DRAG_THROTTLE_DELAY = 50; // ms
|
||||||
|
|
||||||
|
interface DraggingProps {
|
||||||
|
hideContextMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hook to encapsulate dragging logic. */
|
||||||
|
export function useDragging({ hideContextMenu }: DraggingProps) {
|
||||||
|
const { setContainMovement, containMovement, setNodes } = useOssFlow();
|
||||||
|
const setIsDragging = useDraggingStore(state => state.setIsDragging);
|
||||||
|
const getLayout = useGetLayout();
|
||||||
|
const { selected, schema } = useOssEdit();
|
||||||
|
const dropTarget = useDropTarget();
|
||||||
|
const { moveItems } = useMoveItems();
|
||||||
|
|
||||||
|
function applyContainMovement(target: string[], value: boolean) {
|
||||||
|
setNodes(prev =>
|
||||||
|
prev.map(node =>
|
||||||
|
target.includes(node.id)
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
extent: value ? 'parent' : undefined,
|
||||||
|
expandParent: value ? true : undefined
|
||||||
|
}
|
||||||
|
: node
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event: React.MouseEvent, target: Node) {
|
||||||
|
if (event.shiftKey) {
|
||||||
|
setContainMovement(true);
|
||||||
|
applyContainMovement([target.id, ...selected.map(id => String(id))], true);
|
||||||
|
} else {
|
||||||
|
setContainMovement(false);
|
||||||
|
dropTarget.update(event);
|
||||||
|
}
|
||||||
|
hideContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrag = useThrottleCallback((event: React.MouseEvent) => {
|
||||||
|
if (containMovement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsDragging(true);
|
||||||
|
dropTarget.update(event);
|
||||||
|
}, DRAG_THROTTLE_DELAY);
|
||||||
|
|
||||||
|
function handleDragStop(event: React.MouseEvent, target: Node) {
|
||||||
|
if (containMovement) {
|
||||||
|
applyContainMovement([target.id, ...selected.map(id => String(id))], false);
|
||||||
|
} else {
|
||||||
|
const new_parent = dropTarget.evaluate(event);
|
||||||
|
const allSelected = [...selected.filter(id => id != Number(target.id)), Number(target.id)];
|
||||||
|
const operations = allSelected
|
||||||
|
.filter(id => id > 0)
|
||||||
|
.map(id => schema.operationByID.get(id))
|
||||||
|
.filter(operation => !!operation);
|
||||||
|
const blocks = allSelected
|
||||||
|
.filter(id => id < 0)
|
||||||
|
.map(id => schema.blockByID.get(-id))
|
||||||
|
.filter(operation => !!operation);
|
||||||
|
const parents = [...blocks.map(block => block.parent), ...operations.map(operation => operation.parent)].filter(
|
||||||
|
id => !!id
|
||||||
|
);
|
||||||
|
if ((parents.length !== 1 || parents[0] !== new_parent) && (parents.length !== 0 || new_parent !== null)) {
|
||||||
|
void moveItems({
|
||||||
|
itemID: schema.id,
|
||||||
|
data: {
|
||||||
|
layout: getLayout(),
|
||||||
|
operations: operations.map(operation => operation.id),
|
||||||
|
blocks: blocks.map(block => block.id),
|
||||||
|
destination: new_parent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDragging(false);
|
||||||
|
setContainMovement(false);
|
||||||
|
dropTarget.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDragStart,
|
||||||
|
handleDrag,
|
||||||
|
handleDragStop
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { useReactFlow } from 'reactflow';
|
||||||
|
|
||||||
|
import { useDraggingStore } from '@/stores/dragging';
|
||||||
|
|
||||||
|
import { useOssEdit } from '../oss-edit-context';
|
||||||
|
|
||||||
|
/** Hook to encapsulate drop target logic. */
|
||||||
|
export function useDropTarget() {
|
||||||
|
const { getIntersectingNodes, screenToFlowPosition } = useReactFlow();
|
||||||
|
const { selected, schema } = useOssEdit();
|
||||||
|
const dropTarget = useDraggingStore(state => state.dropTarget);
|
||||||
|
const setDropTarget = useDraggingStore(state => state.setDropTarget);
|
||||||
|
|
||||||
|
function evaluate(event: React.MouseEvent): number | null {
|
||||||
|
const mousePosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
||||||
|
let blocks = getIntersectingNodes({
|
||||||
|
x: mousePosition.x,
|
||||||
|
y: mousePosition.y,
|
||||||
|
width: 1,
|
||||||
|
height: 1
|
||||||
|
})
|
||||||
|
.map(node => Number(node.id))
|
||||||
|
.filter(id => id < 0 && !selected.includes(id))
|
||||||
|
.map(id => schema.blockByID.get(-id))
|
||||||
|
.filter(block => !!block);
|
||||||
|
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const successors = schema.hierarchy.expandAllOutputs([...selected]).filter(id => id < 0);
|
||||||
|
blocks = blocks.filter(block => !successors.includes(-block.id));
|
||||||
|
if (blocks.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (blocks.length === 1) {
|
||||||
|
return blocks[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parents = blocks.map(block => block.parent).filter(id => !!id);
|
||||||
|
const potentialTargets = blocks.map(block => block.id).filter(id => !parents.includes(id));
|
||||||
|
if (potentialTargets.length === 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return potentialTargets[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(event: React.MouseEvent) {
|
||||||
|
setDropTarget(evaluate(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
setDropTarget(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get() {
|
||||||
|
return dropTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { get, update, reset, evaluate };
|
||||||
|
}
|
15
rsconcept/frontend/src/stores/dragging.ts
Normal file
15
rsconcept/frontend/src/stores/dragging.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface DraggingStore {
|
||||||
|
isDragging: boolean;
|
||||||
|
setIsDragging: (value: boolean) => void;
|
||||||
|
dropTarget: number | null;
|
||||||
|
setDropTarget: (value: number | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDraggingStore = create<DraggingStore>()(set => ({
|
||||||
|
isDragging: false,
|
||||||
|
setIsDragging: value => set({ isDragging: value }),
|
||||||
|
dropTarget: null,
|
||||||
|
setDropTarget: value => set({ dropTarget: value })
|
||||||
|
}));
|
Loading…
Reference in New Issue
Block a user