diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx index d1cc74a0..3315fcf1 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx @@ -56,7 +56,8 @@ function WorkflowHeader({ editable={true} onChange={onTitleChange} value={title} - className="text-3xl" + titleClassName="text-3xl" + inputClassName="text-3xl" />
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx index cb1cd43b..afae032e 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/CodeBlockNode/CodeBlockNode.tsx @@ -13,12 +13,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { CodeBlockNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function CodeBlockNode({ id, data }: NodeProps) { const { updateNodeData, setNodes } = useReactFlow(); - const nodes = useNodes(); + const nodes = useNodes(); const deleteNodeCallback = useDeleteNodeCallback(); const [label, setLabel] = useState(data.label); const [inputs, setInputs] = useState({ @@ -50,16 +53,22 @@ function CodeBlockNode({ id, data }: NodeProps) { value={label} editable={data.editable} onChange={(value) => { - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Code Block
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx index 9fb2e5dc..d0b0ee6b 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/DownloadNode/DownloadNode.tsx @@ -13,12 +13,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { DownloadNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function DownloadNode({ id, data }: NodeProps) { - const { updateNodeData, setNodes } = useReactFlow(); - const nodes = useNodes(); + const { setNodes } = useReactFlow(); + const nodes = useNodes(); const deleteNodeCallback = useDeleteNodeCallback(); const [label, setLabel] = useState(data.label); @@ -47,16 +50,22 @@ function DownloadNode({ id, data }: NodeProps) { value={label} editable={data.editable} onChange={(value) => { - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Download Block diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx index fd64116d..267b222d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileParserNode/FileParserNode.tsx @@ -12,13 +12,16 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { FileParserNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function FileParserNode({ id, data }: NodeProps) { const { updateNodeData, setNodes } = useReactFlow(); const deleteNodeCallback = useDeleteNodeCallback(); - const nodes = useNodes(); + const nodes = useNodes(); const [label, setLabel] = useState(data.label); const [inputs, setInputs] = useState({ fileUrl: data.fileUrl, @@ -49,16 +52,22 @@ function FileParserNode({ id, data }: NodeProps) { value={label} editable={data.editable} onChange={(value) => { - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> File Parser Block diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx index fd934cbb..e92da0cd 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx @@ -14,12 +14,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { LoopNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function LoopNode({ id, data }: NodeProps) { const { updateNodeData, setNodes } = useReactFlow(); - const nodes = useNodes(); + const nodes = useNodes(); const deleteNodeCallback = useDeleteNodeCallback(); const [label, setLabel] = useState(data.label); const [inputs, setInputs] = useState({ @@ -77,16 +80,22 @@ function LoopNode({ id, data }: NodeProps) { value={label} editable={data.editable} onChange={(value) => { - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Loop Block diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx index 14acf5d1..8fb280fa 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/SendEmailNode/SendEmailNode.tsx @@ -14,12 +14,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { SendEmailNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function SendEmailNode({ id, data }: NodeProps) { const { updateNodeData, setNodes } = useReactFlow(); - const nodes = useNodes(); + const nodes = useNodes(); const deleteNodeCallback = useDeleteNodeCallback(); const [label, setLabel] = useState(data.label); const [inputs, setInputs] = useState({ @@ -62,16 +65,22 @@ function SendEmailNode({ id, data }: NodeProps) { value={label} editable={data.editable} onChange={(value) => { - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Send Email Block diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx index 05365bee..96fc6854 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx @@ -24,6 +24,7 @@ import { import { useState } from "react"; import { AppNode } from ".."; import { + getLabelForExistingNode, getOutputParameterKey, getUpdatedNodesAfterLabelUpdateForParameterKeys, } from "../../workflowEditorUtils"; @@ -421,15 +422,22 @@ function TaskNode({ id, data }: NodeProps) { value={label} editable={editable} onChange={(value) => { - setLabel(value); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Task Block diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx index 68cc12f6..e85f4d23 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TextPromptNode/TextPromptNode.tsx @@ -16,12 +16,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { TextPromptNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function TextPromptNode({ id, data }: NodeProps) { const { updateNodeData, setNodes } = useReactFlow(); - const nodes = useNodes(); + const nodes = useNodes(); const { editable } = data; const deleteNodeCallback = useDeleteNodeCallback(); const [label, setLabel] = useState(data.label); @@ -53,18 +56,24 @@ function TextPromptNode({ id, data }: NodeProps) {
{ - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Text Prompt Block
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx index 3c05c4a8..2d978c88 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/UploadNode/UploadNode.tsx @@ -13,12 +13,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { NodeActionMenu } from "../NodeActionMenu"; import type { UploadNode } from "./types"; import { useState } from "react"; -import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; +import { + getLabelForExistingNode, + getUpdatedNodesAfterLabelUpdateForParameterKeys, +} from "../../workflowEditorUtils"; import { AppNode } from ".."; function UploadNode({ id, data }: NodeProps) { - const { updateNodeData, setNodes } = useReactFlow(); - const nodes = useNodes(); + const { setNodes } = useReactFlow(); + const nodes = useNodes(); const deleteNodeCallback = useDeleteNodeCallback(); const [label, setLabel] = useState(data.label); @@ -47,16 +50,22 @@ function UploadNode({ id, data }: NodeProps) { value={label} editable={data.editable} onChange={(value) => { - setLabel(value); - updateNodeData(id, { label: value }); + const existingLabels = nodes.map((n) => n.data.label); + const newLabel = getLabelForExistingNode( + value, + existingLabels, + ); + setLabel(newLabel); setNodes( getUpdatedNodesAfterLabelUpdateForParameterKeys( id, - value, + newLabel, nodes as Array, ), ); }} + titleClassName="text-base" + inputClassName="text-base" /> Upload Block diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/EditableNodeTitle.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/EditableNodeTitle.tsx index 70a7c1ef..e228ed1b 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/EditableNodeTitle.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/EditableNodeTitle.tsx @@ -1,4 +1,3 @@ -import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, @@ -6,66 +5,70 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/util/utils"; -import { useLayoutEffect, useRef } from "react"; +import { HorizontallyResizingInput } from "./HorizontallyResizingInput"; +import { useState } from "react"; type Props = { value: string; editable: boolean; onChange: (value: string) => void; - className?: string; + titleClassName?: string; + inputClassName?: string; }; -function EditableNodeTitle({ value, editable, onChange, className }: Props) { - const ref = useRef(null); - - useLayoutEffect(() => { - // size the textarea correctly on first render - if (!ref.current) { - return; - } - ref.current.style.width = `${ref.current.scrollWidth + 2}px`; - }, []); - - function setSize() { - if (!ref.current) { - return; - } - ref.current.style.width = "auto"; - ref.current.style.width = `${ref.current.scrollWidth + 2}px`; - } +function EditableNodeTitle({ + value, + editable, + onChange, + titleClassName, + inputClassName, +}: Props) { + const [editing, setEditing] = useState(false); return ( - { - if (!editable) { - event.currentTarget.value = value; - return; - } - onChange(event.target.value); - }} - onKeyDown={(event) => { - if (!editable) { - return; - } - if (event.key === "Enter") { - event.currentTarget.blur(); - } - if (event.key === "Escape") { - event.currentTarget.value = value; - event.currentTarget.blur(); - } - setSize(); - }} - onInput={setSize} - defaultValue={value} - /> + {!editing ? ( +

{ + setEditing(true); + }} + > + {value} +

+ ) : ( + { + if (!editable) { + event.currentTarget.value = value; + return; + } + if (event.currentTarget.value !== value) { + onChange(event.target.value); + } + setEditing(false); + }} + onKeyDown={(event) => { + if (!editable) { + return; + } + if (event.key === "Enter") { + event.currentTarget.blur(); + } + if (event.key === "Escape") { + event.currentTarget.value = value; + event.currentTarget.blur(); + } + }} + defaultValue={value} + /> + )}
Click to edit
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/HorizontallyResizingInput.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/HorizontallyResizingInput.tsx new file mode 100644 index 00000000..4acb7957 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/HorizontallyResizingInput.tsx @@ -0,0 +1,42 @@ +import { Input } from "@/components/ui/input"; +import { useLayoutEffect, useRef } from "react"; + +type Props = React.ComponentProps; + +function HorizontallyResizingInput(props: Props) { + const ref = useRef(null); + + useLayoutEffect(() => { + // size the textarea correctly on first render + if (!ref.current) { + return; + } + ref.current.style.width = `${ref.current.scrollWidth + 2}px`; + }, []); + + function setSize() { + if (!ref.current) { + return; + } + ref.current.style.width = "auto"; + ref.current.style.width = `${ref.current.scrollWidth + 2}px`; + } + + return ( + { + setSize(); + props.onInput?.(event); + }} + ref={ref} + onKeyDown={(event) => { + setSize(); + props.onKeyDown?.(event); + }} + {...props} + /> + ); +} + +export { HorizontallyResizingInput }; diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index 06a918ce..6f836f6d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -740,6 +740,19 @@ function getAdditionalParametersForEmailBlock( return sendEmailParameters; } +function getLabelForExistingNode(label: string, existingLabels: Array) { + if (!existingLabels.includes(label)) { + return label; + } + for (let i = 2; i < existingLabels.length + 1; i++) { + const candidate = `${label} (${i})`; + if (!existingLabels.includes(candidate)) { + return candidate; + } + } + return label; +} + export { createNode, generateNodeData, @@ -751,4 +764,5 @@ export { getOutputParameterKey, getUpdatedNodesAfterLabelUpdateForParameterKeys, getAdditionalParametersForEmailBlock, + getLabelForExistingNode, };