diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a817b6175d..b200f37907 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -71,7 +71,8 @@ This coupling will be removed in the near future as a consequence, the dependenc - https://github.com/eclipse-sirius/sirius-web/issues/2268[#2268] [diagram] Create child nodes where the user has clicked. - https://github.com/eclipse-sirius/sirius-web/issues/2475[#2475] [diagram] Prevent nodes to move each diagram refresh event. Only the size is still updated. - https://github.com/eclipse-sirius/sirius-web/issues/2485[#2485] [view] Change the default size of View Node Description from 1x1 to 150x70. -- https://github.com/eclipse-sirius/sirius-web/issues/2493[2493] [diagram] Disable double click to zoom in with ReactFlow. +- https://github.com/eclipse-sirius/sirius-web/issues/2493[#2493] [diagram] Disable double click to zoom in with ReactFlow. +- https://github.com/eclipse-sirius/sirius-web/issues/2504[#2504] [diagram] Create nodes at the cursor position when using create instance or drop tools. == v2023.10.0 diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx index 672c54ffce..07740f232d 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/DiagramRenderer.tsx @@ -69,8 +69,7 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload, selection, setSe fitviewLifecycle: 'neverRendered', }); - const { layout } = useLayout(); - + const { layout, setNextNodePosition } = useLayout(); const { onDiagramBackgroundClick, hideDiagramPalette } = useDiagramPalette(); const { onDiagramElementClick } = useDiagramElementPalette(); @@ -90,7 +89,6 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload, selection, setSe nodes: nodes as Node[], edges, }; - layout(previousDiagram, convertedDiagram, (laidOutDiagram) => { setNodes(laidOutDiagram.nodes); setEdges(laidOutDiagram.edges); @@ -224,7 +222,7 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload, selection, setSe }, ], }; - + setNextNodePosition(event, ''); setSelection(selection); onDiagramBackgroundClick(event); }; @@ -240,6 +238,11 @@ export const DiagramRenderer = ({ diagramRefreshedEventPayload, selection, setSe const { backgroundColor, smallGridColor, largeGridColor } = dropFeedbackStyleProvider.getDiagramBackgroundStyle(); + const handleNodeClick = (event, node: Node) => { + onDiagramElementClick(event); + setNextNodePosition(event, node.id); + }; + return ( hideDiagramPalette()} onDrop={onDrop} onDragOver={onDragOver} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/drop/useDrop.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/drop/useDrop.tsx index af1d47a78a..cdebba63c8 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/drop/useDrop.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/drop/useDrop.tsx @@ -15,6 +15,7 @@ import { DRAG_SOURCES_TYPE, useMultiToast } from '@eclipse-sirius/sirius-compone import { useContext, useEffect } from 'react'; import { DiagramContext } from '../../contexts/DiagramContext'; import { DiagramContextValue } from '../../contexts/DiagramContext.types'; +import { useLayout } from '../layout/useLayout'; import { GQLDropOnDiagramData, GQLDropOnDiagramInput, @@ -46,6 +47,7 @@ const isErrorPayload = (payload: GQLDropOnDiagramPayload): payload is GQLErrorPa export const useDrop = (): UseDropValue => { const { addErrorMessage, addMessages } = useMultiToast(); const { diagramId, editingContextId } = useContext(DiagramContext); + const { setNextNodePosition } = useLayout(); const [dropMutation, { data: droponDiagramElementData, error: droponDiagramError }] = useMutation< GQLDropOnDiagramData, GQLDropOnDiagramVariables @@ -87,6 +89,7 @@ export const useDrop = (): UseDropValue => { }; dropMutation({ variables: { input } }); + setNextNodePosition(event, diagramElementId ? diagramElementId : ''); }; const onDragOver = (event: React.DragEvent) => { diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.tsx index 6c030275ef..89d547ad72 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.tsx @@ -18,6 +18,7 @@ import { Node, NodeDragHandler, useReactFlow } from 'reactflow'; import { DiagramContext } from '../../contexts/DiagramContext'; import { DiagramContextValue } from '../../contexts/DiagramContext.types'; import { EdgeData, NodeData } from '../DiagramRenderer.types'; +import { useLayout } from '../layout/useLayout'; import { DropNodeContext } from './DropNodeContext'; import { DropNodeContextValue } from './DropNodeContext.types'; import { @@ -144,6 +145,8 @@ export const useDropNode = (): UseDropNodeValue => { } }, [data, loading, error, setDropNodeCompatibilityData]); + const { setNextNodePosition } = useLayout(); + const onDropNode = useDropNodeMutation(); const { getNodes, getIntersectingNodes } = useReactFlow(); @@ -193,7 +196,7 @@ export const useDropNode = (): UseDropNodeValue => { }; const onNodeDragStop: (onDragCancelled: (node: Node) => void) => NodeDragHandler = (onDragCancelled) => { - return (_event, _node) => { + return (event, _node) => { if (draggedNode && draggedNode.id === dropData.draggedNodeId) { const oldParentId: string | null = draggedNode.parentNode || null; const newParentId: string | null = dropData.targetNodeId; @@ -207,6 +210,9 @@ export const useDropNode = (): UseDropNodeValue => { onDragCancelled(draggedNode); } } + if (newParentId) { + setNextNodePosition(event, newParentId); + } } setDraggedNode(null); setDropData({ diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.types.ts index f490df43a1..0be312a780 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/dropNode/useDropNode.types.ts @@ -20,7 +20,6 @@ export interface UseDropNodeValue { dropData: NodeDropData; dropFeedbackStyleProvider: StyleProvider; } - export interface StyleProvider { getNodeStyle(nodeId: string): React.CSSProperties; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/IconLabelNodeLayoutHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/IconLabelNodeLayoutHandler.ts index d899d1a7ff..74a9f6c777 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/IconLabelNodeLayoutHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/IconLabelNodeLayoutHandler.ts @@ -28,6 +28,7 @@ export class IconLabelNodeLayoutHandler implements INodeLayoutHandler, visibleNodes: Node[], _directChildren: Node[], + _newlyAddedNode: Node[], forceWidth?: number ) { const nodeIndex = this.findNodeIndex(visibleNodes, node.id); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ImageNodeLayoutHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ImageNodeLayoutHandler.ts index d5ace57d3b..a7e85e00bc 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ImageNodeLayoutHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ImageNodeLayoutHandler.ts @@ -46,10 +46,11 @@ export class ImageNodeLayoutHandler implements INodeLayoutHandler node: Node, visibleNodes: Node[], directChildren: Node[], + newlyAddedNode: Node[], forceWidth?: number ) { if (directChildren.length > 0) { - this.handleParentNode(layoutEngine, previousDiagram, node, visibleNodes, directChildren); + this.handleParentNode(layoutEngine, previousDiagram, node, visibleNodes, directChildren, newlyAddedNode); } else { node.width = forceWidth ?? defaultWidth; node.height = defaultHeight; @@ -61,9 +62,10 @@ export class ImageNodeLayoutHandler implements INodeLayoutHandler previousDiagram: Diagram | null, node: Node, visibleNodes: Node[], - directChildren: Node[] + directChildren: Node[], + newlyAddedNode: Node[] ) { - layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren); + layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren, newlyAddedNode); const previousNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === node.id); diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutContext.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutContext.tsx new file mode 100644 index 0000000000..fece2df6a7 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutContext.tsx @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import React, { useCallback, useState } from 'react'; +import { + LayoutContextContextProviderProps, + LayoutContextContextProviderState, + LayoutContextContextValue, +} from './LayoutContext.types'; +import { FutureNodePosition } from './useLayout.types'; + +const defaultValue: LayoutContextContextValue = { + futureNodePosition: null, + setFuturNodePosition: () => {}, + resetFuturNodePosition: () => {}, +}; + +export const LayoutContext = React.createContext(defaultValue); + +export const LayoutContextContextProvider = ({ children }: LayoutContextContextProviderProps) => { + const [state, setState] = useState({ + futureNodePosition: null, + }); + + const setFuturNodePosition = useCallback((futureNodePosition: FutureNodePosition) => { + setState((prevState) => ({ ...prevState, futureNodePosition })); + }, []); + + const resetFuturNodePosition = useCallback(() => { + setState((prevState) => ({ ...prevState, futureNodePosition: null })); + }, []); + + return ( + + {children} + + ); +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutContext.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutContext.types.ts new file mode 100644 index 0000000000..309bf91f3d --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutContext.types.ts @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2023 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { FutureNodePosition } from './useLayout.types'; +export interface LayoutContextContextValue { + futureNodePosition: FutureNodePosition | null; + setFuturNodePosition: (futureNodePosition: FutureNodePosition) => void; + resetFuturNodePosition: () => void; +} + +export interface LayoutContextContextProviderProps { + children: React.ReactNode; +} + +export interface LayoutContextContextProviderState { + futureNodePosition: FutureNodePosition | null; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.ts index b066b5ea57..790fab444f 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.ts @@ -32,6 +32,7 @@ export class LayoutEngine implements ILayoutEngine { previousDiagram: Diagram | null, visibleNodes: Node[], nodesToLayout: Node[], + newlyAddedNode: Node[], forceWidth?: number ) { nodesToLayout.forEach((node) => { @@ -40,7 +41,7 @@ export class LayoutEngine implements ILayoutEngine { ); if (nodeLayoutHandler) { const directChildren = visibleNodes.filter((visibleNode) => visibleNode.parentNode === node.id); - nodeLayoutHandler.handle(this, previousDiagram, node, visibleNodes, directChildren, forceWidth); + nodeLayoutHandler.handle(this, previousDiagram, node, visibleNodes, directChildren, newlyAddedNode, forceWidth); node.style = { ...node.style, diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.types.ts index e718ae4a6f..e83ff396fd 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/LayoutEngine.types.ts @@ -20,6 +20,7 @@ export interface ILayoutEngine { previousDiagram: Diagram | null, visibleNodes: Node[], nodesToLayout: Node[], + newlyAddedNode: Node[], forceWidth?: number ); } @@ -32,6 +33,7 @@ export interface INodeLayoutHandler { node: Node, visibleNodes: Node[], directChildren: Node[], + newlyAddedNode: Node[], forceWidth?: number ); } diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ListNodeLayoutHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ListNodeLayoutHandler.ts index 7fd61cc064..b98543b39b 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ListNodeLayoutHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/ListNodeLayoutHandler.ts @@ -40,6 +40,7 @@ export class ListNodeLayoutHandler implements INodeLayoutHandler { node: Node, visibleNodes: Node[], directChildren: Node[], + newlyAddedNode: Node[], forceWidth?: number ) { const nodeIndex = findNodeIndex(visibleNodes, node.id); @@ -47,7 +48,16 @@ export class ListNodeLayoutHandler implements INodeLayoutHandler { const borderWidth = nodeElement ? parseFloat(window.getComputedStyle(nodeElement).borderWidth) : 0; if (directChildren.length > 0) { - this.handleParentNode(layoutEngine, previousDiagram, node, visibleNodes, directChildren, borderWidth, forceWidth); + this.handleParentNode( + layoutEngine, + previousDiagram, + node, + visibleNodes, + directChildren, + newlyAddedNode, + borderWidth, + forceWidth + ); } else { this.handleLeafNode(previousDiagram, node, visibleNodes, borderWidth, forceWidth); } @@ -74,10 +84,11 @@ export class ListNodeLayoutHandler implements INodeLayoutHandler { node: Node, visibleNodes: Node[], directChildren: Node[], + newlyAddedNode: Node[], borderWidth: number, forceWidth?: number ) { - layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren, forceWidth); + layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren, newlyAddedNode, forceWidth); const nodeIndex = findNodeIndex(visibleNodes, node.id); const labelElement = document.getElementById(`${node.id}-label-${nodeIndex}`); @@ -97,7 +108,7 @@ export class ListNodeLayoutHandler implements INodeLayoutHandler { southBorderNodeFootprintWidth ); - layoutEngine.layoutNodes(previousDiagram, visibleNodes, directNodesChildren, widerWidth); + layoutEngine.layoutNodes(previousDiagram, visibleNodes, directNodesChildren, newlyAddedNode, widerWidth); } directNodesChildren.forEach((child, index) => { diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/RectangleNodeLayoutHandler.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/RectangleNodeLayoutHandler.ts index 66a5b46ea0..792b216ec2 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/RectangleNodeLayoutHandler.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/RectangleNodeLayoutHandler.ts @@ -42,6 +42,7 @@ export class RectangleNodeLayoutHandler implements INodeLayoutHandler, visibleNodes: Node[], directChildren: Node[], + newlyAddedNode: Node[], forceWidth?: number ) { const nodeIndex = findNodeIndex(visibleNodes, node.id); @@ -49,7 +50,16 @@ export class RectangleNodeLayoutHandler implements INodeLayoutHandler 0) { - this.handleParentNode(layoutEngine, previousDiagram, node, visibleNodes, directChildren, borderWidth, forceWidth); + this.handleParentNode( + layoutEngine, + previousDiagram, + node, + visibleNodes, + directChildren, + newlyAddedNode, + borderWidth, + forceWidth + ); } else { this.handleLeafNode(previousDiagram, node, visibleNodes, borderWidth, forceWidth); } @@ -61,10 +71,11 @@ export class RectangleNodeLayoutHandler implements INodeLayoutHandler, visibleNodes: Node[], directChildren: Node[], + newlyAddedNode: Node[], borderWidth: number, forceWidth?: number ) { - layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren); + layoutEngine.layoutNodes(previousDiagram, visibleNodes, directChildren, newlyAddedNode); const nodeIndex = findNodeIndex(visibleNodes, node.id); const labelElement = document.getElementById(`${node.id}-label-${nodeIndex}`); @@ -76,7 +87,11 @@ export class RectangleNodeLayoutHandler implements INodeLayoutHandler { const previousNode = (previousDiagram?.nodes ?? []).find((previouseNode) => previouseNode.id === child.id); - if (previousNode) { + const createdNode = newlyAddedNode.find((n) => n.id === child.id); + + if (!!createdNode) { + child.position = createdNode.position; + } else if (previousNode) { child.position = previousNode.position; } else { child.position = getChildNodePosition(visibleNodes, child, labelElement, borderWidth); @@ -90,7 +105,6 @@ export class RectangleNodeLayoutHandler implements INodeLayoutHandler { const gap = 20; -export const layout = (previousDiagram: Diagram | null, diagram: Diagram): Diagram => { - layoutDiagram(previousDiagram, diagram); +export const layout = ( + previousDiagram: Diagram | null, + diagram: Diagram, + futureNodePosition: FutureNodePosition | null +): Diagram => { + layoutDiagram(previousDiagram, diagram, futureNodePosition); return diagram; }; -const layoutDiagram = (previousDiagram: Diagram | null, diagram: Diagram) => { +const layoutDiagram = ( + previousDiagram: Diagram | null, + diagram: Diagram, + futureNodePosition: FutureNodePosition | null +) => { const allVisibleNodes = diagram.nodes.filter((node) => !node.hidden); const nodesToLayout = allVisibleNodes.filter((node) => !node.parentNode); - new LayoutEngine().layoutNodes(previousDiagram, allVisibleNodes, nodesToLayout); + let newlyAddedNode: Node[] = previousDiagram?.nodes.length + ? allVisibleNodes.filter((node) => !previousDiagram?.nodes.map((n) => n.id).find((n) => n === node.id)) + : []; + + if (futureNodePosition && newlyAddedNode) { + newlyAddedNode = newlyAddedNode.map((node) => { + return { ...node, position: futureNodePosition.position }; + }); + } + + new LayoutEngine().layoutNodes(previousDiagram, allVisibleNodes, nodesToLayout, newlyAddedNode); // Update position of root nodes nodesToLayout.forEach((node, index) => { const previousNode = (previousDiagram?.nodes ?? []).find((previousNode) => previousNode.id === node.id); + const createdNode = newlyAddedNode.find((n) => n.id === node.id); - if (previousNode) { + if (!!createdNode) { + node.position = createdNode.position; + } else if (previousNode) { node.position = previousNode.position; } else { const maxBorderNodeWidthWest = getChildren(node, allVisibleNodes) diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.ts index 5912392d0c..9e5b0f8c5f 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.ts @@ -12,8 +12,11 @@ *******************************************************************************/ import { ServerContext, ServerContextValue } from '@eclipse-sirius/sirius-components-core'; -import { useContext, useEffect, useState } from 'react'; -import { Diagram } from '../DiagramRenderer.types'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { useReactFlow, useStoreApi } from 'reactflow'; +import { Diagram, EdgeData, NodeData } from '../DiagramRenderer.types'; +import { LayoutContext } from './LayoutContext'; +import { LayoutContextContextValue } from './LayoutContext.types'; import { cleanLayoutArea, layout, performDefaultAutoLayout, prepareLayoutArea } from './layout'; import { UseLayoutState, UseLayoutValue } from './useLayout.types'; @@ -30,6 +33,12 @@ export const useLayout = (): UseLayoutValue => { const { httpOrigin } = useContext(ServerContext); const [state, setState] = useState(initialState); + const reactFlowInstance = useReactFlow(); + const { futureNodePosition, setFuturNodePosition, resetFuturNodePosition } = + useContext(LayoutContext); + const { domNode } = useStoreApi().getState(); + const element = domNode?.getBoundingClientRect(); + const layoutAreaPrepared = () => { const currentStep = 'LAYOUT'; setState((prevState) => ({ ...prevState, currentStep })); @@ -59,7 +68,7 @@ export const useLayout = (): UseLayoutValue => { hiddenContainer: layoutArea, })); } else if (state.currentStep === 'LAYOUT' && state.hiddenContainer && state.diagramToLayout) { - const laidoutDiagram = layout(state.previousDiagram, state.diagramToLayout); + const laidoutDiagram = layout(state.previousDiagram, state.diagramToLayout, futureNodePosition); setState((prevState) => ({ ...prevState, diagramToLayout: null, @@ -71,10 +80,39 @@ export const useLayout = (): UseLayoutValue => { state.onLaidoutDiagram(state.laidoutDiagram); setState(() => initialState); } - }, [state.currentStep, state.hiddenContainer]); + }, [state.currentStep, state.hiddenContainer, futureNodePosition]); + + const setNextNodePosition = useCallback( + (event: React.MouseEvent, parentId: string) => { + if (element) { + let position = reactFlowInstance.project({ + x: event.clientX - element.left, + y: event.clientY - element.top, + }); + + let parentNode = reactFlowInstance.getNode(parentId); + while (parentNode) { + position = { + x: position.x - parentNode.position.x, + y: position.y - parentNode.position.y, + }; + parentNode = reactFlowInstance.getNode(parentNode.parentNode ?? ''); + } + + setFuturNodePosition({ + diagramElementId: parentId, + position: position, + }); + } else { + resetFuturNodePosition(); + } + }, + [element?.top, element?.left] + ); return { layout: layoutDiagram, autoLayout: performDefaultAutoLayout, + setNextNodePosition, }; }; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts index 5cb612e9ac..58687a5f81 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/renderer/layout/useLayout.types.ts @@ -11,7 +11,7 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { Edge, Node } from 'reactflow'; +import { Edge, Node, XYPosition } from 'reactflow'; import { Diagram, NodeData } from '../DiagramRenderer.types'; export interface UseLayoutValue { @@ -21,10 +21,15 @@ export interface UseLayoutValue { callback: (laidoutDiagram: Diagram) => void ) => void; autoLayout: (nodes: Node[], edges: Edge[], zoomLevel: number) => Promise<{ nodes: Node[] }>; + setNextNodePosition: (event: React.MouseEvent, parentId: string) => void; } export type Step = 'INITIAL_STEP' | 'BEFORE_LAYOUT' | 'LAYOUT' | 'AFTER_LAYOUT'; +export type FutureNodePosition = { + position: XYPosition; + diagramElementId: string; +}; export interface UseLayoutState { hiddenContainer: HTMLDivElement | null; currentStep: Step; diff --git a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/representation/DiagramRepresentation.tsx b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/representation/DiagramRepresentation.tsx index bdbc90fcd0..ec53027175 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/representation/DiagramRepresentation.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams-reactflow/src/representation/DiagramRepresentation.tsx @@ -27,6 +27,7 @@ import { DiagramDirectEditContextProvider } from '../renderer/direct-edit/Diagra import { DropNodeContextProvider } from '../renderer/dropNode/DropNodeContext'; import { MarkerDefinitions } from '../renderer/edge/MarkerDefinitions'; import { FullscreenContextProvider } from '../renderer/fullscreen/FullscreenContext'; +import { LayoutContextContextProvider } from '../renderer/layout/LayoutContext'; import { DiagramElementPaletteContextProvider } from '../renderer/palette/DiagramElementPaletteContext'; import { DiagramPaletteContextProvider } from '../renderer/palette/DiagramPaletteContext'; import { @@ -96,29 +97,31 @@ export const DiagramRepresentation = ({ return ( - - - - - - -
- - - - -
-
-
-
-
-
-
+ + + + + + + +
+ + + + +
+
+
+
+
+
+
+
); };