DB2: Duplicate/Copy ui-pages with all widgets?

@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"}}]