Context parameters in UI (#902)
This commit is contained in:
@@ -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 };
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
@@ -56,7 +56,7 @@ export type WorkflowParameter = WorkflowParameterBase & {
|
||||
|
||||
export type ContextParameter = WorkflowParameterBase & {
|
||||
parameter_type: "context";
|
||||
source: WorkflowParameter;
|
||||
source: OutputParameter | ContextParameter | WorkflowParameter;
|
||||
value: unknown;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user