mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #2698 from node-red/import-dupes
Improved handling of importing duplicate subflow/config nodes
This commit is contained in:
commit
576c528573
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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,157 @@ 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;
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
newSubflowIds.forEach(function(id) {
|
||||
var n = newSubflows[id];
|
||||
RED.events.emit("subflows:change",n);
|
||||
})
|
||||
// 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 +1090,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 +1136,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 +1231,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 +1240,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 +1274,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 +1294,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 +1315,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 +1363,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 +1403,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 +1452,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 +1723,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 +1820,7 @@ RED.nodes = (function() {
|
||||
}
|
||||
|
||||
function clear() {
|
||||
nodes = [];
|
||||
nodes = {};
|
||||
links = [];
|
||||
nodeTabMap = {};
|
||||
configNodes = {};
|
||||
@ -1650,7 +1848,7 @@ RED.nodes = (function() {
|
||||
RED.events.emit("workspace:clear");
|
||||
|
||||
// var node_defs = {};
|
||||
// var nodes = [];
|
||||
// var nodes = {};
|
||||
// var configNodes = {};
|
||||
// var links = [];
|
||||
// var defaultWorkspace;
|
||||
@ -1710,7 +1908,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 +1932,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 +1989,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 +2046,8 @@ RED.nodes = (function() {
|
||||
|
||||
import: importNodes,
|
||||
|
||||
identifyImportConflicts: identifyImportConflicts,
|
||||
|
||||
getAllFlowNodes: getAllFlowNodes,
|
||||
createExportableNodeSet: createExportableNodeSet,
|
||||
createCompleteNodeSet: createCompleteNodeSet,
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
}
|
||||
})
|
||||
|
@ -37,8 +37,8 @@
|
||||
invertState = this.options.invertState;
|
||||
}
|
||||
var baseClass = this.options.baseClass || "red-ui-button";
|
||||
var enabledIcon = this.options.enabledIcon || "fa-check-square-o";
|
||||
var disabledIcon = this.options.disabledIcon || "fa-square-o";
|
||||
var enabledIcon = this.options.hasOwnProperty('enabledIcon')?this.options.enabledIcon : "fa-check-square-o";
|
||||
var disabledIcon = this.options.hasOwnProperty('disabledIcon')?this.options.disabledIcon : "fa-square-o";
|
||||
var enabledLabel = this.options.hasOwnProperty('enabledLabel') ? this.options.enabledLabel : RED._("editor:workspace.enabled");
|
||||
var disabledLabel = this.options.hasOwnProperty('disabledLabel') ? this.options.disabledLabel : RED._("editor:workspace.disabled");
|
||||
|
||||
@ -46,25 +46,35 @@
|
||||
this.element.on("focus", function() {
|
||||
that.button.focus();
|
||||
});
|
||||
this.button = $('<button type="button" class="red-ui-toggleButton '+baseClass+' toggle single"><i class="fa"></i> <span></span></button>');
|
||||
this.button = $('<button type="button" class="red-ui-toggleButton '+baseClass+' toggle single"><span></span></button>');
|
||||
this.buttonLabel = $("<span>").appendTo(this.button);
|
||||
|
||||
if (this.options.class) {
|
||||
this.button.addClass(this.options.class)
|
||||
}
|
||||
this.element.after(this.button);
|
||||
this.buttonIcon = this.button.find("i");
|
||||
this.buttonLabel = this.button.find("span");
|
||||
|
||||
if (enabledIcon && disabledIcon) {
|
||||
this.buttonIcon = $('<i class="fa"></i>').prependTo(this.button);
|
||||
}
|
||||
|
||||
// Quick hack to find the maximum width of the button
|
||||
this.button.addClass("selected");
|
||||
this.buttonIcon.addClass(enabledIcon);
|
||||
if (this.buttonIcon) {
|
||||
this.buttonIcon.addClass(enabledIcon);
|
||||
}
|
||||
this.buttonLabel.text(enabledLabel);
|
||||
var width = this.button.width();
|
||||
this.button.removeClass("selected");
|
||||
this.buttonIcon.removeClass(enabledIcon);
|
||||
that.buttonIcon.addClass(disabledIcon);
|
||||
if (this.buttonIcon) {
|
||||
this.buttonIcon.removeClass(enabledIcon);
|
||||
that.buttonIcon.addClass(disabledIcon);
|
||||
}
|
||||
that.buttonLabel.text(disabledLabel);
|
||||
width = Math.max(width,this.button.width());
|
||||
this.buttonIcon.removeClass(disabledIcon);
|
||||
if (this.buttonIcon) {
|
||||
this.buttonIcon.removeClass(disabledIcon);
|
||||
}
|
||||
|
||||
// Fix the width of the button so it doesn't jump around when toggled
|
||||
if (width > 0) {
|
||||
@ -73,7 +83,7 @@
|
||||
|
||||
this.button.on("click",function(e) {
|
||||
e.stopPropagation();
|
||||
if (that.buttonIcon.hasClass(disabledIcon)) {
|
||||
if (!that.state) {
|
||||
that.element.prop("checked",!invertState);
|
||||
} else {
|
||||
that.element.prop("checked",invertState);
|
||||
@ -84,13 +94,19 @@
|
||||
this.element.on("change", function(e) {
|
||||
if ($(this).prop("checked") !== invertState) {
|
||||
that.button.addClass("selected");
|
||||
that.buttonIcon.addClass(enabledIcon);
|
||||
that.buttonIcon.removeClass(disabledIcon);
|
||||
that.state = true;
|
||||
if (that.buttonIcon) {
|
||||
that.buttonIcon.addClass(enabledIcon);
|
||||
that.buttonIcon.removeClass(disabledIcon);
|
||||
}
|
||||
that.buttonLabel.text(enabledLabel);
|
||||
} else {
|
||||
that.button.removeClass("selected");
|
||||
that.buttonIcon.addClass(disabledIcon);
|
||||
that.buttonIcon.removeClass(enabledIcon);
|
||||
that.state = false;
|
||||
if (that.buttonIcon) {
|
||||
that.buttonIcon.addClass(disabledIcon);
|
||||
that.buttonIcon.removeClass(enabledIcon);
|
||||
}
|
||||
that.buttonLabel.text(disabledLabel);
|
||||
}
|
||||
})
|
||||
|
@ -167,7 +167,7 @@
|
||||
this._selected = new Set();
|
||||
this._topList = $('<ol class="red-ui-treeList-list">').css({
|
||||
position:'absolute',
|
||||
top: 0,
|
||||
top:0,
|
||||
left:0,
|
||||
right:0,
|
||||
bottom:0
|
||||
@ -181,6 +181,9 @@
|
||||
that._addSubtree(that._topList,container,item,0);
|
||||
}
|
||||
};
|
||||
if (this.options.header) {
|
||||
topListOptions.header = this.options.header;
|
||||
}
|
||||
if (this.options.rootSortable !== false && !!this.options.sortable) {
|
||||
topListOptions.sortable = this.options.sortable;
|
||||
topListOptions.connectWith = '.red-ui-treeList-sortable';
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -19,7 +19,7 @@
|
||||
color: $secondary-text-color;
|
||||
cursor: pointer;
|
||||
input {
|
||||
display:none;
|
||||
display:none !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
|
Loading…
Reference in New Issue
Block a user