From 9b99aeda3640153e8ee9c914c2afced1953b3b19 Mon Sep 17 00:00:00 2001 From: ogabrielluiz Date: Mon, 10 Jun 2024 19:51:18 -0300 Subject: [PATCH 1/2] refactor: Generate unique names for flows and folders with the same name --- src/backend/base/langflow/api/v1/flows.py | 67 ++++++++++++++++----- src/backend/base/langflow/api/v1/folders.py | 34 +++++++---- 2 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py index c1ccf68db..08957e502 100644 --- a/src/backend/base/langflow/api/v1/flows.py +++ b/src/backend/base/langflow/api/v1/flows.py @@ -31,25 +31,60 @@ def create_flow( flow: FlowCreate, current_user: User = Depends(get_current_active_user), ): - """Create a new flow.""" - if flow.user_id is None: - flow.user_id = current_user.id + try: + """Create a new flow.""" + if flow.user_id is None: + flow.user_id = current_user.id - db_flow = Flow.model_validate(flow, from_attributes=True) - db_flow.updated_at = datetime.now(timezone.utc) + # First check if the flow.name is unique + # there might be flows with name like: "MyFlow", "MyFlow (1)", "MyFlow (2)" + # so we need to check if the name is unique with `like` operator + # if we find a flow with the same name, we add a number to the end of the name + # based on the highest number found + if session.exec(select(Flow).where(Flow.name == flow.name).where(Flow.user_id == current_user.id)).first(): + flows = session.exec( + select(Flow).where(Flow.name.like(f"{flow.name} (%")).where(Flow.user_id == current_user.id) + ).all() + if flows: + numbers = [int(flow.name.split("(")[1].split(")")[0]) for flow in flows] + flow.name = f"{flow.name} ({max(numbers) + 1})" + else: + flow.name = f"{flow.name} (1)" - if db_flow.folder_id is None: - # Make sure flows always have a folder - default_folder = session.exec( - select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME, Folder.user_id == current_user.id) - ).first() - if default_folder: - db_flow.folder_id = default_folder.id + db_flow = Flow.model_validate(flow, from_attributes=True) + db_flow.updated_at = datetime.now(timezone.utc) - session.add(db_flow) - session.commit() - session.refresh(db_flow) - return db_flow + if db_flow.folder_id is None: + # Make sure flows always have a folder + default_folder = session.exec( + select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME, Folder.user_id == current_user.id) + ).first() + if default_folder: + db_flow.folder_id = default_folder.id + + session.add(db_flow) + session.commit() + session.refresh(db_flow) + return db_flow + except Exception as e: + # If it is a validation error, return the error message + if hasattr(e, "errors"): + raise HTTPException(status_code=400, detail=str(e)) from e + elif "UNIQUE constraint failed" in str(e): + # Get the name of the column that failed + columns = str(e).split("UNIQUE constraint failed: ")[1].split(".")[1].split("\n")[0] + # UNIQUE constraint failed: flow.user_id, flow.name + # or UNIQUE constraint failed: flow.name + # if the column has id in it, we want the other column + column = columns.split(",")[1] if "id" in columns.split(",")[0] else columns.split(",")[0] + + raise HTTPException( + status_code=400, detail=f"{column.capitalize().replace('_', ' ')} must be unique" + ) from e + elif isinstance(e, HTTPException): + raise e + else: + raise HTTPException(status_code=500, detail=str(e)) from e @router.get("/", response_model=list[FlowRead], status_code=200) diff --git a/src/backend/base/langflow/api/v1/folders.py b/src/backend/base/langflow/api/v1/folders.py index d55f9bd15..c9d2c0b31 100644 --- a/src/backend/base/langflow/api/v1/folders.py +++ b/src/backend/base/langflow/api/v1/folders.py @@ -1,7 +1,5 @@ from typing import List -from langflow.helpers.flow import generate_unique_flow_name -from langflow.helpers.folders import generate_unique_folder_name import orjson from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status from sqlalchemy import or_, update @@ -9,6 +7,8 @@ from sqlmodel import Session, select from langflow.api.v1.flows import create_flows from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName +from langflow.helpers.flow import generate_unique_flow_name +from langflow.helpers.folders import generate_unique_folder_name from langflow.services.auth.utils import get_current_active_user from langflow.services.database.models.flow.model import Flow, FlowCreate, FlowRead from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME @@ -35,17 +35,27 @@ def create_folder( try: new_folder = Folder.model_validate(folder, from_attributes=True) new_folder.user_id = current_user.id - - folder_results = session.exec( - select(Folder).where( - Folder.name.like(f"{new_folder.name}%"), # type: ignore - Folder.user_id == current_user.id, + # First check if the folder.name is unique + # there might be flows with name like: "MyFlow", "MyFlow (1)", "MyFlow (2)" + # so we need to check if the name is unique with `like` operator + # if we find a flow with the same name, we add a number to the end of the name + # based on the highest number found + if session.exec( + statement=select(Folder).where(Folder.name == new_folder.name).where(Folder.user_id == current_user.id) + ).first(): + folder_results = session.exec( + select(Folder).where( + Folder.name.like(f"{new_folder.name}%"), # type: ignore + Folder.user_id == current_user.id, + ) ) - ) - existing_folder_names = [folder.name for folder in folder_results] - - if existing_folder_names: - new_folder.name = f"{new_folder.name} ({len(existing_folder_names) + 1})" + if folder_results: + folder_names = [folder.name for folder in folder_results] + folder_numbers = [int(name.split("(")[-1].split(")")[0]) for name in folder_names if "(" in name] + if folder_numbers: + new_folder.name = f"{new_folder.name} ({max(folder_numbers) + 1})" + else: + new_folder.name = f"{new_folder.name} (1)" session.add(new_folder) session.commit() From fe49ba9e236db987a14e256cd207eb09b519ebaa Mon Sep 17 00:00:00 2001 From: igorrCarvalho Date: Mon, 10 Jun 2024 19:57:55 -0300 Subject: [PATCH 2/2] Refactor: remove delete visual shortcut background color on hover --- .../components/PageComponent/index.tsx | 52 +++++++++---------- .../components/nodeToolbarComponent/index.tsx | 46 +++++++++------- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 8a8bd4d6c..43e6a320c 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -62,19 +62,19 @@ export default function Page({ const preventDefault = true; const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow); const autoSaveCurrentFlow = useFlowsManagerStore( - (state) => state.autoSaveCurrentFlow + (state) => state.autoSaveCurrentFlow, ); const types = useTypesStore((state) => state.types); const templates = useTypesStore((state) => state.templates); const setFilterEdge = useFlowStore((state) => state.setFilterEdge); const reactFlowWrapper = useRef(null); const [showCanvas, setSHowCanvas] = useState( - Object.keys(templates).length > 0 && Object.keys(types).length > 0 + Object.keys(templates).length > 0 && Object.keys(types).length > 0, ); const reactFlowInstance = useFlowStore((state) => state.reactFlowInstance); const setReactFlowInstance = useFlowStore( - (state) => state.setReactFlowInstance + (state) => state.setReactFlowInstance, ); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); @@ -91,10 +91,10 @@ export default function Page({ const paste = useFlowStore((state) => state.paste); const resetFlow = useFlowStore((state) => state.resetFlow); const lastCopiedSelection = useFlowStore( - (state) => state.lastCopiedSelection + (state) => state.lastCopiedSelection, ); const setLastCopiedSelection = useFlowStore( - (state) => state.setLastCopiedSelection + (state) => state.setLastCopiedSelection, ); const onConnect = useFlowStore((state) => state.onConnect); const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); @@ -117,7 +117,7 @@ export default function Page({ clonedSelection!, clonedNodes, clonedEdges, - getRandomName() + getRandomName(), ); const newGroupNode = generateNodeFromFlow(newFlow, getNodeId); const newEdges = reconnectEdges(newGroupNode, removedEdges); @@ -125,8 +125,8 @@ export default function Page({ ...clonedNodes.filter( (oldNodes) => !clonedSelection?.nodes.some( - (selectionNode) => selectionNode.id === oldNodes.id - ) + (selectionNode) => selectionNode.id === oldNodes.id, + ), ), newGroupNode, ]); @@ -136,8 +136,8 @@ export default function Page({ !clonedSelection!.nodes.some( (selectionNode) => selectionNode.id === oldEdge.target || - selectionNode.id === oldEdge.source - ) + selectionNode.id === oldEdge.source, + ), ), ...newEdges, ]); @@ -180,7 +180,7 @@ export default function Page({ function handleUndo(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if (!isWrappedWithClass(e, "noundo")) { undo(); } @@ -188,7 +188,7 @@ export default function Page({ function handleRedo(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if (!isWrappedWithClass(e, "noundo")) { redo(); } @@ -196,7 +196,7 @@ export default function Page({ function handleGroup(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if (selectionMenuVisible) { handleGroupNode(); } @@ -205,7 +205,7 @@ export default function Page({ function handleDuplicate(e: KeyboardEvent) { e.preventDefault(); e.stopPropagation(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); const selectedNode = nodes.filter((obj) => obj.selected); if (selectedNode.length > 0) { paste( @@ -213,14 +213,14 @@ export default function Page({ { x: position.current.x, y: position.current.y, - } + }, ); } } function handleCopy(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if ( !isWrappedWithClass(e, "nocopy") && window.getSelection()?.toString().length === 0 && @@ -232,7 +232,7 @@ export default function Page({ function handleCut(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if ( !isWrappedWithClass(e, "nocopy") && window.getSelection()?.toString().length === 0 && @@ -244,7 +244,7 @@ export default function Page({ function handlePaste(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if ( !isWrappedWithClass(e, "nocopy") && window.getSelection()?.toString().length === 0 && @@ -260,7 +260,7 @@ export default function Page({ function handleDelete(e: KeyboardEvent) { e.preventDefault(); - e.stopImmediatePropagation(); + (e as unknown as Event).stopImmediatePropagation(); if (!isWrappedWithClass(e, "nodelete") && lastSelection) { takeSnapshot(); deleteNode(lastSelection.nodes.map((node) => node.id)); @@ -297,7 +297,7 @@ export default function Page({ useEffect(() => { setSHowCanvas( - Object.keys(templates).length > 0 && Object.keys(types).length > 0 + Object.keys(templates).length > 0 && Object.keys(types).length > 0, ); }, [templates, types]); @@ -306,7 +306,7 @@ export default function Page({ takeSnapshot(); onConnect(params); }, - [takeSnapshot, onConnect] + [takeSnapshot, onConnect], ); const onNodeDragStart: NodeDragHandler = useCallback(() => { @@ -347,7 +347,7 @@ export default function Page({ // Extract the data from the drag event and parse it as a JSON object const data: { type: string; node?: APIClassType } = JSON.parse( - event.dataTransfer.getData("nodedata") + event.dataTransfer.getData("nodedata"), ); const newId = getNodeId(data.type); @@ -363,7 +363,7 @@ export default function Page({ }; paste( { nodes: [newNode], edges: [] }, - { x: event.clientX, y: event.clientY } + { x: event.clientX, y: event.clientY }, ); } else if (event.dataTransfer.types.some((types) => types === "Files")) { takeSnapshot(); @@ -392,7 +392,7 @@ export default function Page({ } }, // Specify dependencies for useCallback - [getNodeId, setNodes, takeSnapshot, paste] + [getNodeId, setNodes, takeSnapshot, paste], ); const onEdgeUpdateStart = useCallback(() => { @@ -408,7 +408,7 @@ export default function Page({ setEdges((els) => updateEdge(oldEdge, newConnection, els)); } }, - [setEdges] + [setEdges], ); const onEdgeUpdateEnd = useCallback((_, edge: Edge): void => { @@ -441,7 +441,7 @@ export default function Page({ (flow: OnSelectionChangeParams): void => { setLastSelection(flow); }, - [] + [], ); const onPaneClick = useCallback((flow) => { diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index c953548db..49636c4cb 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -49,7 +49,7 @@ export default function NodeToolbarComponent({ const [showconfirmShare, setShowconfirmShare] = useState(false); const [showOverrideModal, setShowOverrideModal] = useState(false); const [flowComponent, setFlowComponent] = useState( - createFlowComponent(cloneDeep(data), version) + createFlowComponent(cloneDeep(data), version), ); const preventDefault = true; const isMac = navigator.platform.toUpperCase().includes("MAC"); @@ -66,7 +66,7 @@ export default function NodeToolbarComponent({ data.node.template[templateField].type === "Any" || data.node.template[templateField].type === "int" || data.node.template[templateField].type === "dict" || - data.node.template[templateField].type === "NestedDict") + data.node.template[templateField].type === "NestedDict"), ).length; const hasStore = useStoreStore((state) => state.hasStore); @@ -221,7 +221,7 @@ export default function NodeToolbarComponent({ const updateNodeInternals = useUpdateNodeInternals(); const setLastCopiedSelection = useFlowStore( - (state) => state.setLastCopiedSelection + (state) => state.setLastCopiedSelection, ); const setSuccessData = useAlertStore((state) => state.setSuccessData); @@ -295,7 +295,7 @@ export default function NodeToolbarComponent({ nodes, edges, setNodes, - setEdges + setEdges, ); break; case "override": @@ -319,14 +319,14 @@ export default function NodeToolbarComponent({ y: 10, paneX: nodes.find((node) => node.id === data.id)?.position.x, paneY: nodes.find((node) => node.id === data.id)?.position.y, - } + }, ); break; } }; const isSaved = flows.some((flow) => - Object.values(flow).includes(data.node?.display_name!) + Object.values(flow).includes(data.node?.display_name!), ); function displayShortcut({ @@ -344,7 +344,7 @@ export default function NodeToolbarComponent({ } }); const filteredShortcut = fixedShortcut.filter( - (key) => !key.toLowerCase().includes("shift") + (key) => !key.toLowerCase().includes("shift"), ); let shortcutWPlus: string[] = []; if (!hasShift) shortcutWPlus = filteredShortcut.join("+").split(" "); @@ -368,7 +368,7 @@ export default function NodeToolbarComponent({ const setNode = useFlowStore((state) => state.setNode); const handleOnNewValue = ( - newValue: string | string[] | boolean | Object[] + newValue: string | string[] | boolean | Object[], ): void => { if (data.node!.template[name].value !== newValue) { takeSnapshot(); @@ -414,6 +414,7 @@ export default function NodeToolbarComponent({ const [openModal, setOpenModal] = useState(false); const hasCode = Object.keys(data.node!.template).includes("code"); + const [deleteIsFocus, setDeleteIsFocus] = useState(false); return ( <> @@ -423,8 +424,8 @@ export default function NodeToolbarComponent({ name.split(" ")[0].toLowerCase() === "code" - )! + ({ name }) => name.split(" ")[0].toLowerCase() === "code", + )!, )} side="top" > @@ -443,8 +444,8 @@ export default function NodeToolbarComponent({ name.split(" ")[0].toLowerCase() === "advanced" - )! + ({ name }) => name.split(" ")[0].toLowerCase() === "advanced", + )!, )} side="top" > @@ -483,14 +484,14 @@ export default function NodeToolbarComponent({ name.split(" ")[0].toLowerCase() === "freeze" - )! + ({ name }) => name.split(" ")[0].toLowerCase() === "freeze", + )!, )} side="top" > @@ -539,7 +540,7 @@ export default function NodeToolbarComponent({
- + setDeleteIsFocus(true)} + onBlur={() => setDeleteIsFocus(false)} + >
{" "} Delete{" "} - +