Merge 15c2faffdec7b709826fdba8061a346e7f7c3e27 into 2feb290ae3c6cd88c16e4c27c2006a569e0146e2

This commit is contained in:
Gauthier Dandele 2025-02-25 11:13:45 -05:00 committed by GitHub
commit cc5651c0c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1715,6 +1715,30 @@ RED.nodes = (function() {
}
/**
* Analyzes the array of nodes passed as an argument to find unknown node types.
*
* @param {Array<object>} nodes An array of nodes to analyse
* @returns {Array<string>} An array with unknown types
*/
function identifyUnknowTypes(nodes) {
const unknownTypes = [];
for (const node of nodes) {
// TODO: remove workspace
const knowTypes = ["workspace", "tab", "subflow", "group", "junction"];
if (!knowTypes.includes(node.type) &&
node.type.substring(0, 8) != "subflow:" &&
!registry.getNodeType(node.type) &&
!unknownTypes.includes(node.type)) {
unknownTypes.push(node.type);
}
}
return unknownTypes;
}
/**
* Replace the provided nodes.
* This must contain complete Subflow defs or complete Flow Tabs.
@ -1824,6 +1848,205 @@ RED.nodes = (function() {
}
/**
* Makes a copy of the Config Node received as parameter.
*
* @remarks The id is not modified
*
* @param {object} configNode The Config Node to copy.
* @param {object} def The Config Node definition.
* @param {object} [options]
* @param {boolean} [options.markChanged = false] Whether the Config Node
* should have changed. Default `false`.
* @returns {object} The new Config Node copied.
*/
function copyConfigNode(configNode, def, options = {}) {
const newNode = {
_config: {},
_configNodeReferences: new Set(),
_def: def,
id: configNode.id,
type: configNode.type,
changed: false,
icon: configNode.icon,
info: configNode.info,
label: def.label,
users: [],
};
if (configNode.z) {
newNode.z = configNode.z;
}
if (options.markChanged) {
newNode.changed = true;
}
// Whether the config node is disabled
if (configNode.hasOwnProperty("d")) {
newNode.d = configNode.d;
}
// Copy editable properties
for (const d in def.defaults) {
if (def.defaults.hasOwnProperty(d)) {
newNode._config[d] = JSON.stringify(configNode[d]);
newNode[d] = configNode[d];
if (def.defaults[d].type) {
// Add the config node used by this config node to the list
newNode._configNodeReferences.add(configNode[d])
}
}
}
// Copy credentials - ONLY if the node contains it to avoid erase it
if (def.hasOwnProperty("credentials") && configNode.hasOwnProperty("credentials")) {
newNode.credentials = {};
for (const c in def.credentials) {
if (def.credentials.hasOwnProperty(c) && configNode.credentials.hasOwnProperty(c)) {
newNode.credentials[c] = configNode.credentials[c];
}
}
}
return newNode;
}
/**
* Makes a copy of the Node received as parameter.
*
* @remarks The id is not modified
*
* @param {object} node The Node to copy.
* @param {object} def The Node definition.
* @param {object} [options]
* @param {boolean} [options.markChanged = false] Whether the Node
* should have changed. Default `false`.
* @returns {object} The new Config Node copied.
*/
function copyNode(node, def, options = {}) {
const newNode = {
_config: {},
_def: def,
id: node.id,
type: node.type,
changed: false,
dirty: true,
info: node.info,
// TODO: parseFloat(node.x) || 0
x: parseFloat(node.x || 0),
y: parseFloat(node.y || 0),
z: node.z,
};
// Whether the node shown its label
if (node.hasOwnProperty("l")) {
newNode.l = node.l;
}
// Whether the node is disabled
if (node.hasOwnProperty("d")) {
newNode.d = node.d;
}
// Whether the node is into a group
if (node.hasOwnProperty("g")) {
newNode.g = node.g;
}
if (options.markChanged) {
newNode.changed = true;
}
if (node.type !== "group" && node.type !== "junction") {
newNode.wires = node.wires || [];
newNode.inputLabels = node.inputLabels;
newNode.outputLabels = node.outputLabels;
newNode.icon = node.icon;
}
if (node.type === "group") {
for (const d in newNode._def.defaults) {
if (newNode._def.defaults.hasOwnProperty(d) && d !== "inputs" && d !== "outputs") {
newNode[d] = node[d];
newNode._config[d] = JSON.stringify(node[d]);
}
}
newNode._config.x = node.x;
newNode._config.y = node.y;
if (node.hasOwnProperty("w")) { // Weight
newNode.w = node.w;
}
if (node.hasOwnProperty("h")) { // Height
newNode.h = node.h;
}
} else if (node.type === "junction") {
newNode._config.x = node.x;
newNode._config.y = node.y;
newNode.wires = node.wires || [];
newNode.inputs = 1;
newNode.outputs = 1;
newNode.w = 0;
newNode.h = 0;
} else if (node.type.substring(0, 7) === "subflow") {
newNode.name = node.name;
newNode.inputs = node.inputs ?? 0;
newNode.outputs = node.outputs ?? 0;
newNode.env = node.env;
} else {
newNode._config.x = node.x;
newNode._config.y = node.y;
if (node.hasOwnProperty("inputs") && def.defaults.hasOwnProperty("inputs")) {
newNode._config.inputs = JSON.stringify(node.inputs);
newNode.inputs = parseInt(node.inputs, 10);
} else {
newNode.inputs = def.inputs;
}
if (node.hasOwnProperty("outputs") && def.defaults.hasOwnProperty("outputs")) {
newNode._config.outputs = JSON.stringify(node.outputs);
newNode.outputs = parseInt(node.outputs, 10);
} else {
newNode.outputs = def.outputs;
}
// The node declares outputs in its defaults, but has not got a valid value
// Defer to the length of the wires array
if (node.hasOwnProperty('wires')) {
if (isNaN(newNode.outputs)) {
newNode.outputs = newNode.wires.length;
} else if (newNode.wires.length > newNode.outputs) {
// If 'wires' is longer than outputs, clip wires
console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
// TODO: Pas dans l'autre sens ?
newNode.wires = newNode.wires.slice(0, newNode.outputs);
}
}
// Copy editable properties
for (const d in def.defaults) {
if (def.defaults.hasOwnProperty(d) && d !== "inputs" && d !== "outputs") {
newNode._config[d] = JSON.stringify(node[d]);
newNode[d] = node[d];
}
}
// Copy credentials - ONLY if the node contains it to avoid erase it
if (def.hasOwnProperty("credentials") && node.hasOwnProperty("credentials")) {
newNode.credentials = {};
for (const c in def.credentials) {
if (def.credentials.hasOwnProperty(c) && node.credentials.hasOwnProperty(c)) {
newNode.credentials[c] = node.credentials[c];
}
}
}
}
return newNode;
}
/**
* Options:
* - generateIds - whether to replace all node ids
@ -1930,21 +2153,26 @@ RED.nodes = (function() {
isInitialLoad = true;
initialLoad = JSON.parse(JSON.stringify(newNodes));
}
var unknownTypes = [];
const unknownTypes = identifyUnknowTypes(newNodes);
if (!isInitialLoad && unknownTypes.length) {
const typeList = $("<ul>");
unknownTypes.forEach(function (type) {
$("<li>").text(type).appendTo(typeList);
});
const title = "<p>" + RED._("clipboard.importUnrecognised", { count: unknownTypes.length }) + "</p>";
RED.notify(title + typeList[0].outerHTML, {
type: "error",
fixed: false,
timeout: 10000
});
}
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" &&
n.type != "subflow" &&
n.type != "group" &&
n.type != 'junction' &&
!registry.getNodeType(n.type) &&
n.type.substring(0,8) != "subflow:" &&
unknownTypes.indexOf(n.type)==-1) {
unknownTypes.push(n.type);
}
if (n.z) {
nodeZmap[n.z] = nodeZmap[n.z] || [];
nodeZmap[n.z].push(n);
@ -1967,15 +2195,6 @@ RED.nodes = (function() {
n.z = recoveryWorkspace.id;
nodeZmap[recoveryWorkspace.id].push(n);
}
}
if (!isInitialLoad && unknownTypes.length > 0) {
var typeList = $("<ul>");
unknownTypes.forEach(function(t) {
$("<li>").text(t).appendTo(typeList);
})
typeList = typeList[0].outerHTML;
RED.notify("<p>"+RED._("clipboard.importUnrecognised",{count:unknownTypes.length})+"</p>"+typeList,"error",false,10000);
}
var activeWorkspace = RED.workspaces.active();
@ -2154,43 +2373,7 @@ RED.nodes = (function() {
}
if (!existingConfigNode || existingConfigNode._def.exclusive) { //} || !compareNodes(existingConfigNode,n,true) || existingConfigNode.z !== n.z) {
configNode = {
id:n.id,
z:n.z,
type:n.type,
info: n.info,
users:[],
_config:{},
_configNodeReferences: new Set()
};
if (!n.z) {
delete configNode.z;
}
if (options.markChanged) {
configNode.changed = true
}
if (n.hasOwnProperty('d')) {
configNode.d = n.d;
}
for (d in def.defaults) {
if (def.defaults.hasOwnProperty(d)) {
configNode[d] = n[d];
configNode._config[d] = JSON.stringify(n[d]);
if (def.defaults[d].type) {
configNode._configNodeReferences.add(n[d])
}
}
}
if (def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
configNode.credentials = {};
for (d in def.credentials) {
if (def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) {
configNode.credentials[d] = n.credentials[d];
}
}
}
configNode.label = def.label;
configNode._def = def;
configNode = copyConfigNode(n, def, options);
if (createNewIds || options.importMap[n.id] === "copy") {
configNode.id = getID();
}
@ -2251,222 +2434,133 @@ RED.nodes = (function() {
if (n.type !== "workspace" && n.type !== "tab" && n.type !== "subflow") {
def = registry.getNodeType(n.type);
if (!def || def.category != "config") {
var node = {
x:parseFloat(n.x || 0),
y:parseFloat(n.y || 0),
z:n.z,
type: n.type,
info: n.info,
changed:false,
_config:{}
}
if (n.type !== "group" && n.type !== 'junction') {
node.wires = n.wires||[];
node.inputLabels = n.inputLabels;
node.outputLabels = n.outputLabels;
node.icon = n.icon;
}
if (n.type === 'junction') {
node.wires = n.wires||[];
}
if (n.hasOwnProperty('l')) {
node.l = n.l;
}
if (n.hasOwnProperty('d')) {
node.d = n.d;
}
if (n.hasOwnProperty('g')) {
node.g = n.g;
}
if (options.markChanged) {
node.changed = true
}
if (createNewIds || options.importMap[n.id] === "copy") {
if (subflow_denylist[n.z]) {
continue;
} else if (subflow_map[node.z]) {
node.z = subflow_map[node.z].id;
} else if (subflow_map[n.z]) {
n.z = subflow_map[n.z].id;
} else {
node.z = workspace_map[node.z];
if (!workspaces[node.z]) {
n.z = workspace_map[n.z];
if (!workspaces[n.z]) {
if (createMissingWorkspace) {
if (missingWorkspace === null) {
missingWorkspace = RED.workspaces.add(null,true);
new_workspaces.push(missingWorkspace);
}
node.z = missingWorkspace.id;
n.z = missingWorkspace.id;
} else {
node.z = activeWorkspace;
n.z = activeWorkspace;
}
}
}
node.id = getID();
// TODO: If updated here, conflict with importMap in L2634
//n.id = getID();
} else {
node.id = n.id;
const keepNodesCurrentZ = reimport && node.z && (RED.workspaces.contains(node.z) || RED.nodes.subflow(node.z))
if (!keepNodesCurrentZ && (node.z == null || (!workspace_map[node.z] && !subflow_map[node.z]))) {
const keepNodesCurrentZ = reimport && n.z && (RED.workspaces.contains(n.z) || RED.nodes.subflow(n.z))
if (!keepNodesCurrentZ && (n.z == null || (!workspace_map[n.z] && !subflow_map[n.z]))) {
if (createMissingWorkspace) {
if (missingWorkspace === null) {
missingWorkspace = RED.workspaces.add(null,true);
new_workspaces.push(missingWorkspace);
}
node.z = missingWorkspace.id;
n.z = missingWorkspace.id;
} else {
node.z = activeWorkspace;
n.z = activeWorkspace;
}
}
}
node._def = def;
if (node.type === "group") {
node._def = RED.group.def;
for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d];
node._config[d] = JSON.stringify(n[d]);
}
}
node._config.x = node.x;
node._config.y = node.y;
if (n.hasOwnProperty('w')) {
node.w = n.w
}
if (n.hasOwnProperty('h')) {
node.h = n.h
}
} 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 (!subflow){
node._def = {
color:"#fee",
defaults: {},
label: "unknown: "+n.type,
labelStyle: "red-ui-flow-node-label-italic",
outputs: n.outputs|| (n.wires && n.wires.length) || 0,
set: registry.getNodeSet("node-red/unknown")
}
var orig = {};
for (var p in n) {
if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
orig[p] = n[p];
}
}
node._orig = orig;
node.name = n.type;
node.type = "unknown";
} else {
// Update Subflow instance properties from Subflow definition
if (n.type.substring(0, 7) === "subflow") {
const parentId = n.type.split(":")[1];
const subflow = subflow_denylist[parentId] || subflow_map[parentId] || getSubflow(parentId);
if (subflow) {
// If the parent Subflow is found, update Subflow instance properties
if (subflow_denylist[parentId] || createNewIds || options.importMap[n.id] === "copy") {
parentId = subflow.id;
node.type = "subflow:"+parentId;
node._def = registry.getNodeType(node.type);
delete node.i;
}
node.name = n.name;
node.outputs = subflow.out.length;
node.inputs = subflow.in.length;
node.env = n.env;
}
} else if (n.type === 'junction') {
node._def = {defaults:{}}
node._config.x = node.x
node._config.y = node.y
node.inputs = 1
node.outputs = 1
node.w = 0;
node.h = 0;
} else {
if (!node._def) {
if (node.x && node.y) {
node._def = {
color:"#fee",
defaults: {},
label: "unknown: "+n.type,
labelStyle: "red-ui-flow-node-label-italic",
outputs: n.outputs|| (n.wires && n.wires.length) || 0,
set: registry.getNodeSet("node-red/unknown")
}
} else {
node._def = {
category:"config",
set: registry.getNodeSet("node-red/unknown")
};
node.users = [];
// This is a config node, so delete the default
// non-config node properties
delete node.x;
delete node.y;
delete node.wires;
delete node.inputLabels;
delete node.outputLabels;
if (!n.z) {
delete node.z;
}
}
var orig = {};
for (var p in n) {
if (n.hasOwnProperty(p) && p!="x" && p!="y" && p!="z" && p!="id" && p!="wires") {
orig[p] = n[p];
}
}
node._orig = orig;
node.name = n.type;
node.type = "unknown";
}
if (node._def.category != "config") {
if (n.hasOwnProperty('inputs') && node._def.defaults.hasOwnProperty("inputs")) {
node.inputs = parseInt(n.inputs, 10);
node._config.inputs = JSON.stringify(n.inputs);
} else {
node.inputs = node._def.inputs;
}
if (n.hasOwnProperty('outputs') && node._def.defaults.hasOwnProperty("outputs")) {
node.outputs = parseInt(n.outputs, 10);
node._config.outputs = JSON.stringify(n.outputs);
} else {
node.outputs = node._def.outputs;
n.type = "subflow:" + subflow.id;
def = registry.getNodeType(n.type);
// TODO: Why delete `node.i`
delete n.i;
}
// The node declares outputs in its defaults, but has not got a valid value
// Defer to the length of the wires array
if (node.hasOwnProperty('wires')) {
if (isNaN(node.outputs)) {
node.outputs = node.wires.length;
} else if (node.wires.length > node.outputs) {
// If 'wires' is longer than outputs, clip wires
console.log("Warning: node.wires longer than node.outputs - trimming wires:", node.id, " wires:", node.wires.length, " outputs:", node.outputs);
node.wires = node.wires.slice(0, node.outputs);
}
}
for (d in node._def.defaults) {
if (node._def.defaults.hasOwnProperty(d) && d !== 'inputs' && d !== 'outputs') {
node[d] = n[d];
node._config[d] = JSON.stringify(n[d]);
}
}
node._config.x = node.x;
node._config.y = node.y;
if (node._def.hasOwnProperty('credentials') && n.hasOwnProperty('credentials')) {
node.credentials = {};
for (d in node._def.credentials) {
if (node._def.credentials.hasOwnProperty(d) && n.credentials.hasOwnProperty(d)) {
node.credentials[d] = n.credentials[d];
}
}
}
// Get inputs/outputs count from parent
n.inputs = subflow.in.length;
n.outputs = subflow.out.length;
} else {
// If the parent Subflow is not found, this Subflow instance will be marked as unknown
}
}
node_map[n.id] = node;
// If an 'unknown' config node, it will not have been caught by the
// proper config node handling, so needs adding to new_nodes here
if (node.type === 'junction') {
new_junctions.push(node)
} else if (node.type === "unknown" || node._def.category !== "config") {
new_nodes.push(node);
} else if (node.type === "group") {
new_groups.push(node);
new_group_set.add(node.id);
// Try to fix the node definition
let isUnknownNode = false;
if (!def) {
if (n.type === "group") {
// Group Node Definition
def = RED.group.def;
} else if (n.type === "junction") {
// Junction Node Definition
def = { defaults: {} };
} else if (!n.hasOwnProperty("x") && !n.hasOwnProperty("y")) {
// Assumes its an unknown Config Node
isUnknownNode = true;
def = {
category: "config",
defaults: {},
set: registry.getNodeSet("node-red/unknown")
};
} else {
// Unknown Node
isUnknownNode = true;
def = {
color: "#fee",
defaults: {},
label: "unknown: " + n.type,
labelStyle: "red-ui-flow-node-label-italic",
// TODO: ?? instead of ||
outputs: n.outputs || (n.wires && n.wires.length) || 0,
set: registry.getNodeSet("node-red/unknown")
};
}
}
// Now all properties have been fixed, copy the node
// NOTE: If def is unknown node, editable properties are not copied
// TODO: Group Node has config as category - why?
const isConfigNode = def?.category === "config" && n.type !== "group";
if (isConfigNode) {
node_map[n.id] = copyConfigNode(n, def, options);
} else {
// Node, Group, Junction or Subflow
node_map[n.id] = copyNode(n, def, options);
}
// Unknown Node - Copy editable properties so that the node is always exportable
if (isUnknownNode) {
const propertiesNotCopyable = ["x", "y", "z", "id", "wires"];
node_map[n.id]._orig = Object.entries(n).reduce(function (orig, [prop, value]) {
if (n.hasOwnProperty(prop) && !propertiesNotCopyable.includes(prop)) {
orig[prop] = value;
}
return orig;
}, {});
node_map[n.id].name = n.type;
node_map[n.id].type = "unknown";
}
// Now the node has been copied, change the `id` if it's a copy
if (createNewIds || options.importMap[n.id] === "copy") {
node_map[n.id].id = getID();
}
if (n.type === 'junction') {
new_junctions.push(node_map[n.id])
} else if (n.type === "group") {
new_groups.push(node_map[n.id]);
new_group_set.add(node_map[n.id].id);
} else {
new_nodes.push(node_map[n.id]);
}
}
}