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 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 ? (
<div className="round-button-div">
<div className="flex items-center">
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -251,39 +251,53 @@ export const MenuBar = ({}: {}): JSX.Element => {
></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</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
content={
SAVED_HOVER +
new Date(updatedAt ?? "").toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
second: "numeric",
})
shouldAutosave ? (
SAVED_HOVER +
(updatedAt
? new Date(updatedAt).toLocaleString("en-US", {
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"
styleClasses="cursor-default"
>
<div className="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">
<Button
unstyled
disabled={shouldAutosave || !changesNotSaved}
className={cn(
!shouldAutosave && changesNotSaved
? "hover:text-primary"
: "",
)}
onClick={handleSave}
>
<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-2 text-sm text-muted-foreground transition-all">
{(saveLoading || !changesNotSaved || isBuilding) && (
<IconComponent
name={
isBuilding || saveLoading
? "Loader2"
: changesNotSaved
? "Save"
: "CheckCircle2"
}
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading
@ -291,7 +305,8 @@ export const MenuBar = ({}: {}): JSX.Element => {
: "animate-wiggle",
)}
/>
</Button>
)}
<div>{printByBuildStatus()}</div>
</div>
<button
@ -306,8 +321,8 @@ export const MenuBar = ({}: {}): JSX.Element => {
}}
className={
isBuilding
? "flex items-center gap-1.5 text-status-red opacity-100 transition-all"
: "opacity-0"
? "flex items-center gap-1.5 text-status-red transition-all"
: "hidden"
}
>
<IconComponent name="Square" className="h-4 w-4" />
@ -315,7 +330,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
</button>
</div>
</ShadTooltip>
)}
</div>
</div>
) : (
<></>

View file

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

View file

@ -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) {

View file

@ -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) => {

View file

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

View file

@ -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;

View file

@ -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 (
<ConfirmationModal
open={true}
onClose={onCancel}
destructiveCancel
title={"Exit without saving?"}
cancelText={"Exit anyway"}
confirmationText={"Save and Exit"}
icon={"Save"}
onConfirm={onSave}
title={truncate(flowName, { length: 32 }) + " has unsaved changes"}
cancelText={autoSave ? undefined : "Exit anyway"}
confirmationText={autoSave ? "Exit" : "Save and Exit"}
onConfirm={autoSave ? onProceed : onSave}
onCancel={onProceed}
loading={autoSave ? unsavedChanges : false}
size="x-small"
>
<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>
);

View file

@ -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 {
<div className={version ? "mt-2" : "mt-1"}> v{version}</div>
</a>
</div>
{blocker.state === "blocked" && (
{blocker.state === "blocked" && currentSavedFlow && (
<SaveChangesModal
onSave={handleSave}
onCancel={() => (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}
/>
)}
</>

View file

@ -228,6 +228,7 @@ const useFlowStore = create<FlowStoreType>((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<FlowStoreType>((set, get) => ({
edges: newChange,
flowState: undefined,
});
get().updateCurrentFlow({ edges: newChange });
if (get().autoSaveFlow) {
get().autoSaveFlow!();
}

View file

@ -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;

View file

@ -437,12 +437,13 @@ export type ConfirmationModalType = {
destructive?: boolean;
destructiveCancel?: boolean;
modalContentTitle?: string;
cancelText: string;
loading?: boolean;
cancelText?: string;
confirmationText: string;
children:
| [React.ReactElement<ContentProps>, React.ReactElement<TriggerProps>]
| React.ReactElement<ContentProps>;
icon: string;
icon?: string;
data?: any;
index?: number;
onConfirm: (index, data) => void;

View file

@ -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(