diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/editor-oss-graph.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/editor-oss-graph.tsx
index 8c206c30..066aa2a0 100644
--- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/editor-oss-graph.tsx
+++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/editor-oss-graph.tsx
@@ -3,11 +3,14 @@
import { ReactFlowProvider } from 'reactflow';
import { OssFlow } from './oss-flow';
+import { OssFlowState } from './oss-flow-state';
export function EditorOssGraph() {
return (
-
+
+
+
);
}
diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx
index 3ddf347a..d3b76bc7 100644
--- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx
+++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/graph/block-node.tsx
@@ -11,12 +11,14 @@ import { globalIDs } from '@/utils/constants';
import { type BlockInternalNode } from '../../../../models/oss-layout';
import { useOssEdit } from '../../oss-edit-context';
+import { useOssFlow } from '../oss-flow-context';
export const BLOCK_NODE_MIN_WIDTH = 160;
export const BLOCK_NODE_MIN_HEIGHT = 100;
export function BlockNode(node: BlockInternalNode) {
const { selected, schema } = useOssEdit();
+ const { dropTarget } = useOssFlow();
const showCoordinates = useOSSGraphStore(state => state.showCoordinates);
const setHover = useOperationTooltipStore(state => state.setHoverItem);
@@ -42,7 +44,8 @@ export function BlockNode(node: BlockInternalNode) {
diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow-context.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow-context.tsx
new file mode 100644
index 00000000..2171ddfb
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow-context.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { createContext, use } from 'react';
+
+interface IOssFlowContext {
+ dropTarget: number | null;
+ setDropTarget: React.Dispatch
>;
+ containMovement: boolean;
+ setContainMovement: React.Dispatch>;
+}
+
+export const OssFlowContext = createContext(null);
+export const useOssFlow = () => {
+ const context = use(OssFlowContext);
+ if (context === null) {
+ throw new Error('useOssFlow has to be used within ');
+ }
+ return context;
+};
diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow-state.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow-state.tsx
new file mode 100644
index 00000000..980122e2
--- /dev/null
+++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow-state.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import { useState } from 'react';
+
+import { OssFlowContext } from './oss-flow-context';
+
+export const OssFlowState = ({ children }: React.PropsWithChildren) => {
+ const [dropTarget, setDropTarget] = useState(null);
+ const [containMovement, setContainMovement] = useState(false);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx
index 29953dd3..61ca52a1 100644
--- a/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx
+++ b/rsconcept/frontend/src/features/oss/pages/oss-page/editor-oss-graph/oss-flow.tsx
@@ -11,6 +11,7 @@ import {
useReactFlow,
useStoreApi
} from 'reactflow';
+import clsx from 'clsx';
import { useDeleteBlock } from '@/features/oss/backend/use-delete-block';
import { type IOperationSchema } from '@/features/oss/models/oss';
@@ -30,6 +31,7 @@ import { useOssEdit } from '../oss-edit-context';
import { ContextMenu, type ContextMenuData } from './context-menu/context-menu';
import { OssNodeTypes } from './graph/oss-node-types';
+import { useOssFlow } from './oss-flow-context';
import { ToolbarOssGraph } from './toolbar-oss-graph';
import { useGetLayout } from './use-get-layout';
@@ -51,7 +53,8 @@ export function OssFlow() {
isMutable,
canDeleteOperation: canDelete
} = useOssEdit();
- const { fitView, screenToFlowPosition } = useReactFlow();
+ const { fitView, screenToFlowPosition, getIntersectingNodes } = useReactFlow();
+ const { setDropTarget, setContainMovement, containMovement } = useOssFlow();
const store = useStoreApi();
const { resetSelectedElements, addSelectedNodes } = store.getState();
@@ -109,8 +112,6 @@ export function OssFlow() {
height: block.height
},
parentId: block.parent ? `-${block.parent}` : undefined,
- expandParent: true,
- extent: 'parent' as const,
zIndex: Z_BLOCK
};
}),
@@ -120,8 +121,6 @@ export function OssFlow() {
data: { label: operation.alias, operation: operation },
position: computeRelativePosition(schema, { x: operation.x, y: operation.y }, operation.parent),
parentId: operation.parent ? `-${operation.parent}` : undefined,
- expandParent: true,
- extent: 'parent' as const,
zIndex: Z_SCHEMA
}))
]);
@@ -266,6 +265,81 @@ export function OssFlow() {
setMouseCoords(targetPosition);
}
+ 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);
+ }
+ setIsContextMenuOpen(false);
+ }
+
+ function handleDrag(event: React.MouseEvent) {
+ if (containMovement) {
+ return;
+ }
+ const mousePosition = screenToFlowPosition({ x: event.clientX, y: event.clientY });
+ const blocks = getIntersectingNodes({
+ x: mousePosition.x,
+ y: mousePosition.y,
+ width: 1,
+ height: 1
+ })
+ .map(node => Number(node.id))
+ .filter(id => id < 0)
+ .map(id => schema.blockByID.get(-id))
+ .filter(block => !!block);
+
+ if (blocks.length === 0) {
+ setDropTarget(null);
+ return;
+ } else if (blocks.length === 1) {
+ setDropTarget(blocks[0].id);
+ return;
+ }
+
+ 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) {
+ setDropTarget(null);
+ return;
+ } else {
+ setDropTarget(potentialTargets[0]);
+ return;
+ }
+ }
+
+ function handleDragStop(_: 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 {
+ // TODO: process drop event
+ }
+ setContainMovement(false);
+ setDropTarget(null);
+ }
+
return (
setIsContextMenuOpen(false)} {...menuProps} />
-
+
setIsContextMenuOpen(false)}
onNodeDoubleClick={handleNodeDoubleClick}
onNodeContextMenu={handleContextMenu}
- onNodeDragStart={() => setIsContextMenuOpen(false)}
+ onNodeDragStart={handleDragStart}
+ onNodeDrag={handleDrag}
+ onNodeDragStop={handleDragStop}
>
{showGrid ? : null}
diff --git a/rsconcept/frontend/src/styling/components.css b/rsconcept/frontend/src/styling/components.css
index 20a16396..44d3c01c 100644
--- a/rsconcept/frontend/src/styling/components.css
+++ b/rsconcept/frontend/src/styling/components.css
@@ -304,6 +304,10 @@
outline-color: var(--color-graph-selected);
border-color: var(--color-foreground);
}
+
+ .cursor-relocate .dragging & {
+ cursor: move;
+ }
}
@utility cc-node-block {
@@ -326,4 +330,8 @@
&:hover {
color: var(--color-foreground);
}
+
+ .cursor-relocate .dragging & {
+ cursor: move;
+ }
}