@WhiteLion - I’ve updated the flow so that it can now process multiple source files. The folder structure that I used was
Files start in “Unprocessed”. After processing, the converted files are in “Converted” and the original files are moved to “Processed”. The setup is done in a change node:
Here is the updated flow (it uses the node-red-contrib-fs-ops node for directory operations). I have tested this on a Mac and it does appear to work.
[{"id":"182cccfc9c426c18","type":"inject","z":"eb38bf467edfbe13","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":131.66665649414062,"y":963,"wires":[["694aca2344b84760"]]},{"id":"694aca2344b84760","type":"change","z":"eb38bf467edfbe13","name":"Set Path Names","rules":[{"t":"set","p":"sourcePath","pt":"msg","to":"/Users/<YOUR_USER_NAME>/Downloads/ConvertFlows/Unprocessed/","tot":"str"},{"t":"set","p":"processedPath","pt":"msg","to":"/Users/<YOUR_USER_NAME>/Downloads/ConvertFlows/Processed/","tot":"str"},{"t":"set","p":"convertedPath","pt":"msg","to":"/Users/<YOUR_USER_NAME>/Downloads/ConvertFlows/Converted/","tot":"str"},{"t":"set","p":"convertedSuffix","pt":"msg","to":"_converted","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":296.6666564941406,"y":963,"wires":[["bb67a4a7d75aa207"]]},{"id":"bb67a4a7d75aa207","type":"fs-ops-dir","z":"eb38bf467edfbe13","name":"Get File List","path":"sourcePath","pathType":"msg","filter":".json","filterType":"str","dir":"files","dirType":"msg","x":483.6665954589844,"y":963.0000610351562,"wires":[["7c570e5434036325"]]},{"id":"7c570e5434036325","type":"function","z":"eb38bf467edfbe13","name":"Setup file names and paths","func":"// Get list of files\nvar fileNames = msg.files;\n\n// Get the number of files\n// Set len to 0 if file list is empty\nvar len = fileNames?.length ?? 0;\n\n// Check if there are no files\nif (len === 0) {\n // Return null to stop the flow if there are no files\n return null;\n} else {\n // Loop through each file in the array\n for (let i = 0; i < len; i++) {\n // Create a new message object for this iteration \n var newMsg = {\n ...msg, // Copies all properties from the original msg object\n };\n\n // Get the current file name from the array\n var sourceFile = fileNames[i];\n\n // Construct the full path for the original file\n var originalFileName = newMsg.sourcePath + sourceFile;\n\n // Split the file name to get the name and extension\n // Use a nullish coalescing operator to handle cases where split returns an empty array\n var fileNameParts = sourceFile.split('.') ?? [];\n\n // Check if there are at least two parts (name and extension)\n var targetFile = ''\n \n if (fileNameParts.length > 1) {\n // Construct the new file name with the suffix\n targetFile = fileNameParts[0] + newMsg.convertedSuffix + \".\" + fileNameParts[1];\n } else {\n // Handle files with no extension by just adding the suffix\n targetFile = fileNameParts[0] + newMsg.convertedSuffix;\n }\n\n // Construct the full path for the converted file\n var convertedFileName = newMsg.convertedPath + targetFile;\n\n // Update the message properties for this specific file\n newMsg.originalFileName = originalFileName;\n newMsg.convertedFileName = convertedFileName;\n newMsg.sourceFile = sourceFile;\n newMsg.targetFile = targetFile;\n newMsg.fileNameParts = fileNameParts;\n\n // Send the new message for this file\n // The loop continues to the next file\n node.send(newMsg);\n }\n}\n\n// Return null to prevent the original message from being sent again.\nreturn null;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":711.6666412353516,"y":963.6666641235352,"wires":[["0b331da803d44492"]]},{"id":"0b331da803d44492","type":"file in","z":"eb38bf467edfbe13","name":"Read Flow JSON","filename":"originalFileName","filenameType":"msg","format":"utf8","chunk":false,"sendError":true,"encoding":"none","allProps":false,"x":190.99996948242188,"y":1042.6666641235352,"wires":[["e5aff01ee835279e"]]},{"id":"e5aff01ee835279e","type":"json","z":"eb38bf467edfbe13","name":"Parse JSON","property":"payload","action":"obj","pretty":false,"x":409,"y":1040.6666641235352,"wires":[["69eaa151ef4f34af"]]},{"id":"69eaa151ef4f34af","type":"function","z":"eb38bf467edfbe13","name":"Copy & Replace `ui_group` IDs and Page ID","func":"// Function for generating IDs for Dashboard 1.0 (shorter length)\nfunction generateOldIdFormat() {\n return 'id_' + Math.random().toString(36).substr(2, 8);\n}\n\n// Function for generating IDs for Dashboard 2.0 (longer length)\nfunction generateNewIdFormat() {\n return Math.random().toString(36).substr(2, 16);\n}\n\nlet nodes = msg.payload;\nlet idMap = {}; // Map to store old and new IDs\n\n// 1. Duplicate `ui-page` nodes\nnodes.forEach((node) => {\n if (node.type === 'ui-page') {\n let oldId = node.id;\n node.id = generateNewIdFormat(); // Generate a new ID for the page\n node.name += ' (copy)'; // Add (copy) to the page name\n if (node.path) {\n node.path += '_copy'; // ➡️ Add a suffix to the path\n }\n idMap[oldId] = node.id; // Map the old ID to the new one\n }\n});\n\n// 2. Duplicate `ui_group` (Dashboard 1.0) and `ui-group` (Dashboard 2.0) nodes\nnodes.forEach((node) => {\n if (node.type === 'ui_group' || node.type === 'ui-group') {\n let oldId = node.id;\n node.id = (node.type === 'ui-group') ? generateNewIdFormat() : generateOldIdFormat();\n node.name += ' (copy)';\n idMap[oldId] = node.id;\n }\n});\n\n// 3. Update all references to the new `ui-page` and `ui-group` IDs\nnodes.forEach((node) => {\n if (node.page && idMap[node.page]) {\n node.page = idMap[node.page]; // Change the page ID\n }\n if (node.group && idMap[node.group]) {\n node.group = idMap[node.group]; // Change the group ID\n }\n // Also update tab references, though less common in modern dashboards\n if (node.tab && idMap[node.tab]) {\n node.tab = idMap[node.tab];\n }\n});\n\nmsg.payload = nodes;\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":692.6666564941406,"y":1039.333351135254,"wires":[["1de299744667831f"]]},{"id":"1de299744667831f","type":"json","z":"eb38bf467edfbe13","name":"Stringify JSON","property":"payload","action":"str","pretty":true,"x":204.00006103515625,"y":1129.6666641235352,"wires":[["cd466af3234c729d"]]},{"id":"cd466af3234c729d","type":"file","z":"eb38bf467edfbe13","name":"Write New Flow JSON","filename":"convertedFileName","filenameType":"msg","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":430.6671676635742,"y":1128.6666641235352,"wires":[["a829ae8c8baf534a"]]},{"id":"a829ae8c8baf534a","type":"fs-ops-move","z":"eb38bf467edfbe13","name":"Move Processed File","sourcePath":"sourcePath","sourcePathType":"msg","sourceFilename":"sourceFile","sourceFilenameType":"msg","destPath":"processedPath","destPathType":"msg","destFilename":"sourceFile","destFilenameType":"msg","link":false,"x":688.6665954589844,"y":1127.6666641235352,"wires":[[]]},{"id":"d9a4b6ad93a5217f","type":"global-config","env":[],"modules":{"node-red-contrib-fs-ops":"1.6.0"}}]