Repository: kestra-io/kestra
Issue: Save flow as draft
Labels: enhancement

### Feature description

As a Kestra beginner,

I imagined I could start a flow and leave it as a draft,

But I couldn’t save something with syntax errors

As a result, I spent more time than expected fixing my flow,

So I end up being late for other activities in my day.

*What would have helped?*

→ A way to save in draft. *Maybe in a deactivated mode?*

Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/ui/src/components/graph/Topology.vue b/ui/src/components/graph/Topology.vue
index 4696dd861f..8bff2a8b29 100644
--- a/ui/src/components/graph/Topology.vue
+++ b/ui/src/components/graph/Topology.vue
@@ -132,11 +132,14 @@
         return (props.isCreating ? "creation" : `${flow.namespace}.${flow.id}`) + "_draft";
     })
 
+    const autoRestorelocalStorageKey = computed(() => {
+        return "autoRestore-"+localStorageKey.value;
+    })
+    
     watch(() => store.getters["flow/taskError"], async () => {
         taskError.value = store.getters["flow/taskError"];
     });
 
-
     const flowables = () => {
         return props.flowGraph && props.flowGraph.flowables ? props.flowGraph.flowables : [];
     }
@@ -184,14 +187,18 @@
         generateGraph();
 
         if(!props.isReadOnly) {
-            const sourceFromLocalStorage = localStorage.getItem(localStorageKey.value);
+            let restoredLocalStorageKey;
+            const sourceFromLocalStorage = localStorage.getItem((restoredLocalStorageKey = autoRestorelocalStorageKey.value)) ?? localStorage.getItem((restoredLocalStorageKey = localStorageKey.value));
             if (sourceFromLocalStorage !== null) {
-                toast.confirm(props.isCreating ? t("save draft.retrieval.creation") : t("save draft.retrieval.existing", {flowFullName: `${flow.namespace}.${flow.id}`}), () => {
-                    flowYaml.value = sourceFromLocalStorage;
-                    onEdit(flowYaml.value);
-                })
+                if(restoredLocalStorageKey === autoRestorelocalStorageKey.value){
+                    onEdit(sourceFromLocalStorage);
+                }else {
+                    toast.confirm(props.isCreating ? t("save draft.retrieval.creation") : t("save draft.retrieval.existing", {flowFullName: `${flow.namespace}.${flow.id}`}), () => {
+                        onEdit(sourceFromLocalStorage);
+                    })
+                }
 
-                localStorage.removeItem(localStorageKey.value);
+                localStorage.removeItem(restoredLocalStorageKey);
             }
         }
     }
@@ -463,7 +470,10 @@
 
         window.removeEventListener("beforeunload", persistEditorWidth);
 
-        persistEditorWidth();
+        // Will get redirected to login page
+        if (!store.getters["auth/isLogged"] && haveChange.value) {
+            persistEditorContent(true);
+        }
     })
 
 
@@ -552,38 +562,33 @@
         }
     };
 
-    const showDraftPopup = (draftReason, refreshAfterSelect = false) => {
+    const persistEditorContent = (autoRestore) => {
+        if(autoRestore && localStorage.getItem(localStorageKey.value)) {
+            return;
+        }
+
+        localStorage.setItem(autoRestore ? autoRestorelocalStorageKey.value : localStorageKey.value, flowYaml.value);
+        store.dispatch("core/isUnsaved", false);
+        haveChange.value = false;
+    }
+
+    const showDraftPopup = (draftReason) => {
         toast.confirm(draftReason + " " + (props.isCreating ? t("save draft.prompt.creation") : t("save draft.prompt.existing", {
             namespace: flow.namespace,
             id: flow.id
         })),
             () => {
-                localStorage.setItem(localStorageKey.value, flowYaml.value);
-                store.dispatch("core/isUnsaved", false);
-                if(refreshAfterSelect){
-                    router.go();
-                }else {
-                    router.push({
-                        name: "flows/list"
-                    })
-                }
-            },
-            () => {
-                if(refreshAfterSelect) {
-                    store.dispatch("core/isUnsaved", false);
-                    router.go();
-                }
+                persistEditorContent(false);
             }
-        )
+        );
     }
 
     const onEdit = (event) => {
+        flowYaml.value = event;
+        haveChange.value = true;
+        store.dispatch("core/isUnsaved", true);
         return store.dispatch("flow/validateFlow", {flow: event})
             .then(value => {
-                flowYaml.value = event;
-                haveChange.value = true;
-                store.dispatch("core/isUnsaved", true);
-
                 if (!value[0].constraints) {
                     // flowYaml need to be update before
                     // loadGraphFromSource to avoid
@@ -598,10 +603,7 @@
                 }
 
                 return value;
-            }).catch(e => {
-                showDraftPopup(t("save draft.prompt.http_error"), true);
-                return Promise.reject(e);
-            })
+            });
     }
 
     const onDelete = (event) => {
diff --git a/ui/src/utils/axios.js b/ui/src/utils/axios.js
index 57dfe048af..e25e4b01bc 100644
--- a/ui/src/utils/axios.js
+++ b/ui/src/utils/axios.js
@@ -96,9 +96,14 @@ export default (callback, store, router) => {
                 return Promise.reject(errorResponse);
             }
 
-            if (errorResponse.response.status === 401 && (!store.getters["auth/isLogged"] || store.getters["auth/expired"])) {
-                window.location = "/ui/login?from=" + window.location.pathname +
-                    (window.location.search ? "?" + window.location.search : "")
+            if (errorResponse.response.status === 401
+                && !store.getters["auth/isLogged"]) {
+                if(window.location.pathname === "/ui/login"){
+                    return Promise.reject(errorResponse);
+                }
+
+                window.location = `/ui/login?from=${window.location.pathname +
+                (window.location.search ?? "")}`
             }
 
             if (errorResponse.response.status === 401 &&
@@ -111,6 +116,23 @@ export default (callback, store, router) => {
                 return Promise.reject(errorResponse);
             }
 
+            // Authentication expired
+            if (errorResponse.response.status === 401 &&
+                store.getters["auth/isLogged"]) {
+                document.body.classList.add("login")
+
+                store.dispatch("core/isUnsaved", false);
+                store.commit("auth/setUser", undefined);
+                store.commit("layout/setTopNavbar", undefined);
+                router.push({
+                    name: "login",
+                    query: {
+                        expired: 1,
+                        from: window.location.pathname + (window.location.search ?? "")
+                    }
+                })
+            }
+
             if (errorResponse.response.status === 400){
                 return Promise.reject(errorResponse.response.data)
             }
diff --git a/ui/src/utils/toast.js b/ui/src/utils/toast.js
index cd7de19296..a67f20c439 100644
--- a/ui/src/utils/toast.js
+++ b/ui/src/utils/toast.js
@@ -38,9 +38,6 @@ export default {
                         .then(() => {
                             callback();
                         })
-                        .catch(() => {
-                            callbackIfCancel()
-                        })
                 },
                 saved: function(name, title, options) {
                     ElNotification.closeAll();


Test patch: