Project mode by tabs/flow?

Hello, I regularly do flows that I share with the community, and I find that the sharing system via import / export is not very practical ...

I have set up with a friend a Flow sharing system on github using the Node-RED APIs, but I find that this is a basic functionality which would be really welcome :slight_smile:

Export mode to github sublow :

[{"id":"729bb0ac.7a14a","type":"subflow","name":"Flow to GitHub","info":"","category":"","in":[{"x":80,"y":80,"wires":[{"id":"408be944.f061c8"}]}],"out":[],"env":[{"name":"owner","type":"str","value":"","ui":{"label":{"en-US":"Owner"}}},{"name":"repo","type":"str","value":"","ui":{"label":{"en-US":"Repo"}}},{"name":"path_abs_file","type":"str","value":"","ui":{"label":{"en-US":"Path abs File"}}},{"name":"flow_name","type":"str","value":"","ui":{"label":{"en-US":"Flow Name"}}},{"name":"commit","type":"str","value":"","ui":{"label":{"en-US":"Commit"}}}],"color":"#DDAA99"},{"id":"ab50264c.6e1c08","type":"http request","z":"729bb0ac.7a14a","name":"get list Flows","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://{{user_nr}}:{{password_nr}}@localhost:1880/flows","tls":"","persist":false,"proxy":"","authType":"","x":310,"y":180,"wires":[["1ffc7c81.a29383"]]},{"id":"1ffc7c81.a29383","type":"function","z":"729bb0ac.7a14a","name":"search ID of Flow","func":"msg.id_flow = 'global' ;\n\nmsg.payload.forEach( (f) => {\n    // label => Flow else msg.id_flow = global (subflow)\n    if(f.label===msg.flow_name ){\n        msg.id_flow = f.id;\n    }\n});\n\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":510,"y":180,"wires":[["9785d63a.081b98"]]},{"id":"9785d63a.081b98","type":"switch","z":"729bb0ac.7a14a","name":"If id_flow exist","property":"id_flow","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":720,"y":180,"wires":[["80f932e1.fcfae"],["45f4b435.53a2cc"]]},{"id":"512fcbd.083b934","type":"http request","z":"729bb0ac.7a14a","name":"GitHub : Commit Flow","method":"PUT","ret":"obj","paytoqs":"ignore","url":"https://api.github.com/repos/{{flow_owner}}/{{flow_repo}}/contents/{{flow_file}}","tls":"","persist":false,"proxy":"","authType":"","x":840,"y":680,"wires":[["9286263c.e17ff8"]]},{"id":"408be944.f061c8","type":"change","z":"729bb0ac.7a14a","name":"Param into msg","rules":[{"t":"set","p":"flow_name","pt":"msg","to":"flow_name","tot":"env"},{"t":"set","p":"flow_file","pt":"msg","to":"path_abs_file","tot":"env"},{"t":"set","p":"flow_repo","pt":"msg","to":"repo","tot":"env"},{"t":"set","p":"flow_owner","pt":"msg","to":"owner","tot":"env"},{"t":"set","p":"commit","pt":"msg","to":"commit","tot":"env"}],"action":"","property":"","from":"","to":"","reg":false,"x":240,"y":80,"wires":[["ab50264c.6e1c08"]]},{"id":"80f932e1.fcfae","type":"change","z":"729bb0ac.7a14a","name":"Error","rules":[{"t":"set","p":"payload","pt":"msg","to":"\"Flow \" & $.flow_name & \" not exist !\"","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":940,"y":180,"wires":[["863ba6f6.99cc38"]]},{"id":"294ec987.ad4976","type":"http request","z":"729bb0ac.7a14a","name":"GitHub : Get sha of low_file","method":"GET","ret":"obj","paytoqs":"ignore","url":"https://api.github.com/repos/{{flow_owner}}/{{flow_repo}}/contents/{{flow_file}}","tls":"","persist":false,"proxy":"","authType":"","x":760,"y":560,"wires":[["ab48eded.d9429"]]},{"id":"b41d3f05.c51da","type":"change","z":"729bb0ac.7a14a","name":"Param http","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\t\"accept\":\"application/vnd.github.v3+json\",\t\"message\":\"Update\",\t\"content\":$base64encode($.payload),\t\"encoding\":\"utf-8\"\t}","tot":"jsonata"},{"t":"set","p":"headers","pt":"msg","to":"{\"User-Agent\":\"\",\"Authorization\":\"token \"&$.token }","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":490,"y":560,"wires":[["294ec987.ad4976"]]},{"id":"ab48eded.d9429","type":"change","z":"729bb0ac.7a14a","name":"Set flow_sha","rules":[{"t":"set","p":"flow_sha","pt":"msg","to":"payload.sha","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":560,"wires":[["4b843044.03641"]]},{"id":"4076ad5f.e2bef4","type":"http request","z":"729bb0ac.7a14a","name":"get Flow of Id","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://{{user_nr}}:{{password_nr}}@localhost:1880/flow/{{id_flow}}","tls":"","persist":false,"proxy":"","authType":"","x":480,"y":460,"wires":[["caa3060f.9736d8"]]},{"id":"caa3060f.9736d8","type":"change","z":"729bb0ac.7a14a","name":"Set flow_content base64","rules":[{"t":"set","p":"flow_content","pt":"msg","to":"$base64encode($.payload)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":460,"wires":[["b41d3f05.c51da"]]},{"id":"4b843044.03641","type":"function","z":"729bb0ac.7a14a","name":"Param http GitHub Commit","func":"msg.headers = {\n    \"User-Agent\":\"\",\n    \"Authorization\":\"token \" + msg.token\n};\n\nmsg.payload = {\n    \"accept\":\"application/vnd.github.v3+json\",\n    \"message\":msg.commit,\n    \"content\": msg.flow_content,\n    \"encoding\":\"utf-8\"\n} ;\n\nmsg.method = 'PUT' ; \n\nif(msg.flow_sha != undefined ){\n    msg.payload.sha = msg.flow_sha ;\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":540,"y":680,"wires":[["512fcbd.083b934"]]},{"id":"863ba6f6.99cc38","type":"debug","z":"729bb0ac.7a14a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1130,"y":180,"wires":[]},{"id":"45f4b435.53a2cc","type":"switch","z":"729bb0ac.7a14a","name":"If id_flow == global => Subflow","property":"id_flow","propertyType":"msg","rules":[{"t":"eq","v":"global","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":170,"y":340,"wires":[["a6c31417.226148"],["4076ad5f.e2bef4"]]},{"id":"a6c31417.226148","type":"http request","z":"729bb0ac.7a14a","name":"get Flow of global","method":"GET","ret":"obj","paytoqs":"ignore","url":"http://{{user_nr}}:{{password_nr}}@localhost:1880/flow/{{id_flow}}","tls":"","persist":false,"proxy":"","authType":"","x":490,"y":280,"wires":[["ca0f65e5.0343a8"]]},{"id":"ca0f65e5.0343a8","type":"function","z":"729bb0ac.7a14a","name":"search content of SubFlow","func":"msg.payload.subflows.forEach( (f) => {\n    // name => SubFlow\n    if(f.name===msg.flow_name ){\n        msg.subflow = f;\n    }\n});\n\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":760,"y":280,"wires":[["480c33ac.05a9ac"]]},{"id":"480c33ac.05a9ac","type":"change","z":"729bb0ac.7a14a","name":"Convert payload string","rules":[{"t":"set","p":"payload","pt":"msg","to":"$string($.subflow)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":360,"wires":[["caa3060f.9736d8"]]},{"id":"9286263c.e17ff8","type":"debug","z":"729bb0ac.7a14a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1090,"y":680,"wires":[]},{"id":"30fad59e.43ca3a","type":"inject","z":"4787bdd6.9aa874","name":"Manual","props":[{"p":"token","v":"gfdgfdgdf","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":110,"y":970,"wires":[["c10eec2.9b9d51"]]},{"id":"c10eec2.9b9d51","type":"credentials","z":"4787bdd6.9aa874","name":"Credentials","props":[{"value":"token","type":"msg"},{"value":"user_nr","type":"msg"},{"value":"password_nr","type":"msg"}],"x":250,"y":970,"wires":[["78528e2f.4f953"]]},{"id":"ca4787ed.90fdf8","type":"subflow:729bb0ac.7a14a","z":"4787bdd6.9aa874","name":"Export Flow to Github","env":[{"name":"owner","value":"m4dm4rtig4n","type":"str"},{"name":"repo","value":"node-red-enedis-gateway","type":"str"},{"name":"path_abs_file","value":"enedis.flow","type":"str"},{"name":"flow_name","value":"Enedis","type":"str"},{"name":"commit","value":"Check influxdb output","type":"str"}],"x":660,"y":940,"wires":[]},{"id":"78528e2f.4f953","type":"change","z":"4787bdd6.9aa874","name":"","rules":[{"t":"set","p":"commit","pt":"msg","to":"Fix divers bug","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":970,"wires":[["743efd59.d9c634","ca4787ed.90fdf8"]]}]

Import node call subflow :

[{"id":"3f067bb0.be5694","type":"subflow","name":"GitHub to Flow","info":"","category":"","in":[{"x":60,"y":120,"wires":[{"id":"f096e2c8.6dcd8"}]}],"out":[],"env":[{"name":"Url Git","type":"str","value":""}],"color":"#DDAA99"},{"id":"f096e2c8.6dcd8","type":"change","z":"3f067bb0.be5694","name":"Param into msg","rules":[{"t":"set","p":"flow_git","pt":"msg","to":"Url Git","tot":"env"}],"action":"","property":"","from":"","to":"","reg":false,"x":210,"y":120,"wires":[["3b0af01f.b841f"]]},{"id":"c4e5aead.108f7","type":"http request","z":"3f067bb0.be5694","name":"get Flow of git","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":570,"y":120,"wires":[["5d0856dc.666f88"]]},{"id":"3b0af01f.b841f","type":"change","z":"3f067bb0.be5694","name":"Url Git","rules":[{"t":"set","p":"url","pt":"msg","to":"flow_git","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":390,"y":120,"wires":[["c4e5aead.108f7"]]},{"id":"46447174.105c","type":"function","z":"3f067bb0.be5694","name":"Format Token URL","func":"msg.content = msg.payload;\nmsg.url = \"http://localhost:1880/auth/token\";\nmsg.payload = {\n    'client_id': 'node-red-admin',\n    'grant_type': 'password',\n    'scope': '*',\n    'username': msg.user_nr,\n    'password': msg.password_nr,\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":910,"y":160,"wires":[["cfc9b165.3e53c"]]},{"id":"cfc9b165.3e53c","type":"http request","z":"3f067bb0.be5694","name":"Get token","method":"POST","ret":"txt","paytoqs":"body","url":"http://localhost:1880/auth/token","tls":"","persist":false,"proxy":"","authType":"","x":1100,"y":160,"wires":[["86be6512.cb4fc8"]]},{"id":"86be6512.cb4fc8","type":"json","z":"3f067bb0.be5694","name":"","property":"payload","action":"","pretty":false,"x":1250,"y":160,"wires":[["ca09c96f.e564d8"]]},{"id":"24be036c.9513bc","type":"function","z":"3f067bb0.be5694","name":"params","func":"msg.flow_name = msg.content.label ; \n\nvar access_token = msg.payload.access_token\n\nmsg.access_token = access_token;\nmsg.headers = {};\nmsg.headers['Authorization'] = 'Bearer '+access_token;\n\nmsg.url = 'http://localhost:1880/flows'\nmsg.type= 'flow';\nif( msg.content.type === 'subflow' ){\n    msg.type='subflow';\n    msg.flow_name = msg.content.name ; \n    msg.url = 'http://localhost:1880/flow/global' ;\n}\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":180,"y":220,"wires":[["a7c9013e.1898b"]]},{"id":"1d972e1d.cc7272","type":"function","z":"3f067bb0.be5694","name":"Param http","func":"var access_token = msg.access_token\n\nmsg.method = 'POST' ; // add flow if flow_id not exist\nmsg.url = 'http://localhost:1880/flow' ;\nmsg.headers = {};\nmsg.headers['Authorization'] = 'Bearer '+access_token;\n\nif(msg.type==='flow' ){\n    if(msg.flow_id != undefined ){\n        msg.method = 'PUT' ;  // update flow  \n        msg.url += '/'+ msg.flow_id ;\n    }\n}else{// update subflow  \n    msg.method = 'PUT' ;  // update flow  \n    msg.url += '/global' ;\n}\n\nmsg.payload = msg.content;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":810,"y":220,"wires":[["7860653e.b2904c"]]},{"id":"a7c9013e.1898b","type":"http request","z":"3f067bb0.be5694","name":"get list Flows","method":"GET","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"basic","x":350,"y":220,"wires":[["40bc6ff6.837f3"]]},{"id":"40bc6ff6.837f3","type":"function","z":"3f067bb0.be5694","name":"search ID of Flows","func":"msg.flow_id2 = msg.flow_id ;\nmsg.flow_id = undefined ;\n\nnode.log('type : '+msg.type);\n\nif(msg.type==='flow'){\n    msg.payload.forEach( (f) => {\n        if(f.label===msg.flow_name){\n            msg.flow_id = f.id;\n        }\n    });\n\n}else{\n    for(let i=0 ; i<msg.payload.subflows.length; i++){\n        const f = msg.payload.subflows[i] ;\n        // name => SubFlow\n        if(f.name===msg.flow_name ){\n            msg.flow_id = 'global';\n            msg.payload.subflows[i] = msg.content ;\n        }\n    }\n    if(msg.flow_id===undefined){\n        msg.payload.subflows.push(msg.content) ;\n    }\n    // il faut renvoyer le global entier modifié\n    msg.content=msg.payload;\n\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":570,"y":220,"wires":[["1d972e1d.cc7272"]]},{"id":"7860653e.b2904c","type":"http request","z":"3f067bb0.be5694","name":"Create Or update Flow","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":1040,"y":220,"wires":[["8e24916e.62eb1"]]},{"id":"8e24916e.62eb1","type":"debug","z":"3f067bb0.be5694","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1240,"y":220,"wires":[]},{"id":"5d0856dc.666f88","type":"switch","z":"3f067bb0.be5694","name":"Auth ?","property":"user_nr","propertyType":"msg","rules":[{"t":"empty"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":730,"y":120,"wires":[["2158d050.550da"],["46447174.105c"]]},{"id":"d4cf0165.6f89","type":"link out","z":"3f067bb0.be5694","name":"","links":["ef0433e3.96e54"],"x":1015,"y":80,"wires":[]},{"id":"ca09c96f.e564d8","type":"link out","z":"3f067bb0.be5694","name":"","links":["ef0433e3.96e54"],"x":1335,"y":160,"wires":[]},{"id":"ef0433e3.96e54","type":"link in","z":"3f067bb0.be5694","name":"","links":["d4cf0165.6f89","ca09c96f.e564d8"],"x":85,"y":220,"wires":[["24be036c.9513bc"]]},{"id":"2158d050.550da","type":"change","z":"3f067bb0.be5694","name":"Save content","rules":[{"t":"set","p":"content","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":80,"wires":[["d4cf0165.6f89"]]},{"id":"b591e346.b4ae7","type":"inject","z":"c07c0ac5.ca6f98","name":"Import","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":130,"y":120,"wires":[["135dc559.69ad1b"]]},{"id":"135dc559.69ad1b","type":"credentials","z":"c07c0ac5.ca6f98","name":"Credentials","props":[{"value":"user_nr","type":"msg"},{"value":"password_nr","type":"msg"}],"x":330,"y":120,"wires":[["7db677b7.764f18"]]},{"id":"7db677b7.764f18","type":"subflow:3f067bb0.be5694","z":"c07c0ac5.ca6f98","name":"Import Github Flow to onprem","env":[{"name":"Url Git","value":"https://raw.githubusercontent.com/m4dm4rtig4n/node-red-enedis-gateway/main/enedis.flow","type":"str"},{"name":"Flow Name","value":"Test","type":"str"}],"x":580,"y":120,"wires":[]}]