diff --git a/src/frontend/src/components/authGuard/index.tsx b/src/frontend/src/components/authGuard/index.tsx index 680c11ee1..418d4561d 100644 --- a/src/frontend/src/components/authGuard/index.tsx +++ b/src/frontend/src/components/authGuard/index.tsx @@ -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 ; } else { return children; diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx index e58e36bf5..c03d13e9e 100644 --- a/src/frontend/src/controllers/API/api.tsx +++ b/src/frontend/src/controllers/API/api.tsx @@ -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; diff --git a/src/frontend/src/hooks/flows/use-save-flow.ts b/src/frontend/src/hooks/flows/use-save-flow.ts index cd797146a..c4eb55e27 100644 --- a/src/frontend/src/hooks/flows/use-save-flow.ts +++ b/src/frontend/src/hooks/flows/use-save-flow.ts @@ -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 => { - setSaveLoading(true); - return new Promise((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((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; diff --git a/src/frontend/src/modals/buildInProgressModal/index.tsx b/src/frontend/src/modals/buildInProgressModal/index.tsx deleted file mode 100644 index 359a6bfa5..000000000 --- a/src/frontend/src/modals/buildInProgressModal/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import ConfirmationModal from "../confirmationModal"; - -export function BuildInProgressModal({ - onStopBuild, - onCancel, -}: { - onStopBuild: () => void; - onCancel: () => void; -}): JSX.Element { - return ( - - - The flow is currently building. Do you want to stop the build and exit? - - - ); -} diff --git a/src/frontend/src/modals/confirmationModal/index.tsx b/src/frontend/src/modals/confirmationModal/index.tsx index e22f1d162..8a6f8c008 100644 --- a/src/frontend/src/modals/confirmationModal/index.tsx +++ b/src/frontend/src/modals/confirmationModal/index.tsx @@ -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 ( {triggerChild} @@ -89,34 +93,40 @@ function ConfirmationModal({ {ContentChild} - - - {cancelText && onCancel && ( - - )} - + {shouldShowFooter ? ( + + {shouldShowConfirm && ( + + )} + {shouldShowCancel && ( + + )} + + ) : ( + <> + )} ); } diff --git a/src/frontend/src/modals/saveChangesModal/index.tsx b/src/frontend/src/modals/saveChangesModal/index.tsx index 716fc9571..0e19f2d22 100644 --- a/src/frontend/src/modals/saveChangesModal/index.tsx +++ b/src/frontend/src/modals/saveChangesModal/index.tsx @@ -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 ( { + setSaving(true); + onSave(); + } + } onCancel={onProceed} - loading={autoSave ? unsavedChanges : false} + loading={autoSave ? true : saving} size="x-small" > {autoSave ? ( - unsavedChanges ? ( - "Saving flow automatically..." - ) : ( - "Flow saved! Click 'Exit' to leave the page." - ) +
+ + Saving your changes... +
) : ( <> -
- +
+ Last saved: {lastSaved ?? "Never"}
Unsaved changes will be permanently lost.{" "} diff --git a/src/frontend/src/pages/FlowPage/index.tsx b/src/frontend/src/pages/FlowPage/index.tsx index 3502ccebe..131e9ee8f 100644 --- a/src/frontend/src/pages/FlowPage/index.tsx +++ b/src/frontend/src/pages/FlowPage/index.tsx @@ -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 ( <>
@@ -140,19 +176,12 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
{blocker.state === "blocked" && ( <> - {isBuilding && ( - blocker.reset?.()} - /> - )} {!isBuilding && currentSavedFlow && ( blocker.reset?.()} onProceed={handleExit} flowName={currentSavedFlow.name} - unsavedChanges={changesNotSaved} lastSaved={ updatedAt ? new Date(updatedAt).toLocaleString("en-US", { diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index bce079119..d3fe911f2 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -440,14 +440,14 @@ export type ConfirmationModalType = { modalContentTitle?: string; loading?: boolean; cancelText?: string; - confirmationText: string; + confirmationText?: string; children: | [React.ReactElement, React.ReactElement] | React.ReactElement; icon?: string; data?: any; index?: number; - onConfirm: (index, data) => void; + onConfirm?: (index, data) => void; open?: boolean; onClose?: () => void; size?: