Subflow for generating D2 UI widget

Creating all the files for a basic D2 UI node is somewhat tedious, so I have developed a subflow to create the necessary files, with basic content, which will build to a functioning widget.

If anyone would like to try it out that would be great. In particular I have no idea if it will work on Windows, with the file paths specified with backslashes.

These instructions are from the subflow help text:

A subflow that generates a basic UI widget for node-red Dashboard 2 (@flowfuse/node-red-dashboard).

Usage

  1. Clone or download the skeleton node definition files from
    GitHub - colinl/node-red-dashboard-2-skeleton-node: A skeleton set of files to be processed by the Create D2 UI Node to create a basic ui node to a folder accessible to node-red.

  2. Install the node node-red-contrib-fs-ops. When I have time I will remove this dependency, but at the moment it is required.

  3. Add an instance of the subflow to the node red flow, with an inject node to trigger it and
    a debug node on the output.
    Import the flow below, consisting of the subflow, an Inject node to trigger it and a debug node to show the result.

  4. Configure the subflow as described below.

  5. Click the inject node to generate the UI node.

Configuration

  • Skeleton source dir - Set this to full path to the folder where you have downloaded the skeleton node files.

  • Destination dir - The full path to where you would like the node to be generated. The flow will create the directory if necessary.

  • Allow Overwrite Destination - If false then the destination directory must be empty (or non-existent). If the directory is not empty then the flow will fail, passing on an appropriate error message. If set true then the flow will overwrite the destination files.

  • Node Name - This is the name that the node will be given. It is the name that appears in the node-red section in package.json.

  • Widget component name - This is the name that the dashboard knows the widget by. The name of the vue file in ui/components will be <ComponentName>.vue

  • Package name - The name of the package. It is the name that will be used to install it if it is released. So it might be @me/node-red-dashboard-2-<Node Name>

  • Source repository - The url to the source directory that will be put in package.json. The repository need not exist at this time.

  • Author Name - The author name that will be put in package.json.

  • Author URL - The author URL that will be put in package.json.

Outputs

When it has finished the node sends a message with a string in the payload. If it succeeded then the payload is "Done", otherwise it will indicate the error.

Building the widget

To build and install the widget locally:

  1. cd into the destination directory configured above.

  2. Run npm install to install dependencies.

  3. Build it using NODE_ENV=development npm run build which builds a version with a map files for easier debugging.

  4. From the node red user directory (often .node-red) install it using npm install /path/to/ui-node.

  5. Restart node-red.

Here is the subflow

[{"id":"dc8909c81da41d3c","type":"subflow","name":"D2 UI Node Creator","info":"A subflow that generates a basic UI widget for node-red Dashboard 2 (@flowfuse/node-red-dashboard).\n\n## Usage\n1. Clone or download the skeleton node definition files from\nhttps://github.com/colinl/node-red-dashboard-2-skeleton-node to a folder accessible to node-red.\n2. Add an instance of the subflow to the node red flow, with an inject node to trigger it and\na debug node on the output.\n3. Configure the subflow as described below.\n4. Click the inject node to generate the UI node.\n\n## Configuration\n* **Skeleton source dir** - Set this to full path to the folder where you have downloaded the skeleton node files.\n\n* **Destination dir** - The full path to where you would like the node to be generated. The flow will create the\ndirectory if necessary. \n\n* **Allow Overwrite Destination** - If `false` then the destination directory must be empty (or non-existent). If the\ndirectory is not empty then the flow will fail, passing on an appropriate error message.  If set `true` then the flow\nwill overwrite the destination files.\n\n* **Node Name** - This is the name that the node will be given.  It is the name that appears in the `node-red` section\nin `package.json`.\n\n* **Widget component name** - This is the name that the dashboard knows the widget by.  The name of the vue file in \n`ui/components` will be `<ComponentName>.vue`\n\n* **Package name** - The name of the package. It is the name that will be used to install it if it is released. So it \nmight be `@me/node-red-dashboard-2-<Node Name>`\n\n* **Source repository** - The url to the source directory that will be put in package.json.  The repository need not\nexist at this time.\n\n* **Author Name** - The author name that will be put in `package.json`.\n\n* **Author URL** - The author URL that will be put in `package.json`.\n\n## Outputs\n\nWhen it has finished the node sends a message with a string in the payload. If it succeeded then the payload is \"Done\",\notherwise it will indicate the error.\n\n## Building the widget\nTo build and install the widget locally:\n1. `cd` into the destination directory configured above.\n2. Run `npm install` to install dependencies.\n3. Build it using `NODE_ENV=development npm run build` which builds a version with a map files\nfor easier debugging.\n4. From the node red user directory (often `.node-red`) install it using `npm install /path/to/ui-node`.\n5. Restart node-red.","category":"","in":[{"x":40,"y":160,"wires":[{"id":"dee5145fe37addda"}]}],"out":[{"x":1380,"y":600,"wires":[{"id":"9c4fefc4aca32858","port":0},{"id":"443f83dc86ee0e6f","port":0},{"id":"0b38110ee98d622c","port":0}]}],"env":[{"name":"TEMPLATE_DIR","type":"str","value":"/path/to/skeleton-source","ui":{"label":{"en-US":"Skeleton source dir"},"type":"input","opts":{"types":["str","env"]}}},{"name":"DESTINATION_DIR","type":"str","value":"/path/to/destination-dir","ui":{"label":{"en-US":"Destination dir"},"type":"input","opts":{"types":["str","env"]}}},{"name":"ALLOW_OVERWRITE","type":"bool","value":"false","ui":{"label":{"en-US":"Allow Overwrite Destination"},"type":"input","opts":{"types":["bool"]}}},{"name":"NODE_NAME","type":"str","value":"ui-mynode","ui":{"label":{"en-US":"Node Name"},"type":"input","opts":{"types":["str","env"]}}},{"name":"NodeName","type":"str","value":"UIMynode","ui":{"label":{"en-US":"Widget component name"},"type":"input","opts":{"types":["str","env"]}}},{"name":"PACKAGE_NAME","type":"str","value":"@me/node-red-dashboard-2-ui-mynode","ui":{"label":{"en-US":"Package name"},"type":"input","opts":{"types":["str","num","bool","json","bin","env","conf-types"]}}},{"name":"REPOSITORY","type":"str","value":"https://github.com/me/node-red-dashboard-2-ui-mynode.git","ui":{"label":{"en-US":"Source repository"},"type":"input","opts":{"types":["str","env"]}}},{"name":"AUTHOR_NAME","type":"str","value":"My Name","ui":{"label":{"en-US":"Author Name"},"type":"input","opts":{"types":["str","env"]}}},{"name":"AUTHOR_URL","type":"str","value":"https://github/me","ui":{"label":{"en-US":"Author URL"},"type":"input","opts":{"types":["str","env"]}}}],"meta":{"module":"D2 UI Node Creator","version":"1.0.0","author":"Colin Law","desc":"A subflow to create a basic flowfuse dashboard UI node","license":"Apache-2.0"},"color":"#DDAA99","status":{"x":1180,"y":880,"wires":[{"id":"85b4f6863b6f2a01","port":0}]}},{"id":"43e304633fc739de","type":"fs-ops-stats","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Check template dir exists","path":"","pathType":"str","filename":"${TEMPLATE_DIR}","filenameType":"str","stats":"stats","sizeType":"msg","x":310,"y":160,"wires":[["d4ee892bb9a294d3"]]},{"id":"d978c5a54c218e5f","type":"catch","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"","scope":["43e304633fc739de"],"uncaught":false,"x":242.9999771118164,"y":119.99992513656616,"wires":[["560a800d3a24c36a"]]},{"id":"560a800d3a24c36a","type":"template","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"template dir not found","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Error: Template folder {{{env.TEMPLATE_DIR}}} not found.","output":"str","x":412.9999771118164,"y":119.99992513656616,"wires":[["9c4fefc4aca32858"]]},{"id":"d4ee892bb9a294d3","type":"fs-ops-mkdir","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Create destination dir","path":"","pathType":"str","dirname":"${DESTINATION_DIR}","dirnameType":"str","recursive":true,"mode":"777","fullpath":"directory","fullpathType":"msg","x":580,"y":200,"wires":[["f65f6fab9ef044c1"]]},{"id":"877ea824ae42694f","type":"catch","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"","scope":["d4ee892bb9a294d3","d57d9e1c13161f9e"],"uncaught":false,"x":522.9999771118164,"y":159.99992513656616,"wires":[["57faa25e61252961"]]},{"id":"57faa25e61252961","type":"template","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Cannot create destination dir","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Error: Unable to create Destination dir {{{env.DESTINATION_DIR}}} not found.","output":"str","x":732.9999771118164,"y":159.99992513656616,"wires":[["9c4fefc4aca32858"]]},{"id":"d57d9e1c13161f9e","type":"fs-ops-mkdir","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Create examples dir","path":"${DESTINATION_DIR}","pathType":"str","dirname":"examples","dirnameType":"str","recursive":false,"mode":"777","fullpath":"directory","fullpathType":"msg","x":300,"y":300,"wires":[["71ea4bec3183f258"]]},{"id":"354f8c3bf93fe38d","type":"split","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","property":"payload","x":470,"y":500,"wires":[["01b8d48d5154dde6"]]},{"id":"01b8d48d5154dde6","type":"switch","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Folder/file","property":"payload.type","propertyType":"msg","rules":[{"t":"eq","v":"directory","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":700,"y":500,"wires":[["940ac4a55c8227b0"],["be2e2db50a439e33"]]},{"id":"ba6cec4ffb9f4cb2","type":"change","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Move child array to payload","rules":[{"t":"move","p":"payload.child","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":980,"y":440,"wires":[["354f8c3bf93fe38d"]]},{"id":"d99a5d55c1bd45b6","type":"change","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Move filename to payload","rules":[{"t":"move","p":"payload.path","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1010,"y":560,"wires":[["6d5262f4c7981687"]]},{"id":"a3cf456c8644db8b","type":"function","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Read Folder","func":"// fs is defined in setup tab\nvar path = (msg.path)?msg.path:(env.get(\"TEMPLATE_DIR\"));\nvar setRecursive = true //(msg.recursive && typeof msg.recursive === \"boolean\")?  msg.recursive : env.get(\"setRecursive\");\n//node.status({text:\"reading\",fill:\"blue\"})\nfunction getDirectoryRecursive(__dirname) {\n    var files =  fs.readdirSync(__dirname)\n    for(var i=0; i<files.length;i++){\n        var file  = fs.statSync(`${__dirname}/${files[i]}`);\n        file.name = files[i]\n        file.path = `${__dirname}/${files[i]}`\n        file.child = []\n        if(file.isFile()){\n            file.type = \"file\"\n            files[i] = file\n        }\n        else if(file.isDirectory()){\n            //node.warn(`Directory: ${file.name}`)\n            file.type = \"directory\"\n            files[i] = file\n             file.child=getDirectoryRecursive(file.path)\n        }\n    }\n    //node.warn(`files: ${JSON.stringify(files)}`)\n     return files\n}\nfunction getDirectory(__dirname) {\n    var files =  fs.readdirSync(__dirname)\n    for(var i=0; i<files.length;i++){\n        var file  = fs.statSync(`${__dirname}/${files[i]}`);\n        file.name = files[i]\n        file.path = `${__dirname}/${files[i]}`\n        if(file.isFile()){\n            file.type = \"file\"\n            files[i] = file\n        }\n        else if(file.isDirectory()){\n            file.type = \"directory\"\n            files[i] = file\n        }\n    }\n     return files\n}\nmsg.payload = (setRecursive)?getDirectoryRecursive(path):getDirectory(path)\n\nreturn msg\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[{"var":"fs","module":"fs"}],"x":250,"y":500,"wires":[["354f8c3bf93fe38d","9ccb5ee41e3c7486"]],"info":"// nrlint function-eslint:off\n"},{"id":"424cd6fc6701ff50","type":"function","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"Replace strings","func":"/** Replaces template strings in file contents string with the user defined ones\n * File contents in msg.payload\n*/\nconst strings = [\n    {find: \"_node_name_\", replace: env.get(\"NODE_NAME\")},\n    {find: \"_NodeName_\", replace: env.get(\"NodeName\")},\n    {find: \"_package_name_\", replace: env.get(\"PACKAGE_NAME\")},\n    {find: \"_repo_\", replace: env.get(\"REPOSITORY\")},\n    {find: \"_author_name_\", replace: env.get(\"AUTHOR_NAME\")},\n    {find: \"_author_url_\", replace: env.get(\"AUTHOR_URL\")},\n]\nstrings.forEach((element) => msg.payload = msg.payload.replaceAll(element.find, element.replace)) \nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":720,"y":740,"wires":[["ca6564e80feccd49"]]},{"id":"a712dd8a1085b294","type":"file in","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"","filename":"filename","filenameType":"msg","format":"utf8","chunk":false,"sendError":false,"encoding":"none","allProps":false,"x":540,"y":740,"wires":[["424cd6fc6701ff50"]]},{"id":"394f691e58ab1999","type":"function","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"Determine destination filepaths","func":"// source path is in payload\n// remove any double slashes from source\nmsg.payload = msg.payload.replace(/\\/\\//g,'/')\nlet sourcePath = msg.payload // full source path\n// remove source folder from path\nlet templatePath = env.get(\"TEMPLATE_DIR\")\n// strip trailing slash if any\ntemplatePath = templatePath.replace(/\\/$/, '')\n//node.warn(`source path: ${sourcePath}`)\n// remove source path from path\nmsg.payload = msg.payload.replace(templatePath, '')\n// change any _node_name in filename to correct name\nmsg.payload = msg.payload.replace(\"_node_name_\", env.get(\"NODE_NAME\"))\n// change any _NodeName_ in filename to correct name\nmsg.payload = msg.payload.replace(\"_NodeName_\", env.get(\"NodeName\"))\n// Rename README_skeleton.md to README.md\n//node.warn(`filename: ${msg.payload}`)\nmsg.payload = msg.payload.replace('README_skeleton.md', 'README.md')\n// destination root with trailing slash removed (if any)\nconst destRoot = env.get(\"DESTINATION_DIR\").replace(/\\/$/, '')\n// source to msg.filename, dest to msg.destination\nmsg.filename = sourcePath\nmsg.destination = `${destRoot}${msg.payload}`\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":310,"y":740,"wires":[["a712dd8a1085b294"]]},{"id":"ca6564e80feccd49","type":"file","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"Write file","filename":"destination","filenameType":"msg","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":917.0000228881836,"y":740.0000748634338,"wires":[["78f72d9b5f995de7"]]},{"id":"c3584a81ce2236c2","type":"change","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Create Folders","rules":[{"t":"set","p":"payload","pt":"msg","to":"Create Folders","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":270,"y":200,"wires":[["dcd741a1f3ad05e5"]]},{"id":"dcd741a1f3ad05e5","type":"link out","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"link out 28","mode":"link","links":["85b4f6863b6f2a01"],"x":405,"y":200,"wires":[]},{"id":"85b4f6863b6f2a01","type":"link in","z":"dc8909c81da41d3c","name":"link in 11","links":["dcd741a1f3ad05e5","173e5a50f49e6ce2","2339050d3e5e1fbf","edcf2fab9032529a"],"x":1105,"y":880,"wires":[[]]},{"id":"54e46ead4026f24c","type":"change","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Status Find Files","rules":[{"t":"set","p":"payload","pt":"msg","to":"Find Files","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":270,"y":640,"wires":[["173e5a50f49e6ce2"]]},{"id":"173e5a50f49e6ce2","type":"link out","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"link out 29","mode":"link","links":["85b4f6863b6f2a01"],"x":805,"y":640,"wires":[]},{"id":"c7413506b1781678","type":"change","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"Status Write Folders","rules":[{"t":"set","p":"payload","pt":"msg","to":"Write Folders","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":280,"y":840,"wires":[["edcf2fab9032529a"]]},{"id":"78f72d9b5f995de7","type":"trigger","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"Trigger 2 sec then Done","op1":"","op2":"Done","op1type":"nul","op2type":"str","duration":"2","extend":true,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":690,"y":800,"wires":[["edcf2fab9032529a","443f83dc86ee0e6f"]]},{"id":"edcf2fab9032529a","type":"link out","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","name":"link out 31","mode":"link","links":["85b4f6863b6f2a01"],"x":985,"y":840,"wires":[]},{"id":"9ccb5ee41e3c7486","type":"switch","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"No files?","property":"payload","propertyType":"msg","rules":[{"t":"empty"}],"checkall":"true","repair":false,"outputs":1,"x":380,"y":600,"wires":[["06b8c6ab8c2051a9"]]},{"id":"06b8c6ab8c2051a9","type":"change","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Empty","rules":[{"t":"set","p":"payload","pt":"msg","to":"Source dir empty","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":550,"y":600,"wires":[["173e5a50f49e6ce2","0b38110ee98d622c"]]},{"id":"71ea4bec3183f258","type":"link out","z":"dc8909c81da41d3c","name":"link out 32","mode":"link","links":["9ead11ec13641b4e"],"x":995,"y":280,"wires":[]},{"id":"9ead11ec13641b4e","type":"link in","z":"dc8909c81da41d3c","name":"link in 12","links":["71ea4bec3183f258"],"x":75,"y":500,"wires":[["697694900fdab62b"]]},{"id":"6d5262f4c7981687","type":"link out","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"link out 33","mode":"link","links":["593ecf8e13c32e98"],"x":1155,"y":560,"wires":[]},{"id":"593ecf8e13c32e98","type":"link in","z":"dc8909c81da41d3c","name":"link in 13","links":["6d5262f4c7981687"],"x":75,"y":740,"wires":[["b27fa7149941cc66"]]},{"id":"f65f6fab9ef044c1","type":"fs-ops-dir","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"","path":"${DESTINATION_DIR}","pathType":"str","filter":"*","filterType":"str","dir":"payload","dirType":"msg","x":760,"y":200,"wires":[["c8823bdbc2c5d52f"]]},{"id":"c8823bdbc2c5d52f","type":"switch","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Destination Empty (yes, o/p 2)","property":"payload","propertyType":"msg","rules":[{"t":"nempty"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":310,"y":240,"wires":[["62a985630acfc967"],["d57d9e1c13161f9e"]]},{"id":"62a985630acfc967","type":"switch","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Allow Overwrite? yes o/p2","property":"ALLOW_OVERWRITE","propertyType":"env","rules":[{"t":"false"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":600,"y":240,"wires":[["cbcd118f2d842a5b"],["d57d9e1c13161f9e"]]},{"id":"cbcd118f2d842a5b","type":"template","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","name":"Cannot create destination dir","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Error: Destination dir {{{env.DESTINATION_DIR}}} is not empty and Allow Overwrite is not selected.","output":"str","x":880,"y":240,"wires":[["9c4fefc4aca32858"]]},{"id":"16816ef72fa49650","type":"debug","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"debug 2518","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":910,"y":480,"wires":[]},{"id":"940ac4a55c8227b0","type":"switch","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Ignore .git","property":"payload.name","propertyType":"msg","rules":[{"t":"neq","v":".git","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":740,"y":440,"wires":[["ba6cec4ffb9f4cb2"]]},{"id":"be2e2db50a439e33","type":"switch","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","name":"Ignore README.md","property":"payload.name","propertyType":"msg","rules":[{"t":"neq","v":"README.md","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":780,"y":560,"wires":[["d99a5d55c1bd45b6"]]},{"id":"9c4fefc4aca32858","type":"junction","z":"dc8909c81da41d3c","x":1160,"y":120,"wires":[[]]},{"id":"dee5145fe37addda","type":"junction","z":"dc8909c81da41d3c","g":"e083ec2ba754a902","x":140,"y":160,"wires":[["43e304633fc739de","c3584a81ce2236c2"]]},{"id":"697694900fdab62b","type":"junction","z":"dc8909c81da41d3c","g":"9ac137160d9fce44","x":140,"y":500,"wires":[["a3cf456c8644db8b","54e46ead4026f24c"]]},{"id":"b27fa7149941cc66","type":"junction","z":"dc8909c81da41d3c","g":"9155bf97a7a37bb0","x":140,"y":740,"wires":[["394f691e58ab1999","c7413506b1781678"]]},{"id":"443f83dc86ee0e6f","type":"junction","z":"dc8909c81da41d3c","x":1100,"y":800,"wires":[[]]},{"id":"0b38110ee98d622c","type":"junction","z":"dc8909c81da41d3c","x":1240,"y":600,"wires":[[]]},{"id":"e083ec2ba754a902","type":"group","z":"dc8909c81da41d3c","name":"Check folders","style":{"stroke":"#ffC000","label":true},"nodes":["43e304633fc739de","d978c5a54c218e5f","560a800d3a24c36a","d4ee892bb9a294d3","877ea824ae42694f","57faa25e61252961","d57d9e1c13161f9e","c3584a81ce2236c2","dcd741a1f3ad05e5","dee5145fe37addda","f65f6fab9ef044c1","c8823bdbc2c5d52f","62a985630acfc967","cbcd118f2d842a5b"],"x":114,"y":78.99992513656616,"w":912,"h":262.00007486343384},{"id":"9ac137160d9fce44","type":"group","z":"dc8909c81da41d3c","name":"Get filepaths recursive","style":{"stroke":"#ffC000","label":true},"nodes":["354f8c3bf93fe38d","01b8d48d5154dde6","ba6cec4ffb9f4cb2","d99a5d55c1bd45b6","a3cf456c8644db8b","54e46ead4026f24c","173e5a50f49e6ce2","697694900fdab62b","9ccb5ee41e3c7486","06b8c6ab8c2051a9","16816ef72fa49650","940ac4a55c8227b0","be2e2db50a439e33","6d5262f4c7981687"],"x":114,"y":399,"w":1082,"h":282},{"id":"9155bf97a7a37bb0","type":"group","z":"dc8909c81da41d3c","name":"Process and write files","style":{"stroke":"#ffC000","label":true},"nodes":["424cd6fc6701ff50","a712dd8a1085b294","394f691e58ab1999","ca6564e80feccd49","c7413506b1781678","b27fa7149941cc66","78f72d9b5f995de7","edcf2fab9032529a"],"x":114,"y":699,"w":912,"h":182},{"id":"e9c7d4c7322106e3","type":"subflow:dc8909c81da41d3c","z":"a7948090b517fd33","name":"","x":520,"y":420,"wires":[["6f9fb2840c578fdf"]]},{"id":"75bd132bf74b9aec","type":"inject","z":"a7948090b517fd33","name":"Convert","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":310,"y":420,"wires":[["e9c7d4c7322106e3"]]},{"id":"6f9fb2840c578fdf","type":"debug","z":"a7948090b517fd33","name":"Conversion result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":760,"y":420,"wires":[]}]

[Edit] There have been problems running this with node-red 3.x, so it is advised that at least 4.0 should be used.

5 Likes

Hi Colin,

Thanks for this contribution! Will indeed be very helpful!

I have tried to do it this way, however I cannot enter my info in the config screen. Every time I press the "Done" button, the following error occurs in my developer tools:

image

P.S. My browser is a recent Chrome version, running on Windows 10. My Node-RED v3.1.3 runs on a Raspberry Pi.

Some remarks about your tutorial:

  1. I would move the subflow from the bottom to a new point 2 (Import the following subflow...).

  2. I would add following screenshot to point 2:
    image
    Because I never use subflows, this is helpful (to find the subflow).

  3. You need to add an extra point to explain that first the node-red-contrib-fs-ops need to be installed. Although if you could do without this node that would be nice (e.g. using a function node). Because now my palette is polluted with a series of old unmaintained nodes that I don't need:

    image

1 Like

That is very odd, my subflow doesn't actually do anything at that point, it is just node red. If you leave all the parameters at default, or just make a minor change, do you see the problem? I know it won't run like that.
Which version of nodejs are you using?

In fact I had forgotten that I had used that. Do you have a recommendation for doing directory listings, creating directories etc? I can't immediately see anything else to do the job.

1 Like

there is nothing built in. you would need to use fs as a module in function node. (fs is a node built in nodejs module)

1 Like

Yes indeed the approach from @Steve-Mcl: a few simple function nodes, using the NodeJs fs module.

30 Jun 19:56:15 - [info] Node-RED version: v3.1.3
30 Jun 19:56:15 - [info] Node.js version: v18.19.0
30 Jun 19:56:15 - [info] Linux 6.1.0-rpi7-rpi-v8 arm64 LE

Yes I have never used subflows, but indeed I don't see anything magic in your config screen.

When I press the "Done" button, I first arrive in this loop:

Further down the call stack, these Properties are being serialized to a json string. But there you indeed can see that there is some kind of cyclic information:

Not familiar with this kind of stuff to be honest. So I have no idea how this hierarchy is being build, or what causes this error. Just thinking out loud: do you perhaps have a 4.x version while I have a 3.x version?

I am running NR 4, but this is just basic subflow stuff.
I don't know if any of my systems are still running 3, I will have a look.

Do you have NR running on your PC where you could try it in the meantime? The resulting files would be transferable to the PI.

Well as luck would have it, one of my Pis is running 3.1.3 with nodejs 18.19.0! Indeed it does fail for me too. There must have been a fix at some point. I am upgrading it to 3.1.11 to see if that fixes it.

1 Like

The Sherlock Holmes of Node-RED :wink:
Don't put too much effort in it! I will update my Node-RED as soon as possible, if that is the solution.

I have upgraded to 3.1.11 and still see the problem, but it does run on a pi running NR 4. I have no idea why it won't run on the other one. It is an ancient pi B2 (bought in 2014) Running Raspbian Stretch, but I don't think that should make any difference.

I may just say that it isn't guaranteed to with NR 3 and leave it at that.

1 Like

I have updated the post with the recommendation to use NR 4, and clarified point 2 in the instructions.

1 Like

I am working on getting rid of that dependency now.

In the meantime I have updated the skeleton to include support for class, ui_update and the use of the state store for state, rather than the data store.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.