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
-
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. -
Install the
node node-red-contrib-fs-ops
. When I have time I will remove this dependency, but at the moment it is required. -
Add an instance of the subflow to the node red flow, with an inject node to trigger it andImport the flow below, consisting of the subflow, an Inject node to trigger it and a debug node to show the result.
a debug node on the output. -
Configure the subflow as described below.
-
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 settrue
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 inpackage.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:
-
cd
into the destination directory configured above. -
Run
npm install
to install dependencies. -
Build it using
NODE_ENV=development npm run build
which builds a version with a map files for easier debugging. -
From the node red user directory (often
.node-red
) install it usingnpm install /path/to/ui-node
. -
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.