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,
};