Context parameters in UI (#902)

This commit is contained in:
Kerem Yilmaz
2024-10-03 12:19:45 -07:00
committed by GitHub
parent 9e50456995
commit 83d0879034
18 changed files with 342 additions and 270 deletions

View File

@@ -0,0 +1,46 @@
import { useNodes } from "@xyflow/react";
import { useWorkflowParametersState } from "../editor/useWorkflowParametersState";
import { AppNode } from "../editor/nodes";
import { getOutputParameterKey } from "../editor/workflowEditorUtils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type Props = {
value?: string;
onChange: (value: string) => void;
};
function SourceParameterKeySelector({ value, onChange }: Props) {
const [workflowParameters] = useWorkflowParametersState();
const nodes = useNodes<AppNode>();
const contextParameterKeys = workflowParameters
.filter((parameter) => parameter.parameterType !== "credential")
.map((parameter) => parameter.key);
const outputParameterKeys = nodes
.filter((node) => node.type !== "nodeAdder")
.map((node) => getOutputParameterKey(node.data.label));
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger>
<SelectValue placeholder="Select a parameter" />
</SelectTrigger>
<SelectContent>
{[...contextParameterKeys, ...outputParameterKeys].map(
(parameterKey) => (
<SelectItem key={parameterKey} value={parameterKey}>
{parameterKey}
</SelectItem>
),
)}
</SelectContent>
</Select>
);
}
export { SourceParameterKeySelector };

View File

@@ -17,13 +17,13 @@ import { useEffect, useState } from "react";
import { import {
AWSSecretParameter, AWSSecretParameter,
BitwardenSensitiveInformationParameter, BitwardenSensitiveInformationParameter,
ContextParameter,
WorkflowApiResponse, WorkflowApiResponse,
WorkflowParameterValueType, WorkflowParameterValueType,
} from "../types/workflowTypes"; } from "../types/workflowTypes";
import { import {
BitwardenLoginCredentialParameterYAML, BitwardenLoginCredentialParameterYAML,
BlockYAML, BlockYAML,
ContextParameterYAML,
ParameterYAML, ParameterYAML,
WorkflowCreateYAMLRequest, WorkflowCreateYAMLRequest,
WorkflowParameterYAML, WorkflowParameterYAML,
@@ -65,7 +65,11 @@ import { ReloadIcon } from "@radix-ui/react-icons";
function convertToParametersYAML( function convertToParametersYAML(
parameters: ParametersState, parameters: ParametersState,
): Array<WorkflowParameterYAML | BitwardenLoginCredentialParameterYAML> { ): Array<
| WorkflowParameterYAML
| BitwardenLoginCredentialParameterYAML
| ContextParameterYAML
> {
return parameters.map((parameter) => { return parameters.map((parameter) => {
if (parameter.parameterType === "workflow") { if (parameter.parameterType === "workflow") {
return { return {
@@ -77,6 +81,13 @@ function convertToParametersYAML(
? {} ? {}
: { default_value: parameter.defaultValue }), : { default_value: parameter.defaultValue }),
}; };
} else if (parameter.parameterType === "context") {
return {
parameter_type: "context",
key: parameter.key,
description: parameter.description || null,
source_parameter_key: parameter.sourceParameterKey,
};
} else { } else {
return { return {
parameter_type: "bitwarden_login_credential", parameter_type: "bitwarden_login_credential",
@@ -99,7 +110,7 @@ export type ParametersState = Array<
key: string; key: string;
parameterType: "workflow"; parameterType: "workflow";
dataType: WorkflowParameterValueType; dataType: WorkflowParameterValueType;
description?: string; description?: string | null;
defaultValue: unknown; defaultValue: unknown;
} }
| { | {
@@ -107,7 +118,13 @@ export type ParametersState = Array<
parameterType: "credential"; parameterType: "credential";
collectionId: string; collectionId: string;
urlParameterKey: string; urlParameterKey: string;
description?: string; description?: string | null;
}
| {
key: string;
parameterType: "context";
sourceParameterKey: string;
description?: string | null;
} }
>; >;
@@ -225,15 +242,10 @@ function FlowRenderer({
(parameter) => { (parameter) => {
return ( return (
parameter.parameter_type === "aws_secret" || parameter.parameter_type === "aws_secret" ||
parameter.parameter_type === "bitwarden_sensitive_information" || parameter.parameter_type === "bitwarden_sensitive_information"
parameter.parameter_type === "context"
); );
}, },
) as Array< ) as Array<AWSSecretParameter | BitwardenSensitiveInformationParameter>;
| AWSSecretParameter
| BitwardenSensitiveInformationParameter
| ContextParameter
>;
const echoParameters = convertEchoParameters(filteredParameters); const echoParameters = convertEchoParameters(filteredParameters);

View File

@@ -51,7 +51,8 @@ function WorkflowEditor() {
.filter( .filter(
(parameter) => (parameter) =>
parameter.parameter_type === "workflow" || parameter.parameter_type === "workflow" ||
parameter.parameter_type === "bitwarden_login_credential", parameter.parameter_type === "bitwarden_login_credential" ||
parameter.parameter_type === "context",
) )
.map((parameter) => { .map((parameter) => {
if (parameter.parameter_type === "workflow") { if (parameter.parameter_type === "workflow") {
@@ -60,6 +61,14 @@ function WorkflowEditor() {
parameterType: "workflow", parameterType: "workflow",
dataType: parameter.workflow_parameter_type, dataType: parameter.workflow_parameter_type,
defaultValue: parameter.default_value, defaultValue: parameter.default_value,
description: parameter.description,
};
} else if (parameter.parameter_type === "context") {
return {
key: parameter.key,
parameterType: "context",
sourceParameterKey: parameter.source.key,
description: parameter.description,
}; };
} else { } else {
return { return {
@@ -67,6 +76,7 @@ function WorkflowEditor() {
parameterType: "credential", parameterType: "credential",
collectionId: parameter.bitwarden_collection_id, collectionId: parameter.bitwarden_collection_id,
urlParameterKey: parameter.url_parameter_key, urlParameterKey: parameter.url_parameter_key,
description: parameter.description,
}; };
} }
})} })}

View File

@@ -1,29 +1,21 @@
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { CodeIcon } from "@radix-ui/react-icons"; import { CodeIcon } from "@radix-ui/react-icons";
import { import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
Handle, import { useState } from "react";
NodeProps,
Position,
useNodes,
useReactFlow,
} from "@xyflow/react";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) { function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData } = useReactFlow();
const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
code: data.code, code: data.code,
}); });
@@ -52,22 +44,7 @@ function CodeBlockNode({ id, data }: NodeProps<CodeBlockNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -1,29 +1,19 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { DownloadIcon } from "@radix-ui/react-icons"; import { DownloadIcon } from "@radix-ui/react-icons";
import { import { Handle, NodeProps, Position } from "@xyflow/react";
Handle,
NodeProps,
Position,
useNodes,
useReactFlow,
} from "@xyflow/react";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function DownloadNode({ id, data }: NodeProps<DownloadNode>) { function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
const { setNodes } = useReactFlow(); const [label, setLabel] = useNodeLabelChangeHandler({
const nodes = useNodes<AppNode>(); id,
initialValue: data.label,
});
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label);
return ( return (
<div> <div>
@@ -49,22 +39,7 @@ function DownloadNode({ id, data }: NodeProps<DownloadNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -1,31 +1,23 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { CursorTextIcon } from "@radix-ui/react-icons"; import { CursorTextIcon } from "@radix-ui/react-icons";
import { import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
Handle, import { useState } from "react";
NodeProps,
Position,
useNodes,
useReactFlow,
} from "@xyflow/react";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function FileParserNode({ id, data }: NodeProps<FileParserNode>) { function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData } = useReactFlow();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const nodes = useNodes<AppNode>();
const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
fileUrl: data.fileUrl, fileUrl: data.fileUrl,
}); });
const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
return ( return (
<div> <div>
@@ -51,22 +43,7 @@ function FileParserNode({ id, data }: NodeProps<FileParserNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -1,6 +1,7 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { UpdateIcon } from "@radix-ui/react-icons"; import { UpdateIcon } from "@radix-ui/react-icons";
import type { Node } from "@xyflow/react"; import type { Node } from "@xyflow/react";
import { import {
@@ -10,21 +11,20 @@ import {
useNodes, useNodes,
useReactFlow, useReactFlow,
} from "@xyflow/react"; } from "@xyflow/react";
import { useState } from "react";
import { AppNode } from "..";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function LoopNode({ id, data }: NodeProps<LoopNode>) { function LoopNode({ id, data }: NodeProps<LoopNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData } = useReactFlow();
const nodes = useNodes<AppNode>(); const nodes = useNodes<AppNode>();
const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
loopValue: data.loopValue, loopValue: data.loopValue,
}); });
@@ -79,22 +79,7 @@ function LoopNode({ id, data }: NodeProps<LoopNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -2,29 +2,21 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { EnvelopeClosedIcon } from "@radix-ui/react-icons"; import { EnvelopeClosedIcon } from "@radix-ui/react-icons";
import { import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
Handle, import { useState } from "react";
NodeProps,
Position,
useNodes,
useReactFlow,
} from "@xyflow/react";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) { function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData } = useReactFlow();
const nodes = useNodes<AppNode>(); const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
recipients: data.recipients, recipients: data.recipients,
subject: data.subject, subject: data.subject,
@@ -64,22 +56,7 @@ function SendEmailNode({ id, data }: NodeProps<SendEmailNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -11,6 +11,7 @@ import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { ListBulletIcon } from "@radix-ui/react-icons"; import { ListBulletIcon } from "@radix-ui/react-icons";
import { import {
Edge, Edge,
@@ -23,11 +24,7 @@ import {
} from "@xyflow/react"; } from "@xyflow/react";
import { useState } from "react"; import { useState } from "react";
import { AppNode } from ".."; import { AppNode } from "..";
import { import { getOutputParameterKey } from "../../workflowEditorUtils";
getUniqueLabelForExistingNode,
getOutputParameterKey,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; import { EditableNodeTitle } from "../components/EditableNodeTitle";
import { NodeActionMenu } from "../NodeActionMenu"; import { NodeActionMenu } from "../NodeActionMenu";
import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch"; import { TaskNodeDisplayModeSwitch } from "./TaskNodeDisplayModeSwitch";
@@ -58,7 +55,7 @@ function getPreviousNodeIds(
} }
function TaskNode({ id, data }: NodeProps<TaskNode>) { function TaskNode({ id, data }: NodeProps<TaskNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData } = useReactFlow();
const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>("basic"); const [displayMode, setDisplayMode] = useState<TaskNodeDisplayMode>("basic");
const { editable } = data; const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
@@ -75,7 +72,11 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
getOutputParameterKey(label), getOutputParameterKey(label),
); );
const [label, setLabel] = useState(data.label); const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
url: data.url, url: data.url,
navigationGoal: data.navigationGoal, navigationGoal: data.navigationGoal,
@@ -421,22 +422,7 @@ function TaskNode({ id, data }: NodeProps<TaskNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={editable} editable={editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -4,35 +4,28 @@ import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { CursorTextIcon } from "@radix-ui/react-icons"; import { CursorTextIcon } from "@radix-ui/react-icons";
import { import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
Handle, import { useState } from "react";
NodeProps,
Position,
useNodes,
useReactFlow,
} from "@xyflow/react";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) { function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
const { updateNodeData, setNodes } = useReactFlow(); const { updateNodeData } = useReactFlow();
const nodes = useNodes<AppNode>();
const { editable } = data; const { editable } = data;
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label);
const [inputs, setInputs] = useState({ const [inputs, setInputs] = useState({
prompt: data.prompt, prompt: data.prompt,
jsonSchema: data.jsonSchema, jsonSchema: data.jsonSchema,
}); });
const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
return ( return (
<div> <div>
<Handle <Handle
@@ -57,22 +50,7 @@ function TextPromptNode({ id, data }: NodeProps<TextPromptNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -1,29 +1,19 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
import { UploadIcon } from "@radix-ui/react-icons"; import { UploadIcon } from "@radix-ui/react-icons";
import { import { Handle, NodeProps, Position } from "@xyflow/react";
Handle,
NodeProps,
Position,
useNodes,
useReactFlow,
} from "@xyflow/react";
import { EditableNodeTitle } from "../components/EditableNodeTitle"; 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 {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
} from "../../workflowEditorUtils";
import { AppNode } from "..";
function UploadNode({ id, data }: NodeProps<UploadNode>) { function UploadNode({ id, data }: NodeProps<UploadNode>) {
const { setNodes } = useReactFlow();
const nodes = useNodes<AppNode>();
const deleteNodeCallback = useDeleteNodeCallback(); const deleteNodeCallback = useDeleteNodeCallback();
const [label, setLabel] = useState(data.label); const [label, setLabel] = useNodeLabelChangeHandler({
id,
initialValue: data.label,
});
return ( return (
<div> <div>
@@ -49,22 +39,7 @@ function UploadNode({ id, data }: NodeProps<UploadNode>) {
<EditableNodeTitle <EditableNodeTitle
value={label} value={label}
editable={data.editable} editable={data.editable}
onChange={(value) => { onChange={setLabel}
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
}}
titleClassName="text-base" titleClassName="text-base"
inputClassName="text-base" inputClassName="text-base"
/> />

View File

@@ -1,7 +1,7 @@
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { useLayoutEffect, useRef } from "react"; import { useLayoutEffect, useRef } from "react";
type Props = React.ComponentProps<typeof Input>; type Props = React.ComponentPropsWithoutRef<typeof Input>;
function HorizontallyResizingInput(props: Props) { function HorizontallyResizingInput(props: Props) {
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);

View File

@@ -17,9 +17,10 @@ import { WorkflowParameterInput } from "../../WorkflowParameterInput";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { getDefaultValueForParameterType } from "../workflowEditorUtils"; import { getDefaultValueForParameterType } from "../workflowEditorUtils";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { SourceParameterKeySelector } from "../../components/SourceParameterKeySelector";
type Props = { type Props = {
type: "workflow" | "credential"; type: "workflow" | "credential" | "context";
onClose: () => void; onClose: () => void;
onSave: (value: ParametersState[number]) => void; onSave: (value: ParametersState[number]) => void;
}; };
@@ -32,6 +33,16 @@ const workflowParameterTypeOptions = [
{ label: "JSON", value: WorkflowParameterValueType.JSON }, { label: "JSON", value: WorkflowParameterValueType.JSON },
]; ];
function header(type: "workflow" | "credential" | "context") {
if (type === "workflow") {
return "Add Workflow Parameter";
}
if (type === "credential") {
return "Add Credential Parameter";
}
return "Add Context Parameter";
}
function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) { function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
const [key, setKey] = useState(""); const [key, setKey] = useState("");
const [urlParameterKey, setUrlParameterKey] = useState(""); const [urlParameterKey, setUrlParameterKey] = useState("");
@@ -46,13 +57,14 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
hasDefaultValue: false, hasDefaultValue: false,
defaultValue: null, defaultValue: null,
}); });
const [sourceParameterKey, setSourceParameterKey] = useState<
string | undefined
>(undefined);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<header className="flex items-center justify-between"> <header className="flex items-center justify-between">
<span> <span>{header(type)}</span>
Add {type === "workflow" ? "Workflow" : "Credential"} Parameter
</span>
<Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} /> <Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} />
</header> </header>
<div className="space-y-1"> <div className="space-y-1">
@@ -170,6 +182,15 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
</div> </div>
</> </>
)} )}
{type === "context" && (
<div className="space-y-1">
<Label className="text-xs text-slate-300">Source Parameter</Label>
<SourceParameterKeySelector
value={sourceParameterKey}
onChange={setSourceParameterKey}
/>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
onClick={() => { onClick={() => {
@@ -211,6 +232,22 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
description, description,
}); });
} }
if (type === "context") {
if (!sourceParameterKey) {
toast({
variant: "destructive",
title: "Failed to save parameters",
description: "Source parameter key is required",
});
return;
}
onSave({
key,
parameterType: "context",
sourceParameterKey: sourceParameterKey,
description,
});
}
}} }}
> >
Save Save

View File

@@ -17,9 +17,10 @@ import { Checkbox } from "@/components/ui/checkbox";
import { getDefaultValueForParameterType } from "../workflowEditorUtils"; import { getDefaultValueForParameterType } from "../workflowEditorUtils";
import { WorkflowParameterInput } from "../../WorkflowParameterInput"; import { WorkflowParameterInput } from "../../WorkflowParameterInput";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { SourceParameterKeySelector } from "../../components/SourceParameterKeySelector";
type Props = { type Props = {
type: "workflow" | "credential"; type: "workflow" | "credential" | "context";
onClose: () => void; onClose: () => void;
onSave: (value: ParametersState[number]) => void; onSave: (value: ParametersState[number]) => void;
initialValues: ParametersState[number]; initialValues: ParametersState[number];
@@ -33,6 +34,16 @@ const workflowParameterTypeOptions = [
{ label: "JSON", value: WorkflowParameterValueType.JSON }, { label: "JSON", value: WorkflowParameterValueType.JSON },
]; ];
function header(type: "workflow" | "credential" | "context") {
if (type === "workflow") {
return "Edit Workflow Parameter";
}
if (type === "credential") {
return "Edit Credential Parameter";
}
return "Edit Context Parameter";
}
function WorkflowParameterEditPanel({ function WorkflowParameterEditPanel({
type, type,
onClose, onClose,
@@ -46,7 +57,7 @@ function WorkflowParameterEditPanel({
: "", : "",
); );
const [description, setDescription] = useState( const [description, setDescription] = useState(
initialValues.description || "", initialValues.description ?? "",
); );
const [collectionId, setCollectionId] = useState( const [collectionId, setCollectionId] = useState(
initialValues.parameterType === "credential" initialValues.parameterType === "credential"
@@ -75,12 +86,18 @@ function WorkflowParameterEditPanel({
}, },
); );
const [sourceParameterKey, setSourceParameterKey] = useState<
string | undefined
>(
initialValues.parameterType === "context"
? initialValues.sourceParameterKey
: undefined,
);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<header className="flex items-center justify-between"> <header className="flex items-center justify-between">
<span> <span>{header(type)}</span>
Edit {type === "workflow" ? "Workflow" : "Credential"} Parameter
</span>
<Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} /> <Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} />
</header> </header>
<div className="space-y-1"> <div className="space-y-1">
@@ -198,6 +215,15 @@ function WorkflowParameterEditPanel({
</div> </div>
</> </>
)} )}
{type === "context" && (
<div className="space-y-1">
<Label className="text-xs text-slate-300">Source Parameter</Label>
<SourceParameterKeySelector
value={sourceParameterKey}
onChange={setSourceParameterKey}
/>
</div>
)}
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
onClick={() => { onClick={() => {
@@ -239,6 +265,22 @@ function WorkflowParameterEditPanel({
description, description,
}); });
} }
if (type === "context") {
if (!sourceParameterKey) {
toast({
variant: "destructive",
title: "Failed to save parameters",
description: "Source parameter key is required",
});
return;
}
onSave({
key,
parameterType: "context",
sourceParameterKey,
description,
});
}
}} }}
> >
Save Save

View File

@@ -40,7 +40,7 @@ function WorkflowParametersPanel() {
active: boolean; active: boolean;
operation: "add" | "edit"; operation: "add" | "edit";
parameter?: ParametersState[number] | null; parameter?: ParametersState[number] | null;
type: "workflow" | "credential"; type: "workflow" | "credential" | "context";
}>({ }>({
active: false, active: false,
operation: "add", operation: "add",
@@ -91,6 +91,17 @@ function WorkflowParametersPanel() {
> >
Credential Parameter Credential Parameter
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setOperationPanelState({
active: true,
operation: "add",
type: "context",
});
}}
>
Context Parameter
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -213,6 +224,7 @@ function WorkflowParametersPanel() {
operationPanelState.parameter && ( operationPanelState.parameter && (
<div className="w-80 rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl"> <div className="w-80 rounded-xl border border-slate-700 bg-slate-950 p-5 shadow-xl">
<WorkflowParameterEditPanel <WorkflowParameterEditPanel
key={operationPanelState.parameter?.key}
type={operationPanelState.type} type={operationPanelState.type}
initialValues={operationPanelState.parameter} initialValues={operationPanelState.parameter}
onSave={(editedParameter) => { onSave={(editedParameter) => {
@@ -224,6 +236,16 @@ function WorkflowParametersPanel() {
) { ) {
return editedParameter; return editedParameter;
} }
if (
parameter.parameterType === "context" &&
parameter.sourceParameterKey ===
operationPanelState.parameter?.key
) {
return {
...parameter,
sourceParameterKey: editedParameter.key,
};
}
return parameter; return parameter;
}), }),
); );

View File

@@ -612,6 +612,7 @@ import type {
BitwardenSensitiveInformationParameter, BitwardenSensitiveInformationParameter,
ContextParameter, ContextParameter,
} from "../types/workflowTypes"; } from "../types/workflowTypes";
import { ParametersState } from "./FlowRenderer";
/** /**
* If a parameter is not displayed in the editor, we should echo its value back when saved. * If a parameter is not displayed in the editor, we should echo its value back when saved.
@@ -646,13 +647,6 @@ function convertEchoParameters(
parameter.bitwarden_master_password_aws_secret_key, parameter.bitwarden_master_password_aws_secret_key,
}; };
} }
if (parameter.parameter_type === "context") {
return {
key: parameter.key,
parameter_type: "context",
source_parameter_key: parameter.source.key,
};
}
throw new Error("Unknown parameter type"); throw new Error("Unknown parameter type");
}); });
} }
@@ -708,6 +702,33 @@ function getUpdatedNodesAfterLabelUpdateForParameterKeys(
}); });
} }
function getUpdatedParametersAfterLabelUpdateForSourceParameterKey(
id: string,
newLabel: string,
nodes: Array<Node>,
parameters: ParametersState,
): ParametersState {
const node = nodes.find((node) => node.id === id);
if (!node) {
return parameters;
}
const oldLabel = node.data.label as string;
const oldOutputParameterKey = getOutputParameterKey(oldLabel);
const newOutputParameterKey = getOutputParameterKey(newLabel);
return parameters.map((parameter) => {
if (parameter.parameterType === "context") {
return {
...parameter,
sourceParameterKey:
parameter.sourceParameterKey === oldOutputParameterKey
? newOutputParameterKey
: oldOutputParameterKey,
};
}
return parameter;
});
}
const sendEmailExpectedParameters = [ const sendEmailExpectedParameters = [
{ {
key: SMTP_HOST_PARAMETER_KEY, key: SMTP_HOST_PARAMETER_KEY,
@@ -808,4 +829,5 @@ export {
isOutputParameterKey, isOutputParameterKey,
getBlockNameOfOutputParameterKey, getBlockNameOfOutputParameterKey,
getDefaultValueForParameterType, getDefaultValueForParameterType,
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
}; };

View File

@@ -0,0 +1,51 @@
import { useNodes, useReactFlow } from "@xyflow/react";
import { AppNode } from "../editor/nodes";
import {
getUniqueLabelForExistingNode,
getUpdatedNodesAfterLabelUpdateForParameterKeys,
getUpdatedParametersAfterLabelUpdateForSourceParameterKey,
} from "../editor/workflowEditorUtils";
import { useState } from "react";
import { useWorkflowParametersState } from "../editor/useWorkflowParametersState";
type Props = {
id: string;
initialValue: string;
};
function useNodeLabelChangeHandler({ id, initialValue }: Props) {
const [label, setLabel] = useState(initialValue);
const nodes = useNodes<AppNode>();
const { setNodes } = useReactFlow();
const [workflowParameters, setWorkflowParameters] =
useWorkflowParametersState();
function handleLabelChange(value: string) {
const existingLabels = nodes.map((n) => n.data.label);
const labelWithoutWhitespace = value.replace(/\s+/g, "_");
const newLabel = getUniqueLabelForExistingNode(
labelWithoutWhitespace,
existingLabels,
);
setLabel(newLabel);
setNodes(
getUpdatedNodesAfterLabelUpdateForParameterKeys(
id,
newLabel,
nodes as Array<AppNode>,
),
);
setWorkflowParameters(
getUpdatedParametersAfterLabelUpdateForSourceParameterKey(
id,
newLabel,
nodes,
workflowParameters,
),
);
}
return [label, handleLabelChange] as const;
}
export { useNodeLabelChangeHandler };

View File

@@ -56,7 +56,7 @@ export type WorkflowParameter = WorkflowParameterBase & {
export type ContextParameter = WorkflowParameterBase & { export type ContextParameter = WorkflowParameterBase & {
parameter_type: "context"; parameter_type: "context";
source: WorkflowParameter; source: OutputParameter | ContextParameter | WorkflowParameter;
value: unknown; value: unknown;
}; };