Skip to content

Commit

Permalink
feat: add copy/paste fn
Browse files Browse the repository at this point in the history
  • Loading branch information
MXerFix committed Aug 29, 2024
1 parent 171e0c0 commit c153961
Show file tree
Hide file tree
Showing 3 changed files with 318 additions and 9 deletions.
10 changes: 9 additions & 1 deletion frontend/src/contexts/flowContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable react-refresh/only-export-components */
import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useParams } from "react-router-dom"
import { Edge } from "reactflow"
import { Edge, ReactFlowInstance } from "reactflow"
import { v4 } from "uuid"
import { get_flows, save_flows } from "../api/flows"
import { FLOW_COLORS } from "../consts"
Expand Down Expand Up @@ -49,6 +49,8 @@ const globalFlow: FlowType = {
}

type TabContextType = {
reactFlowInstance: ReactFlowInstance | null
setReactFlowInstance: React.Dispatch<React.SetStateAction<ReactFlowInstance | null>>
tab: string
setTab: React.Dispatch<React.SetStateAction<string>>
flows: FlowType[]
Expand All @@ -65,6 +67,8 @@ type TabContextType = {
}

const initialValue: TabContextType = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
tab: "",
setTab: () => {},
flows: [],
Expand All @@ -85,6 +89,8 @@ const initialValue: TabContextType = {
export const flowContext = createContext(initialValue)

export const FlowProvider = ({ children }: { children: React.ReactNode }) => {

const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance | null>(null)
const [tab, setTab] = useState(initialValue.tab)
const { flowId } = useParams()
const [flows, setFlows] = useState<FlowType[]>([])
Expand Down Expand Up @@ -219,6 +225,8 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
return (
<flowContext.Provider
value={{
reactFlowInstance,
setReactFlowInstance,
tab,
setTab,
flows,
Expand Down
274 changes: 270 additions & 4 deletions frontend/src/contexts/undoRedoContext.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { cloneDeep } from "lodash"
import { createContext, useCallback, useContext, useEffect, useState } from "react"
import { Edge, Node, useReactFlow } from "reactflow"
import { addEdge, Edge, Node, OnSelectionChangeParams, useReactFlow } from "reactflow"
import { v4 } from "uuid"
import { NodeDataType } from "../types/NodeTypes"
import { flowContext } from "./flowContext"
import { notificationsContext } from "./notificationsContext"

type undoRedoContextType = {
undo: () => void
redo: () => void
takeSnapshot: () => void
copy: (selection: OnSelectionChangeParams) => void
paste: (
selectionInstance: OnSelectionChangeParams,
position: { x: number; y: number; paneX?: number; paneY?: number }
) => void
copiedSelection: OnSelectionChangeParams | null
}

type UseUndoRedoOptions = {
Expand All @@ -31,6 +40,9 @@ const initialValue = {
undo: () => {},
redo: () => {},
takeSnapshot: () => {},
copy: () => {},
paste: () => {},
copiedSelection: null,
}

const defaultOptions: UseUndoRedoOptions = {
Expand All @@ -43,6 +55,7 @@ export const undoRedoContext = createContext<undoRedoContextType>(initialValue)

export function UndoRedoProvider({ children }: { children: React.ReactNode }) {
const { tab, flows } = useContext(flowContext)
const { notification: n } = useContext(notificationsContext)

const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []))
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []))
Expand Down Expand Up @@ -77,7 +90,7 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) {
return newFuture
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getNodes, getEdges, past, future, flows, tab, setPast, setFuture, tabIndex])

const undo = useCallback(() => {
Expand All @@ -102,7 +115,7 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) {
setNodes(pastState.nodes)
setEdges(pastState.edges)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setNodes, setEdges, getNodes, getEdges, future, past, setFuture, setPast, tabIndex])

const redo = useCallback(() => {
Expand All @@ -123,7 +136,7 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) {
setNodes(futureState.nodes)
setEdges(futureState.edges)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [future, past, setFuture, setPast, setNodes, setEdges, getNodes, getEdges, future, tabIndex])

useEffect(() => {
Expand All @@ -149,12 +162,265 @@ export function UndoRedoProvider({ children }: { children: React.ReactNode }) {
document.removeEventListener("keydown", keyDownHandler)
}
}, [undo, redo])

const { reactFlowInstance } = useContext(flowContext)
const [copiedSelection, setCopiedSelection] = useState<OnSelectionChangeParams | null>(null)

const copy = (selection: OnSelectionChangeParams) => {
if (selection && (selection.nodes.length || selection.edges.length)) {
setCopiedSelection(cloneDeep(selection))
n.add({
title: "Copied!",
message: `
Copied ${selection.nodes.length} nodes and ${selection.edges.length} edges.
`,
type: "success",
})
} else {
n.add({
title: "Nothing to copy!",
message: "",
type: "warning",
})
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const paste = (
selectionInstance: OnSelectionChangeParams,
position: { x: number; y: number; paneX?: number; paneY?: number }
) => {
if (!reactFlowInstance) {
return n.add({
title: "Fatal error!",
message: "React flow instance not found!",
type: "error",
})
}
if (!selectionInstance.edges.length && !selectionInstance.nodes.length) {
return n.add({
title: "Nothing to paste!",
message: "",
type: "warning",
})
}
const nodes: Node<NodeDataType>[] = reactFlowInstance.getNodes()
let edges: Edge[] = reactFlowInstance.getEdges()
let minimumX = Infinity
let minimumY = Infinity
const idsMap: { [id: string]: string } = {}
const sourceHandlesMap: { [id: string]: string } = {}

selectionInstance.nodes.forEach((n) => {
if (n.position.y < minimumY) {
minimumY = n.position.y
}
if (n.position.x < minimumX) {
minimumX = n.position.x
}
})

const insidePosition =
position.paneX && position.paneY
? { x: position.paneX + position.x, y: position.paneY + position.y }
: reactFlowInstance.project({ x: position.x, y: position.y })

const resultNodes: Node<NodeDataType>[] = []

selectionInstance.nodes.forEach((n: Node<NodeDataType>) => {
// Generate a unique node ID
const newId = v4()
idsMap[n.id] = newId
const newConditions = n.data.conditions?.map((c) => {
const newCondId = v4()
sourceHandlesMap[c.id] = newCondId
return { ...c, id: newCondId }
})
const newResponse = n.data.response
? {
...n.data.response,
id: v4(),
}
: undefined

// Create a new node object
const newNode: Node<NodeDataType> = {
id: newId,
type: n.type,
position: {
x: insidePosition.x + n.position.x - minimumX,
y: insidePosition.y + n.position.y - minimumY,
},
data: {
...cloneDeep(n.data),
conditions: newConditions,
response: newResponse,
flags: [],
id: newId,
},
}

resultNodes.push({ ...newNode, selected: true })
})

if (resultNodes.length < selectionInstance.nodes.length) {
return
}

const newNodes = [
...nodes.map((e: Node<NodeDataType>) => ({ ...e, selected: false })),
...resultNodes,
]

console.log(selectionInstance.edges)

selectionInstance.edges.forEach((e) => {
const source = idsMap[e.source]
const target = idsMap[e.target]
if (e.sourceHandle) {
const sourceHandle = sourceHandlesMap[e.sourceHandle]
const id = v4()
edges = addEdge(
{
source,
target,
sourceHandle,
targetHandle: null,
id,
selected: false,
},
edges.map((e) => ({ ...e, selected: false }))
)
}
})
takeSnapshot()
reactFlowInstance.setNodes(newNodes)
reactFlowInstance.setEdges(edges)
}

// function paste(
// selectionInstance,
// position: { x: number; y: number; paneX?: number; paneY?: number }
// ) {
// let minimumX = Infinity;
// let minimumY = Infinity;
// let idsMap = {};
// let nodes = reactFlowInstance.getNodes();
// let edges = reactFlowInstance.getEdges();
// selectionInstance.nodes.forEach((n) => {
// if (n.position.y < minimumY) {
// minimumY = n.position.y;
// }
// if (n.position.x < minimumX) {
// minimumX = n.position.x;
// }
// });

// const insidePosition = position.paneX
// ? { x: position.paneX + position.x, y: position.paneY + position.y }
// : reactFlowInstance.project({ x: position.x, y: position.y });

// const resultNodes: any[] = []

// selectionInstance.nodes.forEach((n: NodeType) => {
// // Generate a unique node ID
// let newId = getNodeId(n.data.type);
// idsMap[n.id] = newId;

// const positionX = insidePosition.x + n.position.x - minimumX
// const positionY = insidePosition.y + n.position.y - minimumY

// // Create a new node object
// const newNode: NodeType = {
// id: newId,
// type: "genericNode",
// position: {
// x: insidePosition.x + n.position.x - minimumX,
// y: insidePosition.y + n.position.y - minimumY,
// },
// data: {
// ..._.cloneDeep(n.data),
// id: newId,
// },
// };

// // FIXME: CHECK WORK >>>>>>>
// // check for intersections before paste
// if (nodes.some(({ position, id, width, height }) => {
// const xIntersect = ((positionX > position.x - width) && (positionX < (position.x + width)))
// const yIntersect = ((positionY > position.y - height) && (positionY < (position.y + height)))
// const result = xIntersect && yIntersect
// // console.log({id: id, xIntersect: xIntersect, yIntersect: yIntersect, result: result})
// return result
// })) {
// return setErrorData({ title: "Invalid place! Nodes can't intersect!" })
// }
// // FIXME: CHECK WORK >>>>>>>>

// resultNodes.push({ ...newNode, selected: true })

// });

// if (resultNodes.length < selectionInstance.nodes.length) {
// return
// }

// // Add the new node to the list of nodes in state
// nodes = nodes
// .map((e) => ({ ...e, selected: false }))
// .concat(resultNodes);
// reactFlowInstance.setNodes(nodes);

// selectionInstance.edges.forEach((e) => {
// let source = idsMap[e.source];
// let target = idsMap[e.target];
// let sourceHandleSplitted = e.sourceHandle.split("|");
// let sourceHandle =
// source +
// "|" +
// sourceHandleSplitted[1] +
// "|" +
// source
// let targetHandleSplitted = e.targetHandle.split("|");
// let targetHandle =
// targetHandleSplitted.slice(0, -1).join("|") + target;
// let id =
// "reactflow__edge-" +
// source +
// sourceHandle +
// "-" +
// target +
// targetHandle;
// edges = addEdge(
// {
// source,
// target,
// sourceHandle,
// targetHandle,
// id,
// style: { stroke: "inherit" },
// className:
// targetHandle.split("|")[0] === "Text"
// ? "stroke-foreground "
// : "stroke-foreground ",
// animated: targetHandle.split("|")[0] === "Text",
// selected: false,
// },
// edges.map((e) => ({ ...e, selected: false }))
// );
// });
// reactFlowInstance.setEdges(edges);
// }

return (
<undoRedoContext.Provider
value={{
undo,
redo,
takeSnapshot,
copy,
paste,
copiedSelection,
}}>
{children}
</undoRedoContext.Provider>
Expand Down
Loading

0 comments on commit c153961

Please sign in to comment.