diff --git a/package.json b/package.json index db3b94f22..df423ad0f 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "moment": "2.30.1", "moment-timezone": "0.5.48", "mqtt": "5.11.0", - "multer": "1.4.5-lts.2", + "multer": "2.0.1", "mustache": "4.2.0", "node-red-admin": "^4.0.2", "node-watch": "0.7.4", @@ -87,7 +87,7 @@ "@node-rs/bcrypt": "1.10.7" }, "devDependencies": { - "dompurify": "2.5.8", + "dompurify": "3.2.5", "grunt": "1.6.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.5.0", diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index ea199077c..4ed4e3f4a 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -26,7 +26,7 @@ "express": "4.21.2", "memorystore": "1.6.7", "mime": "3.0.0", - "multer": "1.4.5-lts.2", + "multer": "2.0.1", "mustache": "4.2.0", "oauth2orize": "1.12.0", "passport-http-bearer": "1.0.1", 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 a8bf70a19..a479b36e5 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 @@ -660,6 +660,8 @@ "more": "+ __count__ more", "upload": "Upload module tgz file", "refresh": "Refresh module list", + "deprecated": "deprecated", + "deprecatedTip": "This module has been deprecated", "errors": { "catalogLoadFailed": "
Failed to load node catalogue.
Check the browser console for more information
", "installFailed": "Failed to install: __module__
__message__
Check the log for more information
", @@ -1282,5 +1284,15 @@ "environment": "Environment", "header": "Global Environment Variables", "revert": "Revert" + }, + "telemetry": { + "label": "Update Notifications", + "settingsTitle": "Enable Update Notifications", + "settingsDescription": "Node-RED can notify you when there is a new version available. This ensures you keep up to date with the latest features and fixes.
This requires sending anonymised data back to the Node-RED team. It does not include any details of your flows or users.
For full information on what information is collected and how it is used, please see the documentation.
", + "settingsDescription2": "You can change this setting at any time in the editor settings.
", + "enableLabel": "Yes, enable notifications", + "disableLabel": "No, do not enable notifications", + "updateAvailable": "Update available", + "updateAvailableDesc": "Node-RED __version__ is now available" } } 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 20af0bd11..a4cb79698 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 @@ -689,7 +689,7 @@ RED.nodes = (function() { } } - function addNode(n) { + function addNode(n, opt) { let newNode if (!n.__isProxy__) { newNode = new Proxy(n, nodeProxyHandler) @@ -728,7 +728,7 @@ RED.nodes = (function() { nodeLinks[n.id] = {in:[],out:[]}; } } - RED.events.emit('nodes:add',newNode); + RED.events.emit('nodes:add',newNode, opt); return newNode } function addLink(l) { @@ -1859,14 +1859,23 @@ RED.nodes = (function() { * - id:copy - import with new id * - id:replace - import over the top of existing * - modules: map of module:version - hints for unknown nodes + * - applyNodeDefaults - whether to apply default values to the imported nodes (default: false) */ function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) { - const defOpts = { generateIds: false, addFlow: false, markChanged: false, reimport: false, importMap: {} } + const defOpts = { + generateIds: false, + addFlow: false, + markChanged: false, + reimport: false, + importMap: {}, + applyNodeDefaults: false + } options = Object.assign({}, defOpts, options) options.importMap = options.importMap || {} const createNewIds = options.generateIds; const reimport = (!createNewIds && !!options.reimport) const createMissingWorkspace = options.addFlow; + const applyNodeDefaults = options.applyNodeDefaults; var i; var n; var newNodes; @@ -2260,6 +2269,13 @@ RED.nodes = (function() { for (d in def.defaults) { if (def.defaults.hasOwnProperty(d)) { configNode[d] = n[d]; + if (applyNodeDefaults && n[d] === undefined) { + // If the node has a default value, but the imported node does not + // set it, then set it to the default value + if (def.defaults[d].value !== undefined) { + configNode[d] = JSON.parse(JSON.stringify(def.defaults[d].value)) + } + } configNode._config[d] = JSON.stringify(n[d]); if (def.defaults[d].type) { configNode._configNodeReferences.add(n[d]) @@ -2534,6 +2550,13 @@ RED.nodes = (function() { for (d in node._def.defaults) { if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') { node[d] = n[d]; + if (applyNodeDefaults && n[d] === undefined) { + // If the node has a default value, but the imported node does not + // set it, then set it to the default value + if (node._def.defaults[d].value !== undefined) { + node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value)) + } + } node._config[d] = JSON.stringify(n[d]); } } @@ -2787,7 +2810,8 @@ RED.nodes = (function() { workspaces:new_workspaces, subflows:new_subflows, missingWorkspace: missingWorkspace, - removedNodes: removedNodes + removedNodes: removedNodes, + nodeMap: node_map } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index f8130a686..99cb8375b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -358,7 +358,10 @@ var RED = (function() { }); return; } - + if (notificationId === "update-available") { + // re-emit as an event to be handled in editor-client/src/js/ui/palette-editor.js + RED.events.emit("notification/update-available", msg) + } if (msg.text) { msg.default = msg.text; var text = RED._(msg.text,msg); @@ -672,14 +675,48 @@ var RED = (function() { setTimeout(function() { loader.end(); - checkFirstRun(function() { - if (showProjectWelcome) { - RED.projects.showStartup(); - } - }); + checkTelemetry(function () { + checkFirstRun(function() { + if (showProjectWelcome) { + RED.projects.showStartup(); + } + }); + }) },100); } + function checkTelemetry(done) { + const telemetrySettings = RED.settings.telemetryEnabled; + // Can only get telemetry permission from a user with permission to modify settings + if (RED.user.hasPermission("settings.write") && telemetrySettings === undefined) { + + const dialog = RED.popover.dialog({ + title: RED._("telemetry.settingsTitle"), + content: `${RED._("telemetry.settingsDescription")}${RED._("telemetry.settingsDescription2")}`, + closeButton: false, + buttons: [ + { + text: RED._("telemetry.enableLabel"), + click: () => { + RED.settings.set("telemetryEnabled", true) + dialog.close() + done() + } + }, + { + text: RED._("telemetry.disableLabel"), + click: () => { + RED.settings.set("telemetryEnabled", false) + dialog.close() + done() + } + } + ] + }) + } else { + done() + } + } function checkFirstRun(done) { if (RED.settings.theme("tours") === false) { done(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 381bb9d3a..a294bc484 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -163,13 +163,18 @@ RED.popover = (function() { } var timer = null; + let isOpen = false var active; var div; var contentDiv; var currentStyle; var openPopup = function(instant) { + if (isOpen) { + return + } if (active) { + isOpen = true var existingPopover = target.data("red-ui-popover"); if (options.tooltip && existingPopover) { active = false; @@ -334,6 +339,7 @@ RED.popover = (function() { } var closePopup = function(instant) { + isOpen = false $(document).off('mousedown.red-ui-popover'); if (!active) { if (div) { @@ -673,6 +679,74 @@ RED.popover = (function() { show:show, hide:hide } + }, + dialog: function(options) { + + const dialogContent = $(''); + + if (options.closeButton !== false) { + $('').appendTo(dialogContent).click(function(evt) { + evt.preventDefault(); + close(); + }) + } + + const dialogBody = $('').appendTo(dialogContent); + if (options.title) { + $('').text(entry.deprecated).appendTo(message) + } + RED.popover.tooltip(deprecatedWarning, message); + } var descRow = $('
').appendTo(headerRow); $('${label}
${RED._(opt.description)}`; + } + input = $('').appendTo(row) + $('').appendTo(row) input.prop('checked',initialState); } else if (opt.options) { $('').appendTo(row); @@ -210,6 +229,8 @@ RED.userSettings = (function() { var opt = allSettings[id]; if (opt.local) { localStorage.setItem(opt.setting,value); + } else if (opt.global) { + RED.settings.set(opt.setting, value) } else { var currentEditorSettings = RED.settings.get('editor') || {}; currentEditorSettings.view = currentEditorSettings.view || {}; @@ -238,7 +259,7 @@ RED.userSettings = (function() { addPane({ id:'view', - title: RED._("menu.label.view.view"), + title: RED._("menu.label.settings"), get: createViewPane, close: function() { viewSettings.forEach(function(section) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js index f4cda8d2c..c09be65ab 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-annotations.js @@ -24,7 +24,7 @@ RED.view.annotations = (function() { refreshAnnotation = !!evt.node[opts.refresh] delete evt.node[opts.refresh] } else if (typeof opts.refresh === "function") { - refreshAnnotation = opts.refresh(evnt.node) + refreshAnnotation = opts.refresh(evt.node) } if (refreshAnnotation) { refreshAnnotationElement(annotation.id, annotation.node, annotation.element) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js index eecd309d1..f5e0df05f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view-tools.js @@ -176,8 +176,8 @@ RED.view.tools = (function() { } nodes.forEach(function(n) { var modified = false; - var oldValue = n.l === undefined?true:n.l; - var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true; + var showLabel = n._def.hasOwnProperty("showLabel") ? n._def.showLabel : true; + var oldValue = n.l === undefined ? showLabel : n.l; if (labelShown) { if (n.l === false || (!showLabel && !n.hasOwnProperty('l'))) { 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 e7820f83a..08ab0ec0a 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 @@ -100,6 +100,11 @@ RED.view = (function() { var clipboard = ""; let clipboardSource + let currentSuggestion = null; + let suggestedNodes = []; + let suggestedLinks = []; + let suggestedJunctions = []; + // Note: these are the permitted status colour aliases. The actual RGB values // are set in the CSS - flow.scss/colors.scss const status_colours = { @@ -548,6 +553,8 @@ RED.view = (function() { } } + clearSuggestedFlow(); + RED.menu.setDisabled("menu-item-workspace-edit", activeFlowLocked || activeSubflow || event.workspace === 0); RED.menu.setDisabled("menu-item-workspace-delete",activeFlowLocked || event.workspace === 0 || RED.workspaces.count() == 1 || activeSubflow); @@ -653,7 +660,7 @@ RED.view = (function() { return; } var historyEvent = result.historyEvent; - var nn = RED.nodes.add(result.node); + var nn = RED.nodes.add(result.node, { source: 'palette' }); var showLabel = RED.utils.getMessageProperty(RED.settings.get('editor'),"view.view-node-show-label"); if (showLabel !== undefined && (nn._def.hasOwnProperty("showLabel")?nn._def.showLabel:true) && !nn._def.defaults.hasOwnProperty("l")) { @@ -1395,6 +1402,20 @@ RED.view = (function() { var lastAddedX; var lastAddedWidth; + const context = {} + + if (quickAddLink) { + context.source = quickAddLink.node.id; + context.sourcePort = quickAddLink.port; + context.sourcePortType = quickAddLink.portType; + if (quickAddLink?.virtualLink) { + context.virtualLink = true; + } + context.flow = RED.nodes.createExportableNodeSet(RED.nodes.getAllFlowNodes(quickAddLink.node)) + } + + // console.log(context) + RED.typeSearch.show({ x:clientX-mainPos.left-node_width/2 - (ox-point[0]), y:clientY-mainPos.top+ node_height/2 + 5 - (oy-point[1]), @@ -1430,7 +1451,63 @@ RED.view = (function() { keepAdding = false; resetMouseVars(); } + if (typeof type !== 'string') { + if (type.nodes) { + // Importing a flow definition + // console.log('Importing flow definition', type.nodes) + const importResult = importNodes(type.nodes, { + generateIds: true, + touchImport: true, + notify: false, + // Ensure the node gets all of its defaults applied + applyNodeDefaults: true + }) + quickAddActive = false; + ghostNode.remove(); + + if (quickAddLink) { + // Need to attach the link to the suggestion. This is assumed to be the first + // node in the array - as that's the one we've focussed on. + const targetNode = importResult.nodeMap[type.nodes[0].id] + + const drag_line = quickAddLink; + let src = null, dst, src_port; + if (drag_line.portType === PORT_TYPE_OUTPUT && (targetNode.inputs > 0 || drag_line.virtualLink) ) { + src = drag_line.node; + src_port = drag_line.port; + dst = targetNode; + } else if (drag_line.portType === PORT_TYPE_INPUT && (targetNode.outputs > 0 || drag_line.virtualLink)) { + src = targetNode; + dst = drag_line.node; + src_port = 0; + } + if (src && dst) { + var link = {source: src, sourcePort:src_port, target: dst}; + RED.nodes.addLink(link); + const historyEvent = RED.history.peek() + if (historyEvent.t === 'add') { + historyEvent.links = historyEvent.links || [] + historyEvent.links.push(link) + } else { + // TODO: importNodes *can* generate a multi history event + // but we don't currently support that + } + } + if (quickAddLink.el) { + quickAddLink.el.remove(); + } + quickAddLink = null; + } + updateActiveNodes(); + updateSelection(); + redraw(); + + return + } else { + type = type.type + } + } var nn; var historyEvent; if (/^_action_:/.test(type)) { @@ -1479,7 +1556,7 @@ RED.view = (function() { if (nn.type === 'junction') { nn = RED.nodes.addJunction(nn); } else { - nn = RED.nodes.add(nn); + nn = RED.nodes.add(nn, { source: 'typeSearch' }); } if (quickAddLink) { var drag_line = quickAddLink; @@ -1662,6 +1739,22 @@ RED.view = (function() { quickAddActive = false; ghostNode.remove(); } + }, + suggest: function (suggestion) { + if (suggestion?.nodes?.length > 0) { + // Reposition the suggestion relative to the existing ghost node position + const deltaX = suggestion.nodes[0].x - point[0] + const deltaY = suggestion.nodes[0].y - point[1] + suggestion.nodes.forEach(node => { + if (Object.hasOwn(node, 'x')) { + node.x = node.x - deltaX + } + if (Object.hasOwn(node, 'y')) { + node.y = node.y - deltaY + } + }) + } + setSuggestedFlow(suggestion); } }); @@ -4576,20 +4669,28 @@ RED.view = (function() { nodeLayer.selectAll(".red-ui-flow-subflow-port-input").remove(); nodeLayer.selectAll(".red-ui-flow-subflow-port-status").remove(); } - - var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(activeNodes,function(d){return d.id}); + let nodesToDraw = activeNodes; + if (suggestedNodes.length > 0) { + nodesToDraw = [...activeNodes, ...suggestedNodes] + } + var node = nodeLayer.selectAll(".red-ui-flow-node-group").data(nodesToDraw,function(d){return d.id}); node.exit().each(function(d,i) { - RED.hooks.trigger("viewRemoveNode",{node:d,el:this}) + if (!d.__ghost) { + RED.hooks.trigger("viewRemoveNode",{node:d,el:this}) + } }).remove(); var nodeEnter = node.enter().insert("svg:g") .attr("class", "red-ui-flow-node red-ui-flow-node-group") - .classed("red-ui-flow-subflow", activeSubflow != null); + .classed("red-ui-flow-subflow", activeSubflow != null) nodeEnter.each(function(d,i) { this.__outputs__ = []; this.__inputs__ = []; var node = d3.select(this); + if (d.__ghost) { + node.classed("red-ui-flow-node-ghost",true); + } var nodeContents = document.createDocumentFragment(); var isLink = (d.type === "link in" || d.type === "link out") var hideLabel = d.hasOwnProperty('l')?!d.l : isLink; @@ -4624,19 +4725,21 @@ RED.view = (function() { bgButton.setAttribute("width",16); bgButton.setAttribute("height",node_height-12); bgButton.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def)); - d3.select(bgButton) - .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}}) - .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}}) - .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}}) - .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) { - var op = 1; - if (d._def.button.toggle) { - op = d[d._def.button.toggle]?1:0.2; - } - d3.select(this).attr("fill-opacity",op); - }}) - .on("click",nodeButtonClicked) - .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();}) + if (!d.__ghost) { + d3.select(bgButton) + .on("mousedown",function(d) {if (!lasso && isButtonEnabled(d)) {focusView();d3.select(this).attr("fill-opacity",0.2);d3.event.preventDefault(); d3.event.stopPropagation();}}) + .on("mouseup",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);d3.event.preventDefault();d3.event.stopPropagation();}}) + .on("mouseover",function(d) {if (!lasso && isButtonEnabled(d)) { d3.select(this).attr("fill-opacity",0.4);}}) + .on("mouseout",function(d) {if (!lasso && isButtonEnabled(d)) { + var op = 1; + if (d._def.button.toggle) { + op = d[d._def.button.toggle]?1:0.2; + } + d3.select(this).attr("fill-opacity",op); + }}) + .on("click",nodeButtonClicked) + .on("touchstart",function(d) { nodeButtonClicked.call(this,d); d3.event.preventDefault();}) + } buttonGroup.appendChild(bgButton); node[0][0].__buttonGroupButton__ = bgButton; @@ -4651,13 +4754,15 @@ RED.view = (function() { mainRect.setAttribute("ry", 5); mainRect.setAttribute("fill", RED.utils.getNodeColor(d.type,d._def)); node[0][0].__mainRect__ = mainRect; - d3.select(mainRect) - .on("mouseup",nodeMouseUp) - .on("mousedown",nodeMouseDown) - .on("touchstart",nodeTouchStart) - .on("touchend",nodeTouchEnd) - .on("mouseover",nodeMouseOver) - .on("mouseout",nodeMouseOut); + if (!d.__ghost) { + d3.select(mainRect) + .on("mouseup",nodeMouseUp) + .on("mousedown",nodeMouseDown) + .on("touchstart",nodeTouchStart) + .on("touchend",nodeTouchEnd) + .on("mouseover",nodeMouseOver) + .on("mouseout",nodeMouseOut); + } nodeContents.appendChild(mainRect); //node.append("rect").attr("class", "node-gradient-top").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-top)").style("pointer-events","none"); //node.append("rect").attr("class", "node-gradient-bottom").attr("rx", 6).attr("ry", 6).attr("height",30).attr("stroke","none").attr("fill","url(#gradient-bottom)").style("pointer-events","none"); @@ -4739,7 +4844,10 @@ RED.view = (function() { node[0][0].appendChild(nodeContents); - RED.hooks.trigger("viewAddNode",{node:d,el:this}) + if (!d.__ghost) { + // Do not trigger hooks for ghost nodes + RED.hooks.trigger("viewAddNode",{node:d,el:this}) + } }); var nodesReordered = false; @@ -4862,13 +4970,15 @@ RED.view = (function() { var inputPorts = thisNode.selectAll(".red-ui-flow-port-input"); if ((!isLink || (showAllLinkPorts === -1 && !activeLinkNodes[d.id])) && d.inputs === 0 && !inputPorts.empty()) { inputPorts.each(function(d,i) { - RED.hooks.trigger("viewRemovePort",{ - node:d, - el:self, - port:d3.select(this)[0][0], - portType: "input", - portIndex: 0 - }) + if (!d.__ghost) { + RED.hooks.trigger("viewRemovePort",{ + node:d, + el:self, + port:d3.select(this)[0][0], + portType: "input", + portIndex: 0 + }) + } }).remove(); } else if (((isLink && (showAllLinkPorts===PORT_TYPE_INPUT||activeLinkNodes[d.id]))|| d.inputs === 1) && inputPorts.empty()) { var inputGroup = thisNode.append("g").attr("class","red-ui-flow-port-input"); @@ -4886,13 +4996,15 @@ RED.view = (function() { inputGroupPorts[0][0].__data__ = this.__data__; inputGroupPorts[0][0].__portType__ = PORT_TYPE_INPUT; inputGroupPorts[0][0].__portIndex__ = 0; - inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) - .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();}) - .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) - .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) - .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) - .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); - RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0}) + if (!d.__ghost) { + inputGroupPorts.on("mousedown",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);}) + .on("touchstart",function(d){portMouseDown(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();}) + .on("mouseup",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);} ) + .on("touchend",function(d){portMouseUp(d,PORT_TYPE_INPUT,0);d3.event.preventDefault();} ) + .on("mouseover",function(d){portMouseOver(d3.select(this),d,PORT_TYPE_INPUT,0);}) + .on("mouseout",function(d) {portMouseOut(d3.select(this),d,PORT_TYPE_INPUT,0);}); + RED.hooks.trigger("viewAddPort",{node:d,el: this, port: inputGroup[0][0], portType: "input", portIndex: 0}) + } } var numOutputs = d.outputs; if (isLink && d.type === "link out") { @@ -4907,13 +5019,15 @@ RED.view = (function() { // Remove extra ports while (this.__outputs__.length > numOutputs) { var port = this.__outputs__.pop(); - RED.hooks.trigger("viewRemovePort",{ - node:d, - el:this, - port:port, - portType: "output", - portIndex: this.__outputs__.length - }) + if (!d.__ghost) { + RED.hooks.trigger("viewRemovePort",{ + node:d, + el:this, + port:port, + portType: "output", + portIndex: this.__outputs__.length + }) + } port.remove(); } for(var portIndex = 0; portIndex < numOutputs; portIndex++ ) { @@ -4941,16 +5055,20 @@ RED.view = (function() { portPort.__data__ = this.__data__; portPort.__portType__ = PORT_TYPE_OUTPUT; portPort.__portIndex__ = portIndex; - portPort.addEventListener("mousedown", portMouseDownProxy); - portPort.addEventListener("touchstart", portTouchStartProxy); - portPort.addEventListener("mouseup", portMouseUpProxy); - portPort.addEventListener("touchend", portTouchEndProxy); - portPort.addEventListener("mouseover", portMouseOverProxy); - portPort.addEventListener("mouseout", portMouseOutProxy); + if (!d.__ghost) { + portPort.addEventListener("mousedown", portMouseDownProxy); + portPort.addEventListener("touchstart", portTouchStartProxy); + portPort.addEventListener("mouseup", portMouseUpProxy); + portPort.addEventListener("touchend", portTouchEndProxy); + portPort.addEventListener("mouseover", portMouseOverProxy); + portPort.addEventListener("mouseout", portMouseOutProxy); + } this.appendChild(portGroup); this.__outputs__.push(portGroup); - RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex}) + if (!d.__ghost) { + RED.hooks.trigger("viewAddPort",{node:d,el: this, port: portGroup, portType: "output", portIndex: portIndex}) + } } else { portGroup = this.__outputs__[portIndex]; } @@ -5067,8 +5185,10 @@ RED.view = (function() { } } } - - RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) + if (!d.__ghost) { + // Only trigger redraw hooks for non-ghost nodes + RED.hooks.trigger("viewRedrawNode",{node:d,el:this}) + } }); if (nodesReordered) { @@ -5077,13 +5197,20 @@ RED.view = (function() { }) } + let junctionsToDraw = activeJunctions; + if (suggestedJunctions.length > 0) { + junctionsToDraw = [...activeJunctions, ...suggestedJunctions] + } var junction = junctionLayer.selectAll(".red-ui-flow-junction").data( - activeJunctions, + junctionsToDraw, d => d.id ) var junctionEnter = junction.enter().insert("svg:g").attr("class","red-ui-flow-junction") junctionEnter.each(function(d,i) { var junction = d3.select(this); + if (d.__ghost) { + junction.classed("red-ui-flow-junction-ghost",true); + } var contents = document.createDocumentFragment(); // d.added = true; var junctionBack = document.createElementNS("http://www.w3.org/2000/svg","rect"); @@ -5177,8 +5304,12 @@ RED.view = (function() { }) + let linksToDraw = activeLinks + if (suggestedLinks.length > 0) { + linksToDraw = [...activeLinks, ...suggestedLinks] + } var link = linkLayer.selectAll(".red-ui-flow-link").data( - activeLinks, + linksToDraw, function(d) { return d.source.id+":"+d.sourcePort+":"+d.target.id+":"+d.target.i; } @@ -5189,44 +5320,50 @@ RED.view = (function() { var l = d3.select(this); var pathContents = document.createDocumentFragment(); + if (d.__ghost) { + l.classed("red-ui-flow-link-ghost",true); + } + d.added = true; var pathBack = document.createElementNS("http://www.w3.org/2000/svg","path"); pathBack.__data__ = d; pathBack.setAttribute("class","red-ui-flow-link-background red-ui-flow-link-path"+(d.link?" red-ui-flow-link-link":"")); this.__pathBack__ = pathBack; pathContents.appendChild(pathBack); - d3.select(pathBack) - .on("mousedown",linkMouseDown) - .on("touchstart",linkTouchStart) - .on("mousemove", function(d) { - if (mouse_mode === RED.state.SLICING) { + if (!d.__ghost) { + d3.select(pathBack) + .on("mousedown",linkMouseDown) + .on("touchstart",linkTouchStart) + .on("mousemove", function(d) { + if (mouse_mode === RED.state.SLICING) { - selectedLinks.add(d) - l.classed("red-ui-flow-link-splice",true) - redraw() - } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) { - if (!l.classed("red-ui-flow-link-splice")) { - // Find intersection point - var lineLength = pathLine.getTotalLength(); - var pos; - var delta = Infinity; - for (var i = 0; i < lineLength; i++) { - var linePos = pathLine.getPointAtLength(i); - var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor)) - var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor)) - var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY - if (posDelta < delta) { - pos = linePos - delta = posDelta - } - } - d._sliceLocation = pos selectedLinks.add(d) l.classed("red-ui-flow-link-splice",true) redraw() + } else if (mouse_mode === RED.state.SLICING_JUNCTION && !d.link) { + if (!l.classed("red-ui-flow-link-splice")) { + // Find intersection point + var lineLength = pathLine.getTotalLength(); + var pos; + var delta = Infinity; + for (var i = 0; i < lineLength; i++) { + var linePos = pathLine.getPointAtLength(i); + var posDeltaX = Math.abs(linePos.x-(d3.event.offsetX / scaleFactor)) + var posDeltaY = Math.abs(linePos.y-(d3.event.offsetY / scaleFactor)) + var posDelta = posDeltaX*posDeltaX + posDeltaY*posDeltaY + if (posDelta < delta) { + pos = linePos + delta = posDelta + } + } + d._sliceLocation = pos + selectedLinks.add(d) + l.classed("red-ui-flow-link-splice",true) + redraw() + } } - } - }) + }) + } var pathOutline = document.createElementNS("http://www.w3.org/2000/svg","path"); pathOutline.__data__ = d; @@ -5688,16 +5825,21 @@ RED.view = (function() { * - generateIds - whether to automatically generate new ids for all imported nodes * - generateDefaultNames - whether to automatically update any nodes with clashing * default names + * - notify - whether to show a notification if the import was successful */ function importNodes(newNodesObj,options) { options = options || { addFlow: false, touchImport: false, generateIds: false, - generateDefaultNames: false + generateDefaultNames: false, + notify: true, + applyNodeDefaults: false } - var addNewFlow = options.addFlow - var touchImport = options.touchImport; + const addNewFlow = options.addFlow + const touchImport = options.touchImport; + const showNotification = options.notify ?? true + const applyNodeDefaults = options.applyNodeDefaults ?? false if (mouse_mode === RED.state.SELECTING_NODE) { return; @@ -5781,7 +5923,8 @@ RED.view = (function() { addFlow: addNewFlow, importMap: options.importMap, markChanged: true, - modules: modules + modules: modules, + applyNodeDefaults: applyNodeDefaults }); if (importResult) { var new_nodes = importResult.nodes; @@ -5792,6 +5935,7 @@ RED.view = (function() { var new_subflows = importResult.subflows; var removedNodes = importResult.removedNodes; var new_default_workspace = importResult.missingWorkspace; + const nodeMap = importResult.nodeMap; if (addNewFlow && new_default_workspace) { RED.workspaces.show(new_default_workspace.id); } @@ -5813,16 +5957,18 @@ RED.view = (function() { var dx = mouse_position[0]; var dy = mouse_position[1]; - if (movingSet.length() > 0) { - var root_node = movingSet.get(0).n; - dx = root_node.x; - dy = root_node.y; + if (!touchImport) { + if (movingSet.length() > 0) { + const root_node = movingSet.get(0).n; + dx = root_node.x; + dy = root_node.y; + } } var minX = 0; var minY = 0; var i; - var node,group; + var node; var l =movingSet.length(); for (i=0;i"+RED._("clipboard.nodesImported")+"
"+countList,{id:"clipboard"}); } - }) - var newGroupCount = new_groups.length; - var newJunctionCount = new_junctions.length; - if (new_workspaces.length > 0) { - counts.push(RED._("clipboard.flow",{count:new_workspaces.length})); } - if (newNodeCount > 0) { - counts.push(RED._("clipboard.node",{count:newNodeCount})); + return { + nodeMap } - if (newGroupCount > 0) { - counts.push(RED._("clipboard.group",{count:newGroupCount})); - } - if (newConfigNodeCount > 0) { - counts.push(RED._("clipboard.configNode",{count:newConfigNodeCount})); - } - if (new_subflows.length > 0) { - counts.push(RED._("clipboard.subflow",{count:new_subflows.length})); - } - if (removedNodes && removedNodes.length > 0) { - counts.push(RED._("clipboard.replacedNodes",{count:removedNodes.length})); - } - if (counts.length > 0) { - var countList = ""+RED._("clipboard.nodesImported")+"
"+countList,{id:"clipboard"}); - } - + } + return { + nodeMap: {} } } catch(error) { if (error.code === "import_conflict") { @@ -6307,6 +6459,157 @@ RED.view = (function() { node.highlighted = true; RED.view.redraw(); } + + /** + * Add a suggested flow to the workspace. + * + * This appears as a ghost set of nodes. + * + * { + * "nodes": [ + * { + * type: "node-type", + * x: 0, + * y: 0, + * } + * ] + * } + * If `nodes` is a single node without an id property, it will be generated + * using its default properties. + * + * If `nodes` has multiple, they must all have ids and will be assumed to be 'importable'. + * In other words, a piece of valid flow json. + * + * Limitations: + * - does not support groups, subflows or whole tabs + * - does not support config nodes + * + * To clear the current suggestion, pass in `null`. + * + * + * @param {Object} suggestion - The suggestion object + */ + function setSuggestedFlow (suggestion) { + if (!currentSuggestion && !suggestion) { + // Avoid unnecessary redraws + return + } + // Clear up any existing suggestion state + clearSuggestedFlow() + currentSuggestion = suggestion + if (suggestion?.nodes?.length > 0) { + const nodeMap = {} + const links = [] + suggestion.nodes.forEach(nodeConfig => { + if (!nodeConfig.type || nodeConfig.type === 'group' || nodeConfig.type === 'subflow' || nodeConfig.type === 'tab') { + // A node type we don't support previewing + return + } + + let node + + if (nodeConfig.type === 'junction') { + node = { + _def: {defaults:{}}, + type: 'junction', + z: RED.workspaces.active(), + id: RED.nodes.id(), + x: nodeConfig.x, + y: nodeConfig.y, + w: 0, h: 0, + outputs: 1, + inputs: 1, + dirty: true, + moved: true + } + } else { + const def = RED.nodes.getType(nodeConfig.type) + if (!def || def.category === 'config') { + // Unknown node or config node + // TODO: unknown node types could happen... + return + } + const result = createNode(nodeConfig.type, nodeConfig.x, nodeConfig.y) + if (!result) { + return + } + node = result.node + node["_"] = node._def._; + + for (let d in node._def.defaults) { + if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'name') { + if (nodeConfig[d] !== undefined) { + node[d] = nodeConfig[d] + } else if (node._def.defaults[d].value) { + node[d] = JSON.parse(JSON.stringify(node._def.defaults[d].value)) + } + } + } + suggestedNodes.push(node) + } + if (node) { + node.id = nodeConfig.id || node.id + node.__ghost = true; + node.dirty = true; + nodeMap[node.id] = node + + if (nodeConfig.wires) { + nodeConfig.wires.forEach((wire, i) => { + if (wire.length > 0) { + wire.forEach(targetId => { + links.push({ + sourceId: nodeConfig.id || node.id, + sourcePort: i, + targetId: targetId, + targetPort: 0, + __ghost: true + }) + }) + } + }) + } + } + }) + links.forEach(link => { + const sourceNode = nodeMap[link.sourceId] + const targetNode = nodeMap[link.targetId] + if (sourceNode && targetNode) { + link.source = sourceNode + link.target = targetNode + suggestedLinks.push(link) + } + }) + } + if (ghostNode) { + if (suggestedNodes.length > 0) { + ghostNode.style('opacity', 0) + } else { + ghostNode.style('opacity', 1) + } + } + redraw(); + } + + function clearSuggestedFlow () { + currentSuggestion = null + suggestedNodes = [] + suggestedLinks = [] + } + + function applySuggestedFlow () { + if (currentSuggestion && currentSuggestion.nodes) { + const nodesToImport = currentSuggestion.nodes + setSuggestedFlow(null) + return importNodes(nodesToImport, { + generateIds: true, + touchImport: true, + notify: false, + // Ensure the node gets all of its defaults applied + applyNodeDefaults: true + }) + } + } + return { init: init, state:function(state) { @@ -6567,6 +6870,8 @@ RED.view = (function() { width: space_width, height: space_height }; - } + }, + setSuggestedFlow, + applySuggestedFlow }; })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/base.scss b/packages/node_modules/@node-red/editor-client/src/sass/base.scss index 63ab6b77f..afbafe049 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/base.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/base.scss @@ -208,12 +208,10 @@ body { } img { - width: auto\9; height: auto; max-width: 100%; vertical-align: middle; border: 0; - -ms-interpolation-mode: bicubic; } blockquote { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss index ad055b97c..944536845 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/flow.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/flow.scss @@ -161,7 +161,15 @@ svg:not(.red-ui-workspace-lasso-active) { fill: var(--red-ui-group-default-label-color); } +.red-ui-flow-node-ghost { + opacity: 0.6; + rect.red-ui-flow-node { + stroke: var(--red-ui-node-border-placeholder); + stroke-dasharray:10,4; + stroke-width: 2; + } +} .red-ui-flow-node-unknown { stroke-dasharray:10,4; @@ -401,6 +409,13 @@ g.red-ui-flow-node-selected { g.red-ui-flow-link-selected path.red-ui-flow-link-line { stroke: var(--red-ui-node-selected-color); } + +g.red-ui-flow-link-ghost path.red-ui-flow-link-line { + stroke: var(--red-ui-node-border-placeholder); + stroke-width: 2; + stroke-dasharray: 10, 4; +} + g.red-ui-flow-link-unknown path.red-ui-flow-link-line { stroke: var(--red-ui-link-unknown-color); stroke-width: 2; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/forms.scss b/packages/node_modules/@node-red/editor-client/src/sass/forms.scss index a281b9265..3fa8bcc65 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/forms.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/forms.scss @@ -216,14 +216,11 @@ .uneditable-input:focus { border-color: var(--red-ui-form-input-focus-color); outline: 0; - outline: thin dotted \9; } input[type="radio"], input[type="checkbox"] { margin: 4px 0 0; - margin-top: 1px \9; - *margin-top: 0; line-height: normal; } @@ -285,12 +282,6 @@ color: var(--red-ui-form-placeholder-color); } - input:-ms-input-placeholder, - div[contenteditable="true"]:-ms-input-placeholder, - textarea:-ms-input-placeholder { - color: var(--red-ui-form-placeholder-color); - } - input::-webkit-input-placeholder, div[contenteditable="true"]::-webkit-input-placeholder, textarea::-webkit-input-placeholder { @@ -568,11 +559,7 @@ input.search-query { padding-right: 14px; - padding-right: 4px \9; padding-left: 14px; - padding-left: 4px \9; - /* IE7-8 doesn't have border-radius, so don't indent the padding */ - margin-bottom: 0; border-radius: 15px; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss index 6262597a1..486396c59 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/mixins.scss @@ -18,7 +18,6 @@ -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; - -ms-user-select: none; user-select: none; } @@ -26,7 +25,6 @@ -webkit-user-select: auto; -khtml-user-select: auto; -moz-user-select: auto; - -ms-user-select: auto; user-select: auto; } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss index cdbfa406b..8955887dd 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/palette-editor.scss @@ -126,15 +126,20 @@ margin-left: 5px; } + .red-ui-palette-module-deprecated { + cursor: pointer; + color: var(--red-ui-text-color-error); + font-size: 0.7em; + border: 1px solid var(--red-ui-text-color-error); + border-radius: 30px; + padding: 2px 5px; + } + .red-ui-palette-module-description { margin-left: 20px; font-size: 0.9em; color: var(--red-ui-secondary-text-color); } - .red-ui-palette-module-link { - } - .red-ui-palette-module-set-button-group { - } .red-ui-palette-module-content { display: none; padding: 10px 3px; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss index 3df2b495b..027e783a3 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/popover.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/popover.scss @@ -205,3 +205,39 @@ background: var(--red-ui-secondary-background); z-index: 2000; } + + +.red-ui-popover.red-ui-dialog { + z-index: 2003; + --red-ui-popover-background: var(--red-ui-secondary-background); + --red-ui-popover-border: var(--red-ui-tourGuide-border); + --red-ui-popover-color: var(--red-ui-primary-text-color); + + .red-ui-popover-content { + h2 { + text-align: center; + margin-top: 0px; + line-height: 1.2em; + color: var(--red-ui-tourGuide-heading-color); + i.fa { + font-size: 1.5em + } + } + } + +} + +.red-ui-dialog-toolbar { + min-height: 36px; + position: relative; + display: flex; + justify-content: flex-end; + gap: 10px; +} +.red-ui-dialog-body { + padding: 20px 40px 10px; + a { + color: var(--red-ui-text-color-link) !important; + text-decoration: none; + } +} diff --git a/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss b/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss index 5e0c7fa47..7abae094c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/userSettings.scss @@ -70,8 +70,14 @@ overflow-y: auto; } .red-ui-settings-row { + display: flex; + gap: 10px; + align-items:flex-start; padding: 5px 10px 2px; } +.red-ui-settings-row input[type="checkbox"] { + margin-top: 8px; +} .red-ui-settings-section { position: relative; &:after { diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-auto-complete.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-auto-complete.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-auto-complete.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-background-deploy.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-background-deploy.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-background-deploy.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-config-select.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-config-select.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-config-select.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-diff-update.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-diff-update.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-diff-update.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer-location.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer-location.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer-location.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-multiplayer.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-multiplayer.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-plugins.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-plugins.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-plugins.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-sf-config.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-sf-config.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-sf-config.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png b/packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-timestamp-formatting.png similarity index 100% rename from packages/node_modules/@node-red/editor-client/src/tours/images/nr4-timestamp-formatting.png rename to packages/node_modules/@node-red/editor-client/src/tours/4.0/images/nr4-timestamp-formatting.png diff --git a/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js b/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js new file mode 100644 index 000000000..02a559136 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/tours/4.0/welcome.js @@ -0,0 +1,231 @@ +export default { + version: "4.0.0", + steps: [ + { + titleIcon: "fa fa-map-o", + title: { + "en-US": "Welcome to Node-RED 4.0!", + "ja": "Node-RED 4.0 へようこそ!", + "fr": "Bienvenue dans Node-RED 4.0!" + }, + description: { + "en-US": "Let's take a moment to discover the new features in this release.
", + "ja": "本リリースの新機能を見つけてみましょう。
", + "fr": "Prenons un moment pour découvrir les nouvelles fonctionnalités de cette version.
" + } + }, + { + title: { + "en-US": "Multiplayer Mode", + "ja": "複数ユーザ同時利用モード", + "fr": "Mode Multi-utilisateur" + }, + image: 'images/nr4-multiplayer-location.png', + description: { + "en-US": `This release includes the first small steps towards making Node-RED easier + to work with when you have multiple people editing flows at the same time.
+When this feature is enabled, you will now see who else has the editor open and some + basic information on where they are in the editor.
+Check the release post for details on how to enable this feature in your settings file.
`, + "ja": `本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。
+本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。
+設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。
`, + "fr": `Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser + lorsque plusieurs personnes modifient des flux en même temps.
+Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont + ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.
+Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité + dans votre fichier de paramètres.
` + } + }, + { + title: { + "en-US": "Better background deploy handling", + "ja": "バックグラウンドのデプロイ処理の改善", + "fr": "Meilleure gestion du déploiement en arrière-plan" + }, + image: 'images/nr4-background-deploy.png', + description: { + "en-US": `If another user deploys changes whilst you are editing, we now use a more discrete notification + that doesn't stop you continuing your work - especially if they are being very productive and deploying lots + of changes.
`, + "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`, + "fr": `Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez + une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.
` + } + }, + { + title: { + "en-US": "Improved flow diffs", + "ja": "フローの差分表示の改善", + "fr": "Amélioration des différences de flux" + }, + image: 'images/nr4-diff-update.png', + description: { + "en-US": `When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration + changes and those that have only been moved.
+
When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.
`, + "ja": `フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。
+
これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。
`, + "fr": `Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les + noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.
+
Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les + plus importants.
` + } + }, + { + title: { + "en-US": "Better Configuration Node UX", + "ja": "設定ノードのUXが向上", + "fr": "Meilleure expérience utilisateur du noeud de configuration" + }, + image: 'images/nr4-config-select.png', + description: { + "en-US": `The Configuration node selection UI has had a small update to have a dedicated 'add' button + next to the select box.
+It's a small change, but should make it easier to work with your config nodes.
`, + "ja": `設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。
+微修正ですが設定ノードの操作が容易になります。
`, + "fr": `L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite + mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.
+C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.
` + } + }, + { + title: { + "en-US": "Timestamp formatting options", + "ja": "タイムスタンプの形式の項目", + "fr": "Options de formatage de l'horodatage" + }, + image: 'images/nr4-timestamp-formatting.png', + description: { + "en-US": `Nodes that let you set a timestamp now have options on what format that timestamp should be in.
+We're keeping it simple to begin with by providing three options:
+
タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。
+次の3つの項目を追加したことで、簡単に選択できるようになりました:
+
Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.
+Nous gardons les choses simples en proposant trois options :
+
The flow/global context inputs and the env input
+ now all include auto-complete suggestions based on the live state of your flows.
flow/globalコンテキストやenvの入力を、現在のフローの状態をもとに自動補完で提案するようになりました。
Les entrées contextuelles flow/global et l'entrée env
+ incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.
Subflows can now be customised to allow each instance to use a different + config node of a selected type.
+For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing + of the messages received can be pointed at a different broker.
+ `, + "ja": `サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。
+例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。
+ `, + "fr": `Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un + noeud de configuration d'un type sélectionné.
+Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement + des messages reçus peut être pointée vers un autre courtier.
+ ` + } + }, + { + title: { + "en-US": "Remembering palette state", + "ja": "パレットの状態を維持", + "fr": "Mémorisation de l'état de la palette" + }, + description: { + "en-US": `The palette now remembers what categories you have hidden between reloads - as well as any + filter you have applied.
`, + "ja": `パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。
`, + "fr": `La palette se souvient désormais des catégories que vous avez masquées entre les rechargements, + ainsi que le filtre que vous avez appliqué.
` + } + }, + { + title: { + "en-US": "Plugins shown in the Palette Manager", + "ja": "パレット管理にプラグインを表示", + "fr": "Affichage des Plugins dans le gestionnaire de palettes" + }, + image: 'images/nr4-plugins.png', + description: { + "en-US": `The palette manager now shows any plugin modules you have installed, such as
+ node-red-debugger. Previously they would only be shown if the plugins include
+ nodes for the palette.
パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。
Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
+ tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient
+ des noeuds pour la palette.
The core nodes have received lots of minor fixes, documentation updates and + small enhancements. Check the full changelog in the Help sidebar for a full list.
+コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。
+Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour. + Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :
+Let's take a moment to discover the new features in this release.
", @@ -16,216 +16,45 @@ export default { }, { title: { - "en-US": "Multiplayer Mode", - "ja": "複数ユーザ同時利用モード", - "fr": "Mode Multi-utilisateur" + "en-US": "Something new", }, - image: 'images/nr4-multiplayer-location.png', + // image: 'images/nr4-multiplayer-location.png', description: { - "en-US": `This release includes the first small steps towards making Node-RED easier - to work with when you have multiple people editing flows at the same time.
-When this feature is enabled, you will now see who else has the editor open and some - basic information on where they are in the editor.
-Check the release post for details on how to enable this feature in your settings file.
`, - "ja": `本リリースには、複数ユーザが同時にフローを編集する時に、Node-REDをより使いやすくするのための最初の微修正が入っています。
-本機能を有効にすると、誰がエディタを開いているか、その人がエディタ上のどこにいるかの基本的な情報が表示されます。
-設定ファイルで本機能を有効化する方法の詳細は、リリースの投稿を確認してください。
`, - "fr": `Cette version inclut les premières étapes visant à rendre Node-RED plus facile à utiliser - lorsque plusieurs personnes modifient des flux en même temps.
-Lorsque cette fonctionnalité est activée, vous pourrez désormais voir si d’autres utilisateurs ont - ouvert l'éditeur. Vous pourrez également savoir où ces utilisateurs se trouvent dans l'éditeur.
-Consultez la note de publication pour plus de détails sur la façon d'activer cette fonctionnalité - dans votre fichier de paramètres.
` + "en-US": `Something new
` } }, - { - title: { - "en-US": "Better background deploy handling", - "ja": "バックグラウンドのデプロイ処理の改善", - "fr": "Meilleure gestion du déploiement en arrière-plan" - }, - image: 'images/nr4-background-deploy.png', - description: { - "en-US": `If another user deploys changes whilst you are editing, we now use a more discrete notification - that doesn't stop you continuing your work - especially if they are being very productive and deploying lots - of changes.
`, - "ja": `他のユーザが変更をデプロイした時に、特に変更が多い生産的な編集作業を妨げないように通知するようになりました。`, - "fr": `Si un autre utilisateur déploie des modifications pendant que vous êtes en train de modifier, vous recevrez - une notification plus discrète qu'auparavant qui ne vous empêche pas de continuer votre travail.
` - } - }, - { - title: { - "en-US": "Improved flow diffs", - "ja": "フローの差分表示の改善", - "fr": "Amélioration des différences de flux" - }, - image: 'images/nr4-diff-update.png', - description: { - "en-US": `When viewing changes made to a flow, Node-RED now distinguishes between nodes that have had configuration - changes and those that have only been moved.
-
When faced with a long list of changes to look at, this makes it much easier to focus on more significant items.
`, - "ja": `フローの変更内容を表示する時に、Node-REDは設定が変更されたノードと、移動されただけのノードを区別するようになりました。
-
これによって、多くの変更内容を確認する際に、重要な項目に焦点を当てることができます。
`, - "fr": `Lors de l'affichage des modifications apportées à un flux, Node-RED fait désormais la distinction entre les - noeuds qui ont changé de configuration et ceux qui ont seulement été déplacés.
-
Face à une longue liste de changements à examiner, il est beaucoup plus facile de se concentrer sur les éléments les - plus importants.
` - } - }, - { - title: { - "en-US": "Better Configuration Node UX", - "ja": "設定ノードのUXが向上", - "fr": "Meilleure expérience utilisateur du noeud de configuration" - }, - image: 'images/nr4-config-select.png', - description: { - "en-US": `The Configuration node selection UI has had a small update to have a dedicated 'add' button - next to the select box.
-It's a small change, but should make it easier to work with your config nodes.
`, - "ja": `設定ノードを選択するUIが修正され、選択ボックスの隣に専用の「追加」ボタンが追加されました。
-微修正ですが設定ノードの操作が容易になります。
`, - "fr": `L'interface utilisateur de la sélection du noeud de configuration a fait l'objet d'une petite - mise à jour afin de disposer d'un bouton « Ajouter » à côté de la zone de sélection.
-C'est un petit changement, mais cela devrait faciliter le travail avec vos noeuds de configuration.
` - } - }, - { - title: { - "en-US": "Timestamp formatting options", - "ja": "タイムスタンプの形式の項目", - "fr": "Options de formatage de l'horodatage" - }, - image: 'images/nr4-timestamp-formatting.png', - description: { - "en-US": `Nodes that let you set a timestamp now have options on what format that timestamp should be in.
-We're keeping it simple to begin with by providing three options:
-
タイムスタンプを設定するノードに、タイムスタンプの形式を指定できる項目が追加されました。
-次の3つの項目を追加したことで、簡単に選択できるようになりました:
-
Les noeuds qui vous permettent de définir un horodatage disposent désormais d'options sur le format dans lequel cet horodatage peut être défini.
-Nous gardons les choses simples en proposant trois options :
-
The flow/global context inputs and the env input
- now all include auto-complete suggestions based on the live state of your flows.
flow/globalコンテキストやenvの入力を、現在のフローの状態をもとに自動補完で提案するようになりました。
Les entrées contextuelles flow/global et l'entrée env
- incluent désormais des suggestions de saisie semi-automatique basées sur l'état actuel de vos flux.
Subflows can now be customised to allow each instance to use a different - config node of a selected type.
-For example, each instance of a subflow that connects to an MQTT Broker and does some post-processing - of the messages received can be pointed at a different broker.
- `, - "ja": `サブフローをカスタマイズして、選択した型の異なる設定ノードを各インスタンスが使用できるようになりました。
-例えば、MQTTブローカへ接続し、メッセージ受信と後処理を行うサブフローの各インスタンスに異なるブローカを指定することも可能です。
- `, - "fr": `Les sous-flux peuvent désormais être personnalisés pour permettre à chaque instance d'utiliser un - noeud de configuration d'un type sélectionné.
-Par exemple, chaque instance d'un sous-flux qui se connecte à un courtier MQTT et effectue un post-traitement - des messages reçus peut être pointée vers un autre courtier.
- ` - } - }, - { - title: { - "en-US": "Remembering palette state", - "ja": "パレットの状態を維持", - "fr": "Mémorisation de l'état de la palette" - }, - description: { - "en-US": `The palette now remembers what categories you have hidden between reloads - as well as any - filter you have applied.
`, - "ja": `パレット上で非表示にしたカテゴリや適用したフィルタが、リロードしても記憶されるようになりました。
`, - "fr": `La palette se souvient désormais des catégories que vous avez masquées entre les rechargements, - ainsi que le filtre que vous avez appliqué.
` - } - }, - { - title: { - "en-US": "Plugins shown in the Palette Manager", - "ja": "パレット管理にプラグインを表示", - "fr": "Affichage des Plugins dans le gestionnaire de palettes" - }, - image: 'images/nr4-plugins.png', - description: { - "en-US": `The palette manager now shows any plugin modules you have installed, such as
- node-red-debugger. Previously they would only be shown if the plugins include
- nodes for the palette.
パレットの管理に node-red-debugger の様なインストールしたプラグインが表示されます。以前はプラグインにパレット向けのノードが含まれている時のみ表示されていました。
Le gestionnaire de palettes affiche désormais tous les plugins que vous avez installés,
- tels que node-red-debugger. Auparavant, ils n'étaient affichés que s'ils contenaient
- des noeuds pour la palette.
The core nodes have received lots of minor fixes, documentation updates and - small enhancements. Check the full changelog in the Help sidebar for a full list.
-コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。
-Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour. - Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :
-The core nodes have received lots of minor fixes, documentation updates and + // small enhancements. Check the full changelog in the Help sidebar for a full list.
+ //コアノードには沢山の軽微な修正、ドキュメント更新、小さな機能拡張が入っています。全リストはヘルプサイドバーにある変更履歴を参照してください。
+ //Les noeuds principaux ont reçu de nombreux correctifs mineurs ainsi que des améliorations. La documentation a été mise à jour. + // Consultez le journal des modifications dans la barre latérale d'aide pour une liste complète. Ci-dessous, les changements les plus importants :
+ //