Rework Workflow Save (#3145)
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import { getClient } from "@/api/AxiosClient";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -9,16 +8,18 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { toast } from "@/components/ui/use-toast";
|
import { toast } from "@/components/ui/use-toast";
|
||||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|
||||||
import { useOnChange } from "@/hooks/useOnChange";
|
import { useOnChange } from "@/hooks/useOnChange";
|
||||||
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab";
|
||||||
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
import { DeleteNodeCallbackContext } from "@/store/DeleteNodeCallbackContext";
|
||||||
import { useDebugStore } from "@/store/useDebugStore";
|
import { useDebugStore } from "@/store/useDebugStore";
|
||||||
import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore";
|
import {
|
||||||
|
useWorkflowHasChangesStore,
|
||||||
|
useWorkflowSave,
|
||||||
|
type WorkflowSaveData,
|
||||||
|
} from "@/store/WorkflowHasChangesStore";
|
||||||
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore";
|
||||||
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore";
|
||||||
import { ReloadIcon } from "@radix-ui/react-icons";
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
BackgroundVariant,
|
BackgroundVariant,
|
||||||
@@ -34,11 +35,9 @@ import {
|
|||||||
useReactFlow,
|
useReactFlow,
|
||||||
} from "@xyflow/react";
|
} from "@xyflow/react";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useBlocker, useParams } from "react-router-dom";
|
import { useBlocker } from "react-router-dom";
|
||||||
import { stringify as convertToYAML } from "yaml";
|
|
||||||
import {
|
import {
|
||||||
AWSSecretParameter,
|
AWSSecretParameter,
|
||||||
debuggableWorkflowBlockTypes,
|
debuggableWorkflowBlockTypes,
|
||||||
@@ -46,18 +45,15 @@ import {
|
|||||||
WorkflowEditorParameterTypes,
|
WorkflowEditorParameterTypes,
|
||||||
WorkflowParameterTypes,
|
WorkflowParameterTypes,
|
||||||
WorkflowParameterValueType,
|
WorkflowParameterValueType,
|
||||||
WorkflowSettings,
|
|
||||||
} from "../types/workflowTypes";
|
} from "../types/workflowTypes";
|
||||||
import {
|
import {
|
||||||
BitwardenCreditCardDataParameterYAML,
|
BitwardenCreditCardDataParameterYAML,
|
||||||
BitwardenLoginCredentialParameterYAML,
|
BitwardenLoginCredentialParameterYAML,
|
||||||
BitwardenSensitiveInformationParameterYAML,
|
BitwardenSensitiveInformationParameterYAML,
|
||||||
BlockYAML,
|
|
||||||
ContextParameterYAML,
|
ContextParameterYAML,
|
||||||
CredentialParameterYAML,
|
CredentialParameterYAML,
|
||||||
OnePasswordCredentialParameterYAML,
|
OnePasswordCredentialParameterYAML,
|
||||||
ParameterYAML,
|
ParameterYAML,
|
||||||
WorkflowCreateYAMLRequest,
|
|
||||||
WorkflowParameterYAML,
|
WorkflowParameterYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import { WorkflowHeader } from "./WorkflowHeader";
|
import { WorkflowHeader } from "./WorkflowHeader";
|
||||||
@@ -265,9 +261,6 @@ function FlowRenderer({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const reactFlowInstance = useReactFlow();
|
const reactFlowInstance = useReactFlow();
|
||||||
const debugStore = useDebugStore();
|
const debugStore = useDebugStore();
|
||||||
const { workflowPermanentId } = useParams();
|
|
||||||
const credentialGetter = useCredentialGetter();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } =
|
||||||
useWorkflowPanelStore();
|
useWorkflowPanelStore();
|
||||||
const { title, initializeTitle } = useWorkflowTitleStore();
|
const { title, initializeTitle } = useWorkflowTitleStore();
|
||||||
@@ -289,107 +282,16 @@ function FlowRenderer({
|
|||||||
initializeTitle(initialTitle);
|
initializeTitle(initialTitle);
|
||||||
}, [initialTitle, initializeTitle]);
|
}, [initialTitle, initializeTitle]);
|
||||||
|
|
||||||
const { hasChanges, setHasChanges } = useWorkflowHasChangesStore();
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
useShouldNotifyWhenClosingTab(hasChanges);
|
const setGetSaveDataRef = useRef(workflowChangesStore.setGetSaveData);
|
||||||
|
setGetSaveDataRef.current = workflowChangesStore.setGetSaveData;
|
||||||
|
const saveWorkflow = useWorkflowSave();
|
||||||
|
useShouldNotifyWhenClosingTab(workflowChangesStore.hasChanges);
|
||||||
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
|
||||||
return hasChanges && nextLocation.pathname !== currentLocation.pathname;
|
return (
|
||||||
});
|
workflowChangesStore.hasChanges &&
|
||||||
|
nextLocation.pathname !== currentLocation.pathname
|
||||||
const saveWorkflowMutation = useMutation({
|
);
|
||||||
mutationFn: async (data: {
|
|
||||||
parameters: Array<ParameterYAML>;
|
|
||||||
blocks: Array<BlockYAML>;
|
|
||||||
title: string;
|
|
||||||
settings: WorkflowSettings;
|
|
||||||
}) => {
|
|
||||||
if (!workflowPermanentId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const client = await getClient(credentialGetter);
|
|
||||||
const extraHttpHeaders: Record<string, string> = {};
|
|
||||||
if (data.settings.extraHttpHeaders) {
|
|
||||||
try {
|
|
||||||
const parsedHeaders = JSON.parse(data.settings.extraHttpHeaders);
|
|
||||||
if (
|
|
||||||
parsedHeaders &&
|
|
||||||
typeof parsedHeaders === "object" &&
|
|
||||||
!Array.isArray(parsedHeaders)
|
|
||||||
) {
|
|
||||||
for (const [key, value] of Object.entries(parsedHeaders)) {
|
|
||||||
if (key && typeof key === "string") {
|
|
||||||
if (key in extraHttpHeaders) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: `Duplicate key '${key}' in extra http headers`,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
extraHttpHeaders[key] = String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Invalid JSON format in extra http headers",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestBody: WorkflowCreateYAMLRequest = {
|
|
||||||
title: data.title,
|
|
||||||
description: workflow.description,
|
|
||||||
proxy_location: data.settings.proxyLocation,
|
|
||||||
webhook_callback_url: data.settings.webhookCallbackUrl,
|
|
||||||
persist_browser_session: data.settings.persistBrowserSession,
|
|
||||||
model: data.settings.model,
|
|
||||||
max_screenshot_scrolls: data.settings.maxScreenshotScrolls,
|
|
||||||
totp_verification_url: workflow.totp_verification_url,
|
|
||||||
extra_http_headers: extraHttpHeaders,
|
|
||||||
use_cache: data.settings.useScriptCache,
|
|
||||||
cache_key: data.settings.scriptCacheKey,
|
|
||||||
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) => {
|
|
||||||
const detail = (error.response?.data as { detail?: string })?.detail;
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: detail ? detail : error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
function doLayout(nodes: Array<AppNode>, edges: Array<Edge>) {
|
||||||
@@ -424,7 +326,7 @@ function FlowRenderer({
|
|||||||
setDebuggableBlockCount(debuggable.length);
|
setDebuggableBlockCount(debuggable.length);
|
||||||
}, [nodes, edges]);
|
}, [nodes, edges]);
|
||||||
|
|
||||||
async function handleSave() {
|
const constructSaveData = useCallback((): WorkflowSaveData => {
|
||||||
const blocks = getWorkflowBlocks(nodes, edges);
|
const blocks = getWorkflowBlocks(nodes, edges);
|
||||||
const settings = getWorkflowSettings(nodes);
|
const settings = getWorkflowSettings(nodes);
|
||||||
const parametersInYAMLConvertibleJSON = convertToParametersYAML(parameters);
|
const parametersInYAMLConvertibleJSON = convertToParametersYAML(parameters);
|
||||||
@@ -447,7 +349,7 @@ function FlowRenderer({
|
|||||||
overallParameters,
|
overallParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
return saveWorkflowMutation.mutateAsync({
|
return {
|
||||||
parameters: [
|
parameters: [
|
||||||
...echoParameters,
|
...echoParameters,
|
||||||
...parametersInYAMLConvertibleJSON,
|
...parametersInYAMLConvertibleJSON,
|
||||||
@@ -456,7 +358,16 @@ function FlowRenderer({
|
|||||||
blocks,
|
blocks,
|
||||||
title,
|
title,
|
||||||
settings,
|
settings,
|
||||||
});
|
workflow,
|
||||||
|
};
|
||||||
|
}, [nodes, edges, parameters, title, workflow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setGetSaveDataRef.current(constructSaveData);
|
||||||
|
}, [constructSaveData]);
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
return await saveWorkflow.mutateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNode({
|
function addNode({
|
||||||
@@ -537,7 +448,7 @@ function FlowRenderer({
|
|||||||
...nodes.slice(previousNodeIndex + 1),
|
...nodes.slice(previousNodeIndex + 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
setHasChanges(true);
|
workflowChangesStore.setHasChanges(true);
|
||||||
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
doLayout(newNodesAfter, [...editedEdges, ...newEdges]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,7 +538,8 @@ function FlowRenderer({
|
|||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
setHasChanges(true);
|
workflowChangesStore.setHasChanges(true);
|
||||||
|
|
||||||
doLayout(newNodesWithUpdatedParameters, newEdges);
|
doLayout(newNodesWithUpdatedParameters, newEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,9 +629,9 @@ function FlowRenderer({
|
|||||||
blocker.proceed?.();
|
blocker.proceed?.();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={saveWorkflowMutation.isPending}
|
disabled={workflowChangesStore.saveIsPending}
|
||||||
>
|
>
|
||||||
{saveWorkflowMutation.isPending && (
|
{workflowChangesStore.saveIsPending && (
|
||||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||||
)}
|
)}
|
||||||
Save changes
|
Save changes
|
||||||
@@ -763,7 +675,7 @@ function FlowRenderer({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
setHasChanges(true);
|
workflowChangesStore.setHasChanges(true);
|
||||||
}
|
}
|
||||||
onNodesChange(changes);
|
onNodesChange(changes);
|
||||||
}}
|
}}
|
||||||
@@ -807,7 +719,7 @@ function FlowRenderer({
|
|||||||
<Panel position="top-center" className={cn("h-20")}>
|
<Panel position="top-center" className={cn("h-20")}>
|
||||||
<WorkflowHeader
|
<WorkflowHeader
|
||||||
debuggableBlockCount={debuggableBlockCount}
|
debuggableBlockCount={debuggableBlockCount}
|
||||||
saving={saveWorkflowMutation.isPending}
|
saving={workflowChangesStore.saveIsPending}
|
||||||
parametersPanelOpen={
|
parametersPanelOpen={
|
||||||
workflowPanelState.active &&
|
workflowPanelState.active &&
|
||||||
workflowPanelState.content === "parameters"
|
workflowPanelState.content === "parameters"
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ function WorkflowDebugger() {
|
|||||||
return state.setCollapsed;
|
return state.setCollapsed;
|
||||||
});
|
});
|
||||||
|
|
||||||
const setHasChanges = useWorkflowHasChangesStore(
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
(state) => state.setHasChanges,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOnCycle = () => {
|
const handleOnCycle = () => {
|
||||||
setOpenDialogue(true);
|
setOpenDialogue(true);
|
||||||
@@ -72,7 +70,7 @@ function WorkflowDebugger() {
|
|||||||
|
|
||||||
useMountEffect(() => {
|
useMountEffect(() => {
|
||||||
setCollapsed(true);
|
setCollapsed(true);
|
||||||
setHasChanges(false);
|
workflowChangesStore.setHasChanges(false);
|
||||||
|
|
||||||
if (workflowPermanentId) {
|
if (workflowPermanentId) {
|
||||||
queryClient.removeQueries({
|
queryClient.removeQueries({
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ function WorkflowEditor() {
|
|||||||
const setCollapsed = useSidebarStore((state) => {
|
const setCollapsed = useSidebarStore((state) => {
|
||||||
return state.setCollapsed;
|
return state.setCollapsed;
|
||||||
});
|
});
|
||||||
const setHasChanges = useWorkflowHasChangesStore(
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
(state) => state.setHasChanges,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: workflow, isLoading } = useWorkflowQuery({
|
const { data: workflow, isLoading } = useWorkflowQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
@@ -29,7 +27,7 @@ function WorkflowEditor() {
|
|||||||
|
|
||||||
useMountEffect(() => {
|
useMountEffect(() => {
|
||||||
setCollapsed(true);
|
setCollapsed(true);
|
||||||
setHasChanges(false);
|
workflowChangesStore.setHasChanges(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isLoading || isGlobalWorkflowsLoading) {
|
if (isLoading || isGlobalWorkflowsLoading) {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function WorkflowHeader({
|
|||||||
saving,
|
saving,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { title, setTitle } = useWorkflowTitleStore();
|
const { title, setTitle } = useWorkflowTitleStore();
|
||||||
const { setHasChanges } = useWorkflowHasChangesStore();
|
const workflowChangesStore = useWorkflowHasChangesStore();
|
||||||
const { blockLabel: urlBlockLabel, workflowPermanentId } = useParams();
|
const { blockLabel: urlBlockLabel, workflowPermanentId } = useParams();
|
||||||
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
const { data: globalWorkflows } = useGlobalWorkflowsQuery();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -68,7 +68,7 @@ function WorkflowHeader({
|
|||||||
editable={true}
|
editable={true}
|
||||||
onChange={(newTitle) => {
|
onChange={(newTitle) => {
|
||||||
setTitle(newTitle);
|
setTitle(newTitle);
|
||||||
setHasChanges(true);
|
workflowChangesStore.setHasChanges(true);
|
||||||
}}
|
}}
|
||||||
value={title}
|
value={title}
|
||||||
titleClassName="text-3xl"
|
titleClassName="text-3xl"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@/routes/workflows/types/workflowTypes";
|
} from "@/routes/workflows/types/workflowTypes";
|
||||||
import { getInitialValues } from "@/routes/workflows/utils";
|
import { getInitialValues } from "@/routes/workflows/utils";
|
||||||
import { useDebugStore } from "@/store/useDebugStore";
|
import { useDebugStore } from "@/store/useDebugStore";
|
||||||
|
import { useWorkflowSave } from "@/store/WorkflowHasChangesStore";
|
||||||
import {
|
import {
|
||||||
useWorkflowSettingsStore,
|
useWorkflowSettingsStore,
|
||||||
type WorkflowSettingsState,
|
type WorkflowSettingsState,
|
||||||
@@ -155,6 +156,7 @@ function NodeHeader({
|
|||||||
const { data: debugSession } = useDebugSessionQuery({
|
const { data: debugSession } = useDebugSessionQuery({
|
||||||
workflowPermanentId,
|
workflowPermanentId,
|
||||||
});
|
});
|
||||||
|
const saveWorkflow = useWorkflowSave();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
|
if (!workflowRun || !workflowPermanentId || !workflowRunId) {
|
||||||
@@ -190,6 +192,8 @@ function NodeHeader({
|
|||||||
|
|
||||||
const runBlock = useMutation({
|
const runBlock = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
|
await saveWorkflow.mutateAsync();
|
||||||
|
|
||||||
if (!workflowPermanentId) {
|
if (!workflowPermanentId) {
|
||||||
console.error("There is no workflowPermanentId");
|
console.error("There is no workflowPermanentId");
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,13 +1,179 @@
|
|||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { stringify as convertToYAML } from "yaml";
|
||||||
|
|
||||||
type WorkflowHasChangesStore = {
|
import { getClient } from "@/api/AxiosClient";
|
||||||
hasChanges: boolean;
|
import { toast } from "@/components/ui/use-toast";
|
||||||
setHasChanges: (hasChanges: boolean) => void;
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||||
|
import type {
|
||||||
|
BlockYAML,
|
||||||
|
ParameterYAML,
|
||||||
|
} from "@/routes/workflows/types/workflowYamlTypes";
|
||||||
|
import type {
|
||||||
|
WorkflowApiResponse,
|
||||||
|
WorkflowSettings,
|
||||||
|
} from "@/routes/workflows/types/workflowTypes";
|
||||||
|
import { WorkflowCreateYAMLRequest } from "@/routes/workflows/types/workflowYamlTypes";
|
||||||
|
type SaveData = {
|
||||||
|
parameters: Array<ParameterYAML>;
|
||||||
|
blocks: Array<BlockYAML>;
|
||||||
|
title: string;
|
||||||
|
settings: WorkflowSettings;
|
||||||
|
workflow: WorkflowApiResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useWorkflowHasChangesStore = create<WorkflowHasChangesStore>((set) => ({
|
type WorkflowHasChangesStore = {
|
||||||
hasChanges: false,
|
getSaveData: () => SaveData | null;
|
||||||
setHasChanges: (hasChanges) => set({ hasChanges }),
|
hasChanges: boolean;
|
||||||
}));
|
saveIsPending: boolean;
|
||||||
|
setGetSaveData: (getSaveData: () => SaveData) => void;
|
||||||
|
setHasChanges: (hasChanges: boolean) => void;
|
||||||
|
setSaveIsPending: (isPending: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export { useWorkflowHasChangesStore };
|
const useWorkflowHasChangesStore = create<WorkflowHasChangesStore>((set) => {
|
||||||
|
return {
|
||||||
|
hasChanges: false,
|
||||||
|
saveIsPending: false,
|
||||||
|
getSaveData: () => null,
|
||||||
|
setGetSaveData: (getSaveData: () => SaveData) => {
|
||||||
|
set({ getSaveData });
|
||||||
|
},
|
||||||
|
setHasChanges: (hasChanges: boolean) => {
|
||||||
|
set({ hasChanges });
|
||||||
|
},
|
||||||
|
setSaveIsPending: (isPending: boolean) => {
|
||||||
|
set({ saveIsPending: isPending });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const useWorkflowSave = () => {
|
||||||
|
const credentialGetter = useCredentialGetter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { getSaveData, setHasChanges, setSaveIsPending } =
|
||||||
|
useWorkflowHasChangesStore();
|
||||||
|
|
||||||
|
const saveWorkflowMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const saveData = getSaveData();
|
||||||
|
|
||||||
|
if (!saveData) {
|
||||||
|
setHasChanges(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await getClient(credentialGetter);
|
||||||
|
const extraHttpHeaders: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (saveData.settings.extraHttpHeaders) {
|
||||||
|
try {
|
||||||
|
const parsedHeaders = JSON.parse(saveData.settings.extraHttpHeaders);
|
||||||
|
if (
|
||||||
|
parsedHeaders &&
|
||||||
|
typeof parsedHeaders === "object" &&
|
||||||
|
!Array.isArray(parsedHeaders)
|
||||||
|
) {
|
||||||
|
for (const [key, value] of Object.entries(parsedHeaders)) {
|
||||||
|
if (key && typeof key === "string") {
|
||||||
|
if (key in extraHttpHeaders) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: `Duplicate key '${key}' in extra http headers`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
extraHttpHeaders[key] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Invalid JSON format in extra http headers",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestBody: WorkflowCreateYAMLRequest = {
|
||||||
|
title: saveData.title,
|
||||||
|
description: saveData.workflow.description,
|
||||||
|
proxy_location: saveData.settings.proxyLocation,
|
||||||
|
webhook_callback_url: saveData.settings.webhookCallbackUrl,
|
||||||
|
persist_browser_session: saveData.settings.persistBrowserSession,
|
||||||
|
model: saveData.settings.model,
|
||||||
|
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
|
||||||
|
totp_verification_url: saveData.workflow.totp_verification_url,
|
||||||
|
extra_http_headers: extraHttpHeaders,
|
||||||
|
use_cache: saveData.settings.useScriptCache,
|
||||||
|
cache_key: saveData.settings.scriptCacheKey,
|
||||||
|
workflow_definition: {
|
||||||
|
parameters: saveData.parameters,
|
||||||
|
blocks: saveData.blocks,
|
||||||
|
},
|
||||||
|
is_saved_task: saveData.workflow.is_saved_task,
|
||||||
|
};
|
||||||
|
|
||||||
|
const yaml = convertToYAML(requestBody);
|
||||||
|
|
||||||
|
return client.put<string, WorkflowApiResponse>(
|
||||||
|
`/workflows/${saveData.workflow.workflow_permanent_id}`,
|
||||||
|
yaml,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
const saveData = getSaveData();
|
||||||
|
|
||||||
|
if (!saveData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Changes saved",
|
||||||
|
description: "Your changes have been saved",
|
||||||
|
variant: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflow", saveData.workflow.workflow_permanent_id],
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["workflows"],
|
||||||
|
});
|
||||||
|
|
||||||
|
setHasChanges(false);
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
const detail = (error.response?.data as { detail?: string })?.detail;
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: detail ? detail : error.message,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSaveIsPending(saveWorkflowMutation.isPending);
|
||||||
|
}, [saveWorkflowMutation.isPending, setSaveIsPending]);
|
||||||
|
|
||||||
|
return saveWorkflowMutation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
useWorkflowSave,
|
||||||
|
useWorkflowHasChangesStore,
|
||||||
|
type SaveData as WorkflowSaveData,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user