Implement 'you have unsaved changes' (#850)
This commit is contained in:
@@ -14,10 +14,18 @@ import {
|
|||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { WorkflowParameterValueType } from "../types/workflowTypes";
|
import {
|
||||||
|
AWSSecretParameter,
|
||||||
|
BitwardenSensitiveInformationParameter,
|
||||||
|
ContextParameter,
|
||||||
|
WorkflowApiResponse,
|
||||||
|
WorkflowParameterValueType,
|
||||||
|
} from "../types/workflowTypes";
|
||||||
import {
|
import {
|
||||||
BitwardenLoginCredentialParameterYAML,
|
BitwardenLoginCredentialParameterYAML,
|
||||||
BlockYAML,
|
BlockYAML,
|
||||||
|
ParameterYAML,
|
||||||
|
WorkflowCreateYAMLRequest,
|
||||||
WorkflowParameterYAML,
|
WorkflowParameterYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
@@ -28,12 +36,32 @@ import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel";
|
|||||||
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel";
|
||||||
import "./reactFlowOverrideStyles.css";
|
import "./reactFlowOverrideStyles.css";
|
||||||
import {
|
import {
|
||||||
|
convertEchoParameters,
|
||||||
createNode,
|
createNode,
|
||||||
generateNodeLabel,
|
generateNodeLabel,
|
||||||
|
getAdditionalParametersForEmailBlock,
|
||||||
getOutputParameterKey,
|
getOutputParameterKey,
|
||||||
getWorkflowBlocks,
|
getWorkflowBlocks,
|
||||||
layout,
|
layout,
|
||||||
} from "./workflowEditorUtils";
|
} from "./workflowEditorUtils";
|
||||||
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
|
import { useBlocker, useParams } from "react-router-dom";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { getClient } from "@/api/AxiosClient";
|
||||||
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import { stringify as convertToYAML } from "yaml";
|
||||||
|
import { toast } from "@/components/ui/use-toast";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
function convertToParametersYAML(
|
function convertToParametersYAML(
|
||||||
parameters: ParametersState,
|
parameters: ParametersState,
|
||||||
@@ -85,13 +113,7 @@ type Props = {
|
|||||||
initialNodes: Array<AppNode>;
|
initialNodes: Array<AppNode>;
|
||||||
initialEdges: Array<Edge>;
|
initialEdges: Array<Edge>;
|
||||||
initialParameters: ParametersState;
|
initialParameters: ParametersState;
|
||||||
handleSave: (
|
workflow: WorkflowApiResponse;
|
||||||
parameters: Array<
|
|
||||||
WorkflowParameterYAML | BitwardenLoginCredentialParameterYAML
|
|
||||||
>,
|
|
||||||
blocks: Array<BlockYAML>,
|
|
||||||
title: string,
|
|
||||||
) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddNodeProps = {
|
export type AddNodeProps = {
|
||||||
@@ -107,8 +129,11 @@ function FlowRenderer({
|
|||||||
initialEdges,
|
initialEdges,
|
||||||
initialNodes,
|
initialNodes,
|
||||||
initialParameters,
|
initialParameters,
|
||||||
handleSave,
|
workflow,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { workflowPermanentId } = useParams();
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
||||||
useWorkflowPanelStore();
|
useWorkflowPanelStore();
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||||
@@ -116,6 +141,66 @@ function FlowRenderer({
|
|||||||
const [parameters, setParameters] = useState(initialParameters);
|
const [parameters, setParameters] = useState(initialParameters);
|
||||||
const [title, setTitle] = useState(initialTitle);
|
const [title, setTitle] = useState(initialTitle);
|
||||||
const nodesInitialized = useNodesInitialized();
|
const nodesInitialized = useNodesInitialized();
|
||||||
|
const { hasChanges, setHasChanges } = useWorkflowHasChangesStore();
|
||||||
|
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||||
|
return hasChanges && nextLocation.pathname !== currentLocation.pathname;
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveWorkflowMutation = useMutation({
|
||||||
|
mutationFn: async (data: {
|
||||||
|
parameters: Array<ParameterYAML>;
|
||||||
|
blocks: Array<BlockYAML>;
|
||||||
|
title: string;
|
||||||
|
}) => {
|
||||||
|
if (!workflowPermanentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
const requestBody: WorkflowCreateYAMLRequest = {
|
||||||
|
title: data.title,
|
||||||
|
description: workflow.description,
|
||||||
|
proxy_location: workflow.proxy_location,
|
||||||
|
webhook_callback_url: workflow.webhook_callback_url,
|
||||||
|
totp_verification_url: workflow.totp_verification_url,
|
||||||
|
workflow_definition: {
|
||||||
|
parameters: data.parameters,
|
||||||
|
blocks: data.blocks,
|
||||||
|
},
|
||||||
|
is_saved_task: workflow.is_saved_task,
|
||||||
|
};
|
||||||
|
const yaml = convertToYAML(requestBody);
|
||||||
|
return client.put<string, WorkflowApiResponse>(
|
||||||
|
`/workflows/${workflowPermanentId}`,
|
||||||
|
yaml,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Changes saved",
|
||||||
|
description: "Your changes have been saved",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflow", workflowPermanentId],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflows"],
|
||||||
|
});
|
||||||
|
setHasChanges(false);
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
||||||
const layoutedElements = layout(nodes, edges);
|
const layoutedElements = layout(nodes, edges);
|
||||||
@@ -130,6 +215,47 @@ function FlowRenderer({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [nodesInitialized]);
|
}, [nodesInitialized]);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const blocks = getWorkflowBlocks(nodes);
|
||||||
|
const parametersInYAMLConvertibleJSON = convertToParametersYAML(parameters);
|
||||||
|
const filteredParameters = workflow.workflow_definition.parameters.filter(
|
||||||
|
(parameter) => {
|
||||||
|
return (
|
||||||
|
parameter.parameter_type === "aws_secret" ||
|
||||||
|
parameter.parameter_type === "bitwarden_sensitive_information" ||
|
||||||
|
parameter.parameter_type === "context"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
) as Array<
|
||||||
|
| AWSSecretParameter
|
||||||
|
| BitwardenSensitiveInformationParameter
|
||||||
|
| ContextParameter
|
||||||
|
>;
|
||||||
|
|
||||||
|
const echoParameters = convertEchoParameters(filteredParameters);
|
||||||
|
|
||||||
|
const overallParameters = [
|
||||||
|
...parameters,
|
||||||
|
...echoParameters,
|
||||||
|
] as Array<ParameterYAML>;
|
||||||
|
|
||||||
|
// if there is an email node, we need to add the email aws secret parameters
|
||||||
|
const emailAwsSecretParameters = getAdditionalParametersForEmailBlock(
|
||||||
|
blocks,
|
||||||
|
overallParameters,
|
||||||
|
);
|
||||||
|
|
||||||
|
return saveWorkflowMutation.mutateAsync({
|
||||||
|
parameters: [
|
||||||
|
...echoParameters,
|
||||||
|
...parametersInYAMLConvertibleJSON,
|
||||||
|
...emailAwsSecretParameters,
|
||||||
|
],
|
||||||
|
blocks,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addNode({
|
function addNode({
|
||||||
nodeType,
|
nodeType,
|
||||||
previous,
|
previous,
|
||||||
@@ -220,7 +346,7 @@ function FlowRenderer({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setHasChanges(true);
|
||||||
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,110 +401,160 @@ function FlowRenderer({
|
|||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
setHasChanges(true);
|
||||||
doLayout(newNodesWithUpdatedParameters, newEdges);
|
doLayout(newNodesWithUpdatedParameters, newEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowParametersStateContext.Provider
|
<>
|
||||||
value={[parameters, setParameters]}
|
<Dialog
|
||||||
>
|
open={blocker.state === "blocked"}
|
||||||
<DeleteNodeCallbackContext.Provider value={deleteNode}>
|
onOpenChange={(open) => {
|
||||||
<ReactFlow
|
if (!open) {
|
||||||
nodes={nodes}
|
blocker.reset?.();
|
||||||
edges={edges}
|
}
|
||||||
onNodesChange={(changes) => {
|
}}
|
||||||
const dimensionChanges = changes.filter(
|
>
|
||||||
(change) => change.type === "dimensions",
|
<DialogContent>
|
||||||
);
|
<DialogHeader>
|
||||||
const tempNodes = [...nodes];
|
<DialogTitle>Unsaved Changes</DialogTitle>
|
||||||
dimensionChanges.forEach((change) => {
|
<DialogDescription>
|
||||||
const node = tempNodes.find((node) => node.id === change.id);
|
Your workflow has unsaved changes. Do you want to save them before
|
||||||
if (node) {
|
leaving?
|
||||||
if (node.measured?.width) {
|
</DialogDescription>
|
||||||
node.measured.width = change.dimensions?.width;
|
</DialogHeader>
|
||||||
}
|
<DialogFooter>
|
||||||
if (node.measured?.height) {
|
<Button
|
||||||
node.measured.height = change.dimensions?.height;
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
blocker.proceed?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Continue without saving
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handleSave().then(() => {
|
||||||
|
blocker.proceed?.();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={saveWorkflowMutation.isPending}
|
||||||
|
>
|
||||||
|
{saveWorkflowMutation.isPending && (
|
||||||
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
)}
|
||||||
|
Save changes
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<WorkflowParametersStateContext.Provider
|
||||||
|
value={[parameters, setParameters]}
|
||||||
|
>
|
||||||
|
<DeleteNodeCallbackContext.Provider value={deleteNode}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={(changes) => {
|
||||||
|
const dimensionChanges = changes.filter(
|
||||||
|
(change) => change.type === "dimensions",
|
||||||
|
);
|
||||||
|
const tempNodes = [...nodes];
|
||||||
|
dimensionChanges.forEach((change) => {
|
||||||
|
const node = tempNodes.find((node) => node.id === change.id);
|
||||||
|
if (node) {
|
||||||
|
if (node.measured?.width) {
|
||||||
|
node.measured.width = change.dimensions?.width;
|
||||||
|
}
|
||||||
|
if (node.measured?.height) {
|
||||||
|
node.measured.height = change.dimensions?.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (dimensionChanges.length > 0) {
|
||||||
|
doLayout(tempNodes, edges);
|
||||||
}
|
}
|
||||||
});
|
if (
|
||||||
if (dimensionChanges.length > 0) {
|
changes.some((change) => {
|
||||||
doLayout(tempNodes, edges);
|
return (
|
||||||
}
|
change.type === "add" ||
|
||||||
onNodesChange(changes);
|
change.type === "remove" ||
|
||||||
}}
|
change.type === "replace"
|
||||||
onEdgesChange={onEdgesChange}
|
);
|
||||||
nodeTypes={nodeTypes}
|
})
|
||||||
edgeTypes={edgeTypes}
|
) {
|
||||||
colorMode="dark"
|
setHasChanges(true);
|
||||||
fitView
|
|
||||||
fitViewOptions={{
|
|
||||||
maxZoom: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
|
||||||
<Controls position="bottom-left" />
|
|
||||||
<Panel position="top-center" className="h-20">
|
|
||||||
<WorkflowHeader
|
|
||||||
title={title}
|
|
||||||
onTitleChange={setTitle}
|
|
||||||
parametersPanelOpen={
|
|
||||||
workflowPanelState.active &&
|
|
||||||
workflowPanelState.content === "parameters"
|
|
||||||
}
|
}
|
||||||
onParametersClick={() => {
|
onNodesChange(changes);
|
||||||
if (
|
}}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={edgeTypes}
|
||||||
|
colorMode="dark"
|
||||||
|
fitView
|
||||||
|
fitViewOptions={{
|
||||||
|
maxZoom: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Background variant={BackgroundVariant.Dots} bgColor="#020617" />
|
||||||
|
<Controls position="bottom-left" />
|
||||||
|
<Panel position="top-center" className="h-20">
|
||||||
|
<WorkflowHeader
|
||||||
|
title={title}
|
||||||
|
onTitleChange={(newTitle) => {
|
||||||
|
setTitle(newTitle);
|
||||||
|
setHasChanges(true);
|
||||||
|
}}
|
||||||
|
parametersPanelOpen={
|
||||||
workflowPanelState.active &&
|
workflowPanelState.active &&
|
||||||
workflowPanelState.content === "parameters"
|
workflowPanelState.content === "parameters"
|
||||||
) {
|
|
||||||
closeWorkflowPanel();
|
|
||||||
} else {
|
|
||||||
setWorkflowPanelState({
|
|
||||||
active: true,
|
|
||||||
content: "parameters",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
onParametersClick={() => {
|
||||||
onSave={() => {
|
if (
|
||||||
const blocksInYAMLConvertibleJSON = getWorkflowBlocks(nodes);
|
workflowPanelState.active &&
|
||||||
const parametersInYAMLConvertibleJSON =
|
workflowPanelState.content === "parameters"
|
||||||
convertToParametersYAML(parameters);
|
) {
|
||||||
handleSave(
|
closeWorkflowPanel();
|
||||||
parametersInYAMLConvertibleJSON,
|
} else {
|
||||||
blocksInYAMLConvertibleJSON,
|
setWorkflowPanelState({
|
||||||
title,
|
active: true,
|
||||||
);
|
content: "parameters",
|
||||||
}}
|
});
|
||||||
/>
|
}
|
||||||
</Panel>
|
}}
|
||||||
{workflowPanelState.active && (
|
onSave={async () => {
|
||||||
<Panel position="top-right">
|
await handleSave();
|
||||||
{workflowPanelState.content === "parameters" && (
|
}}
|
||||||
<WorkflowParametersPanel />
|
/>
|
||||||
)}
|
</Panel>
|
||||||
{workflowPanelState.content === "nodeLibrary" && (
|
{workflowPanelState.active && (
|
||||||
|
<Panel position="top-right">
|
||||||
|
{workflowPanelState.content === "parameters" && (
|
||||||
|
<WorkflowParametersPanel />
|
||||||
|
)}
|
||||||
|
{workflowPanelState.content === "nodeLibrary" && (
|
||||||
|
<WorkflowNodeLibraryPanel
|
||||||
|
onNodeClick={(props) => {
|
||||||
|
addNode(props);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
)}
|
||||||
|
{nodes.length === 0 && (
|
||||||
|
<Panel position="top-right">
|
||||||
<WorkflowNodeLibraryPanel
|
<WorkflowNodeLibraryPanel
|
||||||
onNodeClick={(props) => {
|
onNodeClick={(props) => {
|
||||||
addNode(props);
|
addNode(props);
|
||||||
}}
|
}}
|
||||||
|
first
|
||||||
/>
|
/>
|
||||||
)}
|
</Panel>
|
||||||
</Panel>
|
)}
|
||||||
)}
|
</ReactFlow>
|
||||||
{nodes.length === 0 && (
|
</DeleteNodeCallbackContext.Provider>
|
||||||
<Panel position="top-right">
|
</WorkflowParametersStateContext.Provider>
|
||||||
<WorkflowNodeLibraryPanel
|
</>
|
||||||
onNodeClick={(props) => {
|
|
||||||
addNode(props);
|
|
||||||
}}
|
|
||||||
first
|
|
||||||
/>
|
|
||||||
</Panel>
|
|
||||||
)}
|
|
||||||
</ReactFlow>
|
|
||||||
</DeleteNodeCallbackContext.Provider>
|
|
||||||
</WorkflowParametersStateContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,20 @@
|
|||||||
|
import { useMountEffect } from "@/hooks/useMountEffect";
|
||||||
|
import { useSidebarStore } from "@/store/SidebarStore";
|
||||||
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||||
import {
|
|
||||||
convertEchoParameters,
|
|
||||||
getAdditionalParametersForEmailBlock,
|
|
||||||
getElements,
|
|
||||||
} from "./workflowEditorUtils";
|
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
BlockYAML,
|
|
||||||
ParameterYAML,
|
|
||||||
WorkflowCreateYAMLRequest,
|
|
||||||
} from "../types/workflowYamlTypes";
|
|
||||||
import { getClient } from "@/api/AxiosClient";
|
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|
||||||
import { stringify as convertToYAML } from "yaml";
|
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
|
||||||
import { FlowRenderer } from "./FlowRenderer";
|
import { FlowRenderer } from "./FlowRenderer";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { getElements } from "./workflowEditorUtils";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import {
|
|
||||||
AWSSecretParameter,
|
|
||||||
BitwardenSensitiveInformationParameter,
|
|
||||||
ContextParameter,
|
|
||||||
} from "../types/workflowTypes";
|
|
||||||
import { useSidebarStore } from "@/store/SidebarStore";
|
|
||||||
import { useMountEffect } from "@/hooks/useMountEffect";
|
|
||||||
|
|
||||||
function WorkflowEditor() {
|
function WorkflowEditor() {
|
||||||
const { workflowPermanentId } = useParams();
|
const { workflowPermanentId } = useParams();
|
||||||
const credentialGetter = useCredentialGetter();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const setCollapsed = useSidebarStore((state) => {
|
const setCollapsed = useSidebarStore((state) => {
|
||||||
return state.setCollapsed;
|
return state.setCollapsed;
|
||||||
});
|
});
|
||||||
|
const setHasChanges = useWorkflowHasChangesStore(
|
||||||
|
(state) => state.setHasChanges,
|
||||||
|
);
|
||||||
|
|
||||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
@@ -40,59 +22,7 @@ function WorkflowEditor() {
|
|||||||
|
|
||||||
useMountEffect(() => {
|
useMountEffect(() => {
|
||||||
setCollapsed(true);
|
setCollapsed(true);
|
||||||
});
|
setHasChanges(false);
|
||||||
|
|
||||||
const saveWorkflowMutation = useMutation({
|
|
||||||
mutationFn: async (data: {
|
|
||||||
parameters: Array<ParameterYAML>;
|
|
||||||
blocks: Array<BlockYAML>;
|
|
||||||
title: string;
|
|
||||||
}) => {
|
|
||||||
if (!workflow || !workflowPermanentId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const client = await getClient(credentialGetter);
|
|
||||||
const requestBody: WorkflowCreateYAMLRequest = {
|
|
||||||
title: data.title,
|
|
||||||
description: workflow.description,
|
|
||||||
proxy_location: workflow.proxy_location,
|
|
||||||
webhook_callback_url: workflow.webhook_callback_url,
|
|
||||||
totp_verification_url: workflow.totp_verification_url,
|
|
||||||
workflow_definition: {
|
|
||||||
parameters: data.parameters,
|
|
||||||
blocks: data.blocks,
|
|
||||||
},
|
|
||||||
is_saved_task: workflow.is_saved_task,
|
|
||||||
};
|
|
||||||
const yaml = convertToYAML(requestBody);
|
|
||||||
return client
|
|
||||||
.put(`/workflows/${workflowPermanentId}`, yaml, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "text/plain",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.data);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast({
|
|
||||||
title: "Changes saved",
|
|
||||||
description: "Your changes have been saved",
|
|
||||||
variant: "success",
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["workflow", workflowPermanentId],
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["workflows"],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error: AxiosError) => {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@@ -139,42 +69,7 @@ function WorkflowEditor() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
handleSave={(parameters, blocks, title) => {
|
workflow={workflow}
|
||||||
const filteredParameters =
|
|
||||||
workflow.workflow_definition.parameters.filter((parameter) => {
|
|
||||||
return (
|
|
||||||
parameter.parameter_type === "aws_secret" ||
|
|
||||||
parameter.parameter_type ===
|
|
||||||
"bitwarden_sensitive_information" ||
|
|
||||||
parameter.parameter_type === "context"
|
|
||||||
);
|
|
||||||
}) as Array<
|
|
||||||
| AWSSecretParameter
|
|
||||||
| BitwardenSensitiveInformationParameter
|
|
||||||
| ContextParameter
|
|
||||||
>;
|
|
||||||
|
|
||||||
const echoParameters = convertEchoParameters(filteredParameters);
|
|
||||||
|
|
||||||
const overallParameters = [
|
|
||||||
...parameters,
|
|
||||||
...echoParameters,
|
|
||||||
] as Array<ParameterYAML>;
|
|
||||||
|
|
||||||
// if there is an email node, we need to add the email aws secret parameters
|
|
||||||
const emailAwsSecretParameters =
|
|
||||||
getAdditionalParametersForEmailBlock(blocks, overallParameters);
|
|
||||||
|
|
||||||
saveWorkflowMutation.mutate({
|
|
||||||
parameters: [
|
|
||||||
...echoParameters,
|
|
||||||
...parameters,
|
|
||||||
...emailAwsSecretParameters,
|
|
||||||
],
|
|
||||||
blocks,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,11 +25,15 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useReactFlow } from "@xyflow/react";
|
import { useReactFlow } from "@xyflow/react";
|
||||||
|
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
||||||
|
|
||||||
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16;
|
||||||
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
const WORKFLOW_EDIT_PANEL_GAP = 1 * 16;
|
||||||
|
|
||||||
function WorkflowParametersPanel() {
|
function WorkflowParametersPanel() {
|
||||||
|
const setHasChanges = useWorkflowHasChangesStore(
|
||||||
|
(state) => state.setHasChanges,
|
||||||
|
);
|
||||||
const [workflowParameters, setWorkflowParameters] =
|
const [workflowParameters, setWorkflowParameters] =
|
||||||
useWorkflowParametersState();
|
useWorkflowParametersState();
|
||||||
const [operationPanelState, setOperationPanelState] = useState<{
|
const [operationPanelState, setOperationPanelState] = useState<{
|
||||||
@@ -144,6 +148,7 @@ function WorkflowParametersPanel() {
|
|||||||
(p) => p.key !== parameter.key,
|
(p) => p.key !== parameter.key,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
setHasChanges(true);
|
||||||
setNodes((nodes) => {
|
setNodes((nodes) => {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
if (node.type === "task") {
|
if (node.type === "task") {
|
||||||
@@ -187,6 +192,7 @@ function WorkflowParametersPanel() {
|
|||||||
type={operationPanelState.type}
|
type={operationPanelState.type}
|
||||||
onSave={(parameter) => {
|
onSave={(parameter) => {
|
||||||
setWorkflowParameters([...workflowParameters, parameter]);
|
setWorkflowParameters([...workflowParameters, parameter]);
|
||||||
|
setHasChanges(true);
|
||||||
setOperationPanelState({
|
setOperationPanelState({
|
||||||
active: false,
|
active: false,
|
||||||
operation: "add",
|
operation: "add",
|
||||||
@@ -210,6 +216,7 @@ function WorkflowParametersPanel() {
|
|||||||
type={operationPanelState.type}
|
type={operationPanelState.type}
|
||||||
initialValues={operationPanelState.parameter}
|
initialValues={operationPanelState.parameter}
|
||||||
onSave={(editedParameter) => {
|
onSave={(editedParameter) => {
|
||||||
|
setHasChanges(true);
|
||||||
setWorkflowParameters(
|
setWorkflowParameters(
|
||||||
workflowParameters.map((parameter) => {
|
workflowParameters.map((parameter) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
13
skyvern-frontend/src/store/WorkflowHasChangesStore.ts
Normal file
13
skyvern-frontend/src/store/WorkflowHasChangesStore.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type WorkflowHasChangesStore = {
|
||||||
|
hasChanges: boolean;
|
||||||
|
setHasChanges: (hasChanges: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useWorkflowHasChangesStore = create<WorkflowHasChangesStore>((set) => ({
|
||||||
|
hasChanges: false,
|
||||||
|
setHasChanges: (hasChanges) => set({ hasChanges }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export { useWorkflowHasChangesStore };
|
||||||
Reference in New Issue
Block a user