From 00d2cffe4684e0e8eb09ea5d94dce7a7fbce75ce Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:17:42 -0300 Subject: [PATCH] fix: auto save ui and env var (#3384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added new confirmation modal for saving * Fixed save button * fixed header classes * updated docs link * Added different message to auto saving * Changed tooltip to appear in saved text, not in button * Changed tooltip back to previous when auto saving is enabled * changed auto_save to auto_saving * Fixed build not appearing and icons * Changed modal when autosave is enabled * 🐛 (menuBar/index.tsx): fix condition for disabling save button to include isBuilding flag to prevent saving during build process * fix current flow not being updated on set nodes and edges and fix modal not letting user leave when flow is empty * Removed console log * Fix add flow not adding the flow that comes from the backend --------- Co-authored-by: cristhianzl Co-authored-by: Cristhian Zanforlin Lousa <72977554+Cristhianzl@users.noreply.github.com> --- .../components/menuBar/index.tsx | 79 +++++++++++-------- .../src/components/headerComponent/index.tsx | 10 +-- src/frontend/src/constants/constants.ts | 2 +- src/frontend/src/hooks/flows/use-add-flow.ts | 7 +- .../src/hooks/flows/use-autosave-flow.ts | 2 +- .../src/modals/confirmationModal/index.tsx | 40 +++++----- .../src/modals/flowSettingsModal/index.tsx | 2 +- .../src/modals/saveChangesModal/index.tsx | 54 +++++++++++-- src/frontend/src/pages/FlowPage/index.tsx | 23 +++++- src/frontend/src/stores/flowStore.ts | 2 + src/frontend/src/style/applies.css | 7 +- src/frontend/src/types/components/index.ts | 5 +- src/frontend/vite.config.mts | 4 +- 13 files changed, 157 insertions(+), 80 deletions(-) diff --git a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx index 578c8a85a..f0e2f51d7 100644 --- a/src/frontend/src/components/headerComponent/components/menuBar/index.tsx +++ b/src/frontend/src/components/headerComponent/components/menuBar/index.tsx @@ -49,7 +49,7 @@ export const MenuBar = ({}: {}): JSX.Element => { const isBuilding = useFlowStore((state) => state.isBuilding); const getTypes = useTypesStore((state) => state.getTypes); const saveFlow = useSaveFlow(); - const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false"; + const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false"; const currentFlow = useFlowStore((state) => state.currentFlow); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow); const updatedAt = currentSavedFlow?.updated_at; @@ -105,7 +105,7 @@ export const MenuBar = ({}: {}): JSX.Element => { useHotkeys(changes, handleSave, { preventDefault: true }); return currentFlow && onFlowPage ? ( -
+
@@ -251,39 +251,53 @@ export const MenuBar = ({}: {}): JSX.Element => { >
- {(updatedAt || saveLoading) && ( +
+ {!shouldAutosave && ( + + )} +

+ Auto-saving is disabled +

+

+ + Enable auto-saving + {" "} + to avoid losing progress. +

+
+ ) } side="bottom" styleClasses="cursor-default" > -
-
- + )} +
{printByBuildStatus()}
- )} +
) : ( <> diff --git a/src/frontend/src/components/headerComponent/index.tsx b/src/frontend/src/components/headerComponent/index.tsx index ded90967b..3c671b90a 100644 --- a/src/frontend/src/components/headerComponent/index.tsx +++ b/src/frontend/src/components/headerComponent/index.tsx @@ -84,8 +84,8 @@ export default function Header(): JSX.Element { }; return ( -
-
+
+
⛓️ @@ -103,7 +103,7 @@ export default function Header(): JSX.Element {
-
+
@@ -129,7 +129,7 @@ export default function Header(): JSX.Element { data-testid="button-store" > -
Store
+
Store
)} diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 7e6e58abc..452830133 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -709,7 +709,7 @@ export const CREATE_API_KEY = `Don’t have an API key? Sign up at`; export const STATUS_BUILD = "Build to validate status."; export const STATUS_INACTIVE = "Execution blocked"; export const STATUS_BUILDING = "Building..."; -export const SAVED_HOVER = "Last saved at "; +export const SAVED_HOVER = "Last saved: "; export const RUN_TIMESTAMP_PREFIX = "Last Run: "; export const STARTER_FOLDER_NAME = "Starter Projects"; export const PRIORITY_SIDEBAR_ORDER = [ diff --git a/src/frontend/src/hooks/flows/use-add-flow.ts b/src/frontend/src/hooks/flows/use-add-flow.ts index b3e827bc5..56bbd0f46 100644 --- a/src/frontend/src/hooks/flows/use-add-flow.ts +++ b/src/frontend/src/hooks/flows/use-add-flow.ts @@ -64,11 +64,10 @@ const useAddFlow = () => { newFlow.folder_id = folder_id; postAddFlow(newFlow, { - onSuccess: ({ id }) => { - newFlow.id = id; + onSuccess: (createdFlow) => { // Add the new flow to the list of flows. const { data, flows: myFlows } = processFlows([ - newFlow, + createdFlow, ...(flows ?? []), ]); setFlows(myFlows); @@ -79,7 +78,7 @@ const useAddFlow = () => { ["saved_components"]: data, }), })); - resolve(id); + resolve(createdFlow.id); }, onError: (error) => { if (error.response?.data?.detail) { diff --git a/src/frontend/src/hooks/flows/use-autosave-flow.ts b/src/frontend/src/hooks/flows/use-autosave-flow.ts index 62fe8562d..3e48e518f 100644 --- a/src/frontend/src/hooks/flows/use-autosave-flow.ts +++ b/src/frontend/src/hooks/flows/use-autosave-flow.ts @@ -5,7 +5,7 @@ import useSaveFlow from "./use-save-flow"; const useAutoSaveFlow = () => { const saveFlow = useSaveFlow(); - const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false"; + const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false"; const autoSaveFlow = shouldAutosave ? useDebounce((flow?: FlowType) => { diff --git a/src/frontend/src/modals/confirmationModal/index.tsx b/src/frontend/src/modals/confirmationModal/index.tsx index f1e0c894c..e22f1d162 100644 --- a/src/frontend/src/modals/confirmationModal/index.tsx +++ b/src/frontend/src/modals/confirmationModal/index.tsx @@ -7,7 +7,6 @@ import { ContentProps, TriggerProps, } from "../../types/components"; -import { nodeIconsLucide } from "../../utils/styleUtils"; import BaseModal from "../baseModal"; const Content: React.FC = ({ children }) => { @@ -36,6 +35,7 @@ function ConfirmationModal({ destructive = false, destructiveCancel = false, icon, + loading, data, index, onConfirm, @@ -71,11 +71,13 @@ function ConfirmationModal({ {triggerChild} {title} - {modalContentTitle && modalContentTitle != "" && ( @@ -96,22 +98,24 @@ function ConfirmationModal({ setModalOpen(false); onConfirm(index, data); }} + loading={loading} data-testid="replace-button" > {confirmationText} - - + {cancelText && onCancel && ( + + )} ); diff --git a/src/frontend/src/modals/flowSettingsModal/index.tsx b/src/frontend/src/modals/flowSettingsModal/index.tsx index e7a9b9078..8a97594ad 100644 --- a/src/frontend/src/modals/flowSettingsModal/index.tsx +++ b/src/frontend/src/modals/flowSettingsModal/index.tsx @@ -33,7 +33,7 @@ export default function FlowSettingsModal({ ); const [isSaving, setIsSaving] = useState(false); const [disableSave, setDisableSave] = useState(true); - const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false"; + const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false"; function handleClick(): void { setIsSaving(true); if (!currentFlow) return; diff --git a/src/frontend/src/modals/saveChangesModal/index.tsx b/src/frontend/src/modals/saveChangesModal/index.tsx index b783e7c7c..716fc9571 100644 --- a/src/frontend/src/modals/saveChangesModal/index.tsx +++ b/src/frontend/src/modals/saveChangesModal/index.tsx @@ -1,21 +1,61 @@ +import ForwardedIconComponent from "@/components/genericIconComponent"; +import { truncate } from "lodash"; import ConfirmationModal from "../confirmationModal"; -export function SaveChangesModal({ onSave, onProceed, onCancel }) { +export function SaveChangesModal({ + onSave, + onProceed, + onCancel, + flowName, + unsavedChanges, + lastSaved, + autoSave, +}: { + onSave: () => void; + onProceed: () => void; + onCancel: () => void; + flowName: string; + unsavedChanges: boolean; + lastSaved: string | undefined; + autoSave: boolean; +}): JSX.Element { return ( - You have unsaved changes. Would you like to save them before exiting? + {autoSave ? ( + unsavedChanges ? ( + "Saving flow automatically..." + ) : ( + "Flow saved! Click 'Exit' to leave the page." + ) + ) : ( + <> +
+ + Last saved: {lastSaved ?? "Never"} +
+ Unsaved changes will be permanently lost.{" "} + + Enable auto-saving + {" "} + to avoid losing progress. + + )}
); diff --git a/src/frontend/src/pages/FlowPage/index.tsx b/src/frontend/src/pages/FlowPage/index.tsx index 9be5720b7..75132fc83 100644 --- a/src/frontend/src/pages/FlowPage/index.tsx +++ b/src/frontend/src/pages/FlowPage/index.tsx @@ -20,7 +20,8 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element { const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow); const changesNotSaved = - customStringify(currentFlow) !== customStringify(currentSavedFlow); + customStringify(currentFlow) !== customStringify(currentSavedFlow) && + (currentFlow?.data?.nodes?.length ?? 0) > 0; const blocker = useBlocker(changesNotSaved); const version = useDarkStore((state) => state.version); @@ -36,6 +37,10 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element { const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading); const getTypes = useTypesStore((state) => state.getTypes); + const updatedAt = currentSavedFlow?.updated_at; + + const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false"; + const handleSave = () => { saveFlow().then(() => (blocker.proceed ? blocker.proceed() : null)); }; @@ -113,11 +118,25 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
⛓️ v{version}
- {blocker.state === "blocked" && ( + {blocker.state === "blocked" && currentSavedFlow && ( (blocker.reset ? blocker.reset() : null)} onProceed={() => (blocker.proceed ? blocker.proceed() : null)} + flowName={currentSavedFlow.name} + unsavedChanges={changesNotSaved} + lastSaved={ + updatedAt + ? new Date(updatedAt).toLocaleString("en-US", { + hour: "numeric", + minute: "numeric", + second: "numeric", + month: "numeric", + day: "numeric", + }) + : undefined + } + autoSave={shouldAutosave} /> )} diff --git a/src/frontend/src/stores/flowStore.ts b/src/frontend/src/stores/flowStore.ts index 3a8289e2e..94bac2eff 100644 --- a/src/frontend/src/stores/flowStore.ts +++ b/src/frontend/src/stores/flowStore.ts @@ -228,6 +228,7 @@ const useFlowStore = create((set, get) => ({ outputs, hasIO: inputs.length > 0 || outputs.length > 0, }); + get().updateCurrentFlow({ nodes: newChange, edges: newEdges }); if (get().autoSaveFlow) { get().autoSaveFlow!(); } @@ -238,6 +239,7 @@ const useFlowStore = create((set, get) => ({ edges: newChange, flowState: undefined, }); + get().updateCurrentFlow({ edges: newChange }); if (get().autoSaveFlow) { get().autoSaveFlow!(); } diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 3f9a8df77..e01d53f95 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -218,9 +218,6 @@ .round-button-form { @apply flex h-12 w-12 cursor-pointer justify-center rounded-full bg-border px-3 py-1 shadow-md; } - .round-button-div { - @apply flex items-center gap-3; - } .build-trigger-loading-icon { @apply stroke-build-trigger; } @@ -568,7 +565,7 @@ @apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium; } .header-menu-bar-display { - @apply flex max-w-[115px] cursor-pointer items-center gap-2 lg:max-w-[145px]; + @apply flex max-w-[110px] cursor-pointer items-center gap-2 lg:max-w-[150px]; } .header-menu-flow-name { @apply flex-1 truncate; @@ -581,7 +578,7 @@ @apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted; } .header-start-display { - @apply flex items-center justify-start gap-2; + @apply flex items-center gap-2; } .header-end-division { @apply flex justify-end px-2; diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index c5f2db077..632b24d1b 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -437,12 +437,13 @@ export type ConfirmationModalType = { destructive?: boolean; destructiveCancel?: boolean; modalContentTitle?: string; - cancelText: string; + loading?: boolean; + cancelText?: string; confirmationText: string; children: | [React.ReactElement, React.ReactElement] | React.ReactElement; - icon: string; + icon?: string; data?: any; index?: number; onConfirm: (index, data) => void; diff --git a/src/frontend/vite.config.mts b/src/frontend/vite.config.mts index 43b0d85ad..cb9feadb4 100644 --- a/src/frontend/vite.config.mts +++ b/src/frontend/vite.config.mts @@ -31,8 +31,8 @@ export default defineConfig(({ mode }) => { outDir: "build", }, define: { - "process.env.LANGFLOW_AUTO_SAVE": JSON.stringify( - process.env.LANGFLOW_AUTO_SAVE, + "process.env.LANGFLOW_AUTO_SAVING": JSON.stringify( + process.env.LANGFLOW_AUTO_SAVING, ), "process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL), "process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify(