Repository: kestra-io/kestra
Issue: Allow creating + saving flows from VS Code with full autocompletion (kestra flow schema is currently not preloaded)

Currently, fetching the flow schema requires manual intervention from the user. Fixing that is a priority in this issue. 

Then, we need to add an option to allow adding flow files directly to the `_flows` folder.

Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/ui/public/init.js b/ui/public/init.js
new file mode 100644
index 0000000000..0dec0bef22
--- /dev/null
+++ b/ui/public/init.js
@@ -0,0 +1,78 @@
+Object.keys(window.webPackagePaths).map(function (key) {
+    window.webPackagePaths[key] = `${window.location.origin}${KESTRA_UI_PATH}vscode-web/dist/node_modules/${key}/${window.webPackagePaths[key]}`;
+});
+
+require.config({
+    baseUrl: `${window.location.origin}${KESTRA_UI_PATH}vscode-web/dist/out`,
+    recordStats: true,
+    trustedTypesPolicy: window.trustedTypes?.createPolicy("amdLoader", {
+        createScriptURL(value) {
+            return value;
+        }
+    }),
+    paths: window.webPackagePaths
+});
+
+// used to configure VSCode startup
+window.product = {
+    productConfiguration: {
+        nameShort: "Kestra VSCode",
+        nameLong: "Kestra VSCode",
+        // configure the open sx marketplace
+        "extensionsGallery": {
+            "serviceUrl": "https://open-vsx.org/vscode/gallery",
+            "itemUrl": "https://open-vsx.org/vscode/item",
+            "resourceUrlTemplate": "https://openvsxorg.blob.core.windows.net/resources/{publisher}/{name}/{version}/{path}"
+        },
+    },
+    // scope the VSCode instance to Kestra File System Provider (defined in Kestra VSCode extension)
+    folderUri: {
+        scheme: "kestra",
+        path: "/" + queryParams["namespace"]
+    },
+    commands: [
+        {
+            id: "custom.postMessage",
+            handler: async (data) => {
+                window.parent.postMessage(data, "*")
+            }
+        }
+    ],
+    additionalBuiltinExtensions: [
+        {
+            scheme: "https",
+            authority: "openvsxorg.blob.core.windows.net",
+            path: "/resources/PROxZIMA/sweetdracula/1.0.9/extension"
+        },
+        {
+            scheme: window.location.protocol.replace(":", ""),
+            authority: window.location.host,
+            path: KESTRA_UI_PATH + "yamlExt"
+        },
+        {
+            scheme: "https",
+            authority: "openvsxorg.blob.core.windows.net",
+            path: "/resources/kestra-io/kestra/0.1.4/extension"
+        }
+    ],
+    "linkProtectionTrustedDomains": [
+        "https://open-vsx.org",
+        "https://openvsxorg.blob.core.windows.net"
+    ],
+    enabledExtensions: [
+        // to handle dark theme
+        "proxzima.sweetdracula",
+        // to apply Kestra's flow validation schema
+        "redhat.vscode-yaml",
+        "kestra-io.kestra"
+    ],
+    configurationDefaults: {
+        "files.autoSave": "off",
+        "editor.fontSize": 12,
+        "workbench.colorTheme": THEME === "dark" ? "Sweet Dracula" : "Default Light Modern",
+        "workbench.startupEditor": "readme",
+        // "workbench.activityBar.visible": false,
+        // provide the Kestra root URL to extension
+        "kestra.api.url": KESTRA_API_URL
+    }
+};
\ No newline at end of file
diff --git a/ui/public/vscode.html b/ui/public/vscode.html
index 05e20cfa10..bda5bfd2f8 100644
--- a/ui/public/vscode.html
+++ b/ui/public/vscode.html
@@ -25,76 +25,7 @@
 
 <script src="./vscode-web/dist/out/vs/loader.js"></script>
 <script src="./vscode-web/dist/out/vs/webPackagePaths.js"></script>
-<script>
-    Object.keys(window.webPackagePaths).map(function (key) {
-        window.webPackagePaths[key] = `${window.location.origin}${KESTRA_UI_PATH}vscode-web/dist/node_modules/${key}/${window.webPackagePaths[key]}`;
-    });
-
-    require.config({
-        baseUrl: `${window.location.origin}${KESTRA_UI_PATH}vscode-web/dist/out`,
-        recordStats: true,
-        trustedTypesPolicy: window.trustedTypes?.createPolicy("amdLoader", {
-            createScriptURL(value) {
-                return value;
-            }
-        }),
-        paths: window.webPackagePaths
-    });
-
-    // used to configure VSCode startup
-    window.product = {
-        productConfiguration: {
-            nameShort: "Kestra VSCode",
-            nameLong: "Kestra VSCode",
-            // configure the open sx marketplace
-            "extensionsGallery": {
-                "serviceUrl": "https://open-vsx.org/vscode/gallery",
-                "itemUrl": "https://open-vsx.org/vscode/item",
-                "resourceUrlTemplate": "https://openvsxorg.blob.core.windows.net/resources/{publisher}/{name}/{version}/{path}"
-            },
-        },
-        // scope the VSCode instance to Kestra File System Provider (defined in Kestra VSCode extension)
-        folderUri: {
-            scheme: "kestra",
-            path: "/" + queryParams["namespace"]
-        },
-        additionalBuiltinExtensions: [
-            {
-                scheme: "https",
-                authority: "openvsxorg.blob.core.windows.net",
-                path: "/resources/PROxZIMA/sweetdracula/1.0.9/extension"
-            },
-            {
-                scheme: window.location.protocol.replace(":", ""),
-                authority: window.location.host,
-                path: KESTRA_UI_PATH + "yamlExt"
-            },
-            {
-                scheme: "https",
-                authority: "openvsxorg.blob.core.windows.net",
-                path: "/resources/kestra-io/kestra/0.1.2/extension"
-            },
-        ],
-        "linkProtectionTrustedDomains": [
-            "https://open-vsx.org",
-            "https://openvsxorg.blob.core.windows.net"
-        ],
-        enabledExtensions: [
-            // to handle dark theme
-            "proxzima.sweetdracula",
-            // to apply Kestra's flow validation schema
-            "redhat.vscode-yaml",
-            "kestra-io.kestra"
-        ],
-        configurationDefaults: {
-            "files.autoSave": "off",
-            "editor.fontSize": 12,
-            "workbench.colorTheme": THEME === "dark" ? "Sweet Dracula" : "Default Light Modern",
-            // provide the Kestra root URL to extension
-            "kestra.api.url": KESTRA_API_URL
-        }
-    };
-</script>
+<script src="./init.js"></script>
 <script src="./vscode-web/dist/out/vs/workbench/workbench.web.main.nls.js"></script>
 <script src="./vscode-web/dist/out/vs/workbench/workbench.web.main.js"></script>
 <script src="./vscode-web/dist/out/vs/code/browser/workbench/workbench.js"></script>
diff --git a/ui/src/components/flows/TriggerFlow.vue b/ui/src/components/flows/TriggerFlow.vue
index 02e60fc699..1616d13d40 100644
--- a/ui/src/components/flows/TriggerFlow.vue
+++ b/ui/src/components/flows/TriggerFlow.vue
@@ -103,6 +103,18 @@
                         this.onClick();
                     }
                 }
+            },
+            flowId: {
+                handler() {
+                    if ((!this.flow || this.flow.id !== this.flowId) && this.flowId && this.namespace) {
+                        this.$store
+                            .dispatch("flow/loadFlow", {
+                                id: this.flowId,
+                                namespace: this.namespace,
+                                allowDeleted: true
+                            });
+                    }
+                }
             }
         }
     };
diff --git a/ui/src/components/namespace/Editor.vue b/ui/src/components/namespace/Editor.vue
index e95578e052..714b4d7a93 100644
--- a/ui/src/components/namespace/Editor.vue
+++ b/ui/src/components/namespace/Editor.vue
@@ -1,10 +1,18 @@
 <template>
     <top-nav-bar :title="routeInfo.title">
         <template #additional-right>
-            <namespace-select class="fit-content"
+            <namespace-select
+                class="fit-content"
                 data-type="flow"
-                              :value="namespace"
-                              @update:model-value="namespaceUpdate"/>
+                :value="namespace"
+                @update:model-value="namespaceUpdate"
+                allow-create
+            />
+            <trigger-flow
+                :disabled="!flow"
+                :flow-id="flow"
+                :namespace="namespace"
+            />
         </template>
     </top-nav-bar>
     <iframe
@@ -13,6 +21,7 @@
         v-if="namespace"
         class="vscode-editor"
         :src="vscodeIndexUrl"
+        ref="vscodeIde"
     />
     <div v-else class="m-3 mw-100">
         <el-alert type="info" :closable="false">
@@ -24,6 +33,7 @@
 <script setup>
     import NamespaceSelect from "./NamespaceSelect.vue";
     import TopNavBar from "../layout/TopNavBar.vue";
+    import TriggerFlow from "../flows/TriggerFlow.vue";
 </script>
 
 <script>
@@ -40,8 +50,43 @@
                         namespace
                     }
                 });
+            },
+            handleTabsDirty(tabs) {
+                // Add tabs not saved
+                this.tabsNotSaved = this.tabsNotSaved.concat(tabs.dirty)
+                // Removed tabs closed
+                this.tabsNotSaved = this.tabsNotSaved.filter(e => !tabs.closed.includes(e))
+                console.log(this.tabsNotSaved)
+                this.$store.dispatch("core/isUnsaved", this.tabsNotSaved.length > 0);
+            }
+        },
+        data() {
+            return {
+                flow: null,
+                tabsNotSaved: []
             }
         },
+        created() {
+            const namespace = localStorage.getItem("defaultNamespace");
+            if (namespace) {
+                this.namespaceUpdate(namespace);
+            }
+        },
+        mounted() {
+            window.addEventListener("message", (event) => {
+                const message = event.data;
+                if (message.type === "kestra.tabFileChanged") {
+                    const path = `/${this.namespace}/_flows/`;
+                    if (message.filePath.path.startsWith(path)) {
+                        this.flow = message.filePath.path.split(path)[1].replace(".yml", "");
+                    } else {
+                        this.flow = null;
+                    }
+                } else if (message.type === "kestra.tabsChanged") {
+                    this.handleTabsDirty(message.tabs);
+                }
+            });
+        },
         computed: {
             routeInfo() {
                 return {
diff --git a/ui/src/components/namespace/NamespaceSelect.vue b/ui/src/components/namespace/NamespaceSelect.vue
index 072d28180e..d090ccccb6 100644
--- a/ui/src/components/namespace/NamespaceSelect.vue
+++ b/ui/src/components/namespace/NamespaceSelect.vue
@@ -6,6 +6,7 @@
         :placeholder="$t('Select namespace')"
         :persistent="false"
         filterable
+        :allow-create="allowCreate"
     >
         <el-option
             v-for="item in groupedNamespaces"
@@ -29,6 +30,10 @@
             value: {
                 type: String,
                 default: undefined
+            },
+            allowCreate: {
+                type: Boolean,
+                default: false
             }
         },
         emits: ["update:modelValue"],
diff --git a/ui/src/utils/submitTask.js b/ui/src/utils/submitTask.js
index 604512a71b..8d4f425ee4 100644
--- a/ui/src/utils/submitTask.js
+++ b/ui/src/utils/submitTask.js
@@ -38,7 +38,8 @@ export const executeTask = (submitor, flow, values, options) => {
         .then(response => {
             submitor.$store.commit("execution/setExecution", response.data)
             if (options.redirect) {
-                submitor.$router.push({name: "executions/update", params: {...{namespace: response.data.namespace, flowId: response.data.flowId, id: response.data.id}, ...{tab: "gantt"}}})
+                const resolved = submitor.$router.resolve({name: "executions/update", params: {...{namespace: response.data.namespace, flowId: response.data.flowId, id: response.data.id}, ...{tab: "gantt"}}})
+                window.open(resolved.href, "_blank")
             }
 
             return response.data;


Test patch: