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 5a35135ee..a59dcacf4 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 @@ -111,6 +111,7 @@ "userSettings": "User Settings", "nodes": "Nodes", "displayStatus": "Show node status", + "displayInfoIcon": "Show node information icon", "displayConfig": "Configuration nodes", "import": "Import", "importExample": "Import example flow", @@ -264,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", 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 258f14569..131d40d18 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 @@ -1495,7 +1495,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 || {}; @@ -1529,7 +1534,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); } } @@ -1564,19 +1569,23 @@ 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; } // Create the Flow JSON for the current configuration // opts.credentials (whether to include (known) credentials) - default: true // opts.dimensions (whether to include node dimensions) - default: false + // opts.includeModuleConfig (whether to include modules) - default: false function createCompleteNodeSet(opts) { var nns = []; var i; @@ -1608,6 +1617,9 @@ RED.nodes = (function() { RED.nodes.eachNode(function(n) { nns.push(convertNode(n, opts)); }) + if (opts?.includeModuleConfig) { + updateGlobalConfigModuleList(nns); + } return nns; } @@ -1835,6 +1847,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: {} } @@ -1970,12 +1983,58 @@ RED.nodes = (function() { } if (!isInitialLoad && unknownTypes.length > 0) { - var typeList = $("
"+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"
"+typeList,"error",false,10000); + const notificationOptions = { + type: "error", + fixed: false, + timeout: 10000, + } + let unknownNotification + let missingModules = [] + if (options.modules) { + missingModules = Object.keys(options.modules).filter(module => !RED.nodes.registry.getModule(module)) + } + if (missingModules.length > 0) { + 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: '"' + missingModules.join('", "') + '"' + }) + } + } + ] + let moduleList = $(""+RED._("clipboard.importWithModuleInfo")+"
"+ + ""+RED._("clipboard.importWithModuleInfoDesc")+"
"+ + moduleList, + notificationOptions + ); + } else { + var typeList = $(""+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"
"+typeList, + notificationOptions + ); + } } var activeWorkspace = RED.workspaces.active(); @@ -2403,6 +2462,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") { @@ -2412,6 +2474,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') && node._def.defaults.hasOwnProperty("inputs")) { @@ -3098,7 +3164,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 148af989f..4ad6c734a 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 @@ -730,7 +730,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); @@ -745,9 +745,9 @@ 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 }); + nodes = RED.nodes.createCompleteNodeSet({ credentials: false, includeModuleConfig: true }); } if (nodes !== null) { if (format === "red-ui-clipboard-dialog-export-fmt-full") { @@ -848,7 +848,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 55c800be1..87d74a43a 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/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 3926ca430..e73a3a9b1 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 @@ -34,7 +34,7 @@ RED.palette.editor = (function() { let packageList; // Nodes tab - filter - let activeFilter = ""; + let activeFilterTerms = []; // Nodes tab - search input let filterInput; // Install tab - search input @@ -421,7 +421,19 @@ RED.palette.editor = (function() { } function filterChange(val) { - activeFilter = val.toLowerCase(); + const activeFilter = val.toLowerCase(); + activeFilterTerms = [] + activeFilter.split(',').forEach(term => { + term = term.trim() + if (term) { + const isExact = term[0] === '"' && term[term.length-1] === '"' + activeFilterTerms.push({ + exact: isExact, + term: isExact ? term.substring(1,term.length-1) : term + }) + } + }) + var visible = nodeList.editableList('filter'); var size = nodeList.editableList('length'); if (val === "") { @@ -583,7 +595,9 @@ RED.palette.editor = (function() { packageList.editableList('addItem',{count:loadedList.length}) return; } + // sort the filtered modules filteredList.sort(activeSort); + // render the items in the package list for (var i=0;i