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 {
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 & {
|
export type ContextParameter = WorkflowParameterBase & {
|
||||||
parameter_type: "context";
|
parameter_type: "context";
|
||||||
source: WorkflowParameter;
|
source: OutputParameter | ContextParameter | WorkflowParameter;
|
||||||
value: unknown;
|
value: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user