Detect importing duplicate nodes and help user resolve

This commit is contained in:
Nick O'Leary 2020-09-16 11:42:22 +01:00
parent 5686158245
commit a48f0827ae
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
12 changed files with 844 additions and 179 deletions

View File

@ -198,6 +198,8 @@
"flow_plural": "__count__ flows",
"subflow": "__count__ subflow",
"subflow_plural": "__count__ subflows",
"replacedNodes": "__count__ node replaced",
"replacedNodes_plural": "__count__ nodes replaced",
"pasteNodes": "Paste flow json or",
"selectFile": "select a file to import",
"importNodes": "Import nodes",
@ -230,13 +232,19 @@
},
"import": {
"import": "Import to",
"importSelected": "Import selected",
"importCopy": "Import copy",
"viewNodes": "View nodes...",
"newFlow": "new flow",
"replace": "replace",
"errors": {
"notArray": "Input not a JSON Array",
"itemNotObject": "Input not a valid flow - item __index__ not a node object",
"missingId": "Input not a valid flow - item __index__ missing 'id' property",
"missingType": "Input not a valid flow - item __index__ missing 'type' property"
}
},
"conflictNotification1": "Some of the nodes you are importing already exist in your workspace.",
"conflictNotification2": "Select which nodes to import and whether to replace the existing nodes, or to import a copy of them."
},
"copyMessagePath": "Path copied",
"copyMessageValue": "Value copied",

View File

@ -37,22 +37,38 @@ RED.history = (function() {
inverseEv.events.push(r);
}
} else if (ev.t == 'replace') {
inverseEv = {
t: 'replace',
config: RED.nodes.createCompleteNodeSet(),
changed: {},
rev: RED.nodes.version()
};
RED.nodes.clear();
var imported = RED.nodes.import(ev.config);
imported[0].forEach(function(n) {
if (ev.changed[n.id]) {
n.changed = true;
inverseEv.changed[n.id] = true;
}
})
if (ev.complete) {
// This is a replace of everything. We can short-cut
// the logic by clearing everyting first, then importing
// the ev.config.
// Used by RED.diff.mergeDiff
inverseEv = {
t: 'replace',
config: RED.nodes.createCompleteNodeSet(),
changed: {},
rev: RED.nodes.version()
};
RED.nodes.clear();
var imported = RED.nodes.import(ev.config);
imported.nodes.forEach(function(n) {
if (ev.changed[n.id]) {
n.changed = true;
inverseEv.changed[n.id] = true;
}
})
RED.nodes.version(ev.rev);
RED.nodes.version(ev.rev);
} else {
var importMap = {};
ev.config.forEach(function(n) {
importMap[n.id] = "replace";
})
var importedResult = RED.nodes.import(ev.config,{importMap: importMap})
inverseEv = {
t: 'replace',
config: importedResult.removedNodes
}
}
} else if (ev.t == 'add') {
inverseEv = {
t: "delete",

View File

@ -16,7 +16,7 @@
RED.nodes = (function() {
var node_defs = {};
var nodes = [];
var nodes = {};
var nodeTabMap = {};
var configNodes = {};
@ -189,6 +189,7 @@ RED.nodes = (function() {
})();
function getID() {
// return Math.floor(Math.random()*15728640 + 1048576).toString(16)
return (1+Math.random()*4294967295).toString(16);
}
@ -216,7 +217,7 @@ RED.nodes = (function() {
});
n.i = nextId+1;
}
nodes.push(n);
nodes[n.id] = n;
if (nodeTabMap[n.z]) {
nodeTabMap[n.z][n.id] = n;
} else {
@ -233,12 +234,8 @@ RED.nodes = (function() {
function getNode(id) {
if (id in configNodes) {
return configNodes[id];
} else {
for (var n in nodes) {
if (nodes[n].id == id) {
return nodes[n];
}
}
} else if (id in nodes) {
return nodes[id];
}
return null;
}
@ -252,58 +249,56 @@ RED.nodes = (function() {
delete configNodes[id];
RED.events.emit('nodes:remove',node);
RED.workspaces.refresh();
} else {
node = getNode(id);
if (node) {
nodes.splice(nodes.indexOf(node),1);
if (nodeTabMap[node.z]) {
delete nodeTabMap[node.z][node.id];
}
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.forEach(removeLink);
var updatedConfigNode = false;
for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
var property = node._def.defaults[d];
if (property.type) {
var type = registry.getNodeType(property.type);
if (type && type.category == "config") {
var configNode = configNodes[node[d]];
if (configNode) {
updatedConfigNode = true;
if (configNode._def.exclusive) {
removeNode(node[d]);
removedNodes.push(configNode);
} else {
var users = configNode.users;
users.splice(users.indexOf(node),1);
}
} else if (id in nodes) {
node = nodes[id];
delete nodes[id]
if (nodeTabMap[node.z]) {
delete nodeTabMap[node.z][node.id];
}
removedLinks = links.filter(function(l) { return (l.source === node) || (l.target === node); });
removedLinks.forEach(removeLink);
var updatedConfigNode = false;
for (var d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d)) {
var property = node._def.defaults[d];
if (property.type) {
var type = registry.getNodeType(property.type);
if (type && type.category == "config") {
var configNode = configNodes[node[d]];
if (configNode) {
updatedConfigNode = true;
if (configNode._def.exclusive) {
removeNode(node[d]);
removedNodes.push(configNode);
} else {
var users = configNode.users;
users.splice(users.indexOf(node),1);
}
}
}
}
}
if (node.type.indexOf("subflow:") === 0) {
var subflowId = node.type.substring(8);
var sf = RED.nodes.subflow(subflowId);
if (sf) {
sf.instances.splice(sf.instances.indexOf(node),1);
}
}
if (updatedConfigNode) {
RED.workspaces.refresh();
}
try {
if (node._def.oneditdelete) {
node._def.oneditdelete.call(node);
}
} catch(err) {
console.log("oneditdelete",node.id,node.type,err.toString());
}
RED.events.emit('nodes:remove',node);
}
if (node.type.indexOf("subflow:") === 0) {
var subflowId = node.type.substring(8);
var sf = RED.nodes.subflow(subflowId);
if (sf) {
sf.instances.splice(sf.instances.indexOf(node),1);
}
}
if (updatedConfigNode) {
RED.workspaces.refresh();
}
try {
if (node._def.oneditdelete) {
node._def.oneditdelete.call(node);
}
} catch(err) {
console.log("oneditdelete",node.id,node.type,err.toString());
}
RED.events.emit('nodes:remove',node);
}
@ -377,10 +372,13 @@ RED.nodes = (function() {
workspacesOrder.splice(workspacesOrder.indexOf(id),1);
var i;
var node;
for (i=0;i<nodes.length;i++) {
node = nodes[i];
if (node.z == id) {
removedNodes.push(node);
// TODO: this should use nodeTabMap
for (i in nodes) {
if (nodes.hasOwnProperty(i)) {
node = nodes[i];
if (node.z == id) {
removedNodes.push(node);
}
}
}
for(i in configNodes) {
@ -487,17 +485,19 @@ RED.nodes = (function() {
}
function subflowContains(sfid,nodeid) {
for (var i=0;i<nodes.length;i++) {
var node = nodes[i];
if (node.z === sfid) {
var m = /^subflow:(.+)$/.exec(node.type);
if (m) {
if (m[1] === nodeid) {
return true;
} else {
var result = subflowContains(m[1],nodeid);
if (result) {
for (var i in nodes) {
if (nodes.hasOwnProperty(i)) {
var node = nodes[i];
if (node.z === sfid) {
var m = /^subflow:(.+)$/.exec(node.type);
if (m) {
if (m[1] === nodeid) {
return true;
} else {
var result = subflowContains(m[1],nodeid);
if (result) {
return true;
}
}
}
}
@ -737,6 +737,16 @@ RED.nodes = (function() {
return node;
}
function createExportableSubflow(id) {
var sf = getSubflow(id);
var nodeSet = [sf];
var sfNodeIds = Object.keys(nodeTabMap[sf.id]||{});
for (var i=0, l=sfNodeIds.length; i<l; i++) {
nodeSet.push(nodeTabMap[sf.id][sfNodeIds[i]]);
}
return createExportableNodeSet(nodeSet);
}
/**
* Converts the current node selection to an exportable JSON Object
**/
@ -826,9 +836,10 @@ RED.nodes = (function() {
nns.push(convertNode(configNodes[i], exportCredentials));
}
}
for (i=0;i<nodes.length;i++) {
var node = nodes[i];
nns.push(convertNode(node, exportCredentials));
for (i in nodes) {
if (nodes.hasOwnProperty(i)) {
nns.push(convertNode(nodes[i], exportCredentials));
}
}
return nns;
}
@ -897,16 +908,152 @@ RED.nodes = (function() {
}
return true;
}
function identifyImportConflicts(importedNodes) {
var imported = {
tabs: {},
subflows: {},
groups: {},
configs: {},
nodes: {},
all: [],
conflicted: {},
zMap: {},
}
importedNodes.forEach(function(n) {
imported.all.push(n);
if (n.type === "tab") {
imported.tabs[n.id] = n;
} else if (n.type === "subflow") {
imported.subflows[n.id] = n;
} else if (n.type === "group") {
imported.groups[n.id] = n;
} else if (n.hasOwnProperty("x") && n.hasOwnProperty("y")) {
imported.nodes[n.id] = n;
} else {
imported.configs[n.id] = n;
}
var nodeZ = n.z || "__global__";
imported.zMap[nodeZ] = imported.zMap[nodeZ] || [];
imported.zMap[nodeZ].push(n)
if (nodes[n.id] || configNodes[n.id] || workspaces[n.id] || subflows[n.id] || groups[n.id]) {
imported.conflicted[n.id] = n;
}
})
return imported;
}
/**
* Replace the provided nodes.
* This must contain complete Subflow defs or complete Flow Tabs.
* It does not replace an individual node in the middle of a flow.
*/
function replaceNodes(newNodes) {
var zMap = {};
var newSubflows = {};
var newConfigNodes = {};
var removedNodes = [];
// Figure out what we're being asked to replace - subflows/configNodes
// TODO: config nodes
newNodes.forEach(function(n) {
if (n.type === "subflow") {
newSubflows[n.id] = n;
} else if (!n.hasOwnProperty('x') && !n.hasOwnProperty('y')) {
newConfigNodes[n.id] = n;
}
if (n.z) {
zMap[n.z] = zMap[n.z] || [];
zMap[n.z].push(n);
}
})
// Filter out config nodes inside a subflow def that is being replaced
var configNodeIds = Object.keys(newConfigNodes);
configNodeIds.forEach(function(id) {
var n = newConfigNodes[id];
if (newSubflows[n.z]) {
// This config node is in a subflow to be replaced.
// - remove from the list as it'll get handled with the subflow
delete newConfigNodes[id];
}
});
// Rebuild the list of ids
configNodeIds = Object.keys(newConfigNodes);
// ------------------------------
// Replace subflow definitions
//
// For each of the subflows to be replaced:
var newSubflowIds = Object.keys(newSubflows);
newSubflowIds.forEach(function(id) {
var n = newSubflows[id];
// Get a snapshot of the existing subflow definition
removedNodes = removedNodes.concat(createExportableSubflow(id));
// Remove the old subflow definition - but leave the instances in place
var removalResult = RED.subflow.removeSubflow(n.id, true);
// Create the list of nodes for the new subflow def
var subflowNodes = [n].concat(zMap[n.id]);
// Import the new subflow - no clashes should occur as we've removed
// the old version
var result = importNodes(subflowNodes);
newSubflows[id] = getSubflow(id);
})
// Having replaced the subflow definitions, now need to update the
// instance nodes.
RED.nodes.eachNode(function(n) {
if (/^subflow:/.test(n.type)) {
var sfId = n.type.substring(8);
if (newSubflows[sfId]) {
// This is an instance of one of the replaced subflows
// - update the new def's instances array to include this one
newSubflows[sfId].instances.push(n);
// - update the instance's _def to point to the new def
n._def = RED.nodes.getType(n.type);
// - set all the flags so the view refreshes properly
n.dirty = true;
n.changed = true;
n._colorChanged = true;
}
}
})
// Just in case the imported subflow changed color.
RED.utils.clearNodeColorCache();
// ------------------------------
// Replace config nodes
//
configNodeIds.forEach(function(id) {
removedNodes = removedNodes.concat(convertNode(getNode(id)));
removeNode(id);
importNodes([newConfigNodes[id]])
});
return {
removedNodes: removedNodes
}
}
/**
* Options:
* - generateIds - whether to replace all node ids
* - addFlow - whether to import nodes to a new tab
* - importMap - how to resolve any conflicts.
* - id:import - import as-is
* - id:copy - import with new id
* - id:replace - import over the top of existing
*/
function importNodes(newNodesObj,options) { // createNewIds,createMissingWorkspace) {
options = options || {
generateIds: false,
addFlow: false
addFlow: false,
}
options.importMap = options.importMap || {};
var createNewIds = options.generateIds;
var createMissingWorkspace = options.addFlow;
var i;
@ -938,14 +1085,44 @@ RED.nodes = (function() {
// copies of the flow would get loaded at the same time.
// If the user hit deploy they would have saved those duplicates.
var seenIds = {};
var existingNodes = [];
var nodesToReplace = [];
newNodes = newNodes.filter(function(n) {
var id = n.id;
if (seenIds[n.id]) {
return false;
}
seenIds[n.id] = true;
if (!options.generateIds) {
if (!options.importMap[id]) {
// No conflict resolution for this node
if (nodes[id] || configNodes[id] || workspaces[id] || subflows[id] || groups[id]) {
existingNodes.push(id);
}
} else if (options.importMap[id] === "replace") {
nodesToReplace.push(n);
return false;
}
}
return true;
})
if (existingNodes.length > 0) {
var existingNodesError = new Error();
existingNodesError.code = "import_conflict";
existingNodesError.importConfig = identifyImportConflicts(newNodes);
throw existingNodesError;
}
var removedNodes;
if (nodesToReplace.length > 0) {
var replaceResult = replaceNodes(nodesToReplace);
removedNodes = replaceResult.removedNodes;
}
var isInitialLoad = false;
if (!initialLoad) {
isInitialLoad = true;
@ -954,6 +1131,7 @@ RED.nodes = (function() {
var unknownTypes = [];
for (i=0;i<newNodes.length;i++) {
n = newNodes[i];
var id = n.id;
// TODO: remove workspace in next release+1
if (n.type != "workspace" &&
n.type != "tab" &&
@ -1048,7 +1226,7 @@ RED.nodes = (function() {
if (defaultWorkspace == null) {
defaultWorkspace = n;
}
if (createNewIds) {
if (createNewIds || options.importMap[n.id] === "copy") {
nid = getID();
workspace_map[n.id] = nid;
n.id = nid;
@ -1057,12 +1235,15 @@ RED.nodes = (function() {
RED.workspaces.add(n);
new_workspaces.push(n);
} else if (n.type === "subflow") {
var matchingSubflow = checkForMatchingSubflow(n,nodeZmap[n.id]);
var matchingSubflow;
if (!options.importMap[n.id]) {
matchingSubflow = checkForMatchingSubflow(n,nodeZmap[n.id]);
}
if (matchingSubflow) {
subflow_denylist[n.id] = matchingSubflow;
} else {
subflow_map[n.id] = n;
if (createNewIds) {
if (createNewIds || options.importMap[n.id] === "copy") {
nid = getID();
n.id = nid;
}
@ -1088,7 +1269,7 @@ RED.nodes = (function() {
n.status.id = getID();
}
new_subflows.push(n);
addSubflow(n,createNewIds);
addSubflow(n,createNewIds || options.importMap[n.id] === "copy");
}
}
}
@ -1108,7 +1289,7 @@ RED.nodes = (function() {
def = registry.getNodeType(n.type);
if (def && def.category == "config") {
var existingConfigNode = null;
if (createNewIds) {
if (createNewIds || options.importMap[n.id] === "copy") {
if (n.z) {
if (subflow_denylist[n.z]) {
continue;
@ -1129,23 +1310,24 @@ RED.nodes = (function() {
}
}
}
existingConfigNode = RED.nodes.node(n.id);
if (existingConfigNode) {
if (n.z && existingConfigNode.z !== n.z) {
existingConfigNode = null;
// Check the config nodes on n.z
for (var cn in configNodes) {
if (configNodes.hasOwnProperty(cn)) {
if (configNodes[cn].z === n.z && compareNodes(configNodes[cn],n,false)) {
existingConfigNode = configNodes[cn];
node_map[n.id] = configNodes[cn];
break;
if (options.importMap[n.id] !== "copy") {
existingConfigNode = RED.nodes.node(n.id);
if (existingConfigNode) {
if (n.z && existingConfigNode.z !== n.z) {
existingConfigNode = null;
// Check the config nodes on n.z
for (var cn in configNodes) {
if (configNodes.hasOwnProperty(cn)) {
if (configNodes[cn].z === n.z && compareNodes(configNodes[cn],n,false)) {
existingConfigNode = configNodes[cn];
node_map[n.id] = configNodes[cn];
break;
}
}
}
}
}
}
}
if (!existingConfigNode || existingConfigNode._def.exclusive) { //} || !compareNodes(existingConfigNode,n,true) || existingConfigNode.z !== n.z) {
@ -1176,7 +1358,7 @@ RED.nodes = (function() {
}
configNode.label = def.label;
configNode._def = def;
if (createNewIds) {
if (createNewIds || options.importMap[n.id] === "copy") {
configNode.id = getID();
}
node_map[n.id] = configNode;
@ -1216,7 +1398,7 @@ RED.nodes = (function() {
if (n.hasOwnProperty('g')) {
node.g = n.g;
}
if (createNewIds) {
if (createNewIds || options.importMap[n.id] === "copy") {
if (subflow_denylist[n.z]) {
continue;
} else if (subflow_map[node.z]) {
@ -1265,7 +1447,7 @@ RED.nodes = (function() {
} else if (n.type.substring(0,7) === "subflow") {
var parentId = n.type.split(":")[1];
var subflow = subflow_denylist[parentId]||subflow_map[parentId]||getSubflow(parentId);
if (createNewIds) {
if (createNewIds || options.importMap[n.id] === "copy") {
parentId = subflow.id;
node.type = "subflow:"+parentId;
node._def = registry.getNodeType(node.type);
@ -1536,24 +1718,35 @@ RED.nodes = (function() {
});
}
return [new_nodes,new_links,new_groups,new_workspaces,new_subflows,missingWorkspace];
return {
nodes:new_nodes,
links:new_links,
groups:new_groups,
workspaces:new_workspaces,
subflows:new_subflows,
missingWorkspace: missingWorkspace,
removedNodes: removedNodes
}
}
// TODO: supports filter.z|type
function filterNodes(filter) {
var result = [];
var searchSet = nodes;
var searchSet = null;
var doZFilter = false;
if (filter.hasOwnProperty("z")) {
if (Object.hasOwnProperty("values") && nodeTabMap.hasOwnProperty(filter.z) ) {
searchSet = Object.values(nodeTabMap[filter.z]);
if (nodeTabMap.hasOwnProperty(filter.z)) {
searchSet = Object.keys(nodeTabMap[filter.z]);
} else {
doZFilter = true;
}
}
if (searchSet === null) {
searchSet = Object.keys(nodes);
}
for (var n=0;n<searchSet.length;n++) {
var node = searchSet[n];
var node = nodes[searchSet[n]];
if (filter.hasOwnProperty("type") && node.type !== filter.type) {
continue;
}
@ -1622,7 +1815,7 @@ RED.nodes = (function() {
}
function clear() {
nodes = [];
nodes = {};
links = [];
nodeTabMap = {};
configNodes = {};
@ -1650,7 +1843,7 @@ RED.nodes = (function() {
RED.events.emit("workspace:clear");
// var node_defs = {};
// var nodes = [];
// var nodes = {};
// var configNodes = {};
// var links = [];
// var defaultWorkspace;
@ -1710,7 +1903,7 @@ RED.nodes = (function() {
if (configNodes.hasOwnProperty(n.id)) {
delete configNodes[n.id];
} else {
nodes.splice(nodes.indexOf(n),1);
delete nodes[n.id];
if (nodeTabMap[n.z]) {
delete nodeTabMap[n.z][n.id];
}
@ -1734,7 +1927,7 @@ RED.nodes = (function() {
RED.view.redraw(true, true);
var result = importNodes(reimportList,{generateIds:false});
var newNodeMap = {};
result[0].forEach(function(n) {
result.nodes.forEach(function(n) {
newNodeMap[n.id] = n;
});
RED.nodes.eachLink(function(l) {
@ -1791,9 +1984,11 @@ RED.nodes = (function() {
groups: function(z) { return groupsByZ[z]||[] },
eachNode: function(cb) {
for (var n=0;n<nodes.length;n++) {
if (cb(nodes[n]) === false) {
break;
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (cb(nodes[id]) === false) {
break;
}
}
}
},
@ -1846,6 +2041,8 @@ RED.nodes = (function() {
import: importNodes,
identifyImportConflicts: identifyImportConflicts,
getAllFlowNodes: getAllFlowNodes,
createExportableNodeSet: createExportableNodeSet,
createCompleteNodeSet: createCompleteNodeSet,

View File

@ -28,6 +28,8 @@ RED.clipboard = (function() {
var libraryBrowser;
var examplesBrowser;
var pendingImportConfig;
function setupDialogs() {
dialog = $('<div id="red-ui-clipboard-dialog" class="hide"><form class="dialog-form form-horizontal"></form></div>')
.appendTo("#red-ui-editor")
@ -42,14 +44,14 @@ RED.clipboard = (function() {
"ui-widget-overlay": "red-ui-editor-dialog"
},
buttons: [
{
{ // red-ui-clipboard-dialog-cancel
id: "red-ui-clipboard-dialog-cancel",
text: RED._("common.label.cancel"),
click: function() {
$( this ).dialog( "close" );
}
},
{
{ // red-ui-clipboard-dialog-download
id: "red-ui-clipboard-dialog-download",
class: "primary",
text: RED._("clipboard.download"),
@ -64,7 +66,7 @@ RED.clipboard = (function() {
$( this ).dialog( "close" );
}
},
{
{ // red-ui-clipboard-dialog-export
id: "red-ui-clipboard-dialog-export",
class: "primary",
text: RED._("clipboard.export.copy"),
@ -134,7 +136,7 @@ RED.clipboard = (function() {
}
}
},
{
{ // red-ui-clipboard-dialog-ok
id: "red-ui-clipboard-dialog-ok",
class: "primary",
text: RED._("common.label.import"),
@ -157,6 +159,38 @@ RED.clipboard = (function() {
}
$( this ).dialog( "close" );
}
},
{ // red-ui-clipboard-dialog-import-conflict
id: "red-ui-clipboard-dialog-import-conflict",
class: "primary",
text: RED._("clipboard.import.importSelected"),
click: function() {
var importMap = {};
$('#red-ui-clipboard-dialog-import-conflicts-list input[type="checkbox"]').each(function() {
importMap[$(this).attr("data-node-id")] = this.checked?"import":"skip";
})
$('.red-ui-clipboard-dialog-import-conflicts-controls input[type="checkbox"]').each(function() {
if (!$(this).attr("disabled")) {
importMap[$(this).attr("data-node-id")] = this.checked?"replace":"copy"
}
})
// skip - don't import
// import - import as-is
// copy - import with new id
// replace - import over the top of existing
pendingImportConfig.importOptions.importMap = importMap;
var newNodes = pendingImportConfig.importNodes.filter(function(n) {
if (!importMap[n.id] || importMap[n.z]) {
importMap[n.id] = importMap[n.z];
}
return importMap[n.id] !== "skip"
})
// console.table(pendingImportConfig.importNodes.map(function(n) { return {id:n.id,type:n.type,result:importMap[n.id]}}))
RED.view.importNodes(newNodes, pendingImportConfig.importOptions);
$( this ).dialog( "close" );
}
}
],
open: function( event, ui ) {
@ -236,6 +270,14 @@ RED.clipboard = (function() {
'</span>'+
'</div>';
importConflictsDialog =
'<div class="form-row">'+
'<div class="form-row"><p data-i18n="clipboard.import.conflictNotification1"></p><p data-i18n="clipboard.import.conflictNotification2"></p></div>'+
'<div class="red-ui-clipboard-dialog-import-conflicts-list-container">'+
'<div id="red-ui-clipboard-dialog-import-conflicts-list"></div>'+
'</div>'+
'</div>';
}
var validateExportFilenameTimeout
@ -445,6 +487,8 @@ RED.clipboard = (function() {
$("#red-ui-clipboard-dialog-cancel").show();
$("#red-ui-clipboard-dialog-export").hide();
$("#red-ui-clipboard-dialog-download").hide();
$("#red-ui-clipboard-dialog-import-conflict").hide();
$("#red-ui-clipboard-dialog-ok").button("disable");
$("#red-ui-clipboard-dialog-import-text").on("keyup", validateImport);
$("#red-ui-clipboard-dialog-import-text").on('paste',function() { setTimeout(validateImport,10)});
@ -485,7 +529,9 @@ RED.clipboard = (function() {
}
$(".red-ui-clipboard-dialog-box").height(dialogHeight);
dialog.dialog("option","title",RED._("clipboard.importNodes")).dialog("open");
dialog.dialog("option","title",RED._("clipboard.importNodes"))
.dialog("option","width",700)
.dialog("open");
popover = RED.popover.create({
target: $("#red-ui-clipboard-dialog-import-text"),
trigger: "manual",
@ -631,6 +677,8 @@ RED.clipboard = (function() {
$("#red-ui-clipboard-dialog-ok").hide();
$("#red-ui-clipboard-dialog-cancel").hide();
$("#red-ui-clipboard-dialog-export").hide();
$("#red-ui-clipboard-dialog-import-conflict").hide();
var selection = RED.workspaces.selection();
if (selection.length > 0) {
$("#red-ui-clipboard-dialog-export-rng-selected").trigger("click");
@ -657,12 +705,15 @@ RED.clipboard = (function() {
}
$(".red-ui-clipboard-dialog-box").height(dialogHeight);
dialog.dialog("option","title",RED._("clipboard.exportNodes")).dialog( "open" );
dialog.dialog("option","title",RED._("clipboard.exportNodes"))
.dialog("option","width",700)
.dialog("open");
$("#red-ui-clipboard-dialog-export-text").trigger("focus");
$("#red-ui-clipboard-dialog-cancel").show();
$("#red-ui-clipboard-dialog-export").show();
$("#red-ui-clipboard-dialog-download").show();
$("#red-ui-clipboard-dialog-import-conflict").hide();
}
@ -752,21 +803,297 @@ RED.clipboard = (function() {
function importNodes(nodesStr,addFlow) {
var newNodes;
try {
nodesStr = nodesStr.trim();
if (nodesStr.length === 0) {
return;
var newNodes = nodesStr;
if (typeof nodesStr === 'string') {
try {
nodesStr = nodesStr.trim();
if (nodesStr.length === 0) {
return;
}
newNodes = JSON.parse(nodesStr);
} catch(err) {
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED";
throw e;
}
newNodes = JSON.parse(nodesStr);
} catch(err) {
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED";
throw e;
}
var importOptions = {generateIds: false, addFlow: addFlow};
try {
RED.view.importNodes(newNodes, importOptions);
} catch(error) {
// Thrown for import_conflict
confirmImport(error.importConfig, newNodes, importOptions);
}
}
function confirmImport(importConfig,importNodes,importOptions) {
var notification = RED.notify("<p>"+RED._("clipboard.import.conflictNotification1")+"</p>",{
type: "info",
fixed: true,
buttons: [
{text: RED._("common.label.cancel"), click: function() { notification.close(); }},
{text: RED._("clipboard.import.viewNodes"), click: function() {
notification.close();
showImportConflicts(importConfig,importNodes,importOptions);
}},
{text: RED._("clipboard.import.importCopy"), click: function() {
notification.close();
// generateIds=true to avoid conflicts
// and default to the 'old' behaviour around matching
// config nodes and subflows
importOptions.generateIds = true;
RED.view.importNodes(importNodes, importOptions);
}}
]
})
}
function showImportConflicts(importConfig,importNodes,importOptions) {
pendingImportConfig = {
importConfig: importConfig,
importNodes: importNodes,
importOptions: importOptions
}
var id,node;
var treeData = [];
var container;
var addedHeader = false;
for (id in importConfig.subflows) {
if (importConfig.subflows.hasOwnProperty(id)) {
if (!addedHeader) {
treeData.push({gutter:$('<span data-i18n="menu.label.subflows"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
addedHeader = true;
}
node = importConfig.subflows[id];
var isConflicted = importConfig.conflicted[node.id];
var isSelected = !isConflicted;
var elements = getNodeElement(node, isConflicted, isSelected );
container = {
id: node.id,
gutter: elements.gutter.element,
element: elements.element,
class: isSelected?"":"disabled",
deferBuild: true,
children: []
}
treeData.push(container);
if (importConfig.zMap[id]) {
importConfig.zMap[id].forEach(function(node) {
var childElements = getNodeElement(node, importConfig.conflicted[node.id], isSelected, elements.gutter.cb);
container.children.push({
id: node.id,
gutter: childElements.gutter.element,
element: childElements.element,
class: isSelected?"":"disabled"
})
});
}
}
}
addedHeader = false;
for (id in importConfig.tabs) {
if (importConfig.tabs.hasOwnProperty(id)) {
if (!addedHeader) {
treeData.push({gutter:$('<span data-i18n="menu.label.flows"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
addedHeader = true;
}
node = importConfig.tabs[id];
var isConflicted = importConfig.conflicted[node.id];
var isSelected = true;
var elements = getNodeElement(node, isConflicted, isSelected);
container = {
id: node.id,
gutter: elements.gutter.element,
element: elements.element,
icon: "red-ui-icons red-ui-icons-flow",
deferBuild: true,
class: isSelected?"":"disabled",
children: []
}
treeData.push(container);
if (importConfig.zMap[id]) {
importConfig.zMap[id].forEach(function(node) {
var childElements = getNodeElement(node, importConfig.conflicted[node.id], isSelected, elements.gutter.cb);
container.children.push({
id: node.id,
gutter: childElements.gutter.element,
element: childElements.element,
class: isSelected?"":"disabled"
})
// console.log(" ["+(importConfig.conflicted[node.id]?"*":" ")+"] "+node.type+" "+node.id);
});
}
}
}
addedHeader = false;
var extraNodes = [];
importConfig.all.forEach(function(node) {
if (node.type !== "tab" && node.type !== "subflow" && !importConfig.tabs[node.z] && !importConfig.subflows[node.z]) {
var isConflicted = importConfig.conflicted[node.id];
var isSelected = !isConflicted || !importConfig.configs[node.id];
var elements = getNodeElement(node, isConflicted, isSelected);
var item = {
id: node.id,
gutter: elements.gutter.element,
element: elements.element,
class: isSelected?"":"disabled"
}
if (importConfig.configs[node.id]) {
extraNodes.push(item);
} else {
if (!addedHeader) {
treeData.push({gutter:$('<span data-i18n="menu.label.nodes"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
addedHeader = true;
}
treeData.push(item);
}
// console.log("["+(importConfig.conflicted[node.id]?"*":" ")+"] "+node.type+" "+node.id);
}
})
if (extraNodes.length > 0) {
treeData.push({gutter:$('<span data-i18n="menu.label.displayConfig"></span>'), label: '', class:"red-ui-clipboard-dialog-import-conflicts-item-header"})
addedHeader = true;
treeData = treeData.concat(extraNodes);
RED.view.importNodes(newNodes,{addFlow: addFlow});
}
dialogContainer.empty();
dialogContainer.append($(importConflictsDialog));
var nodeList = $("#red-ui-clipboard-dialog-import-conflicts-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({
data: treeData
})
dialogContainer.i18n();
var dialogHeight = 400;
var winHeight = $(window).height();
if (winHeight < 600) {
dialogHeight = 400 - (600 - winHeight);
}
$(".red-ui-clipboard-dialog-box").height(dialogHeight);
$("#red-ui-clipboard-dialog-ok").hide();
$("#red-ui-clipboard-dialog-cancel").show();
$("#red-ui-clipboard-dialog-export").hide();
$("#red-ui-clipboard-dialog-download").hide();
$("#red-ui-clipboard-dialog-import-conflict").show();
dialog.dialog("option","title",RED._("clipboard.importNodes"))
.dialog("option","width",500)
.dialog( "open" );
}
function getNodeElement(n, isConflicted, isSelected, parent) {
var element;
if (n.type === "tab") {
element = getFlowLabel(n, isSelected);
} else {
element = getNodeLabel(n, isConflicted, isSelected);
}
var controls = $('<div>',{class:"red-ui-clipboard-dialog-import-conflicts-controls"}).appendTo(element);
controls.on("click", function(evt) { evt.stopPropagation(); });
if (isConflicted && !parent) {
var cb = $('<label><input '+(isSelected?'':'disabled ')+'type="checkbox" data-node-id="'+n.id+'"> <span data-i18n="clipboard.import.replace"></span></label>').appendTo(controls);
if (n.type === "tab" || (n.type !== "subflow" && n.hasOwnProperty("x") && n.hasOwnProperty("y"))) {
cb.hide();
}
}
return {
element: element,
gutter: getGutter(n, isSelected, parent)
}
}
function getGutter(n, isSelected, parent) {
var span = $("<label>",{class:"red-ui-clipboard-dialog-import-conflicts-gutter"});
var cb = $('<input data-node-id="'+n.id+'" type="checkbox" '+(isSelected?"checked":"")+'>').appendTo(span);
if (parent) {
cb.attr("disabled",true);
parent.addChild(cb);
}
span.on("click", function(evt) {
evt.stopPropagation();
})
cb.on("change", function(evt) {
var state = this.checked;
span.parent().toggleClass("disabled",!!!state);
span.parent().find('.red-ui-clipboard-dialog-import-conflicts-controls input[type="checkbox"]').attr("disabled",!!!state);
childItems.forEach(function(c) {
c.attr("checked",state);
c.trigger("change");
});
})
var childItems = [];
var checkbox = {
addChild: function(c) {
childItems.push(c);
}
}
return {
cb: checkbox,
element: span
}
}
function getNodeLabelText(n) {
var label = n.name || n.type+": "+n.id;
if (n._def.label) {
try {
label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||"";
} catch(err) {
console.log("Definition error: "+n.type+".label",err);
}
}
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
return label;
}
function getFlowLabel(n) {
n = JSON.parse(JSON.stringify(n));
n._def = RED.nodes.getType(n.type) || {};
if (n._def) {
n._ = n._def._;
}
var div = $('<div>',{class:"red-ui-info-outline-item red-ui-info-outline-item-flow"});
var contentDiv = $('<div>',{class:"red-ui-search-result-description red-ui-info-outline-item-label"}).appendTo(div);
var label = (typeof n === "string")? n : n.label;
var newlineIndex = label.indexOf("\\n");
if (newlineIndex > -1) {
label = label.substring(0,newlineIndex)+"...";
}
contentDiv.text(label);
// A conflicted flow should not be imported by default.
return div;
}
function getNodeLabel(n, isConflicted) {
n = JSON.parse(JSON.stringify(n));
n._def = RED.nodes.getType(n.type) || {};
if (n._def) {
n._ = n._def._;
}
var div = $('<div>',{class:"red-ui-info-outline-item"});
RED.utils.createNodeIcon(n).appendTo(div);
var contentDiv = $('<div>',{class:"red-ui-search-result-description"}).appendTo(div);
var labelText = getNodeLabelText(n);
var label = $('<div>',{class:"red-ui-search-result-node-label red-ui-info-outline-item-label"}).appendTo(contentDiv);
if (labelText) {
label.text(labelText)
} else {
label.html(n.type)
}
return div;
}
return {

View File

@ -27,26 +27,26 @@
this.partialFlag = false;
this.stateValue = 0;
var initialState = this.element.prop('checked');
this.options = [
this.states = [
$('<span class="red-ui-checkboxSet-option hide"><i class="fa fa-square-o"></i></span>').appendTo(this.uiElement),
$('<span class="red-ui-checkboxSet-option hide"><i class="fa fa-check-square-o"></i></span>').appendTo(this.uiElement),
$('<span class="red-ui-checkboxSet-option hide"><i class="fa fa-minus-square-o"></i></span>').appendTo(this.uiElement)
];
if (initialState) {
this.options[1].show();
this.states[1].show();
} else {
this.options[0].show();
this.states[0].show();
}
this.element.on("change", function() {
if (this.checked) {
that.options[0].hide();
that.options[1].show();
that.options[2].hide();
that.states[0].hide();
that.states[1].show();
that.states[2].hide();
} else {
that.options[1].hide();
that.options[0].show();
that.options[2].hide();
that.states[1].hide();
that.states[0].show();
that.states[2].hide();
}
var isChecked = this.checked;
that.children.forEach(function(child) {
@ -106,17 +106,17 @@
var trueState = this.partialFlag||state;
this.element.prop('checked',trueState);
if (state === true) {
this.options[0].hide();
this.options[1].show();
this.options[2].hide();
this.states[0].hide();
this.states[1].show();
this.states[2].hide();
} else if (state === false) {
this.options[2].hide();
this.options[1].hide();
this.options[0].show();
this.states[2].hide();
this.states[1].hide();
this.states[0].show();
} else if (state === null) {
this.options[0].hide();
this.options[1].hide();
this.options[2].show();
this.states[0].hide();
this.states[1].hide();
this.states[2].show();
}
if (!suppressEvent) {
this.element.trigger('change',null);

View File

@ -91,6 +91,9 @@
if (v!=="auto" && v!=="") {
that.topContainer.css(s,v);
that.uiContainer.css(s,"0");
if (s === "top" && that.options.header) {
that.uiContainer.css(s,"20px")
}
that.element.css(s,'auto');
}
})

View File

@ -453,7 +453,7 @@ RED.subflow = (function() {
$("#red-ui-workspace-chart").css({"margin-top": "0"});
}
function removeSubflow(id) {
function removeSubflow(id, keepInstanceNodes) {
// TODO: A lot of this logic is common with RED.nodes.removeWorkspace
var removedNodes = [];
var removedLinks = [];
@ -462,7 +462,7 @@ RED.subflow = (function() {
var activeSubflow = RED.nodes.subflow(id);
RED.nodes.eachNode(function(n) {
if (n.type == "subflow:"+id) {
if (!keepInstanceNodes && n.type == "subflow:"+id) {
removedNodes.push(n);
}
if (n.z == id) {

View File

@ -79,7 +79,7 @@ RED.sidebar.info.outliner = (function() {
try {
label = (typeof n._def.label === "function" ? n._def.label.call(n) : n._def.label)||"";
} catch(err) {
console.log("Definition error: "+type+".label",err);
console.log("Definition error: "+n.type+".label",err);
}
}
var newlineIndex = label.indexOf("\\n");

View File

@ -499,7 +499,7 @@ RED.view = (function() {
RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
RED.actions.add("core:cut-selection-to-internal-clipboard",function(){copySelection();deleteSelection();});
RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard);});
RED.actions.add("core:paste-from-internal-clipboard",function(){importNodes(clipboard,{generateIds: true});});
RED.actions.add("core:delete-selection",deleteSelection);
RED.actions.add("core:edit-selected-node",editSelection);
RED.actions.add("core:undo",RED.history.pop);
@ -3479,7 +3479,7 @@ RED.view = (function() {
options.push({name:"delete",disabled:(movingSet.length()===0 && selected_link === null),onselect:function() {deleteSelection();}});
options.push({name:"cut",disabled:(movingSet.length()===0),onselect:function() {copySelection();deleteSelection();}});
options.push({name:"copy",disabled:(movingSet.length()===0),onselect:function() {copySelection();}});
options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard,{touchImport:true});}});
options.push({name:"paste",disabled:(clipboard.length===0),onselect:function() {importNodes(clipboard, {generateIds: true, touchImport: true});}});
options.push({name:"edit",disabled:(movingSet.length() != 1),onselect:function() { RED.editor.edit(mdn);}});
options.push({name:"select",onselect:function() {selectAll();}});
options.push({name:"undo",disabled:(RED.history.depth() === 0),onselect:function() {RED.history.pop();}});
@ -3948,7 +3948,14 @@ RED.view = (function() {
}
d.resize = false;
}
if (d._colorChanged) {
var newColor = RED.utils.getNodeColor(d.type,d._def);
this.__mainRect__.setAttribute("fill",newColor);
if (this.__buttonGroupButton__) {
this.__buttonGroupButton__.settAttribute("fill",newColor);
}
delete d._colorChanged;
}
//thisNode.selectAll(".centerDot").attr({"cx":function(d) { return d.w/2;},"cy":function(d){return d.h/2}});
this.setAttribute("transform", "translate(" + (d.x-d.w/2) + "," + (d.y-d.h/2) + ")");
// This might be the first redraw after a node has been click-dragged to start a move.
@ -4628,13 +4635,14 @@ RED.view = (function() {
}
}
/**
* Imports a new collection of nodes from a JSON String.
*
* - all get new IDs assigned
* - all "selected"
* - attached to mouse for placing - "IMPORT_DRAGGING"
* @param {String/Array} newNodesStr nodes to import
* @param {String/Array} newNodesObj nodes to import
* @param {Object} options options object
*
* Options:
@ -4642,10 +4650,11 @@ RED.view = (function() {
* - touchImport - whether this is a touch import. If not, imported nodes are
* attachedto mouse for placing - "IMPORT_DRAGGING" state
*/
function importNodes(newNodesStr,options) {
function importNodes(newNodesObj,options) {
options = options || {
addFlow: false,
touchImport: false
touchImport: false,
generateIds: false
}
var addNewFlow = options.addFlow
var touchImport = options.touchImport;
@ -4653,19 +4662,42 @@ RED.view = (function() {
if (mouse_mode === RED.state.SELECTING_NODE) {
return;
}
var nodesToImport;
if (typeof newNodesObj === "string") {
if (newNodesObj === "") {
return;
}
try {
nodesToImport = JSON.parse(newNodesObj);
} catch(err) {
var e = new Error(RED._("clipboard.invalidFlow",{message:err.message}));
e.code = "NODE_RED";
throw e;
}
} else {
nodesToImport = newNodesObj;
}
if (!$.isArray(nodesToImport)) {
nodesToImport = [nodesToImport];
}
try {
var activeSubflowChanged;
if (activeSubflow) {
activeSubflowChanged = activeSubflow.changed;
}
var result = RED.nodes.import(newNodesStr,{generateIds:true, addFlow: addNewFlow});
var result = RED.nodes.import(nodesToImport,{generateIds:options.generateIds, addFlow: addNewFlow, importMap: options.importMap});
if (result) {
var new_nodes = result[0];
var new_links = result[1];
var new_groups = result[2];
var new_workspaces = result[3];
var new_subflows = result[4];
var new_default_workspace = result[5];
var new_nodes = result.nodes;
var new_links = result.links;
var new_groups = result.groups;
var new_workspaces = result.workspaces;
var new_subflows = result.subflows;
var removedNodes = result.removedNodes;
var new_default_workspace = result.missingWorkspace;
if (addNewFlow && new_default_workspace) {
RED.workspaces.show(new_default_workspace.id);
}
@ -4775,6 +4807,20 @@ RED.view = (function() {
}
}
}
if (removedNodes) {
var replaceEvent = {
t: "replace",
config: removedNodes
}
historyEvent = {
t:"multi",
events: [
replaceEvent,
historyEvent
]
}
}
RED.history.push(historyEvent);
updateActiveNodes();
@ -4806,6 +4852,9 @@ RED.view = (function() {
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 = "<ul><li>"+counts.join("</li><li>")+"</li></ul>";
RED.notify("<p>"+RED._("clipboard.nodesImported")+"</p>"+countList,{id:"clipboard"});
@ -4813,7 +4862,10 @@ RED.view = (function() {
}
} catch(error) {
if (error.code != "NODE_RED") {
if (error.code === "import_conflict") {
// Pass this up for the called to resolve
throw error;
} else if (error.code != "NODE_RED") {
console.log(error.stack);
RED.notify(RED._("notification.error",{message:error.toString()}),"error");
} else {

View File

@ -153,3 +153,61 @@
border-top: none;
}
}
.red-ui-clipboard-dialog-import-conflicts-list-container {
min-height: 300px;
position: relative;
li:not(:first-child) .red-ui-clipboard-dialog-import-conflicts-item-header {
// border-top: 1px solid $secondary-border-color;
}
}
.red-ui-clipboard-dialog-import-conflicts-item-header {
background: $tertiary-background;
& > span:first-child {
color: $header-text-color;
padding-left: 4px;
font-size: 12px;
}
}
.red-ui-clipboard-dialog-import-conflicts-controls {
position: absolute;
top:0;
bottom: 0;
right: 0px;
text-align: center;
color: $form-text-color;
.form-row & label {
padding: 2px 0;
line-height: 23px;
margin-bottom: 0;
width: 80px;
display: inline-block;
position: relative;
height: 100%;
width: 80px;
text-align: center;
border-left: 1px solid $secondary-border-color;
}
input[type="checkbox"] {
display: inline-block;
width: auto;
margin: 0;
}
}
#red-ui-clipboard-dialog-import-conflicts-list .disabled .red-ui-info-outline-item {
opacity: 0.4;
}
.form-row label.red-ui-clipboard-dialog-import-conflicts-gutter {
box-sizing: border-box;
width: 22px;
text-align: center;
.red-ui-editor-dialog & input[type="checkbox"] {
width: auto;
padding: 0;
margin: 0;
}
}

View File

@ -321,15 +321,12 @@ div.red-ui-info-table {
border: none;
border-radius: 0;
}
.red-ui-treeList-label {
font-size: 13px;
padding: 2px 0;
overflow: hidden;
}
.red-ui-info-outline-project {
border-bottom: 1px solid $secondary-border-color;
}
}
.red-ui-info-outline,.red-ui-sidebar-help-toc, #red-ui-clipboard-dialog-import-conflicts-list {
.red-ui-info-outline-item {
display: inline-block;
padding: 0;
@ -366,6 +363,12 @@ div.red-ui-info-table {
}
}
.red-ui-treeList-label {
font-size: 13px;
padding: 2px 0;
overflow: hidden;
}
.red-ui-search-result-node {
width: 24px;
height: 20px;
@ -387,6 +390,7 @@ div.red-ui-info-table {
color: $secondary-text-color;
}
}
.red-ui-info-outline-item-control-spacer {
display: inline-block;
width: 23px;

View File

@ -19,7 +19,7 @@
color: $secondary-text-color;
cursor: pointer;
input {
display:none;
display:none !important;
}
&.disabled {