diff --git a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx index ed4c11722..0ea79c7dd 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/parameterComponent/index.tsx @@ -1,4 +1,3 @@ -import { cloneDeep } from "lodash"; import React, { ReactNode, useContext, @@ -29,6 +28,7 @@ import { import { alertContext } from "../../../../contexts/alertContext"; import { FlowsContext } from "../../../../contexts/flowsContext"; import { typesContext } from "../../../../contexts/typesContext"; +import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import { postCustomComponentUpdate } from "../../../../controllers/API"; import { ParameterComponentType } from "../../../../types/components"; import { NodeDataType } from "../../../../types/flow"; @@ -50,7 +50,6 @@ export default function ParameterComponent({ left, id, data, - setData, tooltipTitle, title, color, @@ -99,6 +98,8 @@ export default function ParameterComponent({ const { data: myData } = useContext(typesContext); + const { takeSnapshot } = useContext(undoRedoContext); + const handleUpdateValues = async (name: string, data: NodeDataType) => { const code = data.node?.template["code"]?.value; if (!code) { @@ -121,9 +122,8 @@ export default function ParameterComponent({ const handleOnNewValue = ( newValue: string | string[] | boolean | Object[] ): void => { - let newData = cloneDeep(data); - newData.node!.template[name].value = newValue; - setData(newData); + takeSnapshot(); + data.node!.template[name].value = newValue; // Set state to pending //@ts-ignore setTabsState((prev: TabsState) => { @@ -484,9 +484,6 @@ export default function ParameterComponent({ field_name={name} setNodeClass={(nodeClass) => { data.node = nodeClass; - const clone = cloneDeep(data); - clone.node = nodeClass; - setData(clone); }} nodeClass={data.node} disabled={disabled} diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 2ceacf654..679e3863b 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -9,6 +9,7 @@ import { Textarea } from "../../components/ui/textarea"; import { useSSE } from "../../contexts/SSEContext"; import { FlowsContext } from "../../contexts/flowsContext"; import { typesContext } from "../../contexts/typesContext"; +import { undoRedoContext } from "../../contexts/undoRedoContext"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; import { validationStatusType } from "../../types/components"; import { NodeDataType } from "../../types/flow"; @@ -22,7 +23,7 @@ import { classNames, getFieldTitle } from "../../utils/utils"; import ParameterComponent from "./components/parameterComponent"; export default function GenericNode({ - data: olddata, + data, xPos, yPos, selected, @@ -32,7 +33,6 @@ export default function GenericNode({ xPos: number; yPos: number; }): JSX.Element { - const [data, setData] = useState(olddata); const { updateFlow, flows, tabId } = useContext(FlowsContext); const updateNodeInternals = useUpdateNodeInternals(); const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } = @@ -46,10 +46,11 @@ export default function GenericNode({ ); const [validationStatus, setValidationStatus] = useState(null); - const [showNode, setShowNode] = useState(true); const [handles, setHandles] = useState([]); let numberOfInputs: boolean[] = []; + const { takeSnapshot } = useContext(undoRedoContext); + function countHandles(): void { numberOfInputs = Object.keys(data.node!.template) .filter((templateField) => templateField.charAt(0) !== "_") @@ -86,7 +87,6 @@ export default function GenericNode({ // State for outline color const { sseData, isBuilding } = useSSE(); useEffect(() => { - olddata.node = data.node; let myFlow = flows.find((flow) => flow.id === tabId); if (reactFlowInstance && myFlow) { let flow = cloneDeep(myFlow); @@ -108,10 +108,12 @@ export default function GenericNode({ }, [data]); useEffect(() => { - setTimeout(() => { - updateNodeInternals(data.id); - }, 300); - }, [showNode]); + setNodeDescription(data.node!.description); + }, [data.node!.description]); + + useEffect(() => { + setNodeName(data.node!.display_name); + }, [data.node!.display_name]); // New useEffect to watch for changes in sseData and update validation status useEffect(() => { @@ -123,15 +125,17 @@ export default function GenericNode({ setValidationStatus(null); } }, [sseData, data.id]); + + const showNode = data.showNode ?? true; + return ( <> {data.showNode = showNode}} numberOfHandles={handles} showNode={showNode} > @@ -180,7 +184,6 @@ export default function GenericNode({ {data.node?.flow && inputName ? (
{ setInputName(false); if (nodeName.trim() !== "") { @@ -200,7 +203,10 @@ export default function GenericNode({
setInputName(true)} + onDoubleClick={() => { + setInputName(true); + takeSnapshot(); + }} >
{data.node?.display_name} @@ -237,7 +243,6 @@ export default function GenericNode({ proxy: data.node!.template[templateField].proxy, })} data={data} - setData={setData} color={ nodeColors[ types[data.node?.template[templateField].type!] @@ -286,7 +291,6 @@ export default function GenericNode({ dataType: data.type, })} data={data} - setData={setData} color={nodeColors[types[data.type]] ?? nodeColors.unknown} title={ data.node?.output_types && @@ -417,7 +421,10 @@ export default function GenericNode({ ) : (
setInputDescription(true)} + onDoubleClick={() => { + setInputDescription(true); + takeSnapshot(); + }} > {data.node?.description}
@@ -449,7 +456,6 @@ export default function GenericNode({ proxy: data.node!.template[templateField].proxy, })} data={data} - setData={setData} color={ nodeColors[ types[data.node?.template[templateField].type!] @@ -507,8 +513,7 @@ export default function GenericNode({ dataType: data.type, })} data={data} - setData={setData} - color={nodeColors[types[data.type]] ?? nodeColors.unknown} + color={nodeColors[types[data.type]] ?? nodeColors.unknown} title={ data.node?.output_types && data.node.output_types.length > 0 ? data.node.output_types.join("|") diff --git a/src/frontend/src/components/keypairListComponent/index.tsx b/src/frontend/src/components/keypairListComponent/index.tsx index 93baa4764..08c1d9da4 100644 --- a/src/frontend/src/components/keypairListComponent/index.tsx +++ b/src/frontend/src/components/keypairListComponent/index.tsx @@ -12,8 +12,6 @@ export default function KeypairListComponent({ disabled, editNode = false, duplicateKey, - advanced = false, - dataValue, }: KeyPairListComponentType): JSX.Element { useEffect(() => { if (disabled) { diff --git a/src/frontend/src/contexts/flowsContext.tsx b/src/frontend/src/contexts/flowsContext.tsx index 0ed9cb783..f6f9660d7 100644 --- a/src/frontend/src/contexts/flowsContext.tsx +++ b/src/frontend/src/contexts/flowsContext.tsx @@ -418,7 +418,7 @@ export function FlowsProvider({ children }: { children: ReactNode }) { const insidePosition = position.paneX ? { x: position.paneX + position.x, y: position.paneY! + position.y } - : reactFlowInstance!.project({ x: position.x, y: position.y }); + : reactFlowInstance!.screenToFlowPosition({ x: position.x, y: position.y }); selectionInstance.nodes.forEach((node: NodeType) => { // Generate a unique node ID diff --git a/src/frontend/src/contexts/undoRedoContext.tsx b/src/frontend/src/contexts/undoRedoContext.tsx index a948e37e3..f02be6257 100644 --- a/src/frontend/src/contexts/undoRedoContext.tsx +++ b/src/frontend/src/contexts/undoRedoContext.tsx @@ -52,15 +52,23 @@ export function UndoRedoProvider({ children }) { const takeSnapshot = useCallback(() => { // push the current graph to the past state - setPast((old) => { - let newPast = cloneDeep(old); - newPast[tabIndex] = old[tabIndex].slice( - old[tabIndex].length - defaultOptions.maxHistorySize + 1, - old[tabIndex].length + let newPast = cloneDeep(past); + let newState = { + nodes: cloneDeep(getNodes()), + edges: cloneDeep(getEdges()), + }; + if ( + past[tabIndex] && + JSON.stringify(past[tabIndex][past[tabIndex].length - 1]) !== + JSON.stringify(newState) + ) { + newPast[tabIndex] = past[tabIndex].slice( + past[tabIndex].length - defaultOptions.maxHistorySize + 1, + past[tabIndex].length ); - newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() }); - return newPast; - }); + newPast[tabIndex].push(newState); + } + setPast(newPast); // whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches setFuture((old) => { diff --git a/src/frontend/src/modals/EditNodeModal/index.tsx b/src/frontend/src/modals/EditNodeModal/index.tsx index 2a88db0d2..59e9c6697 100644 --- a/src/frontend/src/modals/EditNodeModal/index.tsx +++ b/src/frontend/src/modals/EditNodeModal/index.tsx @@ -45,13 +45,11 @@ const EditNodeModal = forwardRef( ( { data, - setData, nodeLength, open, setOpen, }: { data: NodeDataType; - setData: (data: NodeDataType) => void; nodeLength: number; open: boolean; setOpen: (open: boolean) => void; @@ -60,7 +58,7 @@ const EditNodeModal = forwardRef( ) => { const updateNodeInternals = useUpdateNodeInternals(); - let myData = useRef(data); + const [myData, setMyData] = useState(data); const { setTabsState, tabId } = useContext(FlowsContext); const { reactFlowInstance } = useContext(typesContext); @@ -70,30 +68,31 @@ const EditNodeModal = forwardRef( .some((edge) => edge.targetHandle === data.id) ?? false; function changeAdvanced(n) { - myData.current.node!.template[n].advanced = - !myData.current.node!.template[n].advanced; - - myData.current = { - ...myData.current, - }; - setAdv(!adv); + setMyData((old) => { + let newData = cloneDeep(old); + newData.node!.template[n].advanced = + !newData.node!.template[n].advanced; + return newData; + }); } const handleOnNewValue = (newValue: any, name) => { - myData.current.node!.template[name].value = newValue; - setDataValue(newValue); + setMyData((old) => { + let newData = cloneDeep(old); + newData.node!.template[name].value = newValue; + return newData; + }); updateNodeInternals(data.id); }; + useEffect(() => { if (open) { - myData.current = data; // reset data to what it is on node when opening modal + setMyData(data); // reset data to what it is on node when opening modal } }, [open]); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); - const [adv, setAdv] = useState(null); - const [dataValue, setDataValue] = useState(data); return ( { - myData.current = data; + setMyData(data); }} > <> - - {myData.current.type} - ID: {myData.current.id} + + {myData.type} + ID: {myData.id}
@@ -143,13 +142,13 @@ const EditNodeModal = forwardRef( - {Object.keys(myData.current.node!.template) + {Object.keys(myData.node!.template) .filter( (templateParam) => templateParam.charAt(0) !== "_" && - myData.current.node?.template[templateParam].show && + myData.node?.template[templateParam].show && LANGFLOW_SUPPORTED_TYPES.has( - myData.current.node.template[templateParam].type + myData.node.template[templateParam].type ) ) .map((templateParam, index) => ( @@ -157,46 +156,46 @@ const EditNodeModal = forwardRef( - {myData.current.node?.template[templateParam] + {myData.node?.template[templateParam] .display_name - ? myData.current.node.template[ + ? myData.node.template[ templateParam ].display_name - : myData.current.node?.template[ + : myData.node?.template[ templateParam ].name} - {myData.current.node?.template[templateParam] + {myData.node?.template[templateParam] .type === "str" && - !myData.current.node.template[templateParam] + !myData.node.template[templateParam] .options ? (
- {myData.current.node.template[templateParam] + {myData.node.template[templateParam] .list ? ( - ) : myData.current.node.template[ + ) : myData.node.template[ templateParam ].multiline ? ( )}
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "NestedDict" ? (
{ - myData.current.node!.template[ + myData.node!.template[ templateParam ].value = newValue; handleOnNewValue(newValue, templateParam); @@ -267,32 +266,30 @@ const EditNodeModal = forwardRef( id="editnode-div-dict-input" />
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "dict" ? (
1 ? "my-3" : "" )} > { const valueToNumbers = convertValuesToNumbers(newValue); - myData.current.node!.template[ + myData.node!.template[ templateParam ].value = valueToNumbers; setErrorDuplicateKey( @@ -314,7 +311,7 @@ const EditNodeModal = forwardRef( }} />
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "bool" ? (
{" "} @@ -322,7 +319,7 @@ const EditNodeModal = forwardRef( id={"toggle-edit-" + index} disabled={disabled} enabled={ - myData.current.node.template[ + myData.node.template[ templateParam ].value } @@ -335,14 +332,14 @@ const EditNodeModal = forwardRef( size="small" />
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "float" ? (
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "str" && - myData.current.node.template[templateParam] + myData.node.template[templateParam] .options ? (
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "int" ? (
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "file" ? (
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "prompt" ? (
{ - myData.current.node = nodeClass; + myData.node = nodeClass; }} value={ - myData.current.node.template[ + myData.node.template[ templateParam ].value ?? "" } @@ -448,13 +445,13 @@ const EditNodeModal = forwardRef( id={"prompt-area-edit" + index} />
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "code" ? (
- ) : myData.current.node?.template[templateParam] + ) : myData.node?.template[templateParam] .type === "Any" ? ( "-" ) : ( @@ -493,11 +490,11 @@ const EditNodeModal = forwardRef( { - const newData = cloneDeep(myData.current); - myData.current = newData; + data.node = myData.node //@ts-ignore setTabsState((prev: FlowsState) => { return { diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 3fd409628..0513dd66c 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -90,8 +90,6 @@ export default function Page({ useState(null); useEffect(() => { - // this effect is used to attach the global event handlers - const onKeyDown = (event: KeyboardEvent) => { if (!isWrappedWithClass(event, "nocopy")) { if ( @@ -108,10 +106,9 @@ export default function Page({ lastCopiedSelection ) { event.preventDefault(); - let bounds = reactFlowWrapper.current?.getBoundingClientRect(); paste(lastCopiedSelection, { - x: position.x - bounds!.left, - y: position.y - bounds!.top, + x: position.x, + y: position.y, }); } if ( @@ -133,18 +130,26 @@ export default function Page({ } } }; + + document.addEventListener("keydown", onKeyDown); + + return () => { + document.removeEventListener("keydown", onKeyDown); + }; + }, [lastCopiedSelection, lastSelection]); + + useEffect(() => { const handleMouseMove = (event) => { setPosition({ x: event.clientX, y: event.clientY }); }; - document.addEventListener("keydown", onKeyDown); document.addEventListener("mousemove", handleMouseMove); return () => { - document.removeEventListener("keydown", onKeyDown); document.removeEventListener("mousemove", handleMouseMove); }; - }, [position, lastCopiedSelection, lastSelection]); + }, [position]); + const [selectionMenuVisible, setSelectionMenuVisible] = useState(false); const { setExtraComponent, setExtraNavigation } = useContext(locationContext); @@ -152,58 +157,34 @@ export default function Page({ const [nodes, setNodes, onNodesChange] = useNodesState( flow.data?.nodes ?? [] ); + const [edges, setEdges, onEdgesChange] = useEdgesState( flow.data?.edges ?? [] ); const { setViewport } = useReactFlow(); const edgeUpdateSuccessful = useRef(true); - useEffect(() => { - if (reactFlowInstance && flow) { - flow.data = reactFlowInstance.toObject(); - updateFlow(flow); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [edges]); + //update flow when tabs change useEffect(() => { setNodes(flow?.data?.nodes ?? []); setEdges(flow?.data?.edges ?? []); if (reactFlowInstance) { setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 }); - reactFlowInstance.fitView(); } - }, [flow, reactFlowInstance, setEdges, setNodes, setViewport]); - //set extra sidebar - useEffect(() => { - setExtraComponent(); - setExtraNavigation({ title: "Components" }); - }, [setExtraComponent, setExtraNavigation]); - - const [seconds, setSeconds] = useState(0); + }, [flow, reactFlowInstance]); useEffect(() => { const index = flows.findIndex((flowId) => flowId.id === flow.id); const interval = setInterval(() => { - setSeconds((prevSeconds) => { - let updatedSeconds = prevSeconds + 1; - - if (updatedSeconds % 30 === 0) { - saveFlow( - { - ...flows[index]!, - data: reactFlowInstance - ? reactFlowInstance!.toObject() - : flow!.data, - }, - true - ); - updatedSeconds = 0; - } - - return updatedSeconds; - }); - }, 1000); + saveFlow( + { + ...flows[index]!, + data: reactFlowInstance ? reactFlowInstance!.toObject() : flow!.data, + }, + true + ); + }, 30000); return () => { clearInterval(interval); @@ -212,11 +193,11 @@ export default function Page({ const onEdgesChangeMod = useCallback( (change: EdgeChange[]) => { - onEdgesChange(change); - setNodes((node) => { - let newX = _.cloneDeep(node); - return newX; + updateFlow({ + ...flow!, + data: reactFlowInstance ? reactFlowInstance!.toObject() : flow!.data, }); + onEdgesChange(change); //@ts-ignore setTabsState((prev: FlowsState) => { return { @@ -383,6 +364,9 @@ export default function Page({ ); useEffect(() => { + setExtraComponent(); + setExtraNavigation({ title: "Components" }); + return () => { if (tabsState && tabsState[flow.id]?.isPending) { saveFlow({ @@ -520,6 +504,7 @@ export default function Page({ isVisible={selectionMenuVisible} nodes={lastSelection?.nodes} onClick={() => { + takeSnapshot(); if ( validateSelection(lastSelection!, edges).length === 0 ) { diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index 19f1c6e9c..970e05e0d 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -97,7 +97,6 @@ export default function ExtraSidebar(): JSX.Element { useEffect(() => { if (getFilterEdge.length === 0 && search === "") { setFilterData(data); - setFilterEdge([]); setSearch(""); } }, [getFilterEdge]); diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index a396eacbf..fb43309fd 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -23,10 +23,10 @@ import { updateFlowPosition, } from "../../../../utils/reactflowUtils"; import { classNames } from "../../../../utils/utils"; +import { undoRedoContext } from "../../../../contexts/undoRedoContext"; export default function NodeToolbarComponent({ data, - setData, deleteNode, position, setShowNode, @@ -66,6 +66,7 @@ export default function NodeToolbarComponent({ const isGroup = data.node?.flow ? true : false; const { paste, saveComponent, version, flows } = useContext(FlowsContext); + const { takeSnapshot } = useContext(undoRedoContext); const reactFlowInstance = useReactFlow(); const [showModalAdvanced, setShowModalAdvanced] = useState(false); const [showconfirmShare, setShowconfirmShare] = useState(false); @@ -84,6 +85,7 @@ export default function NodeToolbarComponent({ setShowModalAdvanced(true); break; case "show": + takeSnapshot(); setShowNode((prev) => !prev); updateNodeInternals(data.id); break; @@ -99,6 +101,7 @@ export default function NodeToolbarComponent({ case "disabled": break; case "ungroup": + takeSnapshot(); updateFlowPosition(position, data.node?.flow!); expandGroupNode(data, reactFlowInstance, getNodeId); break; @@ -301,7 +304,6 @@ export default function NodeToolbarComponent({ void; title: string; id: sourceHandleType | targetHandleType; color: string; @@ -69,8 +68,6 @@ export type KeyPairListComponentType = { disabled: boolean; editNode?: boolean; duplicateKey?: boolean; - advanced?: boolean | null; - dataValue?: any; editNodeModal?: boolean; }; @@ -470,7 +467,6 @@ export type fileCardPropsType = { export type nodeToolbarPropsType = { data: NodeDataType; deleteNode: (idx: string) => void; - setData: (newState: NodeDataType) => void; position: XYPosition; setShowNode: (boolean: any) => void; numberOfHandles: boolean[] | []; diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index e66c96304..9f1821a08 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -22,6 +22,7 @@ export type NodeType = { }; export type NodeDataType = { + showNode?: boolean; type: string; node?: APIClassType; id: string; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index d69f2fe89..c4f8d621d 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -934,8 +934,6 @@ export function expandGroupNode( updateEdgesIds(flowEdges, idsMap); const gNodes: NodeType[] = flow?.data?.nodes!; const gEdges = flow!.data!.edges; - //TODO update ids of intern nodes and proxy on edges before expanding - console.log(gEdges); //redirect edges to correct proxy node let updatedEdges: Edge[] = []; flowEdges.forEach((edge) => { @@ -1046,7 +1044,6 @@ export function getGroupStatus( let status = { valid: true, params: "Built sucessfully ✨" }; const { nodes } = flow.data!; const ids = nodes.map((n: NodeType) => n.data.id); - ids.forEach((id) => console.log(ssData[id])); ids.forEach((id) => { if (!ssData[id]) { status = ssData[id];