Make label changes unique by adding text (#840)

This commit is contained in:
Kerem Yilmaz
2024-09-17 11:06:41 -07:00
committed by GitHub
parent b200acec9e
commit a45c3df510
12 changed files with 221 additions and 90 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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