fix: update saving behavior when exiting flow (#3673)
* Updated save flow to only execute when the flow has unsaved changes * Made flowPage call saveFlow immediatly when trying to exit page * Changed save changes modal to automatically exit when saved on autoSave * Changed flowPage to save and exit when autoSave * Remove confirmation button if it does not exist and remove footer if confirmation and cancel does not exist * Changed saveChangesModal to not show buttons when autoSave is on and to set loading as true when autoSave is off and the user saves * Add timer to close modal, so, if saving takes less than 1 second, it waits * Changed type of confirmationModal * Added handling for when it's building and there is unsaved changes * Added success toast when flow finishes saving * refactored confirmationModal * check autoLogin !== undefined before calling things * Fix use save flow to compare the passed flow instead of the current one * Changed style of save changes modal * deleted build in progress modal * Removed build in progress modal, just stopping the build * Fix condition on api * Changed condition to be autoLogin !== undefined
This commit is contained in:
parent
c31005a84f
commit
3bc9cc9686
8 changed files with 176 additions and 148 deletions
|
|
@ -24,13 +24,13 @@ export const ProtectedRoute = ({ children }) => {
|
|||
mutateRefresh();
|
||||
};
|
||||
|
||||
if (!autoLogin && isAuthenticated) {
|
||||
if (autoLogin !== undefined && !autoLogin && isAuthenticated) {
|
||||
const intervalId = setInterval(intervalFunction, accessTokenTimer * 1000);
|
||||
intervalFunction();
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
if (!isAuthenticated && !autoLogin) {
|
||||
if (!isAuthenticated && autoLogin !== undefined && !autoLogin) {
|
||||
return <CustomNavigate to="/login" replace />;
|
||||
} else {
|
||||
return children;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { LANGFLOW_ACCESS_TOKEN } from "@/constants/constants";
|
|||
import { useCustomApiHeaders } from "@/customization/hooks/use-custom-api-headers";
|
||||
import useAuthStore from "@/stores/authStore";
|
||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||
import pako from "pako";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Cookies } from "react-cookie";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
|
|
@ -35,7 +34,7 @@ function ApiInterceptor() {
|
|||
error?.response?.status === 403 || error?.response?.status === 401;
|
||||
|
||||
if (isAuthenticationError) {
|
||||
if (!autoLogin) {
|
||||
if (autoLogin !== undefined && !autoLogin) {
|
||||
if (error?.config?.url?.includes("github")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
|
@ -129,7 +128,7 @@ function ApiInterceptor() {
|
|||
api.interceptors.response.eject(interceptor);
|
||||
api.interceptors.request.eject(requestInterceptor);
|
||||
};
|
||||
}, [accessToken, setErrorData, customHeaders]);
|
||||
}, [accessToken, setErrorData, customHeaders, autoLogin]);
|
||||
|
||||
function checkErrorCount() {
|
||||
if (isLoginPage) return;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import useAlertStore from "@/stores/alertStore";
|
|||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { customStringify } from "@/utils/reactflowUtils";
|
||||
|
||||
const useSaveFlow = () => {
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
|
|
@ -21,73 +22,78 @@ const useSaveFlow = () => {
|
|||
const { mutate } = usePatchUpdateFlow();
|
||||
|
||||
const saveFlow = async (flow?: FlowType): Promise<void> => {
|
||||
setSaveLoading(true);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (currentFlow) {
|
||||
flow = flow || {
|
||||
...currentFlow,
|
||||
data: {
|
||||
...flowData,
|
||||
nodes,
|
||||
edges,
|
||||
viewport: reactFlowInstance?.getViewport() ?? {
|
||||
zoom: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
if (
|
||||
customStringify(flow || currentFlow) !== customStringify(currentSavedFlow)
|
||||
) {
|
||||
setSaveLoading(true);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (currentFlow) {
|
||||
flow = flow || {
|
||||
...currentFlow,
|
||||
data: {
|
||||
...flowData,
|
||||
nodes,
|
||||
edges,
|
||||
viewport: reactFlowInstance?.getViewport() ?? {
|
||||
zoom: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (flow && flow.data) {
|
||||
const { id, name, data, description, folder_id, endpoint_name } = flow;
|
||||
if (!currentSavedFlow?.data?.nodes.length || data.nodes.length > 0) {
|
||||
mutate(
|
||||
{ id, name, data, description, folder_id, endpoint_name },
|
||||
{
|
||||
onSuccess: (updatedFlow) => {
|
||||
setSaveLoading(false);
|
||||
if (flows) {
|
||||
// updates flow in state
|
||||
setFlows(
|
||||
flows.map((flow) => {
|
||||
if (flow.id === updatedFlow.id) {
|
||||
return updatedFlow;
|
||||
}
|
||||
return flow;
|
||||
}),
|
||||
);
|
||||
setCurrentFlow(updatedFlow);
|
||||
resolve();
|
||||
} else {
|
||||
};
|
||||
}
|
||||
if (flow && flow.data) {
|
||||
const { id, name, data, description, folder_id, endpoint_name } =
|
||||
flow;
|
||||
if (!currentSavedFlow?.data?.nodes.length || data.nodes.length > 0) {
|
||||
mutate(
|
||||
{ id, name, data, description, folder_id, endpoint_name },
|
||||
{
|
||||
onSuccess: (updatedFlow) => {
|
||||
setSaveLoading(false);
|
||||
if (flows) {
|
||||
// updates flow in state
|
||||
setFlows(
|
||||
flows.map((flow) => {
|
||||
if (flow.id === updatedFlow.id) {
|
||||
return updatedFlow;
|
||||
}
|
||||
return flow;
|
||||
}),
|
||||
);
|
||||
setCurrentFlow(updatedFlow);
|
||||
resolve();
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Failed to save flow",
|
||||
list: ["Flows variable undefined"],
|
||||
});
|
||||
reject(new Error("Flows variable undefined"));
|
||||
}
|
||||
},
|
||||
onError: (e) => {
|
||||
setErrorData({
|
||||
title: "Failed to save flow",
|
||||
list: ["Flows variable undefined"],
|
||||
list: [e.message],
|
||||
});
|
||||
reject(new Error("Flows variable undefined"));
|
||||
}
|
||||
setSaveLoading(false);
|
||||
reject(e);
|
||||
},
|
||||
},
|
||||
onError: (e) => {
|
||||
setErrorData({
|
||||
title: "Failed to save flow",
|
||||
list: [e.message],
|
||||
});
|
||||
setSaveLoading(false);
|
||||
reject(e);
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
} else {
|
||||
setSaveLoading(false);
|
||||
reject(new Error("Can't save empty flow"));
|
||||
}
|
||||
} else {
|
||||
setSaveLoading(false);
|
||||
reject(new Error("Can't save empty flow"));
|
||||
setErrorData({
|
||||
title: "Failed to save flow",
|
||||
list: ["Flow not found"],
|
||||
});
|
||||
reject(new Error("Flow not found"));
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Failed to save flow",
|
||||
list: ["Flow not found"],
|
||||
});
|
||||
reject(new Error("Flow not found"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return saveFlow;
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import ConfirmationModal from "../confirmationModal";
|
||||
|
||||
export function BuildInProgressModal({
|
||||
onStopBuild,
|
||||
onCancel,
|
||||
}: {
|
||||
onStopBuild: () => void;
|
||||
onCancel: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={true}
|
||||
onClose={onCancel}
|
||||
title="Build in Progress"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Stop Build"
|
||||
onConfirm={onStopBuild}
|
||||
onCancel={onCancel}
|
||||
size="x-small"
|
||||
>
|
||||
<ConfirmationModal.Content>
|
||||
The flow is currently building. Do you want to stop the build and exit?
|
||||
</ConfirmationModal.Content>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -66,6 +66,10 @@ function ConfirmationModal({
|
|||
(child) => (child as React.ReactElement).type === Content,
|
||||
);
|
||||
|
||||
const shouldShowConfirm = confirmationText && onConfirm;
|
||||
const shouldShowCancel = cancelText;
|
||||
const shouldShowFooter = shouldShowConfirm || shouldShowCancel;
|
||||
|
||||
return (
|
||||
<BaseModal {...props} open={open} setOpen={setModalOpen}>
|
||||
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
|
||||
|
|
@ -89,34 +93,40 @@ function ConfirmationModal({
|
|||
{ContentChild}
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
className="ml-3"
|
||||
variant={destructive ? "destructive" : "default"}
|
||||
onClick={() => {
|
||||
setFlag(true);
|
||||
setModalOpen(false);
|
||||
onConfirm(index, data);
|
||||
}}
|
||||
loading={loading}
|
||||
data-testid="replace-button"
|
||||
>
|
||||
{confirmationText}
|
||||
</Button>
|
||||
{cancelText && onCancel && (
|
||||
<Button
|
||||
className=""
|
||||
variant={destructiveCancel ? "destructive" : "outline"}
|
||||
onClick={() => {
|
||||
setFlag(true);
|
||||
if (onCancel) onCancel();
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
</BaseModal.Footer>
|
||||
{shouldShowFooter ? (
|
||||
<BaseModal.Footer>
|
||||
{shouldShowConfirm && (
|
||||
<Button
|
||||
className="ml-3"
|
||||
variant={destructive ? "destructive" : "default"}
|
||||
onClick={() => {
|
||||
setFlag(true);
|
||||
setModalOpen(false);
|
||||
onConfirm(index, data);
|
||||
}}
|
||||
loading={loading}
|
||||
data-testid="replace-button"
|
||||
>
|
||||
{confirmationText}
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowCancel && (
|
||||
<Button
|
||||
className=""
|
||||
variant={destructiveCancel ? "destructive" : "outline"}
|
||||
onClick={() => {
|
||||
setFlag(true);
|
||||
setModalOpen(false);
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
)}
|
||||
</BaseModal.Footer>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import ForwardedIconComponent from "@/components/genericIconComponent";
|
||||
import Loading from "@/components/ui/loading";
|
||||
import { truncate } from "lodash";
|
||||
import { useState } from "react";
|
||||
import ConfirmationModal from "../confirmationModal";
|
||||
|
||||
export function SaveChangesModal({
|
||||
|
|
@ -7,7 +9,6 @@ export function SaveChangesModal({
|
|||
onProceed,
|
||||
onCancel,
|
||||
flowName,
|
||||
unsavedChanges,
|
||||
lastSaved,
|
||||
autoSave,
|
||||
}: {
|
||||
|
|
@ -15,34 +16,43 @@ export function SaveChangesModal({
|
|||
onProceed: () => void;
|
||||
onCancel: () => void;
|
||||
flowName: string;
|
||||
unsavedChanges: boolean;
|
||||
lastSaved: string | undefined;
|
||||
autoSave: boolean;
|
||||
}): JSX.Element {
|
||||
const [saving, setSaving] = useState(false);
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open={true}
|
||||
onClose={onCancel}
|
||||
destructiveCancel
|
||||
title={truncate(flowName, { length: 32 }) + " has unsaved changes"}
|
||||
title={
|
||||
(autoSave ? "Flow" : truncate(flowName, { length: 32 })) +
|
||||
" has unsaved changes"
|
||||
}
|
||||
cancelText={autoSave ? undefined : "Exit anyway"}
|
||||
confirmationText={autoSave ? "Exit" : "Save and Exit"}
|
||||
onConfirm={autoSave ? onProceed : onSave}
|
||||
confirmationText={autoSave ? undefined : "Save and Exit"}
|
||||
onConfirm={
|
||||
autoSave
|
||||
? undefined
|
||||
: () => {
|
||||
setSaving(true);
|
||||
onSave();
|
||||
}
|
||||
}
|
||||
onCancel={onProceed}
|
||||
loading={autoSave ? unsavedChanges : false}
|
||||
loading={autoSave ? true : saving}
|
||||
size="x-small"
|
||||
>
|
||||
<ConfirmationModal.Content>
|
||||
{autoSave ? (
|
||||
unsavedChanges ? (
|
||||
"Saving flow automatically..."
|
||||
) : (
|
||||
"Flow saved! Click 'Exit' to leave the page."
|
||||
)
|
||||
<div className="mb-4 flex w-full items-center gap-3 rounded-md bg-gray-100 px-4 py-2 text-gray-800 dark:bg-gray-900/40 dark:text-gray-100">
|
||||
<Loading className="h-5 w-5" />
|
||||
Saving your changes...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-4 flex w-full items-center gap-3 rounded-md bg-yellow-100 px-4 py-2 text-yellow-800">
|
||||
<ForwardedIconComponent name="info" className="h-5 w-5" />
|
||||
<div className="mb-4 flex w-full items-center gap-3 rounded-md bg-yellow-100 px-4 py-2 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-100">
|
||||
<ForwardedIconComponent name="Info" className="h-5 w-5" />
|
||||
Last saved: {lastSaved ?? "Never"}
|
||||
</div>
|
||||
Unsaved changes will be permanently lost.{" "}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { ENABLE_BRANDING } from "@/customization/feature-flags";
|
|||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import useSaveFlow from "@/hooks/flows/use-save-flow";
|
||||
import { SaveChangesModal } from "@/modals/saveChangesModal";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { useTypesStore } from "@/stores/typesStore";
|
||||
import { customStringify } from "@/utils/reactflowUtils";
|
||||
import { useEffect } from "react";
|
||||
import { useBlocker, useParams } from "react-router-dom";
|
||||
import FlowToolbar from "../../components/chatComponent";
|
||||
import { BuildInProgressModal } from "../../modals/buildInProgressModal";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
|
|
@ -19,6 +19,7 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
const changesNotSaved =
|
||||
customStringify(currentFlow) !== customStringify(currentSavedFlow) &&
|
||||
|
|
@ -47,12 +48,26 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
||||
|
||||
const handleSave = () => {
|
||||
saveFlow().then(() => (blocker.proceed ? blocker.proceed() : null));
|
||||
};
|
||||
|
||||
const handleStopBuild = () => {
|
||||
stopBuilding();
|
||||
if (blocker.proceed) blocker.proceed();
|
||||
let saving = true;
|
||||
let proceed = false;
|
||||
setTimeout(() => {
|
||||
saving = false;
|
||||
if (proceed) {
|
||||
blocker.proceed && blocker.proceed();
|
||||
setSuccessData({
|
||||
title: "Flow saved successfully!",
|
||||
});
|
||||
}
|
||||
}, 1200);
|
||||
saveFlow().then(() => {
|
||||
if (!autoSaving || saving === false) {
|
||||
blocker.proceed && blocker.proceed();
|
||||
setSuccessData({
|
||||
title: "Flow saved successfully!",
|
||||
});
|
||||
}
|
||||
proceed = true;
|
||||
});
|
||||
};
|
||||
|
||||
const handleExit = () => {
|
||||
|
|
@ -111,6 +126,27 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
};
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
blocker.state === "blocked" &&
|
||||
autoSaving &&
|
||||
changesNotSaved &&
|
||||
!isBuilding
|
||||
) {
|
||||
handleSave();
|
||||
}
|
||||
}, [blocker.state, isBuilding]);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state === "blocked") {
|
||||
if (isBuilding) {
|
||||
stopBuilding();
|
||||
} else if (!changesNotSaved) {
|
||||
blocker.proceed && blocker.proceed();
|
||||
}
|
||||
}
|
||||
}, [blocker.state, isBuilding]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flow-page-positioning">
|
||||
|
|
@ -140,19 +176,12 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
</div>
|
||||
{blocker.state === "blocked" && (
|
||||
<>
|
||||
{isBuilding && (
|
||||
<BuildInProgressModal
|
||||
onStopBuild={handleStopBuild}
|
||||
onCancel={() => blocker.reset?.()}
|
||||
/>
|
||||
)}
|
||||
{!isBuilding && currentSavedFlow && (
|
||||
<SaveChangesModal
|
||||
onSave={handleSave}
|
||||
onCancel={() => blocker.reset?.()}
|
||||
onProceed={handleExit}
|
||||
flowName={currentSavedFlow.name}
|
||||
unsavedChanges={changesNotSaved}
|
||||
lastSaved={
|
||||
updatedAt
|
||||
? new Date(updatedAt).toLocaleString("en-US", {
|
||||
|
|
|
|||
|
|
@ -440,14 +440,14 @@ export type ConfirmationModalType = {
|
|||
modalContentTitle?: string;
|
||||
loading?: boolean;
|
||||
cancelText?: string;
|
||||
confirmationText: string;
|
||||
confirmationText?: string;
|
||||
children:
|
||||
| [React.ReactElement<ContentProps>, React.ReactElement<TriggerProps>]
|
||||
| React.ReactElement<ContentProps>;
|
||||
icon?: string;
|
||||
data?: any;
|
||||
index?: number;
|
||||
onConfirm: (index, data) => void;
|
||||
onConfirm?: (index, data) => void;
|
||||
open?: boolean;
|
||||
onClose?: () => void;
|
||||
size?:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue