From 6d5948b56e9f2699f8364daae3b926153c215ce2 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 8 Mar 2024 15:52:47 +0000 Subject: [PATCH 1/3] Include module list in global-config node when exporting flows --- .../@node-red/editor-client/src/js/nodes.js | 43 +++++++++++++++++-- .../editor-client/src/js/ui/clipboard.js | 6 +-- .../editor-client/src/js/ui/env-var.js | 3 +- .../@node-red/editor-client/src/js/ui/view.js | 4 +- .../nodes/core/common/91-global-config.html | 3 +- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index f80a2a8d1..59b85031b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -1428,7 +1428,12 @@ RED.nodes = (function() { /** * Converts the current node selection to an exportable JSON Object **/ - function createExportableNodeSet(set, exportedIds, exportedSubflows, exportedConfigNodes) { + function createExportableNodeSet(set, { + exportedIds, + exportedSubflows, + exportedConfigNodes, + includeModuleConfig = false + } = {}) { var nns = []; exportedIds = exportedIds || {}; @@ -1462,7 +1467,7 @@ RED.nodes = (function() { subflowSet = subflowSet.concat(RED.nodes.junctions(subflowId)) subflowSet = subflowSet.concat(RED.nodes.groups(subflowId)) - var exportableSubflow = createExportableNodeSet(subflowSet, exportedIds, exportedSubflows, exportedConfigNodes); + var exportableSubflow = createExportableNodeSet(subflowSet, { exportedIds, exportedSubflows, exportedConfigNodes }); nns = exportableSubflow.concat(nns); } } @@ -1497,13 +1502,16 @@ RED.nodes = (function() { } nns.push(convertedNode); if (node.type === "group") { - nns = nns.concat(createExportableNodeSet(node.nodes, exportedIds, exportedSubflows, exportedConfigNodes)); + nns = nns.concat(createExportableNodeSet(node.nodes, { exportedIds, exportedSubflows, exportedConfigNodes })); } } else { var convertedSubflow = convertSubflow(node, { credentials: false }); nns.push(convertedSubflow); } } + if (includeModuleConfig) { + updateGlobalConfigModuleList(nns) + } return nns; } @@ -1541,6 +1549,7 @@ RED.nodes = (function() { RED.nodes.eachNode(function(n) { nns.push(convertNode(n, opts)); }) + updateGlobalConfigModuleList(nns) return nns; } @@ -2886,7 +2895,33 @@ RED.nodes = (function() { } } } - + function getModuleListForNodes(nodes) { + const modules = {} + nodes.forEach(n => { + const nodeSet = RED.nodes.registry.getNodeSetForType(n.type) + if (nodeSet) { + modules[nodeSet.module] = nodeSet.version + } + }) + return modules + } + function updateGlobalConfigModuleList(nodes) { + const modules = getModuleListForNodes(nodes) + delete modules['node-red'] + const hasModules = (Object.keys(modules).length > 0) + let globalConfigNode = nodes.find(n => n.type === 'global-config') + if (!globalConfigNode && hasModules) { + globalConfigNode = { + id: RED.nodes.id(), + type: 'global-config', + env: [], + modules + } + nodes.push(globalConfigNode) + } else if (globalConfigNode) { + globalConfigNode.modules = modules + } + } return { init: function() { RED.events.on("registry:node-type-added",function(type) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index 690968338..afc4d5d87 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -714,7 +714,7 @@ RED.clipboard = (function() { nodes = RED.view.selection().nodes||[]; } // Don't include the subflow meta-port nodes in the exported selection - nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'})); + nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'}), { includeModuleConfig: true }); } else if (type === 'flow') { var activeWorkspace = RED.workspaces.active(); nodes = RED.nodes.groups(activeWorkspace); @@ -729,7 +729,7 @@ RED.clipboard = (function() { }); var parentNode = RED.nodes.workspace(activeWorkspace)||RED.nodes.subflow(activeWorkspace); nodes.unshift(parentNode); - nodes = RED.nodes.createExportableNodeSet(nodes); + nodes = RED.nodes.createExportableNodeSet(nodes, { includeModuleConfig: true }); } else if (type === 'full') { nodes = RED.nodes.createCompleteNodeSet({ credentials: false }); } @@ -832,7 +832,7 @@ RED.clipboard = (function() { children: [] }; treeSubflows.push(subflows[node.id]) - } else { + } else if (node.type !== 'global-config') { nodes.push(node); } }); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js b/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js index 79c626af4..ea3dbef48 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js @@ -33,8 +33,7 @@ RED.envVar = (function() { id: RED.nodes.id(), type: "global-config", env: [], - name: "global-config", - label: "", + modules: {}, hasUsers: false, users: [], credentials: cred, diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 23385940c..22f75484f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -5801,8 +5801,8 @@ RED.view = (function() { if (globalConfig) { // merge global env to existing global-config - var env0 = gconf.env; - var env1 = globalConfig.env; + var env0 = gconf.env || []; + var env1 = globalConfig.env || [] var newEnv = Array.from(env0); var changed = false; diff --git a/packages/node_modules/@node-red/nodes/core/common/91-global-config.html b/packages/node_modules/@node-red/nodes/core/common/91-global-config.html index 3240d315d..43918f1e7 100644 --- a/packages/node_modules/@node-red/nodes/core/common/91-global-config.html +++ b/packages/node_modules/@node-red/nodes/core/common/91-global-config.html @@ -11,12 +11,13 @@ RED.nodes.registerType('global-config',{ category: 'config', defaults: { - name: { value: "" }, env: { value: [] }, + modules: { value: {} } }, credentials: { map: { type: "map" } }, + label: 'global-config', oneditprepare: function() { $('#node-input-edit-env-var').on('click', function(evt) { RED.actions.invoke('core:show-user-settings', 'envvar') From 3f8a5301faf4446d33aaafda31e32fa6f13bdd5d Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 18 Jul 2024 16:22:42 +0100 Subject: [PATCH 2/3] Allow core:manage-palette action to specify tab and filter --- .../@node-red/editor-client/locales/en-US/editor.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index a88890125..c2e89133b 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -265,6 +265,8 @@ "download": "Download", "importUnrecognised": "Imported unrecognised type:", "importUnrecognised_plural": "Imported unrecognised types:", + "importWithModuleInfo": "Required dependencies missing", + "importWithModuleInfoDesc": "These nodes are not currently installed in your palette and are required for the imported flow:", "importDuplicate": "Imported duplicate node:", "importDuplicate_plural": "Imported duplicate nodes:", "nodesExported": "Nodes exported to clipboard", From be9add2a950c61eecc7e6d028559ee5c6f9cd8cc Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 18 Jul 2024 16:23:02 +0100 Subject: [PATCH 3/3] Handle import of unknown nodes that include module meta --- .../@node-red/editor-client/src/js/nodes.js | 62 +++++++++++++++++-- .../editor-client/src/js/ui/palette-editor.js | 43 ++++++++++++- .../@node-red/editor-client/src/js/ui/view.js | 52 ++++++++-------- .../nodes/core/common/98-unknown.html | 30 +++++++-- 4 files changed, 150 insertions(+), 37 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 97ea4e992..e5a66ccaf 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -1811,6 +1811,7 @@ RED.nodes = (function() { * - id:import - import as-is * - id:copy - import with new id * - id:replace - import over the top of existing + * - modules: map of module:version - hints for unknown nodes */ function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) { const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} } @@ -1946,12 +1947,54 @@ RED.nodes = (function() { } if (!isInitialLoad && unknownTypes.length > 0) { - var typeList = $("
    "); - unknownTypes.forEach(function(t) { - $("
  • ").text(t).appendTo(typeList); - }) - typeList = typeList[0].outerHTML; - RED.notify("

    "+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"

    "+typeList,"error",false,10000); + const notificationOptions = { + type: "error", + fixed: false, + timeout: 10000, + } + let unknownNotification + if (options.modules) { + notificationOptions.fixed = true + delete notificationOptions.timeout + // We have module hint list from imported global-config + // Provide option to install missing modules + notificationOptions.buttons = [ + { + text: "Manage dependencies", + class:"primary", + click: function(e) { + unknownNotification.close(); + + RED.actions.invoke('core:manage-palette', { + view: 'install', + filter: '"' + Object.keys(options.modules).join('", "') + '"' + }) + } + } + ] + let moduleList = $("
      "); + Object.keys(options.modules).forEach(function(t) { + $("
    • ").text(t).appendTo(moduleList); + }) + moduleList = moduleList[0].outerHTML; + unknownNotification = RED.notify( + "

      "+RED._("clipboard.importWithModuleInfo")+"

      "+ + "

      "+RED._("clipboard.importWithModuleInfoDesc")+"

      "+ + moduleList, + notificationOptions + ); + } else { + var typeList = $("
        "); + unknownTypes.forEach(function(t) { + $("
      • ").text(t).appendTo(typeList); + }) + typeList = typeList[0].outerHTML; + + unknownNotification = RED.notify( + "

        "+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"

        "+typeList, + notificationOptions + ); + } } var activeWorkspace = RED.workspaces.active(); @@ -2319,6 +2362,9 @@ RED.nodes = (function() { delete node.z; } } + const unknownTypeDef = RED.nodes.getType('unknown') + node._def.oneditprepare = unknownTypeDef.oneditprepare + var orig = {}; for (var p in n) { if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") { @@ -2328,6 +2374,10 @@ RED.nodes = (function() { node._orig = orig; node.name = n.type; node.type = "unknown"; + if (options.modules) { + // We have a module hint list. Attach to the unknown node so we can reference it later + node.modules = Object.keys(options.modules) + } } if (node._def.category != "config") { if (n.hasOwnProperty('inputs')) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 3d949f8ca..44e6146c8 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -625,8 +625,20 @@ RED.palette.editor = (function() { } }) - RED.actions.add("core:manage-palette",function() { + RED.actions.add("core:manage-palette", function(opts) { RED.userSettings.show('palette'); + if (opts) { + if (opts.view) { + editorTabs.activateTab(opts.view); + } + if (opts.filter) { + if (opts.view === 'install') { + searchInput.searchBox('value', opts.filter) + } else if (opts.view === 'nodes') { + filterInput.searchBox('value', opts.filter) + } + } + } }); RED.events.on('registry:module-updated', function(ns) { @@ -982,8 +994,35 @@ RED.palette.editor = (function() { change: function() { var searchTerm = $(this).val().trim().toLowerCase(); if (searchTerm.length > 0 || loadedList.length < 20) { + const searchTerms = [] + searchTerm.split(',').forEach(term => { + term = term.trim() + if (term) { + const isExact = term[0] === '"' && term[term.length-1] === '"' + searchTerms.push({ + exact: isExact, + term: isExact ? term.substring(1,term.length-1) : term + }) + } + }) filteredList = loadedList.filter(function(m) { - return (m.index.indexOf(searchTerm) > -1); + for (let i = 0; i < searchTerms.length; i++) { + const location = m.index.indexOf(searchTerms[i].term) + if ( + ( + searchTerms[i].exact && + ( + location === 0 && ( + m.index.length === searchTerms[i].term.length || + m.index[searchTerms[i].term.length] === ',' + ) + ) + ) || + (!searchTerms[i].exact && location > -1)) { + return true + } + } + return false }).map(function(f) { return {info:f}}); refreshFilteredItems(); searchInput.searchBox('count',filteredList.length+" / "+loadedList.length); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 14f79977a..5872c0f40 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -5664,27 +5664,29 @@ RED.view = (function() { activeSubflowChanged = activeSubflow.changed; } var filteredNodesToImport = nodesToImport; - var globalConfig = null; - var gconf = null; + var importedGlobalConfig = null; + var existingGlobalConfig = null; RED.nodes.eachConfig(function (conf) { if (conf.type === "global-config") { - gconf = conf; + existingGlobalConfig = conf; } }); - if (gconf) { + if (existingGlobalConfig) { filteredNodesToImport = nodesToImport.filter(function (n) { - return (n.type !== "global-config"); - }); - globalConfig = nodesToImport.find(function (n) { - return (n.type === "global-config"); + if (n.type === "global-config") { + importedGlobalConfig = n + return false + } + return true }); } var result = RED.nodes.import(filteredNodesToImport,{ generateIds: options.generateIds, addFlow: addNewFlow, importMap: options.importMap, - markChanged: true + markChanged: true, + modules: importedGlobalConfig ? (importedGlobalConfig.modules || {}) : {} }); if (result) { var new_nodes = result.nodes; @@ -5808,38 +5810,38 @@ RED.view = (function() { } } - if (globalConfig) { + if (importedGlobalConfig) { // merge global env to existing global-config - var env0 = gconf.env || []; - var env1 = globalConfig.env || [] - var newEnv = Array.from(env0); + var existingEnv = existingGlobalConfig.env || []; + var importedEnv = importedGlobalConfig.env || [] + var newEnv = Array.from(existingEnv); var changed = false; - env1.forEach(function (item1) { - var index = newEnv.findIndex(function (item0) { - return (item0.name === item1.name); + importedEnv.forEach(function (importedItem) { + var index = newEnv.findIndex(function (existingItem) { + return (existingItem.name === importedItem.name); }); if (index >= 0) { - var item0 = newEnv[index]; - if ((item0.type !== item1.type) || - (item0.value !== item1.value)) { - newEnv[index] = item1; + const existingItem = newEnv[index]; + if ((existingItem.type !== importedItem.type) || + (existingItem.value !== importedItem.value)) { + newEnv[index] = importedItem; changed = true; } } else { - newEnv.push(item1); + newEnv.push(importedItem); changed = true; } }); - if(changed) { - gconf.env = newEnv; + if (changed) { + existingGlobalConfig.env = newEnv; var replaceEvent = { t: "edit", - node: gconf, + node: existingGlobalConfig, changed: true, changes: { - env: env0 + env: existingEnv } }; historyEvent = { diff --git a/packages/node_modules/@node-red/nodes/core/common/98-unknown.html b/packages/node_modules/@node-red/nodes/core/common/98-unknown.html index 52071c30f..282ad3415 100644 --- a/packages/node_modules/@node-red/nodes/core/common/98-unknown.html +++ b/packages/node_modules/@node-red/nodes/core/common/98-unknown.html @@ -1,14 +1,22 @@