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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,9 +17,10 @@ import { WorkflowParameterInput } from "../../WorkflowParameterInput";
import { Checkbox } from "@/components/ui/checkbox";
import { getDefaultValueForParameterType } from "../workflowEditorUtils";
import { toast } from "@/components/ui/use-toast";
import { SourceParameterKeySelector } from "../../components/SourceParameterKeySelector";
type Props = {
type: "workflow" | "credential";
type: "workflow" | "credential" | "context";
onClose: () => void;
onSave: (value: ParametersState[number]) => void;
};
@@ -32,6 +33,16 @@ const workflowParameterTypeOptions = [
{ 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) {
const [key, setKey] = useState("");
const [urlParameterKey, setUrlParameterKey] = useState("");
@@ -46,13 +57,14 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
hasDefaultValue: false,
defaultValue: null,
});
const [sourceParameterKey, setSourceParameterKey] = useState<
string | undefined
>(undefined);
return (
<div className="space-y-4">
<header className="flex items-center justify-between">
<span>
Add {type === "workflow" ? "Workflow" : "Credential"} Parameter
</span>
<span>{header(type)}</span>
<Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} />
</header>
<div className="space-y-1">
@@ -170,6 +182,15 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
</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">
<Button
onClick={() => {
@@ -211,6 +232,22 @@ function WorkflowParameterAddPanel({ type, onClose, onSave }: Props) {
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

View File

@@ -17,9 +17,10 @@ import { Checkbox } from "@/components/ui/checkbox";
import { getDefaultValueForParameterType } from "../workflowEditorUtils";
import { WorkflowParameterInput } from "../../WorkflowParameterInput";
import { toast } from "@/components/ui/use-toast";
import { SourceParameterKeySelector } from "../../components/SourceParameterKeySelector";
type Props = {
type: "workflow" | "credential";
type: "workflow" | "credential" | "context";
onClose: () => void;
onSave: (value: ParametersState[number]) => void;
initialValues: ParametersState[number];
@@ -33,6 +34,16 @@ const workflowParameterTypeOptions = [
{ 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({
type,
onClose,
@@ -46,7 +57,7 @@ function WorkflowParameterEditPanel({
: "",
);
const [description, setDescription] = useState(
initialValues.description || "",
initialValues.description ?? "",
);
const [collectionId, setCollectionId] = useState(
initialValues.parameterType === "credential"
@@ -75,12 +86,18 @@ function WorkflowParameterEditPanel({
},
);
const [sourceParameterKey, setSourceParameterKey] = useState<
string | undefined
>(
initialValues.parameterType === "context"
? initialValues.sourceParameterKey
: undefined,
);
return (
<div className="space-y-4">
<header className="flex items-center justify-between">
<span>
Edit {type === "workflow" ? "Workflow" : "Credential"} Parameter
</span>
<span>{header(type)}</span>
<Cross2Icon className="h-6 w-6 cursor-pointer" onClick={onClose} />
</header>
<div className="space-y-1">
@@ -198,6 +215,15 @@ function WorkflowParameterEditPanel({
</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">
<Button
onClick={() => {
@@ -239,6 +265,22 @@ function WorkflowParameterEditPanel({
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

View File

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

View File

@@ -612,6 +612,7 @@ import type {
BitwardenSensitiveInformationParameter,
ContextParameter,
} from "../types/workflowTypes";
import { ParametersState } from "./FlowRenderer";
/**
* 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,
};
}
if (parameter.parameter_type === "context") {
return {
key: parameter.key,
parameter_type: "context",
source_parameter_key: parameter.source.key,
};
}
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 = [
{
key: SMTP_HOST_PARAMETER_KEY,
@@ -808,4 +829,5 @@ export {
isOutputParameterKey,
getBlockNameOfOutputParameterKey,
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 & {
parameter_type: "context";
source: WorkflowParameter;
source: OutputParameter | ContextParameter | WorkflowParameter;
value: unknown;
};