/** * Copyright JS Foundation and other contributors, http://js.foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ RED.subflow = (function() { var _subflowEditTemplate = ''; var _subflowTemplateEditTemplate = ''; function findAvailableSubflowIOPosition(subflow,isInput) { var pos = {x:50,y:30}; if (!isInput) { pos.x += 110; } var ports = [].concat(subflow.out).concat(subflow.in); if (subflow.status) { ports.push(subflow.status); } ports.sort(function(A,B) { return A.x-B.x; }); for (var i=0; i output.i) { subflowMovedLinks.push(l); } } }); subflowRemovedLinks.forEach(function(l) { RED.nodes.removeLink(l)}); subflowMovedLinks.forEach(function(l) { l.sourcePort--; }); removedLinks = removedLinks.concat(subflowRemovedLinks); for (var j=output.i;j ').appendTo(toolbar); // Inputs $(' '+ '
'+ '0'+ '1'+ '
').appendTo(toolbar); // Outputs $('
'+ ''+ '
3
'+ ''+ '
').appendTo(toolbar); // Status $('').appendTo(toolbar); // $(' ').appendTo(toolbar); // $(' ').appendTo(toolbar); // Delete $(' ').appendTo(toolbar); toolbar.i18n(); $("#red-ui-subflow-output-remove").on("click", function(event) { event.preventDefault(); var wasDirty = RED.nodes.dirty(); var wasChanged = activeSubflow.changed; var result = removeSubflowOutput(); if (result) { var inst = refresh(true); RED.history.push({ t:'delete', links:result.links, subflowOutputs: result.subflowOutputs, changed: wasChanged, dirty:wasDirty, subflow: { instances: inst.instances } }); RED.view.select(); RED.nodes.dirty(true); RED.view.redraw(true); } }); $("#red-ui-subflow-output-add").on("click", function(event) { event.preventDefault(); addSubflowOutput(); }); $("#red-ui-subflow-input-add").on("click", function(event) { event.preventDefault(); addSubflowInput(); }); $("#red-ui-subflow-input-remove").on("click", function(event) { event.preventDefault(); var wasDirty = RED.nodes.dirty(); var wasChanged = activeSubflow.changed; activeSubflow.changed = true; var result = removeSubflowInput(); if (result) { var inst = refresh(true); RED.history.push({ t:'delete', links:result.links, changed: wasChanged, subflowInputs: result.subflowInputs, dirty:wasDirty, subflow: { instances: inst.instances } }); RED.view.select(); RED.nodes.dirty(true); RED.view.redraw(true); } }); $("#red-ui-subflow-status").on("change", function(evt) { if (this.checked) { addSubflowStatus(); } else { var currentStatus = activeSubflow.status; var wasChanged = activeSubflow.changed; var result = removeSubflowStatus(); if (result) { activeSubflow.changed = true; var wasDirty = RED.nodes.dirty(); RED.history.push({ t:'delete', links:result.links, changed: wasChanged, dirty:wasDirty, subflow: { id: activeSubflow.id, status: currentStatus } }); RED.view.select(); RED.nodes.dirty(true); RED.view.redraw(); } } }) $("#red-ui-subflow-edit").on("click", function(event) { RED.editor.editSubflow(RED.nodes.subflow(RED.workspaces.active())); event.preventDefault(); }); $("#red-ui-subflow-delete").on("click", function(event) { event.preventDefault(); RED.subflow.delete(RED.workspaces.active()) }); refreshToolbar(activeSubflow); $("#red-ui-workspace-chart").css({"margin-top": "40px"}); $("#red-ui-workspace-toolbar").show(); } function hideWorkspaceToolbar() { $("#red-ui-workspace-toolbar").hide().empty(); $("#red-ui-workspace-chart").css({"margin-top": "0"}); } function deleteSubflow(id) { const subflow = RED.nodes.subflow(id || RED.workspaces.active()); if (!subflow) { return } if (subflow.instances.length > 0) { const msg = $('
') $('

').text(RED._("subflow.subflowInstances",{count: subflow.instances.length})).appendTo(msg); $('

').text(RED._("subflow.confirmDelete")).appendTo(msg); const confirmDeleteNotification = RED.notify(msg, { modal: true, fixed: true, buttons: [ { text: RED._('common.label.cancel'), click: function() { confirmDeleteNotification.close(); } }, { text: RED._('workspace.confirmDelete'), class: "primary", click: function() { confirmDeleteNotification.close(); completeDelete(); } } ] }); return; } else { completeDelete(); } function completeDelete() { const startDirty = RED.nodes.dirty(); const historyEvent = removeSubflow(subflow.id); historyEvent.t = 'delete'; historyEvent.dirty = startDirty; RED.history.push(historyEvent); } } function removeSubflow(id, keepInstanceNodes) { // TODO: A lot of this logic is common with RED.nodes.removeWorkspace var removedNodes = []; var removedLinks = []; var removedGroups = []; var activeSubflow = RED.nodes.subflow(id); RED.nodes.eachNode(function(n) { if (!keepInstanceNodes && n.type == "subflow:"+id) { removedNodes.push(n); } if (n.z == id) { removedNodes.push(n); } }); RED.nodes.eachConfig(function(n) { if (n.z == id) { removedNodes.push(n); } }); RED.nodes.groups(id).forEach(function(n) { removedGroups.push(n); }) var removedConfigNodes = []; for (var i=0;i=0; i--) { RED.nodes.removeGroup(removedGroups[i]); } RED.nodes.removeSubflow(activeSubflow); RED.workspaces.remove(activeSubflow); RED.nodes.dirty(true); RED.view.redraw(); return { nodes:removedNodes, links:removedLinks, groups: removedGroups, subflows: [activeSubflow] } } function init() { RED.events.on("workspace:change",function(event) { var activeSubflow = RED.nodes.subflow(event.workspace); if (activeSubflow) { showWorkspaceToolbar(activeSubflow); } else { hideWorkspaceToolbar(); } }); RED.events.on("view:selection-changed",function(selection) { if (!selection.nodes) { RED.menu.setDisabled("menu-item-subflow-convert",true); } else { RED.menu.setDisabled("menu-item-subflow-convert",false); } }); RED.actions.add("core:create-subflow",createSubflow); RED.actions.add("core:convert-to-subflow",convertToSubflow); $(_subflowEditTemplate).appendTo("#red-ui-editor-node-configs"); $(_subflowTemplateEditTemplate).appendTo("#red-ui-editor-node-configs"); } function createSubflow() { var lastIndex = 0; RED.nodes.eachSubflow(function(sf) { var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name); if (m) { lastIndex = Math.max(lastIndex,m[1]); } }); var name = "Subflow "+(lastIndex+1); var subflowId = RED.nodes.id(); var subflow = { type:"subflow", id:subflowId, name:name, info:"", in: [], out: [] }; RED.nodes.addSubflow(subflow); RED.history.push({ t:'createSubflow', subflow: { subflow:subflow }, dirty:RED.nodes.dirty() }); RED.workspaces.show(subflowId); RED.nodes.dirty(true); } function snapToGrid(x) { if (RED.settings.get("editor").view['view-snap-grid']) { x = Math.round(x / RED.view.gridSize()) * RED.view.gridSize(); } return x; } function nodeOrJunction(id) { var node = RED.nodes.node(id); if (node) { return node; } return RED.nodes.junction(id); } function convertToSubflow() { var selection = RED.view.selection(); if (!selection.nodes) { RED.notify(RED._("subflow.errors.noNodesSelected"),"error"); return; } var i,n; var nodeList = new Set(); var tmplist = selection.nodes.slice(); var includedGroups = new Set(); while(tmplist.length > 0) { n = tmplist.shift(); if (n.type === "group") { includedGroups.add(n.id); tmplist = tmplist.concat(n.nodes); } nodeList.add(n); } nodeList = Array.from(nodeList); var containingGroup = nodeList[0].g; var nodesMovedFromGroup = []; for (i=0; i 1) { RED.notify(RED._("subflow.errors.multipleInputsToSelection"),"error"); return; } var lastIndex = 0; RED.nodes.eachSubflow(function(sf) { var m = (new RegExp("^Subflow (\\d+)$")).exec(sf.name); if (m) { lastIndex = Math.max(lastIndex,m[1]); } }); var name = "Subflow "+(lastIndex+1); var subflowId = RED.nodes.id(); var subflow = { type:"subflow", id:subflowId, name:name, info:"", in: Object.keys(candidateInputNodes).map(function(v,i) { var index = i; return { type:"subflow", direction:"in", x:snapToGrid(candidateInputNodes[v].x-(candidateInputNodes[v].w/2)-80 - offsetX), y:snapToGrid(candidateInputNodes[v].y - offsetY), z:subflowId, i:index, id:RED.nodes.id(), wires:[{id:candidateInputNodes[v].id}] }}), out: candidateOutputs.map(function(v,i) { var index = i; return { type:"subflow", direction:"out", x:snapToGrid(v.source.x+(v.source.w/2)+80 - offsetX), y:snapToGrid(v.source.y - offsetY), z:subflowId, i:index, id:RED.nodes.id(), wires:[{id:v.source.id,port:v.sourcePort}] }}) }; RED.nodes.addSubflow(subflow); var subflowInstance = { id:RED.nodes.id(), type:"subflow:"+subflow.id, x: center[0], y: center[1], z: RED.workspaces.active(), inputs: subflow.in.length, outputs: subflow.out.length, h: Math.max(30/*node_height*/,(subflow.out.length||0) * 15), changed:true } subflowInstance._def = RED.nodes.getType(subflowInstance.type); RED.editor.validateNode(subflowInstance); RED.nodes.add(subflowInstance); if (containingGroup) { RED.group.addToGroup(containingGroup, subflowInstance); nodeList.forEach(function(nl) { if (nl.g === containingGroup.id) { delete nl.g; var index = containingGroup.nodes.indexOf(nl); containingGroup.nodes.splice(index,1); nodesMovedFromGroup.push(nl); } }) containingGroup.dirty = true; } candidateInputs.forEach(function(l) { var link = {source:l.source, sourcePort:l.sourcePort, target: subflowInstance}; new_links.push(link); RED.nodes.addLink(link); }); candidateOutputs.forEach(function(output,i) { output.targets.forEach(function(target) { var link = {source:subflowInstance, sourcePort:i, target: target}; new_links.push(link); RED.nodes.addLink(link); }); }); subflow.in.forEach(function(input) { input.wires.forEach(function(wire) { var link = {source: input, sourcePort: 0, target: nodeOrJunction(wire.id) } new_links.push(link); RED.nodes.addLink(link); }); }); subflow.out.forEach(function(output,i) { output.wires.forEach(function(wire) { var link = {source: nodeOrJunction(wire.id), sourcePort: wire.port , target: output } new_links.push(link); RED.nodes.addLink(link); }); }); for (i=0;i -1) { otherNode.links.splice(i,1); } } } return isLocalLink; }); } n.x -= offsetX; n.y -= offsetY; RED.nodes.moveNodeToTab(n, subflow.id); } var historyEvent = { t:'createSubflow', nodes:[subflowInstance.id], links:new_links, subflow: { subflow: subflow, offsetX: offsetX, offsetY: offsetY }, activeWorkspace: RED.workspaces.active(), removedLinks: removedLinks, dirty:RED.nodes.dirty() } if (containingGroup) { historyEvent = { t:'multi', events: [ historyEvent ] } historyEvent.events.push({ t:'addToGroup', group: containingGroup, nodes: [subflowInstance] }) historyEvent.events.push({ t:'removeFromGroup', group: containingGroup, nodes: nodesMovedFromGroup, reparent: false }) } RED.history.push(historyEvent); RED.editor.validateNode(subflow); RED.nodes.dirty(true); RED.view.updateActive(); RED.view.select(null); RED.view.focus(); } /** * Create interface for controlling env var UI definition */ function buildEnvControl(envList,node) { var tabs = RED.tabs.create({ id: "subflow-env-tabs", onchange: function(tab) { if (tab.id === "subflow-env-tab-preview") { var inputContainer = $("#subflow-input-ui"); var list = envList.editableList("items"); var exportedEnv = exportEnvList(list, true); buildEnvUI(inputContainer, exportedEnv,node); } $("#subflow-env-tabs-content").children().hide(); $("#" + tab.id).show(); } }); tabs.addTab({ id: "subflow-env-tab-edit", label: RED._("editor-tab.envProperties") }); tabs.addTab({ id: "subflow-env-tab-preview", label: RED._("editor-tab.preview") }); var localesList = RED.settings.theme("languages") .map(function(lc) { var name = RED._("languages."+lc); return {text: (name ? name : lc), val: lc}; }) .sort(function(a, b) { return a.text.localeCompare(b.text) }); RED.popover.tooltip($(".node-input-env-locales-row i"),RED._("editor.locale")) var locales = $("#subflow-input-env-locale") localesList.forEach(function(item) { var opt = { value: item.val }; if (item.val === "en-US") { // make en-US default selected opt.selected = ""; } $("

", { class: "form-row" }).appendTo(uiContainer); buildEnvUIRow(row,tenv, tenv.ui || {}, node); } } // buildEnvUI function exportEnvList(list, all) { if (list) { var env = []; list.each(function(i) { var entry = $(this); var item = entry.data('data'); var name = (item.parent?item.name:item.nameField.val()).trim(); if ((name !== "") || (item.ui && (item.ui.type === "none"))) { var valueInput = item.valueField; var value = valueInput.typedInput("value"); var type = valueInput.typedInput("type"); if (all || !item.parent || (item.parent.value !== value || item.parent.type !== type)) { var envItem = { name: name, type: type, value: value, }; if (item.ui) { var ui = { icon: item.ui.icon, label: $.extend(true,{},item.ui.label), type: item.ui.type, opts: $.extend(true,{},item.ui.opts) } // Check to see if this is the default ui definition. // Delete any defaults to keep it compact // { // icon: "", // label: {}, // type: "input", // opts: {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST} // } if (!ui.icon) { delete ui.icon; } if ($.isEmptyObject(ui.label)) { delete ui.label; } switch (ui.type) { case "input": if (JSON.stringify(ui.opts) === JSON.stringify({types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST})) { // This is the default input config. Delete it as it will // be applied automatically delete ui.type; delete ui.opts; } break; case "cred": if (envItem.type === "cred") { delete ui.type; } delete ui.opts; break; case "select": if (ui.opts && $.isEmptyObject(ui.opts.opts)) { // This is the default select config. // Delete it as it will be applied automatically delete ui.opts; } break; case "spinner": if ($.isEmptyObject(ui.opts)) { // This is the default spinner config. // Delete as it will be applied automatically delete ui.opts } break; default: delete ui.opts; } if (!$.isEmptyObject(ui)) { envItem.ui = ui; } } env.push(envItem); } } }); return env; } return null; } function getSubflowInstanceParentEnv(node) { var parentEnv = {}; var envList = []; if (/^subflow:/.test(node.type)) { var subflowDef = RED.nodes.subflow(node.type.substring(8)); if (subflowDef.env) { subflowDef.env.forEach(function(env) { var item = { name:env.name, parent: { type: env.type, value: env.value }, ui: $.extend(true,{},env.ui) } envList.push(item); parentEnv[env.name] = item; }) } if (node.env) { for (var i = 0; i < node.env.length; i++) { var env = node.env[i]; if (parentEnv.hasOwnProperty(env.name)) { parentEnv[env.name].type = env.type; parentEnv[env.name].value = env.value; } else { // envList.push({ // name: env.name, // type: env.type, // value: env.value, // }); } } } } else if (node._def.subflowModule) { var keys = Object.keys(node._def.defaults); keys.forEach(function(name) { if (name !== 'name') { var prop = node._def.defaults[name]; var nodeProp = node[name]; var nodePropType; var nodePropValue = nodeProp; if (prop.ui && prop.ui.type === "cred") { nodePropType = "cred"; } else { switch(typeof nodeProp) { case "string": nodePropType = "str"; break; case "number": nodePropType = "num"; break; case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break; default: nodePropType = nodeProp.type; nodePropValue = nodeProp.value; } } var item = { name: name, type: nodePropType, value: nodePropValue, parent: { type: prop.type, value: prop.value }, ui: $.extend(true,{},prop.ui) } envList.push(item); } }) } return envList; } function exportSubflowInstanceEnv(node) { var env = []; // First, get the values for the SubflowTemplate defined properties // - these are the ones with custom UI elements var parentEnv = getSubflowInstanceParentEnv(node); parentEnv.forEach(function(data) { var item; var ui = data.ui || {}; if (!ui.type) { if (data.parent && data.parent.type === "cred") { ui.type = "cred"; } else { ui.type = "input"; ui.opts = {types:RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST} } } else { ui.opts = ui.opts || {}; } var input = $("#"+getSubflowEnvPropertyName(data.name)); if (input.length || ui.type === "cred") { item = { name: data.name }; switch(ui.type) { case "input": if (ui.opts.types && ui.opts.types.length > 0) { item.value = input.typedInput('value'); item.type = input.typedInput('type'); } else { item.value = input.val(); item.type = 'str'; } break; case "cred": item.value = input.val(); item.type = 'cred'; break; case "spinner": item.value = input.val(); item.type = 'num'; break; case "select": item.value = input.val(); item.type = 'str'; break; case "checkbox": item.type = 'bool'; item.value = ""+input.prop("checked"); break; } if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) { env.push(item); } } }) return env; } function getSubflowEnvPropertyName(name) { return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_"); } // Called by subflow.oneditprepare for both instances and templates function buildEditForm(type,node) { if (type === "subflow-template") { // This is the tabbed UI that offers the env list - with UI options // plus the preview tab buildEnvControl($('#node-input-env-container'), node); RED.editor.envVarList.create($('#node-input-env-container'), node); } else if (type === "subflow") { // This is the rendered version of the subflow env var list buildEnvUI($("#subflow-input-ui"), getSubflowInstanceParentEnv(node), node); } } return { init: init, createSubflow: createSubflow, convertToSubflow: convertToSubflow, // removeSubflow: Internal function to remove subflow removeSubflow: removeSubflow, // delete: Prompt user for confirmation delete: deleteSubflow, refresh: refresh, removeInput: removeSubflowInput, removeOutput: removeSubflowOutput, removeStatus: removeSubflowStatus, buildEditForm: buildEditForm, exportSubflowTemplateEnv: exportEnvList, exportSubflowInstanceEnv: exportSubflowInstanceEnv } })();