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:
parent
14ca9c9f9d
commit
00d2cffe46
13 changed files with 157 additions and 80 deletions
|
|
@ -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>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue