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} editable={true}
onChange={onTitleChange} onChange={onTitleChange}
value={title} value={title}
className="text-3xl" titleClassName="text-3xl"
inputClassName="text-3xl"
/> />
</div> </div>
<div className="flex h-full w-1/3 items-center justify-end gap-4 p-4"> <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 { NodeActionMenu } from "../NodeActionMenu";
import type { CodeBlockNode } from "./types"; import type { CodeBlockNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) { function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData, setNodes } = useReactFlow();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -50,16 +53,22 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Code Block</span> <span className="text-xs text-slate-400">Code Block</span>
</div> </div>

View File

@@ -13,12 +13,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import type { DownloadNode } from "./types"; import type { DownloadNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function DownloadNode({ id, data }: NodeProps<DownloadNode>) { function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { setNodes } = useReactFlow();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
@@ -47,16 +50,22 @@ function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Download Block</span> <span className="text-xs text-slate-400">Download Block</span>
</div> </div>

View File

@@ -12,13 +12,16 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import type { FileParserNode } from "./types"; import type { FileParserNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function FileParserNode({ id, data }: NodeProps<FileParserNode>) { function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData, setNodes } = useReactFlow();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
fileUrl: data.fileUrl, fileUrl: data.fileUrl,
@@ -49,16 +52,22 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">File Parser Block</span> <span className="text-xs text-slate-400">File Parser Block</span>
</div> </div>

View File

@@ -14,12 +14,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import type { LoopNode } from "./types"; import type { LoopNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function LoopNode({ id, data }: NodeProps<LoopNode>) { function LoopNode({ id, data }: NodeProps<LoopNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData, setNodes } = useReactFlow();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -77,16 +80,22 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Loop Block</span> <span className="text-xs text-slate-400">Loop Block</span>
</div> </div>

View File

@@ -14,12 +14,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import type { SendEmailNode } from "./types"; import type { SendEmailNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) { function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData, setNodes } = useReactFlow();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
@@ -62,16 +65,22 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Send Email Block</span> <span className="text-xs text-slate-400">Send Email Block</span>
</div> </div>

View File

@@ -24,6 +24,7 @@ import {
import { useState } from "react"; import { useState } from "react";
import { AppNode } from ".."; import { AppNode } from "..";
import { import {
getLabelForExistingNode,
getOutputParameterKey, getOutputParameterKey,
getUpdatedNodesAfterLabelUpdateForParameterKeys, getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils"; } from "../../workflowEditorUtils";
@@ -421,15 +422,22 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
value={label} value={label}
editable={editable} editable={editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Task Block</span> <span className="text-xs text-slate-400">Task Block</span>
</div> </div>

View File

@@ -16,12 +16,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import type { TextPromptNode } from "./types"; import type { TextPromptNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) { function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData, setNodes } = useReactFlow();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const { editable } = data; const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
@@ -53,18 +56,24 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Text Prompt Block</span> <span className="text-xs text-slate-400">Text Prompt Block</span>
</div> </div>

View File

@@ -13,12 +13,15 @@ import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import type { UploadNode } from "./types"; import type { UploadNode } from "./types";
import { useState } from "react"; import { useState } from "react";
import { getUpdatedNodesAfterLabelUpdateForParameterKeys } from "../../workflowEditorUtils"; import {
getLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from ".."; import { AppNode } from "..";
function UploadNode({ id, data }: NodeProps<UploadNode>) { function UploadNode({ id, data }: NodeProps<UploadNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { setNodes } = useReactFlow();
const nodes = useNodes(); const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useState(data.label);
@@ -47,16 +50,22 @@ function UploadNode({ id, data }: NodeProps<UploadNode>) {
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={(value) => {
setLabel(value); const existingLabels = nodes.map((n) => n.data.label);
updateNodeData(id, { label: value }); const newLabel = getLabelForExistingNode(
value,
existingLabels,
);
setLabel(newLabel);
setNodes( setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys( getUpdatedNodesAfterLabelUpdateForParameterKeys(
id, id,
value, newLabel,
nodes as Array<AppNode>, nodes as Array<AppNode>,
), ),
); );
}} }}
titleClassName="text-base"
inputClassName="text-base"
/> />
<span className="text-xs text-slate-400">Upload Block</span> <span className="text-xs text-slate-400">Upload Block</span>
</div> </div>

View File

@@ -1,4 +1,3 @@
import { Input } from "@/components/ui/input";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -6,66 +5,70 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/util/utils"; import { cn } from "@/util/utils";
import { useLayoutEffect, useRef } from "react"; import { HorizontallyResizingInput } from "./HorizontallyResizingInput";
import { useState } from "react";
type Props = { type Props = {
value: string; value: string;
editable: boolean; editable: boolean;
onChange: (value: string) => void; onChange: (value: string) => void;
className?: string; titleClassName?: string;
inputClassName?: string;
}; };
function EditableNodeTitle({ value, editable, onChange, className }: Props) { function EditableNodeTitle({
const ref = useRef<HTMLInputElement>(null); value,
editable,
useLayoutEffect(() => { onChange,
// size the textarea correctly on first render titleClassName,
if (!ref.current) { inputClassName,
return; }: Props) {
} const [editing, setEditing] = useState(false);
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 ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Input {!editing ? (
disabled={!editable} <h1
ref={ref} className={cn("cursor-text", titleClassName)}
size={1} onClick={() => {
className={cn("nopan w-min border-0 p-0", className)} setEditing(true);
onBlur={(event) => { }}
if (!editable) { >
event.currentTarget.value = value; {value}
return; </h1>
} ) : (
onChange(event.target.value); <HorizontallyResizingInput
}} disabled={!editable}
onKeyDown={(event) => { size={1}
if (!editable) { autoFocus
return; className={cn("nopan w-min border-0 p-0", inputClassName)}
} onBlur={(event) => {
if (event.key === "Enter") { if (!editable) {
event.currentTarget.blur(); event.currentTarget.value = value;
} return;
if (event.key === "Escape") { }
event.currentTarget.value = value; if (event.currentTarget.value !== value) {
event.currentTarget.blur(); onChange(event.target.value);
} }
setSize(); setEditing(false);
}} }}
onInput={setSize} onKeyDown={(event) => {
defaultValue={value} if (!editable) {
/> return;
}
if (event.key === "Enter") {
event.currentTarget.blur();
}
if (event.key === "Escape") {
event.currentTarget.value = value;
event.currentTarget.blur();
}
}}
defaultValue={value}
/>
)}
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Click to edit</TooltipContent> <TooltipContent>Click to edit</TooltipContent>
</Tooltip> </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; 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 { export {
createNode, createNode,
generateNodeData, generateNodeData,
@@ -751,4 +764,5 @@ export {
getOutputParameterKey, getOutputParameterKey,
getUpdatedNodesAfterLabelUpdateForParameterKeys, getUpdatedNodesAfterLabelUpdateForParameterKeys,
getAdditionalParametersForEmailBlock, getAdditionalParametersForEmailBlock,
getLabelForExistingNode,
}; };