fix: auto save ui and env var (#3384)

* 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 <cristhian.lousa@gmail.com>
Co-authored-by: Cristhian Zanforlin Lousa <72977554+Cristhianzl@users.noreply.github.com>
This commit is contained in:
Lucas Oliveira 2024-08-16 15:17:42 -03:00 committed by GitHub
commit 00d2cffe46
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 157 additions and 80 deletions

View file

@ -49,7 +49,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
const isBuilding = useFlowStore((state) => state.isBuilding); const isBuilding = useFlowStore((state) => state.isBuilding);
const getTypes = useTypesStore((state) => state.getTypes); const getTypes = useTypesStore((state) => state.getTypes);
const saveFlow = useSaveFlow(); 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 currentFlow = useFlowStore((state) => state.currentFlow);
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const updatedAt = currentSavedFlow?.updated_at; const updatedAt = currentSavedFlow?.updated_at;
@ -105,7 +105,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
useHotkeys(changes, handleSave, { preventDefault: true }); useHotkeys(changes, handleSave, { preventDefault: true });
return currentFlow && onFlowPage ? ( return currentFlow && onFlowPage ? (
<div className="round-button-div"> <div className="flex items-center">
<div className="header-menu-bar"> <div className="header-menu-bar">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -251,39 +251,53 @@ export const MenuBar = ({}: {}): JSX.Element => {
></FlowSettingsModal> ></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal> <FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</div> </div>
{(updatedAt || saveLoading) && ( <div className="flex items-center">
{!shouldAutosave && (
<Button
variant="primary"
size="icon"
disabled={shouldAutosave || !changesNotSaved || isBuilding}
className={cn("mr-1 h-9 px-2")}
onClick={handleSave}
>
<IconComponent name={"Save"} className={cn("h-5 w-5")} />
</Button>
)}
<ShadTooltip <ShadTooltip
content={ content={
SAVED_HOVER + shouldAutosave ? (
new Date(updatedAt ?? "").toLocaleString("en-US", { SAVED_HOVER +
hour: "numeric", (updatedAt
minute: "numeric", ? new Date(updatedAt).toLocaleString("en-US", {
second: "numeric", hour: "numeric",
}) minute: "numeric",
})
: "Never")
) : (
<div className="flex w-48 flex-col gap-1 py-1">
<h2 className="text-base font-semibold">
Auto-saving is disabled
</h2>
<p className="text-muted-foreground">
<a
href="https://docs.langflow.org/configuration-auto-saving"
className="text-primary underline"
>
Enable auto-saving
</a>{" "}
to avoid losing progress.
</p>
</div>
)
} }
side="bottom" side="bottom"
styleClasses="cursor-default" styleClasses="cursor-default"
> >
<div className="flex cursor-default items-center gap-2 text-sm text-muted-foreground transition-all"> <div className="ml-2 flex cursor-default items-center gap-2 text-sm text-muted-foreground transition-all">
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground transition-all"> <div className="flex cursor-default items-center gap-2 text-sm text-muted-foreground transition-all">
<Button {(saveLoading || !changesNotSaved || isBuilding) && (
unstyled
disabled={shouldAutosave || !changesNotSaved}
className={cn(
!shouldAutosave && changesNotSaved
? "hover:text-primary"
: "",
)}
onClick={handleSave}
>
<IconComponent <IconComponent
name={ name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
isBuilding || saveLoading
? "Loader2"
: changesNotSaved
? "Save"
: "CheckCircle2"
}
className={cn( className={cn(
"h-4 w-4", "h-4 w-4",
isBuilding || saveLoading isBuilding || saveLoading
@ -291,7 +305,8 @@ export const MenuBar = ({}: {}): JSX.Element => {
: "animate-wiggle", : "animate-wiggle",
)} )}
/> />
</Button> )}
<div>{printByBuildStatus()}</div> <div>{printByBuildStatus()}</div>
</div> </div>
<button <button
@ -306,8 +321,8 @@ export const MenuBar = ({}: {}): JSX.Element => {
}} }}
className={ className={
isBuilding isBuilding
? "flex items-center gap-1.5 text-status-red opacity-100 transition-all" ? "flex items-center gap-1.5 text-status-red transition-all"
: "opacity-0" : "hidden"
} }
> >
<IconComponent name="Square" className="h-4 w-4" /> <IconComponent name="Square" className="h-4 w-4" />
@ -315,7 +330,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
</button> </button>
</div> </div>
</ShadTooltip> </ShadTooltip>
)} </div>
</div> </div>
) : ( ) : (
<></> <></>

View file

@ -84,8 +84,8 @@ export default function Header(): JSX.Element {
}; };
return ( return (
<div className="header-arrangement"> <div className="header-arrangement relative">
<div className="header-start-display lg:w-[450px]"> <div className="header-start-display">
<Link to="/all" className="cursor-pointer"> <Link to="/all" className="cursor-pointer">
<span className="ml-4 text-2xl"></span> <span className="ml-4 text-2xl"></span>
</Link> </Link>
@ -103,7 +103,7 @@ export default function Header(): JSX.Element {
<MenuBar /> <MenuBar />
</div> </div>
<div className="round-button-div"> <div className="flex items-center xl:absolute xl:left-1/2 xl:-translate-x-1/2">
<Link to="/all"> <Link to="/all">
<Button <Button
className="gap-2" className="gap-2"
@ -116,7 +116,7 @@ export default function Header(): JSX.Element {
size="sm" size="sm"
> >
<IconComponent name="Home" className="h-4 w-4" /> <IconComponent name="Home" className="h-4 w-4" />
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div> <div className="hidden flex-1 lg:block">{USER_PROJECTS_HEADER}</div>
</Button> </Button>
</Link> </Link>
@ -129,7 +129,7 @@ export default function Header(): JSX.Element {
data-testid="button-store" data-testid="button-store"
> >
<IconComponent name="Store" className="h-4 w-4" /> <IconComponent name="Store" className="h-4 w-4" />
<div className="flex-1">Store</div> <div className="hidden flex-1 lg:block">Store</div>
</Button> </Button>
</Link> </Link>
)} )}

View file

@ -709,7 +709,7 @@ export const CREATE_API_KEY = `Dont have an API key? Sign up at`;
export const STATUS_BUILD = "Build to validate status."; export const STATUS_BUILD = "Build to validate status.";
export const STATUS_INACTIVE = "Execution blocked"; export const STATUS_INACTIVE = "Execution blocked";
export const STATUS_BUILDING = "Building..."; 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 RUN_TIMESTAMP_PREFIX = "Last Run: ";
export const STARTER_FOLDER_NAME = "Starter Projects"; export const STARTER_FOLDER_NAME = "Starter Projects";
export const PRIORITY_SIDEBAR_ORDER = [ export const PRIORITY_SIDEBAR_ORDER = [

View file

@ -64,11 +64,10 @@ const useAddFlow = () => {
newFlow.folder_id = folder_id; newFlow.folder_id = folder_id;
postAddFlow(newFlow, { postAddFlow(newFlow, {
onSuccess: ({ id }) => { onSuccess: (createdFlow) => {
newFlow.id = id;
// Add the new flow to the list of flows. // Add the new flow to the list of flows.
const { data, flows: myFlows } = processFlows([ const { data, flows: myFlows } = processFlows([
newFlow, createdFlow,
...(flows ?? []), ...(flows ?? []),
]); ]);
setFlows(myFlows); setFlows(myFlows);
@ -79,7 +78,7 @@ const useAddFlow = () => {
["saved_components"]: data, ["saved_components"]: data,
}), }),
})); }));
resolve(id); resolve(createdFlow.id);
}, },
onError: (error) => { onError: (error) => {
if (error.response?.data?.detail) { if (error.response?.data?.detail) {

View file

@ -5,7 +5,7 @@ import useSaveFlow from "./use-save-flow";
const useAutoSaveFlow = () => { const useAutoSaveFlow = () => {
const saveFlow = useSaveFlow(); const saveFlow = useSaveFlow();
const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false"; const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false";
const autoSaveFlow = shouldAutosave const autoSaveFlow = shouldAutosave
? useDebounce((flow?: FlowType) => { ? useDebounce((flow?: FlowType) => {

View file

@ -7,7 +7,6 @@ import {
ContentProps, ContentProps,
TriggerProps, TriggerProps,
} from "../../types/components"; } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
const Content: React.FC<ContentProps> = ({ children }) => { const Content: React.FC<ContentProps> = ({ children }) => {
@ -36,6 +35,7 @@ function ConfirmationModal({
destructive = false, destructive = false,
destructiveCancel = false, destructiveCancel = false,
icon, icon,
loading,
data, data,
index, index,
onConfirm, onConfirm,
@ -71,11 +71,13 @@ function ConfirmationModal({
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger> <BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader ?? null}> <BaseModal.Header description={titleHeader ?? null}>
<span className="pr-2">{title}</span> <span className="pr-2">{title}</span>
<GenericIconComponent {icon && (
name={icon} <GenericIconComponent
className="h-6 w-6 pl-1 text-foreground" name={icon}
aria-hidden="true" className="h-6 w-6 pl-1 text-foreground"
/> aria-hidden="true"
/>
)}
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
{modalContentTitle && modalContentTitle != "" && ( {modalContentTitle && modalContentTitle != "" && (
@ -96,22 +98,24 @@ function ConfirmationModal({
setModalOpen(false); setModalOpen(false);
onConfirm(index, data); onConfirm(index, data);
}} }}
loading={loading}
data-testid="replace-button" data-testid="replace-button"
> >
{confirmationText} {confirmationText}
</Button> </Button>
{cancelText && onCancel && (
<Button <Button
className="" className=""
variant={destructiveCancel ? "destructive" : "outline"} variant={destructiveCancel ? "destructive" : "outline"}
onClick={() => { onClick={() => {
setFlag(true); setFlag(true);
if (onCancel) onCancel(); if (onCancel) onCancel();
setModalOpen(false); setModalOpen(false);
}} }}
> >
{cancelText} {cancelText}
</Button> </Button>
)}
</BaseModal.Footer> </BaseModal.Footer>
</BaseModal> </BaseModal>
); );

View file

@ -33,7 +33,7 @@ export default function FlowSettingsModal({
); );
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [disableSave, setDisableSave] = useState(true); const [disableSave, setDisableSave] = useState(true);
const shouldAutosave = process.env.LANGFLOW_AUTO_SAVE !== "false"; const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false";
function handleClick(): void { function handleClick(): void {
setIsSaving(true); setIsSaving(true);
if (!currentFlow) return; if (!currentFlow) return;

View file

@ -1,21 +1,61 @@
import ForwardedIconComponent from "@/components/genericIconComponent";
import { truncate } from "lodash";
import ConfirmationModal from "../confirmationModal"; 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 ( return (
<ConfirmationModal <ConfirmationModal
open={true} open={true}
onClose={onCancel} onClose={onCancel}
destructiveCancel destructiveCancel
title={"Exit without saving?"} title={truncate(flowName, { length: 32 }) + " has unsaved changes"}
cancelText={"Exit anyway"} cancelText={autoSave ? undefined : "Exit anyway"}
confirmationText={"Save and Exit"} confirmationText={autoSave ? "Exit" : "Save and Exit"}
icon={"Save"} onConfirm={autoSave ? onProceed : onSave}
onConfirm={onSave}
onCancel={onProceed} onCancel={onProceed}
loading={autoSave ? unsavedChanges : false}
size="x-small" size="x-small"
> >
<ConfirmationModal.Content> <ConfirmationModal.Content>
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."
)
) : (
<>
<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" />
Last saved: {lastSaved ?? "Never"}
</div>
Unsaved changes will be permanently lost.{" "}
<a
target="_blank"
className="underline"
href="https://docs.langflow.org/configuration-auto-saving"
>
Enable auto-saving
</a>{" "}
to avoid losing progress.
</>
)}
</ConfirmationModal.Content> </ConfirmationModal.Content>
</ConfirmationModal> </ConfirmationModal>
); );

View file

@ -20,7 +20,8 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
const changesNotSaved = const changesNotSaved =
customStringify(currentFlow) !== customStringify(currentSavedFlow); customStringify(currentFlow) !== customStringify(currentSavedFlow) &&
(currentFlow?.data?.nodes?.length ?? 0) > 0;
const blocker = useBlocker(changesNotSaved); const blocker = useBlocker(changesNotSaved);
const version = useDarkStore((state) => state.version); 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 setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
const getTypes = useTypesStore((state) => state.getTypes); const getTypes = useTypesStore((state) => state.getTypes);
const updatedAt = currentSavedFlow?.updated_at;
const shouldAutosave = process.env.LANGFLOW_AUTO_SAVING !== "false";
const handleSave = () => { const handleSave = () => {
saveFlow().then(() => (blocker.proceed ? blocker.proceed() : null)); saveFlow().then(() => (blocker.proceed ? blocker.proceed() : null));
}; };
@ -113,11 +118,25 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
<div className={version ? "mt-2" : "mt-1"}> v{version}</div> <div className={version ? "mt-2" : "mt-1"}> v{version}</div>
</a> </a>
</div> </div>
{blocker.state === "blocked" && ( {blocker.state === "blocked" && currentSavedFlow && (
<SaveChangesModal <SaveChangesModal
onSave={handleSave} onSave={handleSave}
onCancel={() => (blocker.reset ? blocker.reset() : null)} onCancel={() => (blocker.reset ? blocker.reset() : null)}
onProceed={() => (blocker.proceed ? blocker.proceed() : 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}
/> />
)} )}
</> </>

View file

@ -228,6 +228,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
outputs, outputs,
hasIO: inputs.length > 0 || outputs.length > 0, hasIO: inputs.length > 0 || outputs.length > 0,
}); });
get().updateCurrentFlow({ nodes: newChange, edges: newEdges });
if (get().autoSaveFlow) { if (get().autoSaveFlow) {
get().autoSaveFlow!(); get().autoSaveFlow!();
} }
@ -238,6 +239,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
edges: newChange, edges: newChange,
flowState: undefined, flowState: undefined,
}); });
get().updateCurrentFlow({ edges: newChange });
if (get().autoSaveFlow) { if (get().autoSaveFlow) {
get().autoSaveFlow!(); get().autoSaveFlow!();
} }

View file

@ -218,9 +218,6 @@
.round-button-form { .round-button-form {
@apply flex h-12 w-12 cursor-pointer justify-center rounded-full bg-border px-3 py-1 shadow-md; @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 { .build-trigger-loading-icon {
@apply stroke-build-trigger; @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; @apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium;
} }
.header-menu-bar-display { .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 { .header-menu-flow-name {
@apply flex-1 truncate; @apply flex-1 truncate;
@ -581,7 +578,7 @@
@apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted; @apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted;
} }
.header-start-display { .header-start-display {
@apply flex items-center justify-start gap-2; @apply flex items-center gap-2;
} }
.header-end-division { .header-end-division {
@apply flex justify-end px-2; @apply flex justify-end px-2;

View file

@ -437,12 +437,13 @@ export type ConfirmationModalType = {
destructive?: boolean; destructive?: boolean;
destructiveCancel?: boolean; destructiveCancel?: boolean;
modalContentTitle?: string; modalContentTitle?: string;
cancelText: string; loading?: boolean;
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;

View file

@ -31,8 +31,8 @@ export default defineConfig(({ mode }) => {
outDir: "build", outDir: "build",
}, },
define: { define: {
"process.env.LANGFLOW_AUTO_SAVE": JSON.stringify( "process.env.LANGFLOW_AUTO_SAVING": JSON.stringify(
process.env.LANGFLOW_AUTO_SAVE, process.env.LANGFLOW_AUTO_SAVING,
), ),
"process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL), "process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL),
"process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify( "process.env.ACCESS_TOKEN_EXPIRE_SECONDS": JSON.stringify(