Make label changes unique by adding text (#840)
This commit is contained in:
@@ -56,7 +56,8 @@ function WorkflowHeader({
|
||||
editable={true}
|
||||
onChange={onTitleChange}
|
||||
value={title}
|
||||
className="text-3xl"
|
||||
titleClassName="text-3xl"
|
||||
inputClassName="text-3xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-full w-1/3 items-center justify-end gap-4 p-4">
|
||||
|
||||
@@ -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<CodeBlockNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const nodes = useNodes();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -50,16 +53,22 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Code Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<DownloadNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const nodes = useNodes();
|
||||
const { setNodes } = useReactFlow();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
|
||||
@@ -47,16 +50,22 @@ function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Download Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<FileParserNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const nodes = useNodes();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
const [inputs, setInputs] = useState({
|
||||
fileUrl: data.fileUrl,
|
||||
@@ -49,16 +52,22 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">File Parser Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<LoopNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const nodes = useNodes();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -77,16 +80,22 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Loop Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<SendEmailNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const nodes = useNodes();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -62,16 +65,22 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Send Email Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<TaskNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Task Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<TextPromptNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const nodes = useNodes();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const { editable } = data;
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
@@ -53,18 +56,24 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
|
||||
<div className="flex flex-col gap-1">
|
||||
<EditableNodeTitle
|
||||
value={label}
|
||||
editable={editable}
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Text Prompt Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<UploadNode>) {
|
||||
const { updateNodeData, setNodes } = useReactFlow();
|
||||
const nodes = useNodes();
|
||||
const { setNodes } = useReactFlow();
|
||||
const nodes = useNodes<AppNode>();
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
const [label, setLabel] = useState(data.label);
|
||||
|
||||
@@ -47,16 +50,22 @@ function UploadNode({ id, data }: NodeProps<UploadNode>) {
|
||||
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<AppNode>,
|
||||
),
|
||||
);
|
||||
}}
|
||||
titleClassName="text-base"
|
||||
inputClassName="text-base"
|
||||
/>
|
||||
<span className="text-xs text-slate-400">Upload Block</span>
|
||||
</div>
|
||||
|
||||
@@ -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<HTMLInputElement>(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 (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Input
|
||||
disabled={!editable}
|
||||
ref={ref}
|
||||
size={1}
|
||||
className={cn("nopan w-min border-0 p-0", className)}
|
||||
onBlur={(event) => {
|
||||
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 ? (
|
||||
<h1
|
||||
className={cn("cursor-text", titleClassName)}
|
||||
onClick={() => {
|
||||
setEditing(true);
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</h1>
|
||||
) : (
|
||||
<HorizontallyResizingInput
|
||||
disabled={!editable}
|
||||
size={1}
|
||||
autoFocus
|
||||
className={cn("nopan w-min border-0 p-0", inputClassName)}
|
||||
onBlur={(event) => {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Click to edit</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
|
||||
type Props = React.ComponentProps<typeof Input>;
|
||||
|
||||
function HorizontallyResizingInput(props: Props) {
|
||||
const ref = useRef<HTMLInputElement>(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 (
|
||||
<Input
|
||||
size={1}
|
||||
onInput={(event) => {
|
||||
setSize();
|
||||
props.onInput?.(event);
|
||||
}}
|
||||
ref={ref}
|
||||
onKeyDown={(event) => {
|
||||
setSize();
|
||||
props.onKeyDown?.(event);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { HorizontallyResizingInput };
|
||||
@@ -740,6 +740,19 @@ function getAdditionalParametersForEmailBlock(
|
||||
return sendEmailParameters;
|
||||
}
|
||||
|
||||
function getLabelForExistingNode(label: string, existingLabels: Array<string>) {
|
||||
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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user