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();
|
mutateRefresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!autoLogin && isAuthenticated) {
|
if (autoLogin !== undefined && !autoLogin && isAuthenticated) {
|
||||||
const intervalId = setInterval(intervalFunction, accessTokenTimer * 1000);
|
const intervalId = setInterval(intervalFunction, accessTokenTimer * 1000);
|
||||||
intervalFunction();
|
intervalFunction();
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
if (!isAuthenticated && !autoLogin) {
|
if (!isAuthenticated && autoLogin !== undefined && !autoLogin) {
|
||||||
return <CustomNavigate to="/login" replace />;
|
return <CustomNavigate to="/login" replace />;
|
||||||
} else {
|
} else {
|
||||||
return children;
|
return children;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { LANGFLOW_ACCESS_TOKEN } from "@/constants/constants";
|
||||||
import { useCustomApiHeaders } from "@/customization/hooks/use-custom-api-headers";
|
import { useCustomApiHeaders } from "@/customization/hooks/use-custom-api-headers";
|
||||||
import useAuthStore from "@/stores/authStore";
|
import useAuthStore from "@/stores/authStore";
|
||||||
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
|
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
|
||||||
import pako from "pako";
|
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { Cookies } from "react-cookie";
|
import { Cookies } from "react-cookie";
|
||||||
import { BuildStatus } from "../../constants/enums";
|
import { BuildStatus } from "../../constants/enums";
|
||||||
|
|
@ -35,7 +34,7 @@ function ApiInterceptor() {
|
||||||
error?.response?.status === 403 || error?.response?.status === 401;
|
error?.response?.status === 403 || error?.response?.status === 401;
|
||||||
|
|
||||||
if (isAuthenticationError) {
|
if (isAuthenticationError) {
|
||||||
if (!autoLogin) {
|
if (autoLogin !== undefined && !autoLogin) {
|
||||||
if (error?.config?.url?.includes("github")) {
|
if (error?.config?.url?.includes("github")) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +128,7 @@ function ApiInterceptor() {
|
||||||
api.interceptors.response.eject(interceptor);
|
api.interceptors.response.eject(interceptor);
|
||||||
api.interceptors.request.eject(requestInterceptor);
|
api.interceptors.request.eject(requestInterceptor);
|
||||||
};
|
};
|
||||||
}, [accessToken, setErrorData, customHeaders]);
|
}, [accessToken, setErrorData, customHeaders, autoLogin]);
|
||||||
|
|
||||||
function checkErrorCount() {
|
function checkErrorCount() {
|
||||||
if (isLoginPage) return;
|
if (isLoginPage) return;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import useAlertStore from "@/stores/alertStore";
|
||||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { FlowType } from "@/types/flow";
|
import { FlowType } from "@/types/flow";
|
||||||
|
import { customStringify } from "@/utils/reactflowUtils";
|
||||||
|
|
||||||
const useSaveFlow = () => {
|
const useSaveFlow = () => {
|
||||||
const flows = useFlowsManagerStore((state) => state.flows);
|
const flows = useFlowsManagerStore((state) => state.flows);
|
||||||
|
|
@ -21,6 +22,9 @@ const useSaveFlow = () => {
|
||||||
const { mutate } = usePatchUpdateFlow();
|
const { mutate } = usePatchUpdateFlow();
|
||||||
|
|
||||||
const saveFlow = async (flow?: FlowType): Promise<void> => {
|
const saveFlow = async (flow?: FlowType): Promise<void> => {
|
||||||
|
if (
|
||||||
|
customStringify(flow || currentFlow) !== customStringify(currentSavedFlow)
|
||||||
|
) {
|
||||||
setSaveLoading(true);
|
setSaveLoading(true);
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (currentFlow) {
|
if (currentFlow) {
|
||||||
|
|
@ -39,7 +43,8 @@ const useSaveFlow = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (flow && flow.data) {
|
if (flow && flow.data) {
|
||||||
const { id, name, data, description, folder_id, endpoint_name } = flow;
|
const { id, name, data, description, folder_id, endpoint_name } =
|
||||||
|
flow;
|
||||||
if (!currentSavedFlow?.data?.nodes.length || data.nodes.length > 0) {
|
if (!currentSavedFlow?.data?.nodes.length || data.nodes.length > 0) {
|
||||||
mutate(
|
mutate(
|
||||||
{ id, name, data, description, folder_id, endpoint_name },
|
{ id, name, data, description, folder_id, endpoint_name },
|
||||||
|
|
@ -88,6 +93,7 @@ const useSaveFlow = () => {
|
||||||
reject(new Error("Flow not found"));
|
reject(new Error("Flow not found"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return saveFlow;
|
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,
|
(child) => (child as React.ReactElement).type === Content,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const shouldShowConfirm = confirmationText && onConfirm;
|
||||||
|
const shouldShowCancel = cancelText;
|
||||||
|
const shouldShowFooter = shouldShowConfirm || shouldShowCancel;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal {...props} open={open} setOpen={setModalOpen}>
|
<BaseModal {...props} open={open} setOpen={setModalOpen}>
|
||||||
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
|
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
|
||||||
|
|
@ -89,7 +93,9 @@ function ConfirmationModal({
|
||||||
{ContentChild}
|
{ContentChild}
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
|
|
||||||
|
{shouldShowFooter ? (
|
||||||
<BaseModal.Footer>
|
<BaseModal.Footer>
|
||||||
|
{shouldShowConfirm && (
|
||||||
<Button
|
<Button
|
||||||
className="ml-3"
|
className="ml-3"
|
||||||
variant={destructive ? "destructive" : "default"}
|
variant={destructive ? "destructive" : "default"}
|
||||||
|
|
@ -103,20 +109,24 @@ function ConfirmationModal({
|
||||||
>
|
>
|
||||||
{confirmationText}
|
{confirmationText}
|
||||||
</Button>
|
</Button>
|
||||||
{cancelText && onCancel && (
|
)}
|
||||||
|
{shouldShowCancel && (
|
||||||
<Button
|
<Button
|
||||||
className=""
|
className=""
|
||||||
variant={destructiveCancel ? "destructive" : "outline"}
|
variant={destructiveCancel ? "destructive" : "outline"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFlag(true);
|
setFlag(true);
|
||||||
if (onCancel) onCancel();
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
|
onCancel?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</BaseModal.Footer>
|
</BaseModal.Footer>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import ForwardedIconComponent from "@/components/genericIconComponent";
|
import ForwardedIconComponent from "@/components/genericIconComponent";
|
||||||
|
import Loading from "@/components/ui/loading";
|
||||||
import { truncate } from "lodash";
|
import { truncate } from "lodash";
|
||||||
|
import { useState } from "react";
|
||||||
import ConfirmationModal from "../confirmationModal";
|
import ConfirmationModal from "../confirmationModal";
|
||||||
|
|
||||||
export function SaveChangesModal({
|
export function SaveChangesModal({
|
||||||
|
|
@ -7,7 +9,6 @@ export function SaveChangesModal({
|
||||||
onProceed,
|
onProceed,
|
||||||
onCancel,
|
onCancel,
|
||||||
flowName,
|
flowName,
|
||||||
unsavedChanges,
|
|
||||||
lastSaved,
|
lastSaved,
|
||||||
autoSave,
|
autoSave,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -15,34 +16,43 @@ export function SaveChangesModal({
|
||||||
onProceed: () => void;
|
onProceed: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
flowName: string;
|
flowName: string;
|
||||||
unsavedChanges: boolean;
|
|
||||||
lastSaved: string | undefined;
|
lastSaved: string | undefined;
|
||||||
autoSave: boolean;
|
autoSave: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
return (
|
return (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
open={true}
|
open={true}
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
destructiveCancel
|
destructiveCancel
|
||||||
title={truncate(flowName, { length: 32 }) + " has unsaved changes"}
|
title={
|
||||||
|
(autoSave ? "Flow" : truncate(flowName, { length: 32 })) +
|
||||||
|
" has unsaved changes"
|
||||||
|
}
|
||||||
cancelText={autoSave ? undefined : "Exit anyway"}
|
cancelText={autoSave ? undefined : "Exit anyway"}
|
||||||
confirmationText={autoSave ? "Exit" : "Save and Exit"}
|
confirmationText={autoSave ? undefined : "Save and Exit"}
|
||||||
onConfirm={autoSave ? onProceed : onSave}
|
onConfirm={
|
||||||
|
autoSave
|
||||||
|
? undefined
|
||||||
|
: () => {
|
||||||
|
setSaving(true);
|
||||||
|
onSave();
|
||||||
|
}
|
||||||
|
}
|
||||||
onCancel={onProceed}
|
onCancel={onProceed}
|
||||||
loading={autoSave ? unsavedChanges : false}
|
loading={autoSave ? true : saving}
|
||||||
size="x-small"
|
size="x-small"
|
||||||
>
|
>
|
||||||
<ConfirmationModal.Content>
|
<ConfirmationModal.Content>
|
||||||
{autoSave ? (
|
{autoSave ? (
|
||||||
unsavedChanges ? (
|
<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">
|
||||||
"Saving flow automatically..."
|
<Loading className="h-5 w-5" />
|
||||||
) : (
|
Saving your changes...
|
||||||
"Flow saved! Click 'Exit' to leave the page."
|
</div>
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4 flex w-full items-center gap-3 rounded-md bg-yellow-100 px-4 py-2 text-yellow-800">
|
<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" />
|
<ForwardedIconComponent name="Info" className="h-5 w-5" />
|
||||||
Last saved: {lastSaved ?? "Never"}
|
Last saved: {lastSaved ?? "Never"}
|
||||||
</div>
|
</div>
|
||||||
Unsaved changes will be permanently lost.{" "}
|
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 { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||||
import useSaveFlow from "@/hooks/flows/use-save-flow";
|
import useSaveFlow from "@/hooks/flows/use-save-flow";
|
||||||
import { SaveChangesModal } from "@/modals/saveChangesModal";
|
import { SaveChangesModal } from "@/modals/saveChangesModal";
|
||||||
|
import useAlertStore from "@/stores/alertStore";
|
||||||
import { useTypesStore } from "@/stores/typesStore";
|
import { useTypesStore } from "@/stores/typesStore";
|
||||||
import { customStringify } from "@/utils/reactflowUtils";
|
import { customStringify } from "@/utils/reactflowUtils";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useBlocker, useParams } from "react-router-dom";
|
import { useBlocker, useParams } from "react-router-dom";
|
||||||
import FlowToolbar from "../../components/chatComponent";
|
import FlowToolbar from "../../components/chatComponent";
|
||||||
import { BuildInProgressModal } from "../../modals/buildInProgressModal";
|
|
||||||
import { useDarkStore } from "../../stores/darkStore";
|
import { useDarkStore } from "../../stores/darkStore";
|
||||||
import useFlowStore from "../../stores/flowStore";
|
import useFlowStore from "../../stores/flowStore";
|
||||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
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 setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
|
||||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||||
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
|
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||||
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
|
|
||||||
const changesNotSaved =
|
const changesNotSaved =
|
||||||
customStringify(currentFlow) !== customStringify(currentSavedFlow) &&
|
customStringify(currentFlow) !== customStringify(currentSavedFlow) &&
|
||||||
|
|
@ -47,12 +48,26 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
saveFlow().then(() => (blocker.proceed ? blocker.proceed() : null));
|
let saving = true;
|
||||||
};
|
let proceed = false;
|
||||||
|
setTimeout(() => {
|
||||||
const handleStopBuild = () => {
|
saving = false;
|
||||||
stopBuilding();
|
if (proceed) {
|
||||||
if (blocker.proceed) blocker.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 = () => {
|
const handleExit = () => {
|
||||||
|
|
@ -111,6 +126,27 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
};
|
};
|
||||||
}, [id]);
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flow-page-positioning">
|
<div className="flow-page-positioning">
|
||||||
|
|
@ -140,19 +176,12 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
||||||
</div>
|
</div>
|
||||||
{blocker.state === "blocked" && (
|
{blocker.state === "blocked" && (
|
||||||
<>
|
<>
|
||||||
{isBuilding && (
|
|
||||||
<BuildInProgressModal
|
|
||||||
onStopBuild={handleStopBuild}
|
|
||||||
onCancel={() => blocker.reset?.()}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isBuilding && currentSavedFlow && (
|
{!isBuilding && currentSavedFlow && (
|
||||||
<SaveChangesModal
|
<SaveChangesModal
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onCancel={() => blocker.reset?.()}
|
onCancel={() => blocker.reset?.()}
|
||||||
onProceed={handleExit}
|
onProceed={handleExit}
|
||||||
flowName={currentSavedFlow.name}
|
flowName={currentSavedFlow.name}
|
||||||
unsavedChanges={changesNotSaved}
|
|
||||||
lastSaved={
|
lastSaved={
|
||||||
updatedAt
|
updatedAt
|
||||||
? new Date(updatedAt).toLocaleString("en-US", {
|
? new Date(updatedAt).toLocaleString("en-US", {
|
||||||
|
|
|
||||||
|
|
@ -440,14 +440,14 @@ export type ConfirmationModalType = {
|
||||||
modalContentTitle?: string;
|
modalContentTitle?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
cancelText?: string;
|
cancelText?: string;
|
||||||
confirmationText: string;
|
confirmationText?: string;
|
||||||
children:
|
children:
|
||||||
| [React.ReactElement<ContentProps>, React.ReactElement<TriggerProps>]
|
| [React.ReactElement<ContentProps>, React.ReactElement<TriggerProps>]
|
||||||
| React.ReactElement<ContentProps>;
|
| React.ReactElement<ContentProps>;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
index?: number;
|
index?: number;
|
||||||
onConfirm: (index, data) => void;
|
onConfirm?: (index, data) => void;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
size?:
|
size?:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue