From 2bccd557e7f119a61b239a303e8189dc5317273d Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 6 Sep 2024 17:34:17 +0200 Subject: [PATCH 01/22] feat: Copy and paste cards --- components/canvas/Canvas.tsx | 20 ++++++- components/canvas/ChoiceNode.tsx | 26 ++++++--- components/canvas/GenericNode.tsx | 2 +- components/canvas/MainActivityNode.tsx | 16 ++++-- components/canvas/SubActivityNode.tsx | 30 +++++++--- components/canvas/WaitingNode.tsx | 30 +++++++--- components/canvas/hooks/useCopyPaste.tsx | 56 +++++++++++++++++++ components/canvas/hooks/useNodeAdd.tsx | 19 ++++--- .../canvas/utils/copyPasteValidators.tsx | 25 +++++++++ components/canvas/utils/targetIsInSubtree.tsx | 8 +-- components/canvas/utils/validTarget.ts | 28 ++++++---- services/graphApi.ts | 4 +- types/NodeDataApi.ts | 5 ++ 13 files changed, 209 insertions(+), 60 deletions(-) create mode 100644 components/canvas/hooks/useCopyPaste.tsx create mode 100644 components/canvas/utils/copyPasteValidators.tsx diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index b92a35ff..53cf500b 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -7,12 +7,13 @@ import ReactFlow, { Controls, Edge, Node, + Position, ReactFlowProvider, useEdgesState, useNodesState, } from "reactflow"; import "reactflow/dist/style.css"; -import { NodeDataFull } from "types/NodeData"; +import { NodeData, NodeDataFull } from "types/NodeData"; import { NodeDataApi } from "types/NodeDataApi"; import { NodeTypes } from "types/NodeTypes"; import { Project } from "types/Project"; @@ -38,6 +39,9 @@ import { ZoomLevel } from "@/components/canvas/ZoomLevel"; import { edgeElementTypes } from "@/components/canvas/EdgeElementTypes"; import { createHiddenNodes } from "@/components/canvas/utils/createHiddenNodes"; import { createEdges } from "./utils/createEdges"; +import { useCopyPaste } from "./hooks/useCopyPaste"; +import { copyPasteNodeValidator } from "./utils/copyPasteValidators"; +import { validTarget } from "./utils/validTarget"; type CanvasProps = { graph: Graph; @@ -51,7 +55,11 @@ const Canvas = ({ const [selectedNode, setSelectedNode] = useState( undefined ); + const [hoveredNode, setHoveredNode] = useState | undefined>( + undefined + ); const { userCanEdit } = useAccess(project); + const { addNode } = useNodeAdd(); const shapeSize = { height: 140, width: 140 }; @@ -74,6 +82,14 @@ const Canvas = ({ const { deleteEdgeMutation } = useEdgeDelete(); const { socketConnected, socketReason } = useWebSocket(); + useCopyPaste( + hoveredNode, + (node: Node) => + hoveredNode?.id && + validTarget(node, hoveredNode, nodes, false) && + addNode(hoveredNode.id, node.data, Position.Bottom), + copyPasteNodeValidator + ); let columnId: string | null = null; @@ -337,6 +353,8 @@ const Canvas = ({ onNodeDragStop={onNodeDragStop} elevateEdgesOnSelect={true} edgesFocusable={userCanEdit} + onNodeMouseEnter={(e, node) => setHoveredNode(node)} + onNodeMouseLeave={() => setHoveredNode(undefined)} onEdgeMouseEnter={(event, edge) => handleSetSelectedEdge(edge)} onEdgeMouseLeave={() => handleSetSelectedEdge(undefined)} attributionPosition="bottom-right" diff --git a/components/canvas/ChoiceNode.tsx b/components/canvas/ChoiceNode.tsx index bbba430a..ee92b68a 100644 --- a/components/canvas/ChoiceNode.tsx +++ b/components/canvas/ChoiceNode.tsx @@ -57,7 +57,7 @@ export const ChoiceNode = ({ onClick={() => addNode( lastChild || id, - NodeTypes.subActivity, + { type: NodeTypes.subActivity }, lastChild ? Position.Right : Position.Bottom ) } @@ -67,7 +67,7 @@ export const ChoiceNode = ({ onClick={() => addNode( lastChild || id, - NodeTypes.choice, + { type: NodeTypes.choice }, lastChild ? Position.Right : Position.Bottom ) } @@ -77,7 +77,7 @@ export const ChoiceNode = ({ onClick={() => addNode( lastChild || id, - NodeTypes.waiting, + { type: NodeTypes.waiting }, lastChild ? Position.Right : Position.Bottom ) } @@ -94,32 +94,40 @@ export const ChoiceNode = ({ - addNode(id, NodeTypes.subActivity, Position.Right) + addNode(id, { type: NodeTypes.subActivity }, Position.Right) } disabled={isNodeButtonDisabled(id, Position.Right)} /> addNode(id, NodeTypes.choice, Position.Right)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Right) + } disabled={isNodeButtonDisabled(id, Position.Right)} /> addNode(id, NodeTypes.waiting, Position.Right)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Right) + } disabled={isNodeButtonDisabled(id, Position.Right)} /> - addNode(id, NodeTypes.subActivity, Position.Left) + addNode(id, { type: NodeTypes.subActivity }, Position.Left) } disabled={isNodeButtonDisabled(id, Position.Left)} /> addNode(id, NodeTypes.choice, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> addNode(id, NodeTypes.waiting, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> diff --git a/components/canvas/GenericNode.tsx b/components/canvas/GenericNode.tsx index f23d0ab3..d24c57a4 100644 --- a/components/canvas/GenericNode.tsx +++ b/components/canvas/GenericNode.tsx @@ -54,7 +54,7 @@ export const GenericNode = ({ - addNode(id, NodeTypes.mainActivity, nodeButtonsPosition) + addNode(id, { type: NodeTypes.mainActivity }, nodeButtonsPosition) } disabled={isNodeButtonDisabled(id, nodeButtonsPosition)} /> diff --git a/components/canvas/MainActivityNode.tsx b/components/canvas/MainActivityNode.tsx index 241cf45d..2c7362fd 100644 --- a/components/canvas/MainActivityNode.tsx +++ b/components/canvas/MainActivityNode.tsx @@ -50,14 +50,16 @@ export const MainActivityNode = ({ <> addNode(id, NodeTypes.mainActivity, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.mainActivity }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> - addNode(id, NodeTypes.mainActivity, Position.Right) + addNode(id, { type: NodeTypes.mainActivity }, Position.Right) } disabled={isNodeButtonDisabled(id, Position.Right)} /> @@ -65,16 +67,20 @@ export const MainActivityNode = ({ - addNode(id, NodeTypes.subActivity, Position.Bottom) + addNode(id, { type: NodeTypes.subActivity }, Position.Bottom) } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> addNode(id, NodeTypes.choice, Position.Bottom)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Bottom) + } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> addNode(id, NodeTypes.waiting, Position.Bottom)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Bottom) + } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> diff --git a/components/canvas/SubActivityNode.tsx b/components/canvas/SubActivityNode.tsx index 42557849..e5f515f0 100644 --- a/components/canvas/SubActivityNode.tsx +++ b/components/canvas/SubActivityNode.tsx @@ -63,16 +63,20 @@ export const SubActivityNode = ({ - addNode(id, NodeTypes.subActivity, Position.Bottom) + addNode(id, { type: NodeTypes.subActivity }, Position.Bottom) } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> addNode(id, NodeTypes.choice, Position.Bottom)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Bottom) + } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> addNode(id, NodeTypes.waiting, Position.Bottom)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Bottom) + } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> {mergeable && handleMerge && ( @@ -86,32 +90,40 @@ export const SubActivityNode = ({ - addNode(id, NodeTypes.subActivity, Position.Right) + addNode(id, { type: NodeTypes.subActivity }, Position.Right) } disabled={isNodeButtonDisabled(id, Position.Right)} /> addNode(id, NodeTypes.choice, Position.Right)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Right) + } disabled={isNodeButtonDisabled(id, Position.Right)} /> addNode(id, NodeTypes.waiting, Position.Right)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Right) + } disabled={isNodeButtonDisabled(id, Position.Right)} /> - addNode(id, NodeTypes.subActivity, Position.Left) + addNode(id, { type: NodeTypes.subActivity }, Position.Left) } disabled={isNodeButtonDisabled(id, Position.Left)} /> addNode(id, NodeTypes.choice, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> addNode(id, NodeTypes.waiting, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> diff --git a/components/canvas/WaitingNode.tsx b/components/canvas/WaitingNode.tsx index a95bd671..4e7ea810 100644 --- a/components/canvas/WaitingNode.tsx +++ b/components/canvas/WaitingNode.tsx @@ -63,16 +63,20 @@ export const WaitingNode = ({ - addNode(id, NodeTypes.subActivity, Position.Bottom) + addNode(id, { type: NodeTypes.subActivity }, Position.Bottom) } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> addNode(id, NodeTypes.choice, Position.Bottom)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Bottom) + } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> addNode(id, NodeTypes.waiting, Position.Bottom)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Bottom) + } disabled={isNodeButtonDisabled(id, Position.Bottom)} /> {mergeable && handleMerge && ( @@ -86,32 +90,40 @@ export const WaitingNode = ({ - addNode(id, NodeTypes.subActivity, Position.Right) + addNode(id, { type: NodeTypes.subActivity }, Position.Right) } disabled={isNodeButtonDisabled(id, Position.Right)} /> addNode(id, NodeTypes.choice, Position.Right)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Right) + } disabled={isNodeButtonDisabled(id, Position.Right)} /> addNode(id, NodeTypes.waiting, Position.Right)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Right) + } disabled={isNodeButtonDisabled(id, Position.Right)} /> - addNode(id, NodeTypes.subActivity, Position.Left) + addNode(id, { type: NodeTypes.subActivity }, Position.Left) } disabled={isNodeButtonDisabled(id, Position.Left)} /> addNode(id, NodeTypes.choice, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.choice }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> addNode(id, NodeTypes.waiting, Position.Left)} + onClick={() => + addNode(id, { type: NodeTypes.waiting }, Position.Left) + } disabled={isNodeButtonDisabled(id, Position.Left)} /> diff --git a/components/canvas/hooks/useCopyPaste.tsx b/components/canvas/hooks/useCopyPaste.tsx new file mode 100644 index 00000000..1943fe18 --- /dev/null +++ b/components/canvas/hooks/useCopyPaste.tsx @@ -0,0 +1,56 @@ +import { useStoreDispatch } from "@/hooks/storeHooks"; +import { useEffect } from "react"; + +export const useCopyPaste = ( + target: any, + action: (target: any) => void, + validator?: (target: any) => void +) => { + const dispatch = useStoreDispatch(); + + const copyToClipboard = async (target: any) => { + try { + const valid = validator ? validator(target) : true; + if (valid) { + await navigator.clipboard.writeText(JSON.stringify(target)); + dispatch.setSnackMessage("Copied 📋"); + } + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } + } + }; + + const paste = async () => { + try { + let targetToPaste = await navigator.clipboard.readText(); + targetToPaste = JSON.parse(targetToPaste); + const valid = validator ? validator(targetToPaste) : true; + if (valid) { + dispatch.setSnackMessage("Pasted 📝"); + action(targetToPaste); + } + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } + } + }; + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key === "c") { + copyToClipboard(target); + } else if ((event.metaKey || event.ctrlKey) && event.key === "v") { + paste(); + } + }; + + document.addEventListener("keydown", handleKeyDown); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + }; + }, [target]); +}; diff --git a/components/canvas/hooks/useNodeAdd.tsx b/components/canvas/hooks/useNodeAdd.tsx index c589a91c..890f9485 100644 --- a/components/canvas/hooks/useNodeAdd.tsx +++ b/components/canvas/hooks/useNodeAdd.tsx @@ -1,6 +1,6 @@ import { useMutation, useQueryClient } from "react-query"; import { Position } from "reactflow"; -import { NodeTypes } from "types/NodeTypes"; +import { NodeDataApiRequestBody } from "@/types/NodeDataApi"; import { useStoreDispatch } from "@/hooks/storeHooks"; import { addVertice, @@ -15,7 +15,7 @@ import { useState } from "react"; export type NodeAddParams = { parentId: string; - type: NodeTypes; + data: NodeDataApiRequestBody; position: Position; }; @@ -31,16 +31,16 @@ export const useNodeAdd = () => { } | null>(null); const mutation = useMutation( - ({ parentId, type, position }: NodeAddParams) => { + ({ parentId, data, position }: NodeAddParams) => { setNodeAddingChild({ id: parentId, position: position }); dispatch.setSnackMessage("⏳ Adding new card..."); switch (position) { case Position.Left: - return addVerticeLeft({ type }, projectId, parentId); + return addVerticeLeft(data, projectId, parentId); case Position.Right: - return addVerticeRight({ type }, projectId, parentId); + return addVerticeRight(data, projectId, parentId); default: - return addVertice({ type }, projectId, parentId); + return addVertice(data, projectId, parentId); } }, { @@ -55,8 +55,11 @@ export const useNodeAdd = () => { } ); - const addNode = (parentId: string, type: NodeTypes, position: Position) => - mutation.mutate({ parentId, type, position }); + const addNode = ( + parentId: string, + data: NodeDataApiRequestBody, + position: Position + ) => mutation.mutate({ parentId, data, position }); const isNodeButtonDisabled = (nodeId: string, position: Position) => nodeAddingChild?.id === nodeId && nodeAddingChild?.position === position; diff --git a/components/canvas/utils/copyPasteValidators.tsx b/components/canvas/utils/copyPasteValidators.tsx new file mode 100644 index 00000000..a6b1d0cc --- /dev/null +++ b/components/canvas/utils/copyPasteValidators.tsx @@ -0,0 +1,25 @@ +import { NodeDataApiRequestBody } from "@/types/NodeDataApi"; + +const isNodeDataApiRequestBody = (obj: any): obj is NodeDataApiRequestBody => { + return ( + typeof obj === "object" && + obj !== null && + typeof obj.type === "string" && + (typeof obj.description === "undefined" || + obj.description === null || + typeof obj.description === "string") && + (typeof obj.role === "undefined" || + obj.role === null || + typeof obj.role === "string") && + (typeof obj.duration === "undefined" || + obj.duration === null || + typeof obj.duration === "number") && + (typeof obj.unit === "undefined" || + obj.unit === null || + typeof obj.unit === "string") && + (typeof obj.tasks === "undefined" || Array.isArray(obj.tasks)) + ); +}; + +export const copyPasteNodeValidator = (content: any) => + isNodeDataApiRequestBody(content); diff --git a/components/canvas/utils/targetIsInSubtree.tsx b/components/canvas/utils/targetIsInSubtree.tsx index d2a48b61..9aed43a8 100644 --- a/components/canvas/utils/targetIsInSubtree.tsx +++ b/components/canvas/utils/targetIsInSubtree.tsx @@ -1,10 +1,10 @@ import { Node } from "reactflow"; -import { NodeData } from "@/types/NodeData"; +import { NodeDataFull } from "@/types/NodeData"; export const targetIsInSubtree = ( - node: Node, - target: Node, - nodes: Node[], + node: Node, + target: Node, + nodes: Node[], visited = new Set() ) => { if (node.data.columnId !== target.data.columnId) return false; diff --git a/components/canvas/utils/validTarget.ts b/components/canvas/utils/validTarget.ts index 4c8413b5..3e995fb6 100644 --- a/components/canvas/utils/validTarget.ts +++ b/components/canvas/utils/validTarget.ts @@ -1,4 +1,4 @@ -import { NodeData } from "types/NodeData"; +import { NodeData, NodeDataFull } from "types/NodeData"; import { NodeTypes } from "types/NodeTypes"; import { Node } from "reactflow"; import { targetIsInSubtree } from "./targetIsInSubtree"; @@ -6,15 +6,26 @@ import { targetIsInSubtree } from "./targetIsInSubtree"; export const validTarget = ( source: Node | undefined, target: Node | undefined, - nodes: Node[] + nodes: Node[], + isDragAndDrop = true ): boolean => { if (!target || !source) return false; const sourceType = source.type; const targetType = target.type; - const targetIsParent = source?.data?.parents?.includes(target.id); - if (targetIsParent) { - return false; + if (isDragAndDrop) { + const targetIsParent = source?.data?.parents?.includes(target.id); + + if (targetIsParent) { + return false; + } + + if ( + sourceType === NodeTypes.choice && + (target.data.children.length || targetIsInSubtree(source, target, nodes)) + ) { + return false; + } } if ( @@ -33,12 +44,5 @@ export const validTarget = ( return false; } - if ( - sourceType === NodeTypes.choice && - (target.data.children.length || targetIsInSubtree(source, target, nodes)) - ) { - return false; - } - return true; }; diff --git a/services/graphApi.ts b/services/graphApi.ts index aa0c8a7b..39c9e80a 100644 --- a/services/graphApi.ts +++ b/services/graphApi.ts @@ -1,7 +1,7 @@ const baseUrl = "/api/v2.0"; import BaseAPIServices from "./BaseAPIServices"; -import { NodeDataApi } from "@/types/NodeDataApi"; +import { NodeDataApi, NodeDataApiRequestBody } from "@/types/NodeDataApi"; import { Graph } from "types/Graph"; import { NodeTypes } from "types/NodeTypes"; @@ -12,7 +12,7 @@ export const getGraph = (projectId: string | string[]): Promise => { }; export const addVertice = ( - data: { type: NodeTypes }, + data: NodeDataApiRequestBody, projectId: string, parentId: string ): Promise => diff --git a/types/NodeDataApi.ts b/types/NodeDataApi.ts index 757a6e82..a0ee80b9 100644 --- a/types/NodeDataApi.ts +++ b/types/NodeDataApi.ts @@ -14,3 +14,8 @@ export type NodeDataApi = { children: string[]; tasks: Task[]; }; + +export type NodeDataApiRequestBody = Pick & + Partial< + Pick + >; From 4cb04bb75d4154a909708474fbebd8db3cee7528 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 9 Sep 2024 13:17:16 +0200 Subject: [PATCH 02/22] fix: Add missing import --- components/canvas/Canvas.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 50b4939d..1387383e 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -42,6 +42,7 @@ import { createEdges } from "./utils/createEdges"; import { useCopyPaste } from "./hooks/useCopyPaste"; import { copyPasteNodeValidator } from "./utils/copyPasteValidators"; import { validTarget } from "./utils/validTarget"; +import { useNodeAdd } from "./hooks/useNodeAdd"; type CanvasProps = { graph: Graph; From dda80afd88b01c6bb06a5e8125108548a73613f0 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 12 Sep 2024 12:33:48 +0200 Subject: [PATCH 03/22] feat: Right click menu --- components/DeleteNodeDialog.tsx | 3 - components/MenuItemExandable.tsx | 46 +++++++++++++ components/MenuItemExpandable.module.scss | 7 ++ components/canvas/Canvas.tsx | 49 ++++++++++---- components/canvas/ContextMenu.module.scss | 7 ++ components/canvas/ContextMenu.tsx | 64 ++++++++++++++++++ components/canvas/hooks/useCenterCanvas.ts | 17 +++-- components/canvas/hooks/useContextMenu.tsx | 75 ++++++++++++++++++++++ components/canvas/hooks/useCopyPaste.tsx | 2 + 9 files changed, 250 insertions(+), 20 deletions(-) create mode 100644 components/MenuItemExandable.tsx create mode 100644 components/MenuItemExpandable.module.scss create mode 100644 components/canvas/ContextMenu.module.scss create mode 100644 components/canvas/ContextMenu.tsx create mode 100644 components/canvas/hooks/useContextMenu.tsx diff --git a/components/DeleteNodeDialog.tsx b/components/DeleteNodeDialog.tsx index 512fb09d..3756b319 100644 --- a/components/DeleteNodeDialog.tsx +++ b/components/DeleteNodeDialog.tsx @@ -13,7 +13,6 @@ import { ScrimDelete } from "./ScrimDelete"; export function DeleteNodeDialog(props: { objectToDelete: NodeData; onClose: () => void; - visible: boolean; }) { const { accounts } = useMsal(); const account = useAccount(accounts[0] || {}); @@ -45,8 +44,6 @@ export function DeleteNodeDialog(props: { } ); - if (!props.visible) return null; - const handleClose = () => props.onClose(); const handleDelete = () => deleteMutation.mutate({ diff --git a/components/MenuItemExandable.tsx b/components/MenuItemExandable.tsx new file mode 100644 index 00000000..da01087b --- /dev/null +++ b/components/MenuItemExandable.tsx @@ -0,0 +1,46 @@ +import { Menu } from "@equinor/eds-core-react"; +import { ReactElement, useState } from "react"; +import styles from "./MenuItemExpandable.module.scss"; + +type MenuItemExandableProps = { + text: string; + children: ReactElement | ReactElement[]; +}; + +export const MenuItemExandable = ({ + text, + children, +}: MenuItemExandableProps) => { + const [anchorEl, setAnchorEl] = useState(null); + const [isExpanded, setIsExpanded] = useState(false); + + const expandArrow = ( + + + + ); + + return ( + setIsExpanded(true)} + onMouseLeave={() => setIsExpanded(false)} + > + {text} {expandArrow} + + {children} + + + ); +}; diff --git a/components/MenuItemExpandable.module.scss b/components/MenuItemExpandable.module.scss new file mode 100644 index 00000000..54d3dac6 --- /dev/null +++ b/components/MenuItemExpandable.module.scss @@ -0,0 +1,7 @@ +.menu { + margin-left: -4px; +} + +.menu > div { + padding: 0; +} diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 1387383e..181e2e33 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -1,7 +1,7 @@ import { CanvasButtons } from "components/CanvasButtons"; import { ManageLabelBox } from "components/Labels/ManageLabelBox"; import { ResetProcessButton } from "components/ResetProcessButton"; -import { useLayoutEffect, useState } from "react"; +import { useEffect, useLayoutEffect, useRef, useState } from "react"; import ReactFlow, { ControlButton, Controls, @@ -43,6 +43,8 @@ import { useCopyPaste } from "./hooks/useCopyPaste"; import { copyPasteNodeValidator } from "./utils/copyPasteValidators"; import { validTarget } from "./utils/validTarget"; import { useNodeAdd } from "./hooks/useNodeAdd"; +import { useContextMenu } from "./hooks/useContextMenu"; +import { ContextMenu } from "./ContextMenu"; type CanvasProps = { graph: Graph; @@ -61,6 +63,7 @@ const Canvas = ({ ); const { userCanEdit } = useAccess(project); const { addNode } = useNodeAdd(); + const ref = useRef(null); const shapeSize = { height: 140, width: 140 }; @@ -71,7 +74,9 @@ const Canvas = ({ const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); - const [visibleDeleteNodeScrim, setVisibleDeleteNodeScrim] = useState(false); + const [nodeToBeDeleted, setNodeToBeDeleted] = useState< + Node | undefined + >(undefined); const [edgeToBeDeletedId, setEdgeToBeDeletedId] = useState< string | undefined >(undefined); @@ -81,9 +86,15 @@ const Canvas = ({ const { onNodeDragStart, onNodeDrag, onNodeDragStop } = useNodeDrag(); const { mutate: mergeNode, merging } = useNodeMerge(); const { deleteEdgeMutation } = useEdgeDelete(); + const { menuData, onNodeContextMenu, onPaneContextMenu } = + useContextMenu(ref); + + useEffect(() => { + !menuData && setHoveredNode(undefined); + }, [menuData]); const { socketConnected, socketReason } = useWebSocket(); - useCopyPaste( + const { copyToClipboard, paste } = useCopyPaste( hoveredNode, (node: Node) => hoveredNode?.id && @@ -276,7 +287,7 @@ const Canvas = ({ selectedNode && handleSetSelectedNode(selectedNode.id); }, [apiNodes, apiEdges, userCanEdit]); - useCenterCanvas(); + const { centerCanvas } = useCenterCanvas(); return ( <> @@ -301,12 +312,11 @@ const Canvas = ({ /> - {selectedNode && ( + {nodeToBeDeleted && ( { - setVisibleDeleteNodeScrim(false); + setNodeToBeDeleted(undefined); setSelectedNode(undefined); }} /> @@ -335,7 +345,7 @@ const Canvas = ({ )} setSelectedNode(undefined)} - onDelete={() => setVisibleDeleteNodeScrim(true)} + onDelete={() => setNodeToBeDeleted(selectedNode)} canEdit={userCanEdit} selectedNode={selectedNode?.data} /> @@ -356,13 +366,20 @@ const Canvas = ({ onNodeDragStop={onNodeDragStop} elevateEdgesOnSelect={true} edgesFocusable={userCanEdit} - onNodeMouseEnter={(e, node) => setHoveredNode(node)} - onNodeMouseLeave={() => setHoveredNode(undefined)} + onNodeMouseEnter={(e, node) => { + !menuData && setHoveredNode(node); + }} + onNodeMouseLeave={() => { + !menuData && setHoveredNode(undefined); + }} onEdgeMouseEnter={(event, edge) => handleSetSelectedEdge(edge)} onEdgeMouseLeave={() => handleSetSelectedEdge(undefined)} attributionPosition="bottom-right" connectionRadius={100} nodeDragThreshold={15} + onNodeContextMenu={onNodeContextMenu} + onPaneContextMenu={onPaneContextMenu} + ref={ref} > @@ -370,6 +387,16 @@ const Canvas = ({ + {menuData && ( + setNodeToBeDeleted(node)} + onEditNode={(node) => setSelectedNode(node)} + /> + )} ); diff --git a/components/canvas/ContextMenu.module.scss b/components/canvas/ContextMenu.module.scss new file mode 100644 index 00000000..2a74320c --- /dev/null +++ b/components/canvas/ContextMenu.module.scss @@ -0,0 +1,7 @@ +.menu { + position: absolute; +} + +.menu > div { + padding: 0; +} diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx new file mode 100644 index 00000000..304d3f09 --- /dev/null +++ b/components/canvas/ContextMenu.tsx @@ -0,0 +1,64 @@ +import { NodeData } from "@/types/NodeData"; +import { Menu } from "@equinor/eds-core-react"; +import { useCallback, useState } from "react"; +import { Node } from "reactflow"; +import { MenuItemExandable } from "../MenuItemExandable"; +import styles from "./ContextMenu.module.scss"; +import type { MenuData } from "./hooks/useContextMenu"; + +type ContextMenuProps = { + menuData: MenuData; + centerCanvas: () => void; + copyToClipBoard?: (target: Node) => Promise; + paste?: () => void; + onDelete?: (node: Node) => void; + onEditNode?: (node: Node) => void; +}; + +export const ContextMenu = ({ + menuData: { position, node }, + centerCanvas, + copyToClipBoard, + paste, + onDelete, + onEditNode, +}: ContextMenuProps) => { + const [anchorEl, setAnchorEl] = useState(null); + + const modifierKey = window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"; + + const renderNodeMenuItems = useCallback(() => { + if (node) { + return ( + <> + copyToClipBoard?.(node)}> + Copy {modifierKey}C + + Paste {modifierKey}V + onEditNode?.(node)}>Edit + + Sub Activity + Choice + Waiting + + onDelete?.(node)}>Delete ⌫ + + ); + } + }, [node, copyToClipBoard, modifierKey, paste, onDelete, onEditNode]); + + return ( +
+ + {renderNodeMenuItems()} + Fit View + +
+ ); +}; diff --git a/components/canvas/hooks/useCenterCanvas.ts b/components/canvas/hooks/useCenterCanvas.ts index 42abda10..06c836d2 100644 --- a/components/canvas/hooks/useCenterCanvas.ts +++ b/components/canvas/hooks/useCenterCanvas.ts @@ -1,21 +1,26 @@ +import { useCallback, useEffect } from "react"; +import { FitViewOptions, useReactFlow } from "reactflow"; import { useProjectId } from "../../../hooks/useProjectId"; -import { useEffect } from "react"; -import { useReactFlow, FitViewOptions } from "reactflow"; export const useCenterCanvas = () => { - const reactFlow = useReactFlow(); - const { fitView, setViewport, getViewport } = reactFlow; + const { fitView, setViewport, getViewport } = useReactFlow(); const { projectId } = useProjectId(); const fitViewOptions: FitViewOptions = { includeHiddenNodes: true, maxZoom: 0.8, }; - useEffect(() => { + const centerCanvas = useCallback(() => { window.requestAnimationFrame(() => { fitView(fitViewOptions); const viewport = getViewport(); setViewport({ ...viewport, y: 75 }); }); - }, [reactFlow, projectId]); + }, [fitView, getViewport, setViewport]); + + useEffect(() => { + centerCanvas(); + }, [centerCanvas, projectId]); + + return { centerCanvas }; }; diff --git a/components/canvas/hooks/useContextMenu.tsx b/components/canvas/hooks/useContextMenu.tsx new file mode 100644 index 00000000..3cee868f --- /dev/null +++ b/components/canvas/hooks/useContextMenu.tsx @@ -0,0 +1,75 @@ +import { NodeData } from "@/types/NodeData"; +import { Node } from "reactflow"; +import { RefObject, useCallback, useEffect, useState, MouseEvent } from "react"; + +type Position = { + top?: number; + left?: number; + right?: number; + bottom?: number; +}; + +export type MenuData = { + position: Position; + node?: Node; +}; + +export const useContextMenu = (ref: RefObject) => { + const [menuData, setMenuData] = useState(null); + let pane: DOMRect | undefined = undefined; + + const getPosition = (event: MouseEvent): Position => ({ + top: + pane && event.clientY < pane.height ? event.clientY - pane.y : undefined, + left: pane && event.clientX < pane.width ? event.clientX : undefined, + right: + pane && event.clientX >= pane.width + ? pane.width - event.clientX + : undefined, + bottom: + pane && event.clientY >= pane.height + ? pane.height - event.clientY + : undefined, + }); + + const onNodeContextMenu = useCallback( + (event: MouseEvent, node: Node) => { + event.preventDefault(); + if (pane) { + setMenuData({ + position: getPosition(event), + node: node, + }); + } + }, + [setMenuData] + ); + + const onPaneContextMenu = useCallback( + (event: MouseEvent) => { + event.preventDefault(); + if (pane) { + setMenuData({ + position: getPosition(event), + }); + } + }, + [setMenuData] + ); + + useEffect(() => { + document.body.addEventListener("click", () => { + setMenuData(null); + }); + }, []); + + useEffect(() => { + pane = ref?.current?.getBoundingClientRect(); + }, [ref]); + + return { + menuData, + onNodeContextMenu, + onPaneContextMenu, + }; +}; diff --git a/components/canvas/hooks/useCopyPaste.tsx b/components/canvas/hooks/useCopyPaste.tsx index 1943fe18..3e217e9f 100644 --- a/components/canvas/hooks/useCopyPaste.tsx +++ b/components/canvas/hooks/useCopyPaste.tsx @@ -53,4 +53,6 @@ export const useCopyPaste = ( document.removeEventListener("keydown", handleKeyDown); }; }, [target]); + + return { copyToClipboard, paste }; }; From fe8a5e5503a13a75ceaba9de7097ae48b493b711 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 12 Sep 2024 14:38:13 +0200 Subject: [PATCH 04/22] refactor: Use _ for unused parameter --- components/canvas/Canvas.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 1387383e..3288bc14 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -356,7 +356,7 @@ const Canvas = ({ onNodeDragStop={onNodeDragStop} elevateEdgesOnSelect={true} edgesFocusable={userCanEdit} - onNodeMouseEnter={(e, node) => setHoveredNode(node)} + onNodeMouseEnter={(_, node) => setHoveredNode(node)} onNodeMouseLeave={() => setHoveredNode(undefined)} onEdgeMouseEnter={(event, edge) => handleSetSelectedEdge(edge)} onEdgeMouseLeave={() => handleSetSelectedEdge(undefined)} From 51aed1018a5227ab14a7e1b8438fc63ac589ad45 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 12 Sep 2024 14:41:29 +0200 Subject: [PATCH 05/22] feat: Copy and paste error snackmessage --- components/canvas/hooks/useCopyPaste.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/components/canvas/hooks/useCopyPaste.tsx b/components/canvas/hooks/useCopyPaste.tsx index 1943fe18..d0ecc7a5 100644 --- a/components/canvas/hooks/useCopyPaste.tsx +++ b/components/canvas/hooks/useCopyPaste.tsx @@ -1,4 +1,5 @@ import { useStoreDispatch } from "@/hooks/storeHooks"; +import { unknownErrorToString } from "@/utils/isError"; import { useEffect } from "react"; export const useCopyPaste = ( @@ -16,9 +17,7 @@ export const useCopyPaste = ( dispatch.setSnackMessage("Copied 📋"); } } catch (error) { - if (error instanceof Error) { - console.error(error.message); - } + dispatch.setSnackMessage(unknownErrorToString(error)); } }; @@ -28,13 +27,10 @@ export const useCopyPaste = ( targetToPaste = JSON.parse(targetToPaste); const valid = validator ? validator(targetToPaste) : true; if (valid) { - dispatch.setSnackMessage("Pasted 📝"); action(targetToPaste); } } catch (error) { - if (error instanceof Error) { - console.error(error.message); - } + dispatch.setSnackMessage(unknownErrorToString(error)); } }; From 784c8c6b33eae2f0a6f98b99058bc131148cd090 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 12 Sep 2024 15:00:29 +0200 Subject: [PATCH 06/22] fix: Copy paste target area --- components/canvas/hooks/useCopyPaste.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/components/canvas/hooks/useCopyPaste.tsx b/components/canvas/hooks/useCopyPaste.tsx index d0ecc7a5..fe2d0480 100644 --- a/components/canvas/hooks/useCopyPaste.tsx +++ b/components/canvas/hooks/useCopyPaste.tsx @@ -36,9 +36,17 @@ export const useCopyPaste = ( useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if ((event.metaKey || event.ctrlKey) && event.key === "c") { + if ( + (event.metaKey || event.ctrlKey) && + event.key === "c" && + event.target == document.body + ) { copyToClipboard(target); - } else if ((event.metaKey || event.ctrlKey) && event.key === "v") { + } else if ( + (event.metaKey || event.ctrlKey) && + event.key === "v" && + event.target == document.body + ) { paste(); } }; From 78c0343e883a244a2aa6f8ae660be497608ef5cc Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 12 Sep 2024 15:29:08 +0200 Subject: [PATCH 07/22] refactor Copy paste target typing --- components/canvas/hooks/useCopyPaste.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/components/canvas/hooks/useCopyPaste.tsx b/components/canvas/hooks/useCopyPaste.tsx index fe2d0480..d111f313 100644 --- a/components/canvas/hooks/useCopyPaste.tsx +++ b/components/canvas/hooks/useCopyPaste.tsx @@ -1,18 +1,19 @@ import { useStoreDispatch } from "@/hooks/storeHooks"; +import { NodeData } from "@/types/NodeData"; import { unknownErrorToString } from "@/utils/isError"; import { useEffect } from "react"; +import { Node } from "reactflow"; export const useCopyPaste = ( - target: any, + target: Node | undefined, action: (target: any) => void, validator?: (target: any) => void ) => { const dispatch = useStoreDispatch(); - const copyToClipboard = async (target: any) => { + const copyToClipboard = async (target: Node | undefined) => { try { - const valid = validator ? validator(target) : true; - if (valid) { + if (target) { await navigator.clipboard.writeText(JSON.stringify(target)); dispatch.setSnackMessage("Copied 📋"); } From f7a8fddc77843d4afdb438474ef094346ab7682e Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 12 Sep 2024 16:11:11 +0200 Subject: [PATCH 08/22] fix: Menu text and icon spacing --- components/MenuItemExandable.tsx | 20 +++++++------------- components/MenuItemExpandable.module.scss | 4 ++++ components/canvas/ContextMenu.tsx | 13 ++++++++++--- public/expand_arrow_right.svg | 9 +++++++++ 4 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 public/expand_arrow_right.svg diff --git a/components/MenuItemExandable.tsx b/components/MenuItemExandable.tsx index da01087b..74cdc1e4 100644 --- a/components/MenuItemExandable.tsx +++ b/components/MenuItemExandable.tsx @@ -1,5 +1,6 @@ import { Menu } from "@equinor/eds-core-react"; import { ReactElement, useState } from "react"; +import ExpandArrow from "../public/expand_arrow_right.svg"; import styles from "./MenuItemExpandable.module.scss"; type MenuItemExandableProps = { @@ -14,25 +15,18 @@ export const MenuItemExandable = ({ const [anchorEl, setAnchorEl] = useState(null); const [isExpanded, setIsExpanded] = useState(false); - const expandArrow = ( - - - - ); - return ( setIsExpanded(true)} onMouseLeave={() => setIsExpanded(false)} > - {text} {expandArrow} +
{text}
+ right-arrow div { padding: 0; } + +.expandArrowContainer { + margin-left: auto; +} diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index 304d3f09..3378c077 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -32,16 +32,23 @@ export const ContextMenu = ({ return ( <> copyToClipBoard?.(node)}> - Copy {modifierKey}C +
Copy
+
{modifierKey}C
+
+ +
Paste
+
{modifierKey}V
- Paste {modifierKey}V onEditNode?.(node)}>Edit Sub Activity Choice Waiting - onDelete?.(node)}>Delete ⌫ + onDelete?.(node)}> +
Delete
+
+
); } diff --git a/public/expand_arrow_right.svg b/public/expand_arrow_right.svg new file mode 100644 index 00000000..4d6142db --- /dev/null +++ b/public/expand_arrow_right.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file From 60f10e39c2a90e0cfb09499c42540f6a9e909984 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 13 Sep 2024 10:37:05 +0200 Subject: [PATCH 09/22] feat: Manu element for node options --- components/Labels/ActiveFilterSection.tsx | 2 +- components/canvas/ContextMenu.tsx | 29 ++++++++++-- components/canvas/utils/getOptionsAddNode.tsx | 46 +++++++++++++++++++ pages/processes/favourite.tsx | 2 +- pages/processes/index.tsx | 2 +- pages/processes/mine.tsx | 2 +- utils/{stringToArray.ts => stringHelpers.tsx} | 7 +++ utils/unitDefinitions.tsx | 8 +--- 8 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 components/canvas/utils/getOptionsAddNode.tsx rename utils/{stringToArray.ts => stringHelpers.tsx} (66%) diff --git a/components/Labels/ActiveFilterSection.tsx b/components/Labels/ActiveFilterSection.tsx index ff33b8df..f2fbfaf4 100644 --- a/components/Labels/ActiveFilterSection.tsx +++ b/components/Labels/ActiveFilterSection.tsx @@ -3,7 +3,7 @@ import { Button, Chip } from "@equinor/eds-core-react"; import { getLabel } from "services/labelsApi"; import { getUpdatedLabel } from "utils/getUpdatedLabel"; import { getUserById } from "services/userApi"; -import { stringToArray } from "utils/stringToArray"; +import { stringToArray } from "utils/stringHelpers"; import { toggleQueryParam } from "utils/toggleQueryParam"; import { unknownErrorToString } from "utils/isError"; import { useQuery } from "react-query"; diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index 3378c077..f062d6f4 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -1,10 +1,12 @@ import { NodeData } from "@/types/NodeData"; +import { capitalizeFirstLetter } from "@/utils/stringHelpers"; import { Menu } from "@equinor/eds-core-react"; import { useCallback, useState } from "react"; import { Node } from "reactflow"; import { MenuItemExandable } from "../MenuItemExandable"; import styles from "./ContextMenu.module.scss"; import type { MenuData } from "./hooks/useContextMenu"; +import { getOptionsAddNode } from "./utils/getOptionsAddNode"; type ContextMenuProps = { menuData: MenuData; @@ -27,6 +29,27 @@ export const ContextMenu = ({ const modifierKey = window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"; + const renderOptionsAddNode = (node: Node) => { + const optionsAddNode = getOptionsAddNode(node); + const entries = Object.entries(optionsAddNode); + if (entries.length > 0) { + return ( + + {entries.map(([position, nodeTypes]) => ( + + {nodeTypes.map((nodeType) => ( + {nodeType} + ))} + + ))} + + ); + } + }; + const renderNodeMenuItems = useCallback(() => { if (node) { return ( @@ -40,11 +63,7 @@ export const ContextMenu = ({
{modifierKey}V
onEditNode?.(node)}>Edit - - Sub Activity - Choice - Waiting - + {renderOptionsAddNode(node)} onDelete?.(node)}>
Delete
diff --git a/components/canvas/utils/getOptionsAddNode.tsx b/components/canvas/utils/getOptionsAddNode.tsx new file mode 100644 index 00000000..a25cc7bc --- /dev/null +++ b/components/canvas/utils/getOptionsAddNode.tsx @@ -0,0 +1,46 @@ +import { NodeData } from "@/types/NodeData"; +import { NodeTypes } from "@/types/NodeTypes"; +import { Node } from "reactflow"; + +const optionsMatrix = { + [NodeTypes.root]: {}, + [NodeTypes.supplier]: {}, + [NodeTypes.input]: { + right: [NodeTypes.mainActivity], + }, + [NodeTypes.output]: { + left: [NodeTypes.mainActivity], + }, + [NodeTypes.customer]: {}, + [NodeTypes.mainActivity]: { + bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + left: [NodeTypes.mainActivity], + right: [NodeTypes.mainActivity], + }, + [NodeTypes.subActivity]: { + bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + }, + [NodeTypes.waiting]: { + bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + }, + [NodeTypes.choice]: { + bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + }, + [NodeTypes.text]: {}, + [NodeTypes.hidden]: {}, +}; + +export const getOptionsAddNode = (node: Node) => { + const { type, isChoiceChild } = node.data; + let options = optionsMatrix[type]; + + if (isChoiceChild) { + options = { + ...options, + left: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + right: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + }; + } + + return options; +}; diff --git a/pages/processes/favourite.tsx b/pages/processes/favourite.tsx index 7593cec9..d214d9b5 100644 --- a/pages/processes/favourite.tsx +++ b/pages/processes/favourite.tsx @@ -11,7 +11,7 @@ import { SideNavBar } from "components/SideNavBar"; import { SortSelect } from "../../components/SortSelect"; import { Typography } from "@equinor/eds-core-react"; import { getProjects } from "../../services/projectApi"; -import { stringToArray } from "utils/stringToArray"; +import { stringToArray } from "utils/stringHelpers"; import styles from "./FrontPage.module.scss"; import { useQuery } from "react-query"; import { useRouter } from "next/router"; diff --git a/pages/processes/index.tsx b/pages/processes/index.tsx index c44d40d4..c7e5ddf0 100644 --- a/pages/processes/index.tsx +++ b/pages/processes/index.tsx @@ -11,7 +11,7 @@ import { SideNavBar } from "components/SideNavBar"; import { SortSelect } from "@/components/SortSelect"; import { Typography } from "@equinor/eds-core-react"; import { getProjects } from "@/services/projectApi"; -import { stringToArray } from "utils/stringToArray"; +import { stringToArray } from "utils/stringHelpers"; import styles from "./FrontPage.module.scss"; import { useInfiniteQuery } from "react-query"; import { useRouter } from "next/router"; diff --git a/pages/processes/mine.tsx b/pages/processes/mine.tsx index 8950f856..68604efe 100644 --- a/pages/processes/mine.tsx +++ b/pages/processes/mine.tsx @@ -14,7 +14,7 @@ import { Typography } from "@equinor/eds-core-react"; import { getProjects } from "@/services/projectApi"; import { getUserShortName } from "@/utils/getUserShortName"; import { getUserByShortname } from "services/userApi"; -import { stringToArray } from "utils/stringToArray"; +import { stringToArray } from "utils/stringHelpers"; import styles from "./FrontPage.module.scss"; import { useQuery } from "react-query"; import { useRouter } from "next/router"; diff --git a/utils/stringToArray.ts b/utils/stringHelpers.tsx similarity index 66% rename from utils/stringToArray.ts rename to utils/stringHelpers.tsx index 0f92ab39..f4233cc3 100644 --- a/utils/stringToArray.ts +++ b/utils/stringHelpers.tsx @@ -1,3 +1,10 @@ +/** + * Capitalize the first letter and lowercase the rest. + * @param s + */ +export const capitalizeFirstLetter = (s: string): string => + `${s}`.charAt(0).toUpperCase() + `${s}`.slice(1).toLowerCase(); + /** * Helper function to convert a string to an array of strings. * diff --git a/utils/unitDefinitions.tsx b/utils/unitDefinitions.tsx index c62ec916..0bb67908 100644 --- a/utils/unitDefinitions.tsx +++ b/utils/unitDefinitions.tsx @@ -1,5 +1,6 @@ import { NodeData } from "@/types/NodeData"; import { TimeDefinition } from "@/types/TimeDefinition"; +import { capitalizeFirstLetter } from "./stringHelpers"; export const timeDefinitions: TimeDefinition[] = [ { value: "Minute", displayName: "Minute(s)", duration: null }, @@ -39,13 +40,6 @@ export const getTimeDefinitionValue = (displayName: string) => export const getTimeDefinitionDisplayName = (value: string) => timeDefinitions.find((item) => item.value === value)?.displayName || ""; -/** - * Capitalize the first letter and lowercase the rest. - * @param s - */ -const capitalizeFirstLetter = (s: string): string => - `${s}`.charAt(0).toUpperCase() + `${s}`.slice(1).toLowerCase(); - const getShortDisplayName = (displayName: string) => displayName.slice(0, displayName === "Month(s)" ? 2 : 1).toLowerCase(); From 78d66cc2104926957469f8f50498df852cc3bbf0 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 13 Sep 2024 12:00:40 +0200 Subject: [PATCH 10/22] feat: Menu add node API call --- components/MenuItemExpandable.module.scss | 4 ---- components/canvas/ContextMenu.tsx | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/components/MenuItemExpandable.module.scss b/components/MenuItemExpandable.module.scss index bcef4a25..bea41b50 100644 --- a/components/MenuItemExpandable.module.scss +++ b/components/MenuItemExpandable.module.scss @@ -1,7 +1,3 @@ -.menu { - margin-left: -4px; -} - .menu > div { padding: 0; } diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index f062d6f4..b95bc257 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -2,11 +2,13 @@ import { NodeData } from "@/types/NodeData"; import { capitalizeFirstLetter } from "@/utils/stringHelpers"; import { Menu } from "@equinor/eds-core-react"; import { useCallback, useState } from "react"; -import { Node } from "reactflow"; +import { Node, Position } from "reactflow"; import { MenuItemExandable } from "../MenuItemExandable"; import styles from "./ContextMenu.module.scss"; import type { MenuData } from "./hooks/useContextMenu"; import { getOptionsAddNode } from "./utils/getOptionsAddNode"; +import { getNodeTypeName } from "@/utils/getNodeTypeName"; +import { useNodeAdd } from "./hooks/useNodeAdd"; type ContextMenuProps = { menuData: MenuData; @@ -26,6 +28,7 @@ export const ContextMenu = ({ onEditNode, }: ContextMenuProps) => { const [anchorEl, setAnchorEl] = useState(null); + const { addNode, isNodeButtonDisabled } = useNodeAdd(); const modifierKey = window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"; @@ -41,7 +44,15 @@ export const ContextMenu = ({ key={position} > {nodeTypes.map((nodeType) => ( - {nodeType} + + addNode(node.id, { type: nodeType }, position as Position) + } + > + {getNodeTypeName(nodeType)} + ))} ))} From 5e62b66a27798c4687ca81dd429932bbd06e9950 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Sat, 14 Sep 2024 20:17:08 +0200 Subject: [PATCH 11/22] fix: Menu expand direction issue --- components/MenuItemExandable.tsx | 7 ++++++- components/canvas/Canvas.tsx | 3 ++- components/canvas/ContextMenu.tsx | 27 +++++++++++++++++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/components/MenuItemExandable.tsx b/components/MenuItemExandable.tsx index 74cdc1e4..1a919cdb 100644 --- a/components/MenuItemExandable.tsx +++ b/components/MenuItemExandable.tsx @@ -5,11 +5,13 @@ import styles from "./MenuItemExpandable.module.scss"; type MenuItemExandableProps = { text: string; + reverseExpandDir?: boolean; children: ReactElement | ReactElement[]; }; export const MenuItemExandable = ({ text, + reverseExpandDir, children, }: MenuItemExandableProps) => { const [anchorEl, setAnchorEl] = useState(null); @@ -28,9 +30,12 @@ export const MenuItemExandable = ({ className={styles.expandArrowContainer} /> {children} diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 0e6536b8..904d41d8 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -372,7 +372,7 @@ const Canvas = ({ onNodeMouseLeave={() => { !menuData && setHoveredNode(undefined); }} - onEdgeMouseEnter={(event, edge) => handleSetSelectedEdge(edge)} + onEdgeMouseEnter={(_, edge) => handleSetSelectedEdge(edge)} onEdgeMouseLeave={() => handleSetSelectedEdge(undefined)} attributionPosition="bottom-right" connectionRadius={100} @@ -395,6 +395,7 @@ const Canvas = ({ centerCanvas={centerCanvas} onDelete={(node) => setNodeToBeDeleted(node)} onEditNode={(node) => setSelectedNode(node)} + canvasRef={ref} /> )} diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index b95bc257..096d8b14 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -1,14 +1,14 @@ import { NodeData } from "@/types/NodeData"; +import { getNodeTypeName } from "@/utils/getNodeTypeName"; import { capitalizeFirstLetter } from "@/utils/stringHelpers"; import { Menu } from "@equinor/eds-core-react"; -import { useCallback, useState } from "react"; +import { RefObject, useState } from "react"; import { Node, Position } from "reactflow"; import { MenuItemExandable } from "../MenuItemExandable"; import styles from "./ContextMenu.module.scss"; import type { MenuData } from "./hooks/useContextMenu"; -import { getOptionsAddNode } from "./utils/getOptionsAddNode"; -import { getNodeTypeName } from "@/utils/getNodeTypeName"; import { useNodeAdd } from "./hooks/useNodeAdd"; +import { getOptionsAddNode } from "./utils/getOptionsAddNode"; type ContextMenuProps = { menuData: MenuData; @@ -17,6 +17,7 @@ type ContextMenuProps = { paste?: () => void; onDelete?: (node: Node) => void; onEditNode?: (node: Node) => void; + canvasRef?: RefObject; }; export const ContextMenu = ({ @@ -26,21 +27,35 @@ export const ContextMenu = ({ paste, onDelete, onEditNode, + canvasRef, }: ContextMenuProps) => { const [anchorEl, setAnchorEl] = useState(null); const { addNode, isNodeButtonDisabled } = useNodeAdd(); + const isReversedExpandDirection = (fullyExpandedWidth: number) => { + const anchorOffsetLeft = anchorEl?.offsetLeft; + const canvasEdgeRight = canvasRef?.current?.getBoundingClientRect().right; + return !!( + canvasEdgeRight && + anchorOffsetLeft && + canvasEdgeRight - anchorOffsetLeft < fullyExpandedWidth + ); + }; + const modifierKey = window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"; const renderOptionsAddNode = (node: Node) => { const optionsAddNode = getOptionsAddNode(node); const entries = Object.entries(optionsAddNode); if (entries.length > 0) { + const fullyExpandedWidth = 400; + const reversedExpandDir = isReversedExpandDirection(fullyExpandedWidth); return ( - + {entries.map(([position, nodeTypes]) => ( {nodeTypes.map((nodeType) => ( @@ -61,7 +76,7 @@ export const ContextMenu = ({ } }; - const renderNodeMenuItems = useCallback(() => { + const renderNodeMenuItems = () => { if (node) { return ( <> @@ -82,7 +97,7 @@ export const ContextMenu = ({ ); } - }, [node, copyToClipBoard, modifierKey, paste, onDelete, onEditNode]); + }; return (
From d56741ad74dc3cd1bc8c7b740c78b6c76af95755 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Thu, 26 Sep 2024 16:23:54 +0200 Subject: [PATCH 12/22] refactor: Code review feedback --- components/canvas/ContextMenu.tsx | 95 +++++++++---------- components/canvas/utils/getOptionsAddNode.tsx | 2 +- 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index 096d8b14..7187fe54 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -42,61 +42,58 @@ export const ContextMenu = ({ ); }; - const modifierKey = window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl"; + const modifierKey = navigator.userAgent.includes("Mac") ? "⌘" : "Ctrl"; const renderOptionsAddNode = (node: Node) => { const optionsAddNode = getOptionsAddNode(node); - const entries = Object.entries(optionsAddNode); - if (entries.length > 0) { - const fullyExpandedWidth = 400; - const reversedExpandDir = isReversedExpandDirection(fullyExpandedWidth); - return ( - - {entries.map(([position, nodeTypes]) => ( - - {nodeTypes.map((nodeType) => ( - - addNode(node.id, { type: nodeType }, position as Position) - } - > - {getNodeTypeName(nodeType)} - - ))} - - ))} - - ); - } + if (optionsAddNode.length === 0) return; + const fullyExpandedWidth = 400; + const reversedExpandDir = isReversedExpandDirection(fullyExpandedWidth); + return ( + + {optionsAddNode.map(([position, nodeTypes]) => ( + + {nodeTypes.map((nodeType) => ( + + addNode(node.id, { type: nodeType }, position as Position) + } + > + {getNodeTypeName(nodeType)} + + ))} + + ))} + + ); }; const renderNodeMenuItems = () => { - if (node) { - return ( - <> - copyToClipBoard?.(node)}> -
Copy
-
{modifierKey}C
-
- -
Paste
-
{modifierKey}V
-
- onEditNode?.(node)}>Edit - {renderOptionsAddNode(node)} - onDelete?.(node)}> -
Delete
-
-
- - ); - } + if (!node) return; + return ( + <> + copyToClipBoard?.(node)}> +
Copy
+
{modifierKey}+C
+
+ +
Paste
+
{modifierKey}+V
+
+ onEditNode?.(node)}>Edit + {renderOptionsAddNode(node)} + onDelete?.(node)}> +
Delete
+
+
+ + ); }; return ( diff --git a/components/canvas/utils/getOptionsAddNode.tsx b/components/canvas/utils/getOptionsAddNode.tsx index a25cc7bc..a66c2e8e 100644 --- a/components/canvas/utils/getOptionsAddNode.tsx +++ b/components/canvas/utils/getOptionsAddNode.tsx @@ -42,5 +42,5 @@ export const getOptionsAddNode = (node: Node) => { }; } - return options; + return Object.entries(options); }; From f9d92b14936874590748aa07f43efd053dfc6862 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 27 Sep 2024 16:42:36 +0200 Subject: [PATCH 13/22] refactor: Improved optionmatrix typing --- components/canvas/ContextMenu.tsx | 8 ++-- components/canvas/utils/getOptionsAddNode.tsx | 44 ++++++++++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index 7187fe54..5d393789 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -3,7 +3,7 @@ import { getNodeTypeName } from "@/utils/getNodeTypeName"; import { capitalizeFirstLetter } from "@/utils/stringHelpers"; import { Menu } from "@equinor/eds-core-react"; import { RefObject, useState } from "react"; -import { Node, Position } from "reactflow"; +import { Node } from "reactflow"; import { MenuItemExandable } from "../MenuItemExandable"; import styles from "./ContextMenu.module.scss"; import type { MenuData } from "./hooks/useContextMenu"; @@ -59,11 +59,9 @@ export const ContextMenu = ({ > {nodeTypes.map((nodeType) => ( - addNode(node.id, { type: nodeType }, position as Position) - } + onClick={() => addNode(node.id, { type: nodeType }, position)} > {getNodeTypeName(nodeType)} diff --git a/components/canvas/utils/getOptionsAddNode.tsx b/components/canvas/utils/getOptionsAddNode.tsx index a66c2e8e..f37ca970 100644 --- a/components/canvas/utils/getOptionsAddNode.tsx +++ b/components/canvas/utils/getOptionsAddNode.tsx @@ -1,30 +1,52 @@ import { NodeData } from "@/types/NodeData"; import { NodeTypes } from "@/types/NodeTypes"; -import { Node } from "reactflow"; +import { Node, Position } from "reactflow"; -const optionsMatrix = { +type OptionsMatrix = { + [key in NodeTypes]: { + [key in Position]?: NodeTypes[]; + }; +}; + +const optionsMatrix: OptionsMatrix = { [NodeTypes.root]: {}, [NodeTypes.supplier]: {}, [NodeTypes.input]: { - right: [NodeTypes.mainActivity], + [Position.Right]: [NodeTypes.mainActivity], }, [NodeTypes.output]: { - left: [NodeTypes.mainActivity], + [Position.Left]: [NodeTypes.mainActivity], }, [NodeTypes.customer]: {}, [NodeTypes.mainActivity]: { - bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], - left: [NodeTypes.mainActivity], - right: [NodeTypes.mainActivity], + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], + [Position.Left]: [NodeTypes.mainActivity], + [Position.Right]: [NodeTypes.mainActivity], }, [NodeTypes.subActivity]: { - bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], }, [NodeTypes.waiting]: { - bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], }, [NodeTypes.choice]: { - bottom: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], }, [NodeTypes.text]: {}, [NodeTypes.hidden]: {}, @@ -42,5 +64,5 @@ export const getOptionsAddNode = (node: Node) => { }; } - return Object.entries(options); + return Object.entries(options) as [Position, NodeTypes[]][]; }; From 8440d04c6e317071791ea8424d265bfb403a1895 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 30 Sep 2024 19:45:25 +0200 Subject: [PATCH 14/22] refactor: Remove unused node type --- types/NodeTypes.tsx | 1 - utils/getNodeTypeName.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/types/NodeTypes.tsx b/types/NodeTypes.tsx index 2711fa8f..e06b3d12 100644 --- a/types/NodeTypes.tsx +++ b/types/NodeTypes.tsx @@ -4,7 +4,6 @@ export enum NodeTypes { input = "Input", mainActivity = "MainActivity", subActivity = "SubActivity", - text = "Text", waiting = "Waiting", output = "Output", customer = "Customer", diff --git a/utils/getNodeTypeName.ts b/utils/getNodeTypeName.ts index 711b271b..5f519de2 100644 --- a/utils/getNodeTypeName.ts +++ b/utils/getNodeTypeName.ts @@ -12,8 +12,6 @@ export const getNodeTypeName = (type: NodeTypes): string => { return "Main Activity"; case NodeTypes.subActivity: return "Sub Activity"; - case NodeTypes.text: - return "Text"; case NodeTypes.waiting: return "Waiting"; case NodeTypes.output: From a436c528abcf18df1d779bcfd71957b72de1f73b Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 30 Sep 2024 19:46:40 +0200 Subject: [PATCH 15/22] fix: Check copy and delete node validity --- components/canvas/Canvas.tsx | 5 + components/canvas/ContextMenu.tsx | 32 +++-- components/canvas/utils/getOptionsAddNode.tsx | 68 ----------- .../canvas/utils/nodeValidityHelper.tsx | 113 ++++++++++++++++++ types/NodeData.ts | 2 + 5 files changed, 139 insertions(+), 81 deletions(-) delete mode 100644 components/canvas/utils/getOptionsAddNode.tsx create mode 100644 components/canvas/utils/nodeValidityHelper.tsx diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 904d41d8..5812458e 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -45,6 +45,7 @@ import { validTarget } from "./utils/validTarget"; import { useNodeAdd } from "./hooks/useNodeAdd"; import { useContextMenu } from "./hooks/useContextMenu"; import { ContextMenu } from "./ContextMenu"; +import { nodeValidityMap } from "./utils/nodeValidityHelper"; type CanvasProps = { graph: Graph; @@ -154,6 +155,8 @@ const Canvas = ({ mergeable: node.children.length === 0 || node.type === NodeTypes.choice, merging: merging, + deletable: nodeValidityMap[node.type].deletable, + copyable: nodeValidityMap[node.type].copyable, parents: [parent.id], userCanEdit, isChoiceChild: parent.type === NodeTypes.choice, @@ -172,6 +175,8 @@ const Canvas = ({ id: node.id, data: { ...node, + deletable: nodeValidityMap[node.type].deletable, + copyable: nodeValidityMap[node.type].copyable, parents: [], columnId: node.id, shapeHeight: shapeSize.height, diff --git a/components/canvas/ContextMenu.tsx b/components/canvas/ContextMenu.tsx index 5d393789..588a87d8 100644 --- a/components/canvas/ContextMenu.tsx +++ b/components/canvas/ContextMenu.tsx @@ -8,7 +8,7 @@ import { MenuItemExandable } from "../MenuItemExandable"; import styles from "./ContextMenu.module.scss"; import type { MenuData } from "./hooks/useContextMenu"; import { useNodeAdd } from "./hooks/useNodeAdd"; -import { getOptionsAddNode } from "./utils/getOptionsAddNode"; +import { getOptionsAddNode } from "./utils/nodeValidityHelper"; type ContextMenuProps = { menuData: MenuData; @@ -74,22 +74,28 @@ export const ContextMenu = ({ const renderNodeMenuItems = () => { if (!node) return; + const { deletable, copyable } = node.data; return ( <> - copyToClipBoard?.(node)}> -
Copy
-
{modifierKey}+C
-
- -
Paste
-
{modifierKey}+V
-
+ {copyable && ( + <> + copyToClipBoard?.(node)}> +
Copy
+
{modifierKey}+C
+
+ +
Paste
+
{modifierKey}+V
+
+ + )} onEditNode?.(node)}>Edit {renderOptionsAddNode(node)} - onDelete?.(node)}> -
Delete
-
-
+ {deletable && ( + onDelete?.(node)}> +
Delete
+
+ )} ); }; diff --git a/components/canvas/utils/getOptionsAddNode.tsx b/components/canvas/utils/getOptionsAddNode.tsx deleted file mode 100644 index f37ca970..00000000 --- a/components/canvas/utils/getOptionsAddNode.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { NodeData } from "@/types/NodeData"; -import { NodeTypes } from "@/types/NodeTypes"; -import { Node, Position } from "reactflow"; - -type OptionsMatrix = { - [key in NodeTypes]: { - [key in Position]?: NodeTypes[]; - }; -}; - -const optionsMatrix: OptionsMatrix = { - [NodeTypes.root]: {}, - [NodeTypes.supplier]: {}, - [NodeTypes.input]: { - [Position.Right]: [NodeTypes.mainActivity], - }, - [NodeTypes.output]: { - [Position.Left]: [NodeTypes.mainActivity], - }, - [NodeTypes.customer]: {}, - [NodeTypes.mainActivity]: { - [Position.Bottom]: [ - NodeTypes.subActivity, - NodeTypes.choice, - NodeTypes.waiting, - ], - [Position.Left]: [NodeTypes.mainActivity], - [Position.Right]: [NodeTypes.mainActivity], - }, - [NodeTypes.subActivity]: { - [Position.Bottom]: [ - NodeTypes.subActivity, - NodeTypes.choice, - NodeTypes.waiting, - ], - }, - [NodeTypes.waiting]: { - [Position.Bottom]: [ - NodeTypes.subActivity, - NodeTypes.choice, - NodeTypes.waiting, - ], - }, - [NodeTypes.choice]: { - [Position.Bottom]: [ - NodeTypes.subActivity, - NodeTypes.choice, - NodeTypes.waiting, - ], - }, - [NodeTypes.text]: {}, - [NodeTypes.hidden]: {}, -}; - -export const getOptionsAddNode = (node: Node) => { - const { type, isChoiceChild } = node.data; - let options = optionsMatrix[type]; - - if (isChoiceChild) { - options = { - ...options, - left: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], - right: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], - }; - } - - return Object.entries(options) as [Position, NodeTypes[]][]; -}; diff --git a/components/canvas/utils/nodeValidityHelper.tsx b/components/canvas/utils/nodeValidityHelper.tsx new file mode 100644 index 00000000..7af6a5a7 --- /dev/null +++ b/components/canvas/utils/nodeValidityHelper.tsx @@ -0,0 +1,113 @@ +import { NodeData } from "@/types/NodeData"; +import { NodeTypes } from "@/types/NodeTypes"; +import { Node, Position } from "reactflow"; + +type NodeValidity = { + validPositions: { + [key in Position]?: NodeTypes[]; + }; + copyable: boolean; + deletable: boolean; +}; + +type NodeValidityMap = { + [key in NodeTypes]: NodeValidity; +}; + +export const nodeValidityMap: NodeValidityMap = { + [NodeTypes.root]: { + validPositions: {}, + copyable: false, + deletable: false, + }, + [NodeTypes.supplier]: { + validPositions: {}, + copyable: false, + deletable: false, + }, + [NodeTypes.input]: { + validPositions: { + [Position.Right]: [NodeTypes.mainActivity], + }, + copyable: false, + deletable: false, + }, + [NodeTypes.output]: { + validPositions: { + [Position.Left]: [NodeTypes.mainActivity], + }, + copyable: false, + deletable: false, + }, + [NodeTypes.customer]: { + validPositions: {}, + copyable: false, + deletable: false, + }, + [NodeTypes.mainActivity]: { + validPositions: { + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], + [Position.Left]: [NodeTypes.mainActivity], + [Position.Right]: [NodeTypes.mainActivity], + }, + copyable: true, + deletable: true, + }, + [NodeTypes.subActivity]: { + validPositions: { + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], + }, + copyable: true, + deletable: true, + }, + [NodeTypes.waiting]: { + validPositions: { + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], + }, + copyable: true, + deletable: true, + }, + [NodeTypes.choice]: { + validPositions: { + [Position.Bottom]: [ + NodeTypes.subActivity, + NodeTypes.choice, + NodeTypes.waiting, + ], + }, + copyable: true, + deletable: true, + }, + [NodeTypes.hidden]: { + validPositions: {}, + copyable: false, + deletable: false, + }, +}; + +export const getOptionsAddNode = (node: Node) => { + const { type, isChoiceChild } = node.data; + let validPositions = nodeValidityMap[type].validPositions; + + if (isChoiceChild) { + validPositions = { + ...validPositions, + left: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + right: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + }; + } + + return Object.entries(validPositions) as [Position, NodeTypes[]][]; +}; diff --git a/types/NodeData.ts b/types/NodeData.ts index 7c36744d..d530c37d 100644 --- a/types/NodeData.ts +++ b/types/NodeData.ts @@ -11,6 +11,8 @@ export type NodeData = { handleClickNode?: () => void; mergeable?: boolean; merging?: boolean; + deletable: boolean; + copyable: boolean; userCanEdit?: boolean; depth?: number; isChoiceChild?: boolean; From 8f5bedad103b33e5d47cab185f88c2c88b13abdb Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 30 Sep 2024 20:02:16 +0200 Subject: [PATCH 16/22] fix: Enable copy paste while contextmenu is open --- components/canvas/hooks/useCopyPaste.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/components/canvas/hooks/useCopyPaste.tsx b/components/canvas/hooks/useCopyPaste.tsx index d736b289..c53f4d8a 100644 --- a/components/canvas/hooks/useCopyPaste.tsx +++ b/components/canvas/hooks/useCopyPaste.tsx @@ -37,17 +37,9 @@ export const useCopyPaste = ( useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if ( - (event.metaKey || event.ctrlKey) && - event.key === "c" && - event.target == document.body - ) { + if (target && (event.metaKey || event.ctrlKey) && event.key === "c") { copyToClipboard(target); - } else if ( - (event.metaKey || event.ctrlKey) && - event.key === "v" && - event.target == document.body - ) { + } else if ((event.metaKey || event.ctrlKey) && event.key === "v") { paste(); } }; From 937c4efbdda5e8efa68464f04036965633bcb0c0 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 7 Oct 2024 10:55:18 +0200 Subject: [PATCH 17/22] fix: Main activity copy paste --- components/canvas/Canvas.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 5812458e..4e969ff0 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -100,7 +100,11 @@ const Canvas = ({ (node: Node) => hoveredNode?.id && validTarget(node, hoveredNode, nodes, false) && - addNode(hoveredNode.id, node.data, Position.Bottom), + addNode( + hoveredNode.id, + node.data, + node.type === NodeTypes.mainActivity ? Position.Right : Position.Bottom + ), copyPasteNodeValidator ); From 9365a85874e7ad89cbe6cb721bb88a2895c87da7 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 7 Oct 2024 11:01:26 +0200 Subject: [PATCH 18/22] fix: Include children when deleting main and choice --- components/DeleteNodeDialog.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/DeleteNodeDialog.tsx b/components/DeleteNodeDialog.tsx index 79f69a68..9aedb544 100644 --- a/components/DeleteNodeDialog.tsx +++ b/components/DeleteNodeDialog.tsx @@ -81,7 +81,11 @@ export function DeleteNodeDialog(props: { open header={header} onClose={handleClose} - onConfirm={(_, includeChildren) => handleDelete(includeChildren)} + onConfirm={(_, includeChildren) => + handleDelete( + type === mainActivity || type === choice || includeChildren + ) + } error={deleteMutation.error} warningMessage={warningMessage} confirmMessage={confirmMessage} From 3e79a2ca1d76db1e986508cf2c0740e33e2b8efc Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Mon, 7 Oct 2024 14:25:48 +0200 Subject: [PATCH 19/22] fix: Close context menu on drag --- components/canvas/Canvas.tsx | 3 ++- components/canvas/hooks/useContextMenu.tsx | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/canvas/Canvas.tsx b/components/canvas/Canvas.tsx index 4e969ff0..7b35341a 100644 --- a/components/canvas/Canvas.tsx +++ b/components/canvas/Canvas.tsx @@ -87,7 +87,7 @@ const Canvas = ({ const { onNodeDragStart, onNodeDrag, onNodeDragStop } = useNodeDrag(); const { mutate: mergeNode, merging } = useNodeMerge(); const { deleteEdgeMutation } = useEdgeDelete(); - const { menuData, onNodeContextMenu, onPaneContextMenu } = + const { menuData, onNodeContextMenu, onPaneContextMenu, closeContextMenu } = useContextMenu(ref); useEffect(() => { @@ -366,6 +366,7 @@ const Canvas = ({ onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onPaneClick={() => setSelectedNode(undefined)} + onMoveStart={() => closeContextMenu()} minZoom={0.2} nodesDraggable={userCanEdit} nodesConnectable={true} diff --git a/components/canvas/hooks/useContextMenu.tsx b/components/canvas/hooks/useContextMenu.tsx index 3cee868f..75708ee8 100644 --- a/components/canvas/hooks/useContextMenu.tsx +++ b/components/canvas/hooks/useContextMenu.tsx @@ -57,9 +57,11 @@ export const useContextMenu = (ref: RefObject) => { [setMenuData] ); + const closeContextMenu = () => setMenuData(null); + useEffect(() => { document.body.addEventListener("click", () => { - setMenuData(null); + closeContextMenu(); }); }, []); @@ -71,5 +73,6 @@ export const useContextMenu = (ref: RefObject) => { menuData, onNodeContextMenu, onPaneContextMenu, + closeContextMenu, }; }; From eb09cbc2a57b0dc1d3b5121e0218cf4f06b5f481 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 11 Oct 2024 10:03:51 +0200 Subject: [PATCH 20/22] fix: Duplicate to left and right --- services/graphApi.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/graphApi.ts b/services/graphApi.ts index 1c8b4693..0aaf3b3b 100644 --- a/services/graphApi.ts +++ b/services/graphApi.ts @@ -4,7 +4,6 @@ import { NodeData } from "@/types/NodeData"; import BaseAPIServices from "./BaseAPIServices"; import { NodeDataApi, NodeDataApiRequestBody } from "@/types/NodeDataApi"; import { Graph } from "types/Graph"; -import { NodeTypes } from "types/NodeTypes"; export const getGraph = (projectId: string | string[]): Promise => { return BaseAPIServices.get(`${baseUrl}/graph/${projectId}`).then( @@ -55,7 +54,7 @@ export const moveVertice = ( ).then((r) => r.data); export const addVerticeLeft = ( - data: { type: NodeTypes }, + data: NodeDataApiRequestBody, projectId: string, neighbourId: string ): Promise => @@ -65,7 +64,7 @@ export const addVerticeLeft = ( ).then((r) => r.data); export const addVerticeRight = ( - data: { type: NodeTypes }, + data: NodeDataApiRequestBody, projectId: string, neighbourId: string ): Promise => From 4f059cf6027c05a526e8ca8283669424b92fc43c Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 11 Oct 2024 10:09:44 +0200 Subject: [PATCH 21/22] fix: Temporarily disable left button for choice children --- components/canvas/utils/nodeValidityHelper.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/canvas/utils/nodeValidityHelper.tsx b/components/canvas/utils/nodeValidityHelper.tsx index 7af6a5a7..572efa57 100644 --- a/components/canvas/utils/nodeValidityHelper.tsx +++ b/components/canvas/utils/nodeValidityHelper.tsx @@ -104,7 +104,10 @@ export const getOptionsAddNode = (node: Node) => { if (isChoiceChild) { validPositions = { ...validPositions, + /* + Add this back in when the order of choice children has been fixed left: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], + */ right: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], }; } From 61d5f2aeb9025aac4f572247a4eabe8a970e8044 Mon Sep 17 00:00:00 2001 From: Adrian Nesvik Date: Fri, 11 Oct 2024 10:18:34 +0200 Subject: [PATCH 22/22] refactor: Add TODO --- components/canvas/utils/nodeValidityHelper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/canvas/utils/nodeValidityHelper.tsx b/components/canvas/utils/nodeValidityHelper.tsx index 572efa57..c3db8409 100644 --- a/components/canvas/utils/nodeValidityHelper.tsx +++ b/components/canvas/utils/nodeValidityHelper.tsx @@ -105,7 +105,7 @@ export const getOptionsAddNode = (node: Node) => { validPositions = { ...validPositions, /* - Add this back in when the order of choice children has been fixed + TODO: Add this back in when the order of choice children has been fixed left: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting], */ right: [NodeTypes.subActivity, NodeTypes.choice, NodeTypes.waiting],