mirror of https://github.com/node-red/node-red.git
				
				
				
			Merge pull request #2690 from node-red/sf-module
[sf-modules] Support npm subflow modulespull/2820/head
						commit
						c40412d7c6
					
				| 
						 | 
				
			
			@ -60,6 +60,7 @@ module.exports = {
 | 
			
		|||
        runtimeAPI.nodes.addModule(opts).then(function(info) {
 | 
			
		||||
            res.json(info);
 | 
			
		||||
        }).catch(function(err) {
 | 
			
		||||
            console.log(err.stack);
 | 
			
		||||
            apiUtils.rejectHandler(req,res,err);
 | 
			
		||||
        })
 | 
			
		||||
    },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -339,6 +339,17 @@
 | 
			
		|||
        "deleteSubflow": "delete subflow",
 | 
			
		||||
        "info": "Description",
 | 
			
		||||
        "category": "Category",
 | 
			
		||||
        "module": "Module",
 | 
			
		||||
        "license": "License",
 | 
			
		||||
        "licenseOther": "Other",
 | 
			
		||||
        "type": "Node Type",
 | 
			
		||||
        "version": "Version",
 | 
			
		||||
        "versionPlaceholder": "x.y.z",
 | 
			
		||||
        "keys": "Keywords",
 | 
			
		||||
        "keysPlaceholder": "Comma-separated keywords",
 | 
			
		||||
        "author": "Author",
 | 
			
		||||
        "authorPlaceholder": "Your Name <email@example.com>",
 | 
			
		||||
        "desc": "Description",
 | 
			
		||||
        "env": {
 | 
			
		||||
            "restore": "Restore to subflow default",
 | 
			
		||||
            "remove": "Remove environment variable"
 | 
			
		||||
| 
						 | 
				
			
			@ -1079,6 +1090,7 @@
 | 
			
		|||
    "editor-tab": {
 | 
			
		||||
        "properties": "Properties",
 | 
			
		||||
        "envProperties": "Environment Variables",
 | 
			
		||||
        "module": "Module Properties",
 | 
			
		||||
        "description": "Description",
 | 
			
		||||
        "appearance": "Appearance",
 | 
			
		||||
        "preview": "UI Preview",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -339,6 +339,12 @@
 | 
			
		|||
        "deleteSubflow": "サブフローを削除",
 | 
			
		||||
        "info": "詳細",
 | 
			
		||||
        "category": "カテゴリ",
 | 
			
		||||
        "module": "モジュール",
 | 
			
		||||
        "license": "ライセンス",
 | 
			
		||||
        "version": "バージョン",
 | 
			
		||||
        "keys": "キーワード",
 | 
			
		||||
        "author": "作者",
 | 
			
		||||
        "desc": "説明",
 | 
			
		||||
        "env": {
 | 
			
		||||
            "restore": "デフォルト値に戻す",
 | 
			
		||||
            "remove": "環境変数を削除"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -670,6 +670,7 @@ RED.nodes = (function() {
 | 
			
		|||
        node.in = [];
 | 
			
		||||
        node.out = [];
 | 
			
		||||
        node.env = n.env;
 | 
			
		||||
        node.meta = n.meta;
 | 
			
		||||
 | 
			
		||||
        if (exportCreds) {
 | 
			
		||||
            var credentialSet = {};
 | 
			
		||||
| 
						 | 
				
			
			@ -1920,6 +1921,18 @@ RED.nodes = (function() {
 | 
			
		|||
        RED.events.emit("groups:remove",group);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNodeHelp(type) {
 | 
			
		||||
        var helpContent = "";
 | 
			
		||||
        var helpElement = $("script[data-help-name='"+type+"']");
 | 
			
		||||
        if (helpElement) {
 | 
			
		||||
            helpContent = helpElement.html();
 | 
			
		||||
            var helpType = helpElement.attr("type");
 | 
			
		||||
            if (helpType === "text/markdown") {
 | 
			
		||||
                helpContent = RED.utils.renderMarkdown(helpContent);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return helpContent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        init: function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -1999,6 +2012,7 @@ RED.nodes = (function() {
 | 
			
		|||
 | 
			
		||||
        registerType: registry.registerNodeType,
 | 
			
		||||
        getType: registry.getNodeType,
 | 
			
		||||
        getNodeHelp: getNodeHelp,
 | 
			
		||||
        convertNode: convertNode,
 | 
			
		||||
 | 
			
		||||
        add: addNode,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -414,6 +414,7 @@ RED.editor = (function() {
 | 
			
		|||
        for (var cred in credDefinition) {
 | 
			
		||||
            if (credDefinition.hasOwnProperty(cred)) {
 | 
			
		||||
                var input = $("#" + prefix + '-' + cred);
 | 
			
		||||
                if (input.length > 0) {
 | 
			
		||||
                    var value = input.val();
 | 
			
		||||
                    if (credDefinition[cred].type == 'password') {
 | 
			
		||||
                        node.credentials['has_' + cred] = (value !== "");
 | 
			
		||||
| 
						 | 
				
			
			@ -429,6 +430,7 @@ RED.editor = (function() {
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return changed;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -465,6 +467,7 @@ RED.editor = (function() {
 | 
			
		|||
                    definition.oneditprepare.call(node);
 | 
			
		||||
                } catch(err) {
 | 
			
		||||
                    console.log("oneditprepare",node.id,node.type,err.toString());
 | 
			
		||||
                    console.log(err.stack);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // Now invoke any change handlers added to the fields - passing true
 | 
			
		||||
| 
						 | 
				
			
			@ -1192,7 +1195,7 @@ RED.editor = (function() {
 | 
			
		|||
                                    changed = true;
 | 
			
		||||
                                }
 | 
			
		||||
                            } catch(err) {
 | 
			
		||||
                                console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
 | 
			
		||||
                                console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            for (d in editing_node._def.defaults) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1890,7 +1893,7 @@ RED.editor = (function() {
 | 
			
		|||
                        try {
 | 
			
		||||
                            configTypeDef.oneditsave.call(editing_config_node);
 | 
			
		||||
                        } catch(err) {
 | 
			
		||||
                            console.log("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
 | 
			
		||||
                            console.warn("oneditsave",editing_config_node.id,editing_config_node.type,err.toString());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -2250,6 +2253,14 @@ RED.editor = (function() {
 | 
			
		|||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var newMeta = RED.subflow.exportSubflowModuleProperties(editing_node);
 | 
			
		||||
 | 
			
		||||
                        if (!isSameObj(editing_node.meta,newMeta)) {
 | 
			
		||||
                            changes.meta = editing_node.meta;
 | 
			
		||||
                            editing_node.meta = newMeta;
 | 
			
		||||
                            changed = true;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (changed) {
 | 
			
		||||
                            var wasChanged = editing_node.changed;
 | 
			
		||||
                            editing_node.changed = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -2356,6 +2367,16 @@ RED.editor = (function() {
 | 
			
		|||
                };
 | 
			
		||||
                editorTabs.addTab(nodePropertiesTab);
 | 
			
		||||
 | 
			
		||||
                var moduleTab = {
 | 
			
		||||
                    id: "editor-tab-module",
 | 
			
		||||
                    label: RED._("editor-tab.module"),
 | 
			
		||||
                    name: RED._("editor-tab.module"),
 | 
			
		||||
                    content: $('<div>', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(),
 | 
			
		||||
                    iconClass: "fa fa-cube",
 | 
			
		||||
                };
 | 
			
		||||
                editorTabs.addTab(moduleTab);
 | 
			
		||||
                RED.subflow.buildModuleForm(moduleTab.content, editing_node);
 | 
			
		||||
 | 
			
		||||
                var descriptionTab = {
 | 
			
		||||
                    id: "editor-tab-description",
 | 
			
		||||
                    label: RED._("editor-tab.description"),
 | 
			
		||||
| 
						 | 
				
			
			@ -2457,7 +2478,7 @@ RED.editor = (function() {
 | 
			
		|||
                                    changed = true;
 | 
			
		||||
                                }
 | 
			
		||||
                            } catch(err) {
 | 
			
		||||
                                console.log("oneditsave",editing_node.id,editing_node.type,err.toString());
 | 
			
		||||
                                console.warn("oneditsave",editing_node.id,editing_node.type,err.toString());
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            for (d in editing_node._def.defaults) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -147,7 +147,7 @@ RED.palette = (function() {
 | 
			
		|||
        var popOverContent;
 | 
			
		||||
        try {
 | 
			
		||||
            var l = "<p><b>"+RED.text.bidi.enforceTextDirectionWithUCC(label)+"</b></p>";
 | 
			
		||||
            popOverContent = $('<div></div>').append($(l+(info?info:$("script[data-help-name='"+type+"']").html()||"<p>"+RED._("palette.noInfo")+"</p>").trim())
 | 
			
		||||
            popOverContent = $('<div></div>').append($(l+(info?info:RED.nodes.getNodeHelp(type)||"<p>"+RED._("palette.noInfo")+"</p>").trim())
 | 
			
		||||
                                .filter(function(n) {
 | 
			
		||||
                                    return (this.nodeType == 1 && this.nodeName == "P") || (this.nodeType == 3 && this.textContent.trim().length > 0)
 | 
			
		||||
                                }).slice(0,2));
 | 
			
		||||
| 
						 | 
				
			
			@ -264,27 +264,6 @@ RED.palette = (function() {
 | 
			
		|||
 | 
			
		||||
            d.data('popover',popover);
 | 
			
		||||
 | 
			
		||||
            // $(d).popover({
 | 
			
		||||
            //     title:d.type,
 | 
			
		||||
            //     placement:"right",
 | 
			
		||||
            //     trigger: "hover",
 | 
			
		||||
            //     delay: { show: 750, hide: 50 },
 | 
			
		||||
            //     html: true,
 | 
			
		||||
            //     container:'body'
 | 
			
		||||
            // });
 | 
			
		||||
            // d.on("click", function() {
 | 
			
		||||
            //     RED.view.focus();
 | 
			
		||||
            //     var helpText;
 | 
			
		||||
            //     if (nt.indexOf("subflow:") === 0) {
 | 
			
		||||
            //         helpText = RED.utils.renderMarkdown(RED.nodes.subflow(nt.substring(8)).info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
 | 
			
		||||
            //     } else {
 | 
			
		||||
            //         helpText = $("script[data-help-name='"+d.attr("data-palette-type")+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
 | 
			
		||||
            //     }
 | 
			
		||||
            //     // Don't look too closely. RED.sidebar.info.set will set the 'Description'
 | 
			
		||||
            //     // section of the sidebar. Pass in the title of the Help section so it looks
 | 
			
		||||
            //     // right.
 | 
			
		||||
            //     RED.sidebar.type.show(helpText,RED._("sidebar.info.nodeHelp"));
 | 
			
		||||
            // });
 | 
			
		||||
            var chart = $("#red-ui-workspace-chart");
 | 
			
		||||
            var chartSVG = $("#red-ui-workspace-chart>svg").get(0);
 | 
			
		||||
            var activeSpliceLink;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,6 +47,37 @@ RED.subflow = (function() {
 | 
			
		|||
        '</div>'+
 | 
			
		||||
        '</script>';
 | 
			
		||||
 | 
			
		||||
    var _subflowModulePaneTemplate = '<form class="dialog-form form-horizontal" autocomplete="off">'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-module" data-i18n="[append]editor:subflow.module"><i class="fa fa-cube"></i> </label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-module" data-i18n="[placeholder]common.label.name">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-type" data-i18n="[append]editor:subflow.type"> </label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-type">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-version" data-i18n="[append]editor:subflow.version"></label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-version" data-i18n="[placeholder]editor:subflow.versionPlaceholder">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-desc" data-i18n="[append]editor:subflow.desc"></label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-desc">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-license" data-i18n="[append]editor:subflow.license"></label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-license">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-author" data-i18n="[append]editor:subflow.author"></label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-author" data-i18n="[placeholder]editor:subflow.authorPlaceholder">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
        '<div class="form-row">'+
 | 
			
		||||
            '<label for="subflow-input-module-keywords" data-i18n="[append]editor:subflow.keys"></label>'+
 | 
			
		||||
            '<input style="width: calc(100% - 110px)" type="text" id="subflow-input-module-keywords" data-i18n="[placeholder]editor:subflow.keysPlaceholder">'+
 | 
			
		||||
        '</div>'+
 | 
			
		||||
    '</form>';
 | 
			
		||||
 | 
			
		||||
    function findAvailableSubflowIOPosition(subflow,isInput) {
 | 
			
		||||
        var pos = {x:50,y:30};
 | 
			
		||||
        if (!isInput) {
 | 
			
		||||
| 
						 | 
				
			
			@ -993,6 +1024,7 @@ RED.subflow = (function() {
 | 
			
		|||
                                icon: "",
 | 
			
		||||
                                type: "cred"
 | 
			
		||||
                            }
 | 
			
		||||
                            opt.ui.type = "cred";
 | 
			
		||||
                        } else {
 | 
			
		||||
                            opt.ui = opt.ui || {
 | 
			
		||||
                                icon: "",
 | 
			
		||||
| 
						 | 
				
			
			@ -1488,6 +1520,7 @@ RED.subflow = (function() {
 | 
			
		|||
        var locale = RED.i18n.lang();
 | 
			
		||||
        var labelText = lookupLabel(labels, labels["en-US"]||tenv.name, locale);
 | 
			
		||||
        var label = $('<label>').appendTo(row);
 | 
			
		||||
        $('<span> </span>').appendTo(row);
 | 
			
		||||
        var labelContainer = $('<span></span>').appendTo(label);
 | 
			
		||||
        if (ui.icon) {
 | 
			
		||||
            var newPath = RED.utils.separateIconPath(ui.icon);
 | 
			
		||||
| 
						 | 
				
			
			@ -1723,8 +1756,6 @@ RED.subflow = (function() {
 | 
			
		|||
                    parentEnv[env.name] = item;
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
            if (node.env) {
 | 
			
		||||
                for (var i = 0; i < node.env.length; i++) {
 | 
			
		||||
                    var env = node.env[i];
 | 
			
		||||
| 
						 | 
				
			
			@ -1740,6 +1771,40 @@ RED.subflow = (function() {
 | 
			
		|||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1859,6 +1924,122 @@ RED.subflow = (function() {
 | 
			
		|||
        buildPropertiesList(list, node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setupInputValidation(input,validator) {
 | 
			
		||||
        var errorTip;
 | 
			
		||||
        var validateTimeout;
 | 
			
		||||
 | 
			
		||||
        var validateFunction = function() {
 | 
			
		||||
            if (validateTimeout) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            validateTimeout = setTimeout(function() {
 | 
			
		||||
                var error = validator(input.val());
 | 
			
		||||
                // if (!error && errorTip) {
 | 
			
		||||
                //     errorTip.close();
 | 
			
		||||
                //     errorTip = null;
 | 
			
		||||
                // } else if (error && !errorTip) {
 | 
			
		||||
                //     errorTip = RED.popover.create({
 | 
			
		||||
                //         tooltip: true,
 | 
			
		||||
                //         target:input,
 | 
			
		||||
                //         size: "small",
 | 
			
		||||
                //         direction: "bottom",
 | 
			
		||||
                //         content: error,
 | 
			
		||||
                //     }).open();
 | 
			
		||||
                // }
 | 
			
		||||
                input.toggleClass("input-error",!!error);
 | 
			
		||||
                validateTimeout = null;
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        input.on("change keyup paste", validateFunction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function buildModuleForm(container, node) {
 | 
			
		||||
        $(_subflowModulePaneTemplate).appendTo(container);
 | 
			
		||||
        var moduleProps = node.meta || {};
 | 
			
		||||
        [
 | 
			
		||||
            'module',
 | 
			
		||||
            'type',
 | 
			
		||||
            'version',
 | 
			
		||||
            'author',
 | 
			
		||||
            'desc',
 | 
			
		||||
            'keywords',
 | 
			
		||||
            'license'
 | 
			
		||||
        ].forEach(function(property) {
 | 
			
		||||
            $("#subflow-input-module-"+property).val(moduleProps[property]||"")
 | 
			
		||||
        })
 | 
			
		||||
        $("#subflow-input-module-type").attr("placeholder",node.id);
 | 
			
		||||
 | 
			
		||||
        setupInputValidation($("#subflow-input-module-module"), function(newValue) {
 | 
			
		||||
            newValue = newValue.trim();
 | 
			
		||||
            var isValid = newValue.length < 215;
 | 
			
		||||
            isValid = isValid && !/^[._]/.test(newValue);
 | 
			
		||||
            isValid = isValid && !/[A-Z]/.test(newValue);
 | 
			
		||||
            if (newValue !== encodeURIComponent(newValue)) {
 | 
			
		||||
                var m = /^@([^\/]+)\/([^\/]+)$/.exec(newValue);
 | 
			
		||||
                if (m) {
 | 
			
		||||
                    isValid = isValid && (m[1] === encodeURIComponent(m[1]) && m[2] === encodeURIComponent(m[2]))
 | 
			
		||||
                } else {
 | 
			
		||||
                    isValid = false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return isValid?"":"Invalid module name"
 | 
			
		||||
        })
 | 
			
		||||
        setupInputValidation($("#subflow-input-module-version"), function(newValue) {
 | 
			
		||||
            newValue = newValue.trim();
 | 
			
		||||
            var isValid = newValue === "" ||
 | 
			
		||||
                          /^(\d|[1-9]\d*)\.(\d|[1-9]\d*)\.(\d|[1-9]\d*)(-(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9A-Za-z-][0-9A-Za-z-]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(newValue);
 | 
			
		||||
            return isValid?"":"Invalid version number"
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        var licenses = ["none", "Apache-2.0", "BSD-3-Clause", "BSD-2-Clause", "GPL-2.0", "GPL-3.0", "MIT", "MPL-2.0", "CDDL-1.0", "EPL-2.0"];
 | 
			
		||||
        var typedLicenses = {
 | 
			
		||||
            types: licenses.map(function(l) {
 | 
			
		||||
                return {value:l,label:l,hasValue:false}
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        typedLicenses.types.push({
 | 
			
		||||
            value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
 | 
			
		||||
        })
 | 
			
		||||
        if (!moduleProps.license) {
 | 
			
		||||
            typedLicenses.default = "none";
 | 
			
		||||
        } else if (licenses.indexOf(moduleProps.license) > -1) {
 | 
			
		||||
            typedLicenses.default = moduleProps.license;
 | 
			
		||||
        } else {
 | 
			
		||||
            typedLicenses.default = "_custom_";
 | 
			
		||||
        }
 | 
			
		||||
        $("#subflow-input-module-license").typedInput(typedLicenses)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function exportSubflowModuleProperties(node) {
 | 
			
		||||
        var value;
 | 
			
		||||
        var moduleProps = {};
 | 
			
		||||
        [
 | 
			
		||||
            'module',
 | 
			
		||||
            'type',
 | 
			
		||||
            'version',
 | 
			
		||||
            'author',
 | 
			
		||||
            'desc',
 | 
			
		||||
            'keywords'
 | 
			
		||||
        ].forEach(function(property) {
 | 
			
		||||
            value = $("#subflow-input-module-"+property).val().trim();
 | 
			
		||||
            if (value) {
 | 
			
		||||
                moduleProps[property] = value;
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        var selectedLicenseType = $("#subflow-input-module-license").typedInput("type");
 | 
			
		||||
 | 
			
		||||
        if (selectedLicenseType === '_custom_') {
 | 
			
		||||
            value = $("#subflow-input-module-license").val();
 | 
			
		||||
            if (value) {
 | 
			
		||||
                moduleProps.license = value;
 | 
			
		||||
            }
 | 
			
		||||
        } else if (selectedLicenseType !== "none") {
 | 
			
		||||
            moduleProps.license = selectedLicenseType;
 | 
			
		||||
        }
 | 
			
		||||
        return moduleProps;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        init: init,
 | 
			
		||||
        createSubflow: createSubflow,
 | 
			
		||||
| 
						 | 
				
			
			@ -1872,9 +2053,11 @@ RED.subflow = (function() {
 | 
			
		|||
 | 
			
		||||
        buildEditForm: buildEditForm,
 | 
			
		||||
        buildPropertiesForm: buildPropertiesForm,
 | 
			
		||||
        buildModuleForm: buildModuleForm,
 | 
			
		||||
 | 
			
		||||
        exportSubflowTemplateEnv: exportEnvList,
 | 
			
		||||
        exportSubflowInstanceEnv: exportSubflowInstanceEnv
 | 
			
		||||
        exportSubflowInstanceEnv: exportSubflowInstanceEnv,
 | 
			
		||||
        exportSubflowModuleProperties: exportSubflowModuleProperties
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -247,7 +247,7 @@ RED.sidebar.help = (function() {
 | 
			
		|||
            helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
 | 
			
		||||
            title = subflowNode.name || nodeType;
 | 
			
		||||
        } else {
 | 
			
		||||
            helpText = $("script[data-help-name='"+nodeType+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
 | 
			
		||||
            helpText = RED.nodes.getNodeHelp(nodeType)||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
 | 
			
		||||
            title = nodeType;
 | 
			
		||||
        }
 | 
			
		||||
        setInfoText(title, helpText, helpSection);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -382,20 +382,13 @@ RED.sidebar.info = (function() {
 | 
			
		|||
                var category = subflowNode.category||"subflows";
 | 
			
		||||
                $(propRow.children()[1]).text(RED._("palette.label."+category,{defaultValue:category}))
 | 
			
		||||
                $('<tr class="node-info-subflow-row"><td>'+RED._("sidebar.info.instances")+"</td><td>"+subflowUserCount+'</td></tr>').appendTo(tableBody);
 | 
			
		||||
                if (subflowNode.meta) {
 | 
			
		||||
                    propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.module")+'</td><td></td></tr>').appendTo(tableBody);
 | 
			
		||||
                    $(propRow.children()[1]).text(subflowNode.meta.module||"")
 | 
			
		||||
                    propRow = $('<tr class="red-ui-help-info-row"><td>'+RED._("subflow.version")+'</td><td></td></tr>').appendTo(tableBody);
 | 
			
		||||
                    $(propRow.children()[1]).text(subflowNode.meta.version||"")
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // var helpText = "";
 | 
			
		||||
            // if (node.type === "tab" || node.type === "subflow") {
 | 
			
		||||
            // } else {
 | 
			
		||||
            //     if (subflowNode && node.type !== "subflow") {
 | 
			
		||||
            //         // Selected a subflow instance node.
 | 
			
		||||
            //         // - The subflow template info goes into help
 | 
			
		||||
            //         helpText = (RED.utils.renderMarkdown(subflowNode.info||"")||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>'));
 | 
			
		||||
            //     } else {
 | 
			
		||||
            //         helpText = $("script[data-help-name='"+node.type+"']").html()||('<span class="red-ui-help-info-none">'+RED._("sidebar.info.none")+'</span>');
 | 
			
		||||
            //     }
 | 
			
		||||
            //     setInfoText(helpText, helpSection.content);
 | 
			
		||||
            // }
 | 
			
		||||
 | 
			
		||||
            var infoText = "";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -409,23 +402,6 @@ RED.sidebar.info = (function() {
 | 
			
		|||
            }
 | 
			
		||||
            var infoSectionContainer = $("<div>").css("padding","0 6px 6px").appendTo(propertiesPanelContent)
 | 
			
		||||
 | 
			
		||||
            // var editInfo = $('<button class="red-ui-button red-ui-button-small" style="float: right"><i class="fa fa-file-text-o"></button>').appendTo(infoSectionContainer).on("click", function(evt) {
 | 
			
		||||
            //     //.text(RED._("sidebar.info.editDescription"))
 | 
			
		||||
            //     evt.preventDefault();
 | 
			
		||||
            //     evt.stopPropagation();
 | 
			
		||||
            //     if (node.type === 'tab') {
 | 
			
		||||
            //
 | 
			
		||||
            //     } else if (node.type === 'subflow') {
 | 
			
		||||
            //
 | 
			
		||||
            //     } else if (node.type === 'group') {
 | 
			
		||||
            //
 | 
			
		||||
            //     } else if (node._def.category !== 'config') {
 | 
			
		||||
            //         RED.editor.edit(node,"editor-tab-description");
 | 
			
		||||
            //     } else {
 | 
			
		||||
            //
 | 
			
		||||
            //     }
 | 
			
		||||
            // })
 | 
			
		||||
 | 
			
		||||
            setInfoText(infoText, infoSectionContainer);
 | 
			
		||||
 | 
			
		||||
            $(".red-ui-sidebar-info-stack").scrollTop(0);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -765,6 +765,10 @@ button.red-ui-toggleButton.toggle {
 | 
			
		|||
                    width: calc(100% - 10px);
 | 
			
		||||
                    padding-left: 3px;
 | 
			
		||||
                }
 | 
			
		||||
                select {
 | 
			
		||||
                    padding: 0 3px;
 | 
			
		||||
                    font-size: 11px;
 | 
			
		||||
                }
 | 
			
		||||
                .placeholder-input {
 | 
			
		||||
                    span:first-child {
 | 
			
		||||
                        display:inline-block;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -139,6 +139,9 @@
 | 
			
		|||
    stroke-width: 2;
 | 
			
		||||
}
 | 
			
		||||
.red-ui-flow-node-icon-group {
 | 
			
		||||
    text {
 | 
			
		||||
        @include disable-selection;
 | 
			
		||||
    }
 | 
			
		||||
    .fa-lg {
 | 
			
		||||
        @include disable-selection;
 | 
			
		||||
        stroke: none;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,6 +99,8 @@ module.exports = {
 | 
			
		|||
     */
 | 
			
		||||
    get: registry.getNodeConstructor,
 | 
			
		||||
 | 
			
		||||
    registerSubflow: registry.registerSubflow,
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a node's set information.
 | 
			
		||||
     *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,20 +94,6 @@ function checkModulePath(folder) {
 | 
			
		|||
        version: moduleVersion
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkExistingModule(module,version) {
 | 
			
		||||
    var info = registry.getModuleInfo(module);
 | 
			
		||||
    if (info) {
 | 
			
		||||
        if (!version || info.version === version) {
 | 
			
		||||
            var err = new Error("Module already loaded");
 | 
			
		||||
            err.code = "module_already_loaded";
 | 
			
		||||
            throw err;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function installModule(module,version,url) {
 | 
			
		||||
    if (Buffer.isBuffer(module)) {
 | 
			
		||||
        return installTarball(module)
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +104,7 @@ async function installModule(module,version,url) {
 | 
			
		|||
        var installName = module;
 | 
			
		||||
        let isRegistryPackage = true;
 | 
			
		||||
        var isUpgrade = false;
 | 
			
		||||
        var isExisting = false;
 | 
			
		||||
        if (url) {
 | 
			
		||||
            if (pkgurlRe.test(url) || localtgzRe.test(url)) {
 | 
			
		||||
                // Git remote url or Tarball url - check the valid package url
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +145,21 @@ async function installModule(module,version,url) {
 | 
			
		|||
                throw e;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        isUpgrade = checkExistingModule(module,version);
 | 
			
		||||
 | 
			
		||||
        var info = registry.getModuleInfo(module);
 | 
			
		||||
        if (info) {
 | 
			
		||||
            if (!info.user) {
 | 
			
		||||
                log.debug(`Installing existing module: ${module}`)
 | 
			
		||||
                isExisting = true;
 | 
			
		||||
            } else if (!version || info.version === version) {
 | 
			
		||||
                var err = new Error("Module already loaded");
 | 
			
		||||
                err.code = "module_already_loaded";
 | 
			
		||||
                throw err;
 | 
			
		||||
            }
 | 
			
		||||
            isUpgrade = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            isUpgrade = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!isUpgrade) {
 | 
			
		||||
            log.info(log._("server.install.installing",{name: module,version: version||"latest"}));
 | 
			
		||||
| 
						 | 
				
			
			@ -172,7 +173,17 @@ async function installModule(module,version,url) {
 | 
			
		|||
        return exec.run(npmCommand,args,{
 | 
			
		||||
            cwd: installDir
 | 
			
		||||
        }, true).then(result => {
 | 
			
		||||
            if (!isUpgrade) {
 | 
			
		||||
            if (isExisting) {
 | 
			
		||||
                // This is a module we already have installed as a non-user module.
 | 
			
		||||
                // That means it was discovered when loading, but was not listed
 | 
			
		||||
                // in package.json and has been hidden from the editor.
 | 
			
		||||
                // The user has requested to install this module. Having run
 | 
			
		||||
                // the npm install above, it will now be listed in package.json.
 | 
			
		||||
                // Update the registry to mark it as a user module so it will
 | 
			
		||||
                // be available to the editor.
 | 
			
		||||
                log.info(log._("server.install.installed",{name:module}));
 | 
			
		||||
                return require("./registry").setUserInstalled(module,true).then(reportAddedModules);
 | 
			
		||||
            } else if (!isUpgrade) {
 | 
			
		||||
                log.info(log._("server.install.installed",{name:module}));
 | 
			
		||||
                return require("./index").addModule(module).then(reportAddedModules);
 | 
			
		||||
            } else {
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +223,6 @@ async function installModule(module,version,url) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function reportAddedModules(info) {
 | 
			
		||||
    //comms.publish("node/added",info.nodes,false);
 | 
			
		||||
    if (info.nodes.length > 0) {
 | 
			
		||||
        log.info(log._("server.added-types"));
 | 
			
		||||
        for (var i=0;i<info.nodes.length;i++) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -158,13 +158,10 @@ async function loadNodeTemplate(node) {
 | 
			
		|||
        }
 | 
			
		||||
        return node
 | 
			
		||||
    }).catch(err => {
 | 
			
		||||
        node.types = [];
 | 
			
		||||
        if (err.code === 'ENOENT') {
 | 
			
		||||
            if (!node.types) {
 | 
			
		||||
                node.types = [];
 | 
			
		||||
            }
 | 
			
		||||
            node.err = "Error: "+node.template+" does not exist";
 | 
			
		||||
        } else {
 | 
			
		||||
        // ENOENT means no html file. We can live with that. But any other error
 | 
			
		||||
        // should be fatal
 | 
			
		||||
        // node.err = "Error: "+node.template+" does not exist";
 | 
			
		||||
        if (err.code !== 'ENOENT') {
 | 
			
		||||
            node.types = [];
 | 
			
		||||
            node.err = err.toString();
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -311,15 +308,37 @@ function addModule(module) {
 | 
			
		|||
        throw new Error("Settings unavailable");
 | 
			
		||||
    }
 | 
			
		||||
    var nodes = [];
 | 
			
		||||
    if (registry.getModuleInfo(module)) {
 | 
			
		||||
    var existingInfo = registry.getModuleInfo(module);
 | 
			
		||||
    if (existingInfo) {
 | 
			
		||||
        // TODO: nls
 | 
			
		||||
        var e = new Error("module_already_loaded");
 | 
			
		||||
        e.code = "module_already_loaded";
 | 
			
		||||
        return Promise.reject(e);
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
        var moduleFiles = localfilesystem.getModuleFiles(module);
 | 
			
		||||
        return loadNodeFiles(moduleFiles);
 | 
			
		||||
        var moduleFiles = {};
 | 
			
		||||
        var moduleStack = [module];
 | 
			
		||||
        while(moduleStack.length > 0) {
 | 
			
		||||
            var moduleToLoad = moduleStack.shift();
 | 
			
		||||
            var files = localfilesystem.getModuleFiles(moduleToLoad);
 | 
			
		||||
            if (files[moduleToLoad]) {
 | 
			
		||||
                moduleFiles[moduleToLoad] = files[moduleToLoad];
 | 
			
		||||
                if (moduleFiles[moduleToLoad].dependencies) {
 | 
			
		||||
                    log.debug(`Loading dependencies for ${module}`)
 | 
			
		||||
                    for (var i=0; i<moduleFiles[moduleToLoad].dependencies.length; i++) {
 | 
			
		||||
                        var dep = moduleFiles[moduleToLoad].dependencies[i]
 | 
			
		||||
                        if (!registry.getModuleInfo(dep)) {
 | 
			
		||||
                            log.debug(` - load ${dep}`)
 | 
			
		||||
                            moduleStack.push(dep);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            log.debug(` - already loaded ${dep}`)
 | 
			
		||||
                            registry.addModuleDependency(dep,moduleToLoad)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return loadNodeFiles(moduleFiles).then(() => module)
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        return Promise.reject(err);
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@ let loadDenyList = [];
 | 
			
		|||
var settings;
 | 
			
		||||
var disableNodePathScan = false;
 | 
			
		||||
var iconFileExtensions = [".png", ".gif", ".svg"];
 | 
			
		||||
var packageList = {};
 | 
			
		||||
 | 
			
		||||
function init(_settings) {
 | 
			
		||||
    settings = _settings;
 | 
			
		||||
| 
						 | 
				
			
			@ -187,9 +188,17 @@ function scanTreeForNodesModules(moduleName) {
 | 
			
		|||
    var userDir;
 | 
			
		||||
 | 
			
		||||
    if (settings.userDir) {
 | 
			
		||||
        packageList = getPackageList();
 | 
			
		||||
        userDir = path.join(settings.userDir,"node_modules");
 | 
			
		||||
        results = scanDirForNodesModules(userDir,moduleName);
 | 
			
		||||
        results.forEach(function(r) { r.local = true; });
 | 
			
		||||
        results.forEach(function(r) {
 | 
			
		||||
            // If it was found in <userDir>/node_modules then it is considered
 | 
			
		||||
            // a local module.
 | 
			
		||||
            // Also check to see if it is listed in the package.json file as a user-installed
 | 
			
		||||
            // module. This distinguishes modules installed as a dependency
 | 
			
		||||
            r.local = true;
 | 
			
		||||
            r.user = !!packageList[r.package.name];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (dir) {
 | 
			
		||||
| 
						 | 
				
			
			@ -288,20 +297,19 @@ function getNodeFiles(disableNodePathScan) {
 | 
			
		|||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var nodeList = {
 | 
			
		||||
        "node-red": {
 | 
			
		||||
    var nodeList = {};
 | 
			
		||||
    var coreNodeEntry = {
 | 
			
		||||
        name: "node-red",
 | 
			
		||||
        version: settings.version,
 | 
			
		||||
        nodes: {},
 | 
			
		||||
        icons: iconList
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
    nodeFiles.forEach(function(node) {
 | 
			
		||||
        nodeList["node-red"].nodes[node.name] = node;
 | 
			
		||||
        coreNodeEntry.nodes[node.name] = node;
 | 
			
		||||
    });
 | 
			
		||||
    if (settings.coreNodesDir) {
 | 
			
		||||
        var examplesDir = path.join(settings.coreNodesDir,"examples");
 | 
			
		||||
        nodeList["node-red"].examples = {path: examplesDir};
 | 
			
		||||
        coreNodeEntry.examples = {path: examplesDir};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!disableNodePathScan) {
 | 
			
		||||
| 
						 | 
				
			
			@ -310,7 +318,6 @@ function getNodeFiles(disableNodePathScan) {
 | 
			
		|||
        // Filter the module list to ignore global modules
 | 
			
		||||
        // that have also been installed locally - allowing the user to
 | 
			
		||||
        // update a module they may not otherwise be able to touch
 | 
			
		||||
 | 
			
		||||
        moduleFiles.sort(function(A,B) {
 | 
			
		||||
            if (A.local && !B.local) {
 | 
			
		||||
                return -1
 | 
			
		||||
| 
						 | 
				
			
			@ -323,7 +330,7 @@ function getNodeFiles(disableNodePathScan) {
 | 
			
		|||
        moduleFiles = moduleFiles.filter(function(mod) {
 | 
			
		||||
            var result;
 | 
			
		||||
            if (!knownModules[mod.package.name]) {
 | 
			
		||||
                knownModules[mod.package.name] = true;
 | 
			
		||||
                knownModules[mod.package.name] = mod;
 | 
			
		||||
                result = true;
 | 
			
		||||
            } else {
 | 
			
		||||
                result = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -332,48 +339,62 @@ function getNodeFiles(disableNodePathScan) {
 | 
			
		|||
            return result;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        moduleFiles.forEach(function(moduleFile) {
 | 
			
		||||
            var nodeModuleFiles = getModuleNodeFiles(moduleFile);
 | 
			
		||||
            nodeList[moduleFile.package.name] = {
 | 
			
		||||
                name: moduleFile.package.name,
 | 
			
		||||
                version: moduleFile.package.version,
 | 
			
		||||
                path: moduleFile.dir,
 | 
			
		||||
                local: moduleFile.local||false,
 | 
			
		||||
                nodes: {},
 | 
			
		||||
                icons: nodeModuleFiles.icons,
 | 
			
		||||
                examples: nodeModuleFiles.examples
 | 
			
		||||
            };
 | 
			
		||||
            if (moduleFile.package['node-red'].version) {
 | 
			
		||||
                nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
 | 
			
		||||
        // Do a second pass to check we have all the declared node dependencies
 | 
			
		||||
        // As this is only done as part of the initial palette load, `knownModules` will
 | 
			
		||||
        // contain a list of everything discovered during this phase. This means
 | 
			
		||||
        // we can check for missing dependencies here.
 | 
			
		||||
        moduleFiles = moduleFiles.filter(function(mod) {
 | 
			
		||||
            if (Array.isArray(mod.package["node-red"].dependencies)) {
 | 
			
		||||
                const deps = mod.package["node-red"].dependencies;
 | 
			
		||||
                const missingDeps = mod.package["node-red"].dependencies.filter(dep => {
 | 
			
		||||
                    if (knownModules[dep]) {
 | 
			
		||||
                        knownModules[dep].usedBy = knownModules[dep].usedBy || [];
 | 
			
		||||
                        knownModules[dep].usedBy.push(mod.package.name)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
            nodeModuleFiles.files.forEach(function(node) {
 | 
			
		||||
                node.local = moduleFile.local||false;
 | 
			
		||||
                nodeList[moduleFile.package.name].nodes[node.name] = node;
 | 
			
		||||
            });
 | 
			
		||||
            nodeFiles = nodeFiles.concat(nodeModuleFiles.files);
 | 
			
		||||
                })
 | 
			
		||||
                if (missingDeps.length > 0) {
 | 
			
		||||
                    log.error(`Module: ${mod.package.name} missing dependencies:`);
 | 
			
		||||
                    missingDeps.forEach(m => { log.error(` - ${m}`)});
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
        nodeList = convertModuleFileListToObject(moduleFiles);
 | 
			
		||||
    } else {
 | 
			
		||||
        // console.log("node path scan disabled");
 | 
			
		||||
    }
 | 
			
		||||
    nodeList["node-red"] = coreNodeEntry;
 | 
			
		||||
    return nodeList;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getModuleFiles(module) {
 | 
			
		||||
    var nodeList = {};
 | 
			
		||||
 | 
			
		||||
    // Update the package list
 | 
			
		||||
    var moduleFiles = scanTreeForNodesModules(module);
 | 
			
		||||
    if (moduleFiles.length === 0) {
 | 
			
		||||
        var err = new Error(log._("nodes.registry.localfilesystem.module-not-found", {module:module}));
 | 
			
		||||
        err.code = 'MODULE_NOT_FOUND';
 | 
			
		||||
        throw err;
 | 
			
		||||
    }
 | 
			
		||||
    // Unlike when doing the initial palette load, this call cannot verify the
 | 
			
		||||
    // dependencies of the new module as it doesn't have visiblity of what
 | 
			
		||||
    // is in the registry. That will have to be done be the caller in loader.js
 | 
			
		||||
    return convertModuleFileListToObject(moduleFiles);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function convertModuleFileListToObject(moduleFiles) {
 | 
			
		||||
    const nodeList = {};
 | 
			
		||||
    moduleFiles.forEach(function(moduleFile) {
 | 
			
		||||
 | 
			
		||||
        var nodeModuleFiles = getModuleNodeFiles(moduleFile);
 | 
			
		||||
        nodeList[moduleFile.package.name] = {
 | 
			
		||||
            name: moduleFile.package.name,
 | 
			
		||||
            version: moduleFile.package.version,
 | 
			
		||||
            path: moduleFile.dir,
 | 
			
		||||
            local: moduleFile.local||false,
 | 
			
		||||
            user: moduleFile.user||false,
 | 
			
		||||
            nodes: {},
 | 
			
		||||
            icons: nodeModuleFiles.icons,
 | 
			
		||||
            examples: nodeModuleFiles.examples
 | 
			
		||||
| 
						 | 
				
			
			@ -381,7 +402,14 @@ function getModuleFiles(module) {
 | 
			
		|||
        if (moduleFile.package['node-red'].version) {
 | 
			
		||||
            nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version;
 | 
			
		||||
        }
 | 
			
		||||
        if (moduleFile.package['node-red'].dependencies) {
 | 
			
		||||
            nodeList[moduleFile.package.name].dependencies = moduleFile.package['node-red'].dependencies;
 | 
			
		||||
        }
 | 
			
		||||
        if (moduleFile.usedBy) {
 | 
			
		||||
            nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy;
 | 
			
		||||
        }
 | 
			
		||||
        nodeModuleFiles.files.forEach(function(node) {
 | 
			
		||||
            node.local = moduleFile.local||false;
 | 
			
		||||
            nodeList[moduleFile.package.name].nodes[node.name] = node;
 | 
			
		||||
            nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false;
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			@ -412,6 +440,23 @@ function scanIconDir(dir) {
 | 
			
		|||
    })
 | 
			
		||||
    return iconList;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the list of modules installed in this runtime as reported by package.json
 | 
			
		||||
 * Note: these may include non-Node-RED modules
 | 
			
		||||
 */
 | 
			
		||||
function getPackageList() {
 | 
			
		||||
    var list = {};
 | 
			
		||||
    if (settings.userDir) {
 | 
			
		||||
        try {
 | 
			
		||||
            var userPackage = path.join(settings.userDir,"package.json");
 | 
			
		||||
            var pkg = JSON.parse(fs.readFileSync(userPackage,"utf-8"));
 | 
			
		||||
            return pkg.dependencies;
 | 
			
		||||
        } catch(err) {
 | 
			
		||||
            log.error(err);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return list;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    init: init,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ var fs = require("fs");
 | 
			
		|||
 | 
			
		||||
var library = require("./library");
 | 
			
		||||
const {events} = require("@node-red/util")
 | 
			
		||||
var subflows = require("./subflow");
 | 
			
		||||
var settings;
 | 
			
		||||
var loader;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +28,8 @@ var nodeConfigCache = {};
 | 
			
		|||
var moduleConfigs = {};
 | 
			
		||||
var nodeList = [];
 | 
			
		||||
var nodeConstructors = {};
 | 
			
		||||
var subflowModules = {};
 | 
			
		||||
 | 
			
		||||
var nodeTypeToId = {};
 | 
			
		||||
var moduleNodes = {};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +39,7 @@ function init(_settings,_loader) {
 | 
			
		|||
    moduleNodes = {};
 | 
			
		||||
    nodeTypeToId = {};
 | 
			
		||||
    nodeConstructors = {};
 | 
			
		||||
    subflowModules = {};
 | 
			
		||||
    nodeList = [];
 | 
			
		||||
    nodeConfigCache = {};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -54,7 +58,8 @@ function filterNodeInfo(n) {
 | 
			
		|||
        name: n.name,
 | 
			
		||||
        types: n.types,
 | 
			
		||||
        enabled: n.enabled,
 | 
			
		||||
        local: n.local||false
 | 
			
		||||
        local: n.local||false,
 | 
			
		||||
        user: n.user || false
 | 
			
		||||
    };
 | 
			
		||||
    if (n.hasOwnProperty("module")) {
 | 
			
		||||
        r.module = n.module;
 | 
			
		||||
| 
						 | 
				
			
			@ -90,6 +95,7 @@ function saveNodeList() {
 | 
			
		|||
                        name: module,
 | 
			
		||||
                        version: moduleConfigs[module].version,
 | 
			
		||||
                        local: moduleConfigs[module].local||false,
 | 
			
		||||
                        user: moduleConfigs[module].user||false,
 | 
			
		||||
                        nodes: {}
 | 
			
		||||
                    };
 | 
			
		||||
                    if (moduleConfigs[module].hasOwnProperty('pending_version')) {
 | 
			
		||||
| 
						 | 
				
			
			@ -175,6 +181,7 @@ function loadNodeConfigs() {
 | 
			
		|||
function addModule(module) {
 | 
			
		||||
    moduleNodes[module.name] = [];
 | 
			
		||||
    moduleConfigs[module.name] = module;
 | 
			
		||||
    // console.log("registry.js.addModule",module.name,"user?",module.user,"usedBy",module.usedBy,"dependencies",module.dependencies)
 | 
			
		||||
    for (var setName in module.nodes) {
 | 
			
		||||
        if (module.nodes.hasOwnProperty(setName)) {
 | 
			
		||||
            var set = module.nodes[setName];
 | 
			
		||||
| 
						 | 
				
			
			@ -225,6 +232,7 @@ function removeNode(id) {
 | 
			
		|||
    config.types.forEach(function(t) {
 | 
			
		||||
        var typeId = nodeTypeToId[t];
 | 
			
		||||
        if (typeId === id) {
 | 
			
		||||
            delete subflowModules[t];
 | 
			
		||||
            delete nodeConstructors[t];
 | 
			
		||||
            delete nodeTypeToId[t];
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -235,21 +243,47 @@ function removeNode(id) {
 | 
			
		|||
    return filterNodeInfo(config);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeModule(module) {
 | 
			
		||||
function removeModule(name,skipSave) {
 | 
			
		||||
    if (!settings.available()) {
 | 
			
		||||
        throw new Error("Settings unavailable");
 | 
			
		||||
    }
 | 
			
		||||
    var nodes = moduleNodes[module];
 | 
			
		||||
    if (!nodes) {
 | 
			
		||||
        throw new Error("Unrecognised module: "+module);
 | 
			
		||||
    }
 | 
			
		||||
    var infoList = [];
 | 
			
		||||
    for (var i=0;i<nodes.length;i++) {
 | 
			
		||||
        infoList.push(removeNode(module+"/"+nodes[i]));
 | 
			
		||||
    var module = moduleConfigs[name];
 | 
			
		||||
    var nodes = moduleNodes[name];
 | 
			
		||||
    if (!nodes) {
 | 
			
		||||
        throw new Error("Unrecognised module: "+name);
 | 
			
		||||
    }
 | 
			
		||||
    delete moduleNodes[module];
 | 
			
		||||
    delete moduleConfigs[module];
 | 
			
		||||
    if (module.usedBy && module.usedBy > 0) {
 | 
			
		||||
        // We are removing a module that is used by other modules... so whilst
 | 
			
		||||
        // this module should be removed from the editor palette, it needs to
 | 
			
		||||
        // stay in the runtime... for now.
 | 
			
		||||
        module.user = false;
 | 
			
		||||
        for (var i=0;i<nodes.length;i++) {
 | 
			
		||||
            infoList.push(filterNodeInfo(nodes[i]));
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        if (module.dependencies) {
 | 
			
		||||
            module.dependencies.forEach(function(dep) {
 | 
			
		||||
                // Check each dependency of this module to see if it is a non-user-installed
 | 
			
		||||
                // module that we can expect to disappear once npm uninstall is run
 | 
			
		||||
                if (!moduleConfigs[dep].user) {
 | 
			
		||||
                    moduleConfigs[dep].usedBy = moduleConfigs[dep].usedBy.filter(m => m !== name);
 | 
			
		||||
                    if (moduleConfigs[dep].usedBy.length === 0) {
 | 
			
		||||
                        // Remove the dependency
 | 
			
		||||
                        removeModule(dep,true);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        for (var i=0;i<nodes.length;i++) {
 | 
			
		||||
            infoList.push(removeNode(name+"/"+nodes[i]));
 | 
			
		||||
        }
 | 
			
		||||
        delete moduleNodes[name];
 | 
			
		||||
        delete moduleConfigs[name];
 | 
			
		||||
    }
 | 
			
		||||
    if (!skipSave) {
 | 
			
		||||
        saveNodeList();
 | 
			
		||||
    }
 | 
			
		||||
    return infoList;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -302,6 +336,9 @@ function getNodeList(filter) {
 | 
			
		|||
    for (var module in moduleConfigs) {
 | 
			
		||||
        /* istanbul ignore else */
 | 
			
		||||
        if (moduleConfigs.hasOwnProperty(module)) {
 | 
			
		||||
            if (!moduleConfigs[module].user && (moduleConfigs[module].usedBy && moduleConfigs[module].usedBy.length > 0)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            var nodes = moduleConfigs[module].nodes;
 | 
			
		||||
            for (var node in nodes) {
 | 
			
		||||
                /* istanbul ignore else */
 | 
			
		||||
| 
						 | 
				
			
			@ -341,9 +378,13 @@ function getModuleInfo(module) {
 | 
			
		|||
            name: module,
 | 
			
		||||
            version: moduleConfigs[module].version,
 | 
			
		||||
            local: moduleConfigs[module].local,
 | 
			
		||||
            user: moduleConfigs[module].user,
 | 
			
		||||
            path: moduleConfigs[module].path,
 | 
			
		||||
            nodes: []
 | 
			
		||||
        };
 | 
			
		||||
        if (moduleConfigs[module].dependencies) {
 | 
			
		||||
            m.dependencies = moduleConfigs[module].dependencies;
 | 
			
		||||
        }
 | 
			
		||||
        if (moduleConfigs[module] && moduleConfigs[module].pending_version) {
 | 
			
		||||
            m.pending_version = moduleConfigs[module].pending_version;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -393,13 +434,40 @@ function registerNodeConstructor(nodeSet,type,constructor) {
 | 
			
		|||
    events.emit("type-registered",type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function registerSubflow(nodeSet, subflow) {
 | 
			
		||||
    var nodeSetInfo = getFullNodeInfo(nodeSet);
 | 
			
		||||
 | 
			
		||||
    const result = subflows.register(nodeSet,subflow);
 | 
			
		||||
 | 
			
		||||
    if (subflowModules.hasOwnProperty(result.type)) {
 | 
			
		||||
        throw new Error(result.type+" already registered");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nodeSetInfo) {
 | 
			
		||||
        if (nodeSetInfo.types.indexOf(result.type) === -1) {
 | 
			
		||||
            nodeSetInfo.types.push(result.type);
 | 
			
		||||
            nodeTypeToId[result.type] = nodeSetInfo.id;
 | 
			
		||||
        }
 | 
			
		||||
        nodeSetInfo.config = result.config;
 | 
			
		||||
    }
 | 
			
		||||
    subflowModules[result.type] = result;
 | 
			
		||||
    events.emit("type-registered",result.type);
 | 
			
		||||
    return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAllNodeConfigs(lang) {
 | 
			
		||||
    if (!nodeConfigCache[lang]) {
 | 
			
		||||
        var result = "";
 | 
			
		||||
        var script = "";
 | 
			
		||||
        for (var i=0;i<nodeList.length;i++) {
 | 
			
		||||
            var id = nodeList[i];
 | 
			
		||||
            var config = moduleConfigs[getModule(id)].nodes[getNode(id)];
 | 
			
		||||
 | 
			
		||||
            var module = moduleConfigs[getModule(id)]
 | 
			
		||||
            if (!module.user && (module.usedBy && module.usedBy.length > 0)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var config = module.nodes[getNode(id)];
 | 
			
		||||
            if (config.enabled && !config.err) {
 | 
			
		||||
                result += "\n<!-- --- [red-module:"+id+"] --- -->\n";
 | 
			
		||||
                result += config.config;
 | 
			
		||||
| 
						 | 
				
			
			@ -447,7 +515,7 @@ function getNodeConstructor(type) {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (!config || (config.enabled && !config.err)) {
 | 
			
		||||
        return nodeConstructors[type];
 | 
			
		||||
        return nodeConstructors[type] || subflowModules[type];
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -457,6 +525,7 @@ function clear() {
 | 
			
		|||
    moduleConfigs = {};
 | 
			
		||||
    nodeList = [];
 | 
			
		||||
    nodeConstructors = {};
 | 
			
		||||
    subflowModules = {};
 | 
			
		||||
    nodeTypeToId = {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -560,6 +629,17 @@ function setModulePendingUpdated(module,version) {
 | 
			
		|||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setUserInstalled(module,userInstalled) {
 | 
			
		||||
    moduleConfigs[module].user = userInstalled;
 | 
			
		||||
    return saveNodeList().then(function() {
 | 
			
		||||
        return getModuleInfo(module);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
function addModuleDependency(module,usedBy) {
 | 
			
		||||
    moduleConfigs[module].usedBy = moduleConfigs[module].usedBy || [];
 | 
			
		||||
    moduleConfigs[module].usedBy.push(usedBy);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var icon_paths = { };
 | 
			
		||||
var iconCache = {};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -613,6 +693,7 @@ var registry = module.exports = {
 | 
			
		|||
    registerNodeConstructor: registerNodeConstructor,
 | 
			
		||||
    getNodeConstructor: getNodeConstructor,
 | 
			
		||||
 | 
			
		||||
    registerSubflow: registerSubflow,
 | 
			
		||||
 | 
			
		||||
    addModule: addModule,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -620,6 +701,9 @@ var registry = module.exports = {
 | 
			
		|||
    disableNodeSet: disableNodeSet,
 | 
			
		||||
 | 
			
		||||
    setModulePendingUpdated: setModulePendingUpdated,
 | 
			
		||||
    setUserInstalled: setUserInstalled,
 | 
			
		||||
    addModuleDependency:addModuleDependency,
 | 
			
		||||
 | 
			
		||||
    removeModule: removeModule,
 | 
			
		||||
 | 
			
		||||
    getNodeInfo: getNodeInfo,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,127 @@
 | 
			
		|||
function getSubflowType(subflow) {
 | 
			
		||||
    if (subflow.meta && subflow.meta.type) {
 | 
			
		||||
        return subflow.meta.type
 | 
			
		||||
    }
 | 
			
		||||
    return "sf:"+subflow.id
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateSubflowConfig(subflow) {
 | 
			
		||||
 | 
			
		||||
    const subflowType = getSubflowType(subflow)
 | 
			
		||||
    const label = subflow.name || subflowType;
 | 
			
		||||
    const category = subflow.category || "function";
 | 
			
		||||
    const color = subflow.color || "#C0DEED";
 | 
			
		||||
    const inputCount = subflow.in?subflow.in.length:0;
 | 
			
		||||
    const outputCount = subflow.out?subflow.out.length:0;
 | 
			
		||||
    const icon = subflow.icon || "arrow-in.svg";
 | 
			
		||||
 | 
			
		||||
    const defaults = {
 | 
			
		||||
        name: {value: ""}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const credentials = {}
 | 
			
		||||
 | 
			
		||||
    if (subflow.env) {
 | 
			
		||||
        subflow.env.forEach(prop => {
 | 
			
		||||
            var defaultValue;
 | 
			
		||||
 | 
			
		||||
            switch(prop.type) {
 | 
			
		||||
                case "cred": defaultValue = ""; break;
 | 
			
		||||
                case "str": defaultValue = prop.value||""; break;
 | 
			
		||||
                case "bool": defaultValue = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
 | 
			
		||||
                case "num": defaultValue = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
 | 
			
		||||
                default:
 | 
			
		||||
                    defaultValue = {
 | 
			
		||||
                        type: prop.type,
 | 
			
		||||
                        value: prop.value||""
 | 
			
		||||
                    }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            defaults[prop.name] = {
 | 
			
		||||
                value: defaultValue,
 | 
			
		||||
                ui: prop.ui
 | 
			
		||||
            }
 | 
			
		||||
            if (prop.type === 'cred') {
 | 
			
		||||
                defaults[prop.name].ui.type = "cred";
 | 
			
		||||
                credentials[prop.name] = {type:"password"}
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
    const defaultString = JSON.stringify(defaults);
 | 
			
		||||
    const credentialsString = JSON.stringify(credentials);
 | 
			
		||||
 | 
			
		||||
    let nodeHelp = "";
 | 
			
		||||
    if (subflow.info) {
 | 
			
		||||
        nodeHelp = `<script type="text/markdown" data-help-name="${subflowType}">${subflow.info}</script>`
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return `<script type="text/javascript">
 | 
			
		||||
    RED.nodes.registerType("${subflowType}",{
 | 
			
		||||
        subflowModule: true,
 | 
			
		||||
        category: "${category}",
 | 
			
		||||
        color: "${color}",
 | 
			
		||||
        defaults: ${defaultString},
 | 
			
		||||
        credentials: ${credentialsString},
 | 
			
		||||
        inputs:${inputCount},
 | 
			
		||||
        outputs:${outputCount},
 | 
			
		||||
        icon: "${icon}",
 | 
			
		||||
        paletteLabel: "${label}",
 | 
			
		||||
        label: function() {
 | 
			
		||||
            return this.name||"${label}";
 | 
			
		||||
        },
 | 
			
		||||
        labelStyle: function() {
 | 
			
		||||
            return this.name?"node_label_italic":"";
 | 
			
		||||
        },
 | 
			
		||||
        oneditprepare: function() {
 | 
			
		||||
            RED.subflow.buildEditForm('subflow', this);
 | 
			
		||||
        },
 | 
			
		||||
        oneditsave: function() {
 | 
			
		||||
            var props = RED.subflow.exportSubflowInstanceEnv(this);
 | 
			
		||||
            var i=0,l=props.length;
 | 
			
		||||
            for (;i<l;i++) {
 | 
			
		||||
                var prop = props[i];
 | 
			
		||||
                if (this._def.defaults[prop.name].ui && this._def.defaults[prop.name].ui.type === "cred") {
 | 
			
		||||
                    this[prop.name] = "";
 | 
			
		||||
                    this.credentials[prop.name] = prop.value || "";
 | 
			
		||||
                    this.credentials['has_' + prop.name] = (this.credentials[prop.name] !== "");
 | 
			
		||||
                } else {
 | 
			
		||||
                    switch(prop.type) {
 | 
			
		||||
                        case "str": this[prop.name] = prop.value||""; break;
 | 
			
		||||
                        case "bool": this[prop.name] = (typeof prop.value === 'boolean')?prop.value:prop.value === "true" ; break;
 | 
			
		||||
                        case "num": this[prop.name] = (typeof prop.value === 'number')?prop.value:Number(prop.value); break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            this[prop.name] = {
 | 
			
		||||
                                type: prop.type,
 | 
			
		||||
                                value: prop.value||""
 | 
			
		||||
                            }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
</script>
 | 
			
		||||
<script type="text/x-red" data-template-name="${subflowType}">
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
        <label for="node-input-name" data-i18n="[append]editor:common.label.name"><i class="fa fa-tag"></i> </label>
 | 
			
		||||
        <input type="text" id="node-input-name" data-i18n="[placeholder]editor:common.label.name">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="subflow-input-ui"></div>
 | 
			
		||||
</script>
 | 
			
		||||
${nodeHelp}
 | 
			
		||||
`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function register(id,subflow) {
 | 
			
		||||
    return {
 | 
			
		||||
        subflow: subflow,
 | 
			
		||||
        type: getSubflowType(subflow),
 | 
			
		||||
        config: generateSubflowConfig(subflow)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    register: register
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -83,6 +83,9 @@ function createNodeApi(node) {
 | 
			
		|||
    red.nodes.registerType = function(type,constructor,opts) {
 | 
			
		||||
        runtime.nodes.registerType(node.id,type,constructor,opts);
 | 
			
		||||
    }
 | 
			
		||||
    red.nodes.registerSubflow = function(subflowDef) {
 | 
			
		||||
        runtime.nodes.registerSubflow(node.id,subflowDef)
 | 
			
		||||
    }
 | 
			
		||||
    copyObjectProperties(log,red.log,null,["init"]);
 | 
			
		||||
    copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]);
 | 
			
		||||
    if (runtime.adminApi) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -194,7 +194,7 @@ var api = module.exports = {
 | 
			
		|||
        }
 | 
			
		||||
        if (opts.module) {
 | 
			
		||||
            var existingModule = runtime.nodes.getModuleInfo(opts.module);
 | 
			
		||||
            if (existingModule) {
 | 
			
		||||
            if (existingModule && existingModule.user) {
 | 
			
		||||
                if (!opts.version || existingModule.version === opts.version) {
 | 
			
		||||
                    runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}, opts.req);
 | 
			
		||||
                    var err = new Error("Module already loaded");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,7 +93,7 @@ class Flow {
 | 
			
		|||
     * @param  {[type]} msg [description]
 | 
			
		||||
     * @return {[type]}     [description]
 | 
			
		||||
     */
 | 
			
		||||
    log(msg) {
 | 
			
		||||
    info(msg) {
 | 
			
		||||
        Log.log({
 | 
			
		||||
            id: this.id||"global",
 | 
			
		||||
            level: Log.INFO,
 | 
			
		||||
| 
						 | 
				
			
			@ -116,6 +116,17 @@ class Flow {
 | 
			
		|||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * [log description]
 | 
			
		||||
     * @param  {[type]} msg [description]
 | 
			
		||||
     * @return {[type]}     [description]
 | 
			
		||||
     */
 | 
			
		||||
    log(msg) {
 | 
			
		||||
        if (!msg.path) {
 | 
			
		||||
            msg.path = this.path;
 | 
			
		||||
        }
 | 
			
		||||
        this.parent.log(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Start this flow.
 | 
			
		||||
| 
						 | 
				
			
			@ -303,22 +314,14 @@ class Flow {
 | 
			
		|||
            if (node) {
 | 
			
		||||
                delete this.activeNodes[stopList[i]];
 | 
			
		||||
                if (this.subflowInstanceNodes[stopList[i]]) {
 | 
			
		||||
                    try {
 | 
			
		||||
                        (function(subflow) {
 | 
			
		||||
                            promises.push(stopNode(node,false).then(() => subflow.stop()));
 | 
			
		||||
                        })(this.subflowInstanceNodes[stopList[i]]);
 | 
			
		||||
                    } catch(err) {
 | 
			
		||||
                        node.error(err);
 | 
			
		||||
                    }
 | 
			
		||||
                    delete this.subflowInstanceNodes[stopList[i]];
 | 
			
		||||
                } else {
 | 
			
		||||
                }
 | 
			
		||||
                try {
 | 
			
		||||
                    var removed = removedMap[stopList[i]];
 | 
			
		||||
                    promises.push(stopNode(node,removed).catch(()=>{}));
 | 
			
		||||
                } catch(err) {
 | 
			
		||||
                    node.error(err);
 | 
			
		||||
                }
 | 
			
		||||
                }
 | 
			
		||||
                if (removedMap[stopList[i]]) {
 | 
			
		||||
                    events.emit("node-status",{
 | 
			
		||||
                        id: node.id
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -208,7 +208,12 @@ class Subflow extends Flow {
 | 
			
		|||
 | 
			
		||||
        this.node = new Node(subflowInstanceConfig);
 | 
			
		||||
        this.node.on("input", function(msg) { this.send(msg);});
 | 
			
		||||
        this.node.on("close", function() { this.status({}); })
 | 
			
		||||
        // Called when the subflow instance node is being stopped
 | 
			
		||||
        this.node.on("close", function(done) {
 | 
			
		||||
            this.status({});
 | 
			
		||||
            // Stop the complete subflow
 | 
			
		||||
            self.stop().finally(done)
 | 
			
		||||
        })
 | 
			
		||||
        this.node.status = status => this.parent.handleStatus(this.node,status);
 | 
			
		||||
        // Create a context instance
 | 
			
		||||
        // console.log("Node.context",this.type,"id:",this._alias||this.id,"z:",this.z)
 | 
			
		||||
| 
						 | 
				
			
			@ -499,11 +504,49 @@ function remapSubflowNodes(nodes,nodeMap) {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SubflowModule extends Subflow {
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a Subflow Module object.
 | 
			
		||||
     * This is a node that has been published as a subflow.
 | 
			
		||||
     * @param {[type]} parent          [description]
 | 
			
		||||
     * @param {[type]} globalFlow      [description]
 | 
			
		||||
     * @param {[type]} subflowDef      [description]
 | 
			
		||||
     * @param {[type]} subflowInstance [description]
 | 
			
		||||
     */
 | 
			
		||||
    constructor(type, parent,globalFlow,subflowDef,subflowInstance) {
 | 
			
		||||
        super(parent,globalFlow,subflowDef,subflowInstance);
 | 
			
		||||
        this.TYPE = `module:${type}`;
 | 
			
		||||
        this.subflowType = type;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * [log description]
 | 
			
		||||
     * @param  {[type]} msg [description]
 | 
			
		||||
     * @return {[type]}     [description]
 | 
			
		||||
     */
 | 
			
		||||
    log(msg) {
 | 
			
		||||
        if (msg.id) {
 | 
			
		||||
            msg.id = this.id
 | 
			
		||||
        }
 | 
			
		||||
        if (msg.type) {
 | 
			
		||||
            msg.type = this.subflowType
 | 
			
		||||
        }
 | 
			
		||||
        super.log(msg);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createSubflow(parent,globalFlow,subflowDef,subflowInstance) {
 | 
			
		||||
    return new Subflow(parent,globalFlow,subflowDef,subflowInstance)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function createModuleInstance(type, parent,globalFlow,subflowDef,subflowInstance) {
 | 
			
		||||
    return new SubflowModule(type, parent,globalFlow,subflowDef,subflowInstance);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    init: function(runtime) {},
 | 
			
		||||
    create: createSubflow
 | 
			
		||||
    create: createSubflow,
 | 
			
		||||
    createModuleInstance: createModuleInstance
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -698,7 +698,8 @@ const flowAPI = {
 | 
			
		|||
    getNode: getNode,
 | 
			
		||||
    handleError: () => false,
 | 
			
		||||
    handleStatus: () => false,
 | 
			
		||||
    getSetting: k => flowUtil.getEnvVar(k)
 | 
			
		||||
    getSetting: k => flowUtil.getEnvVar(k),
 | 
			
		||||
    log: m => log.log(m)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,6 +19,7 @@ var Log = require("@node-red/util").log;
 | 
			
		|||
var subflowInstanceRE = /^subflow:(.+)$/;
 | 
			
		||||
var typeRegistry = require("@node-red/registry");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
var envVarExcludes = {};
 | 
			
		||||
 | 
			
		||||
function diffNodes(oldNode,newNode) {
 | 
			
		||||
| 
						 | 
				
			
			@ -66,20 +67,78 @@ function mapEnvVarProperties(obj,prop,flow) {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    init: function(runtime) {
 | 
			
		||||
        envVarExcludes = {};
 | 
			
		||||
        if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) {
 | 
			
		||||
            runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    getEnvVar: function(k) {
 | 
			
		||||
        return !envVarExcludes[k]?process.env[k]:undefined
 | 
			
		||||
    },
 | 
			
		||||
    diffNodes: diffNodes,
 | 
			
		||||
    mapEnvVarProperties: mapEnvVarProperties,
 | 
			
		||||
 | 
			
		||||
    parseConfig: function(config) {
 | 
			
		||||
function createNode(flow,config) {
 | 
			
		||||
    var newNode = null;
 | 
			
		||||
    var type = config.type;
 | 
			
		||||
    try {
 | 
			
		||||
        var nodeTypeConstructor = typeRegistry.get(type);
 | 
			
		||||
        if (typeof nodeTypeConstructor === "function") {
 | 
			
		||||
            var conf = clone(config);
 | 
			
		||||
            delete conf.credentials;
 | 
			
		||||
            for (var p in conf) {
 | 
			
		||||
                if (conf.hasOwnProperty(p)) {
 | 
			
		||||
                    mapEnvVarProperties(conf,p,flow);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            try {
 | 
			
		||||
                Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
 | 
			
		||||
                newNode = new nodeTypeConstructor(conf);
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
                Log.log({
 | 
			
		||||
                    level: Log.ERROR,
 | 
			
		||||
                    id:conf.id,
 | 
			
		||||
                    type: type,
 | 
			
		||||
                    msg: err
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        } else if (nodeTypeConstructor) {
 | 
			
		||||
            // console.log(nodeTypeConstructor)
 | 
			
		||||
            var subflowConfig = parseConfig([nodeTypeConstructor.subflow].concat(nodeTypeConstructor.subflow.flow));
 | 
			
		||||
            var instanceConfig = clone(config);
 | 
			
		||||
            instanceConfig.env = clone(nodeTypeConstructor.subflow.env);
 | 
			
		||||
 | 
			
		||||
            instanceConfig.env = nodeTypeConstructor.subflow.env.map(nodeProp => {
 | 
			
		||||
                var nodePropType;
 | 
			
		||||
                var nodePropValue = config[nodeProp.name];
 | 
			
		||||
                if (nodeProp.type === "cred") {
 | 
			
		||||
                    nodePropType = "cred";
 | 
			
		||||
                } else {
 | 
			
		||||
                    switch(typeof config[nodeProp.name]) {
 | 
			
		||||
                        case "string": nodePropType = "str"; break;
 | 
			
		||||
                        case "number": nodePropType = "num"; break;
 | 
			
		||||
                        case "boolean": nodePropType = "bool"; nodePropValue = nodeProp?"true":"false"; break;
 | 
			
		||||
                        default:
 | 
			
		||||
                            nodePropType = config[nodeProp.name].type;
 | 
			
		||||
                            nodePropValue = config[nodeProp.name].value;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                return {
 | 
			
		||||
                    name: nodeProp.name,
 | 
			
		||||
                    type: nodePropType,
 | 
			
		||||
                    value: nodePropValue
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
            var subflow = require("./Subflow").createModuleInstance(
 | 
			
		||||
                nodeTypeConstructor.type,
 | 
			
		||||
                flow,
 | 
			
		||||
                flow.global,
 | 
			
		||||
                subflowConfig.subflows[nodeTypeConstructor.subflow.id],
 | 
			
		||||
                instanceConfig
 | 
			
		||||
            );
 | 
			
		||||
            subflow.start();
 | 
			
		||||
            return subflow.node;
 | 
			
		||||
 | 
			
		||||
            Log.error(Log._("nodes.flow.unknown-type", {type:type}));
 | 
			
		||||
        }
 | 
			
		||||
    } catch(err) {
 | 
			
		||||
        Log.error(err);
 | 
			
		||||
    }
 | 
			
		||||
    return newNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseConfig(config) {
 | 
			
		||||
   var flow = {};
 | 
			
		||||
   flow.allNodes = {};
 | 
			
		||||
   flow.subflows = {};
 | 
			
		||||
| 
						 | 
				
			
			@ -195,7 +254,22 @@ module.exports = {
 | 
			
		|||
       }
 | 
			
		||||
   });
 | 
			
		||||
   return flow;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    init: function(runtime) {
 | 
			
		||||
        envVarExcludes = {};
 | 
			
		||||
        if (runtime.settings.hasOwnProperty('envVarExcludes') && Array.isArray(runtime.settings.envVarExcludes)) {
 | 
			
		||||
            runtime.settings.envVarExcludes.forEach(v => envVarExcludes[v] = true);
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    getEnvVar: function(k) {
 | 
			
		||||
        return !envVarExcludes[k]?process.env[k]:undefined
 | 
			
		||||
    },
 | 
			
		||||
    diffNodes: diffNodes,
 | 
			
		||||
    mapEnvVarProperties: mapEnvVarProperties,
 | 
			
		||||
 | 
			
		||||
    parseConfig: parseConfig,
 | 
			
		||||
 | 
			
		||||
    diffConfigs: function(oldConfig, newConfig) {
 | 
			
		||||
        var id;
 | 
			
		||||
| 
						 | 
				
			
			@ -475,36 +549,5 @@ module.exports = {
 | 
			
		|||
     * @param  {object} config The node configuration object
 | 
			
		||||
     * @return {Node}          The instance of the node
 | 
			
		||||
     */
 | 
			
		||||
    createNode: function(flow,config) {
 | 
			
		||||
        var newNode = null;
 | 
			
		||||
        var type = config.type;
 | 
			
		||||
        try {
 | 
			
		||||
            var nodeTypeConstructor = typeRegistry.get(type);
 | 
			
		||||
            if (nodeTypeConstructor) {
 | 
			
		||||
                var conf = clone(config);
 | 
			
		||||
                delete conf.credentials;
 | 
			
		||||
                for (var p in conf) {
 | 
			
		||||
                    if (conf.hasOwnProperty(p)) {
 | 
			
		||||
                        mapEnvVarProperties(conf,p,flow);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                try {
 | 
			
		||||
                    Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true })
 | 
			
		||||
                    newNode = new nodeTypeConstructor(conf);
 | 
			
		||||
                } catch (err) {
 | 
			
		||||
                    Log.log({
 | 
			
		||||
                        level: Log.ERROR,
 | 
			
		||||
                        id:conf.id,
 | 
			
		||||
                        type: type,
 | 
			
		||||
                        msg: err
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                Log.error(Log._("nodes.flow.unknown-type", {type:type}));
 | 
			
		||||
            }
 | 
			
		||||
        } catch(err) {
 | 
			
		||||
            Log.error(err);
 | 
			
		||||
        }
 | 
			
		||||
        return newNode;
 | 
			
		||||
    }
 | 
			
		||||
    createNode: createNode
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -486,16 +486,14 @@ function log_helper(self, level, msg) {
 | 
			
		|||
    if (self._alias) {
 | 
			
		||||
        o._alias = self._alias;
 | 
			
		||||
    }
 | 
			
		||||
    if (self._flow) {
 | 
			
		||||
        o.path = self._flow.path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (self.z) {
 | 
			
		||||
        o.z = self.z;
 | 
			
		||||
    }
 | 
			
		||||
    if (self.name) {
 | 
			
		||||
        o.name = self.name;
 | 
			
		||||
    }
 | 
			
		||||
    Log.log(o);
 | 
			
		||||
    self._flow.log(o);
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * Log an INFO level message
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -112,6 +112,25 @@ function createNode(node,def) {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function registerSubflow(nodeSet, subflow) {
 | 
			
		||||
    // TODO: extract credentials definition from subflow properties
 | 
			
		||||
    var registeredType = registry.registerSubflow(nodeSet,subflow);
 | 
			
		||||
 | 
			
		||||
    if (subflow.env) {
 | 
			
		||||
        var creds = {};
 | 
			
		||||
        var hasCreds = false;
 | 
			
		||||
        subflow.env.forEach(e => {
 | 
			
		||||
            if (e.type === "cred") {
 | 
			
		||||
                creds[e.name] = {type: "password"};
 | 
			
		||||
                hasCreds = true;
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        if (hasCreds) {
 | 
			
		||||
            credentials.register(registeredType.type,creds);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function init(runtime) {
 | 
			
		||||
    settings = runtime.settings;
 | 
			
		||||
    log = runtime.log;
 | 
			
		||||
| 
						 | 
				
			
			@ -162,11 +181,12 @@ function installModule(module,version,url) {
 | 
			
		|||
 | 
			
		||||
function uninstallModule(module) {
 | 
			
		||||
    var info = registry.getModuleInfo(module);
 | 
			
		||||
    if (!info) {
 | 
			
		||||
    if (!info || !info.user) {
 | 
			
		||||
        throw new Error(log._("nodes.index.unrecognised-module", {module:module}));
 | 
			
		||||
    } else {
 | 
			
		||||
        for (var i=0;i<info.nodes.length;i++) {
 | 
			
		||||
            flows.checkTypeInUse(module+"/"+info.nodes[i].name);
 | 
			
		||||
        var nodeTypesToCheck = info.nodes.map(n => `${module}/${n.name}`);
 | 
			
		||||
        for (var i=0;i<nodeTypesToCheck.length;i++) {
 | 
			
		||||
            flows.checkTypeInUse(nodeTypesToCheck[i]);
 | 
			
		||||
        }
 | 
			
		||||
        return registry.uninstallModule(module).then(function(list) {
 | 
			
		||||
            events.emit("runtime-event",{id:"node/removed",retain:false,payload:list});
 | 
			
		||||
| 
						 | 
				
			
			@ -196,6 +216,7 @@ module.exports = {
 | 
			
		|||
 | 
			
		||||
    // Node type registry
 | 
			
		||||
    registerType: registerType,
 | 
			
		||||
    registerSubflow: registerSubflow,
 | 
			
		||||
    getType: registry.get,
 | 
			
		||||
 | 
			
		||||
    getNodeInfo: registry.getNodeInfo,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
curl -H "Content-Type: application/json" --request POST  --data '{"url":"/Users/nol/code/node-red/node-red/test/resources/subflow/test-subflow-mod-1.0.0.tgz","module":"test-subflow-mod"}' http://localhost:1880/nodes
 | 
			
		||||
 | 
			
		||||
curl --request DELETE http://localhost:1880/nodes/test-subflow-mod
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,18 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "test-subflow-mod",
 | 
			
		||||
  "version": "1.0.1",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "node-red": {
 | 
			
		||||
      "nodes": {
 | 
			
		||||
          "test-subflow": "subflow.js"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": [
 | 
			
		||||
          "node-red-node-random"
 | 
			
		||||
      ]
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
      "node-red-node-random": "*"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
 | 
			
		||||
module.exports = function(RED) {
 | 
			
		||||
    RED.nodes.registerSubflow(JSON.parse(require('fs').readFileSync(require("path").join(__dirname,"subflow.json"))))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,268 @@
 | 
			
		|||
{
 | 
			
		||||
    "id": "caf258cc.4e2c48",
 | 
			
		||||
    "type": "subflow",
 | 
			
		||||
    "name": "Test Subflow",
 | 
			
		||||
    "info":"This is my exportable module subflow\n\nI hope this shows as help!",
 | 
			
		||||
    "category": "common",
 | 
			
		||||
    "in": [
 | 
			
		||||
        {
 | 
			
		||||
            "x": 120,
 | 
			
		||||
            "y": 100,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                {
 | 
			
		||||
                    "id": "2f1d674f.a02d28"
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "out": [
 | 
			
		||||
        {
 | 
			
		||||
            "x": 560,
 | 
			
		||||
            "y": 100,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                {
 | 
			
		||||
                    "id": "1497236e.07f12d",
 | 
			
		||||
                    "port": 0
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "x": 360,
 | 
			
		||||
            "y": 200,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                {
 | 
			
		||||
                    "id": "f4334f5f.4905c",
 | 
			
		||||
                    "port": 0
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "env": [
 | 
			
		||||
        {
 | 
			
		||||
            "name": "FOO",
 | 
			
		||||
            "type": "cred",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "icon": "font-awesome/fa-thermometer-0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "BAR",
 | 
			
		||||
            "type": "str",
 | 
			
		||||
            "value": "1",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "icon": "font-awesome/fa-thermometer-2",
 | 
			
		||||
                "type": "select",
 | 
			
		||||
                "opts": {
 | 
			
		||||
                    "opts": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "l": {
 | 
			
		||||
                                "en-US": "option 1"
 | 
			
		||||
                            },
 | 
			
		||||
                            "v": "1"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            "l": {
 | 
			
		||||
                                "en-US": "option 2"
 | 
			
		||||
                            },
 | 
			
		||||
                            "v": "2"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            "l": {
 | 
			
		||||
                                "en-US": "option 3"
 | 
			
		||||
                            },
 | 
			
		||||
                            "v": "3"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "onewithaverylongname",
 | 
			
		||||
            "type": "str",
 | 
			
		||||
            "value": ""
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "BARRY",
 | 
			
		||||
            "type": "bool",
 | 
			
		||||
            "value": "true",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "icon": "font-awesome/fa-thermometer-4",
 | 
			
		||||
                "type": "checkbox"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "WILMA",
 | 
			
		||||
            "type": "num",
 | 
			
		||||
            "value": "10",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "icon": "font-awesome/fbomb",
 | 
			
		||||
                "type": "spinner"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "awg",
 | 
			
		||||
            "type": "num",
 | 
			
		||||
            "value": "",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "icon": "font-awesome/fa-address-book-o",
 | 
			
		||||
                "type": "spinner"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "awf",
 | 
			
		||||
            "type": "str",
 | 
			
		||||
            "value": "",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "type": "select",
 | 
			
		||||
                "opts": {
 | 
			
		||||
                    "opts": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "l": {
 | 
			
		||||
                                "en-US": "one"
 | 
			
		||||
                            },
 | 
			
		||||
                            "v": "1"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            "l": {
 | 
			
		||||
                                "en-US": "two"
 | 
			
		||||
                            },
 | 
			
		||||
                            "v": "2"
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            "l": {
 | 
			
		||||
                                "en-US": "three"
 | 
			
		||||
                            },
 | 
			
		||||
                            "v": "3"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "aawf",
 | 
			
		||||
            "type": "bool",
 | 
			
		||||
            "value": "true",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "type": "checkbox"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "awgawgawg",
 | 
			
		||||
            "type": "str",
 | 
			
		||||
            "value": "",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "type": "none"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "name": "seagseg",
 | 
			
		||||
            "type": "str",
 | 
			
		||||
            "value": "",
 | 
			
		||||
            "ui": {
 | 
			
		||||
                "type": "hide"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    "meta": {
 | 
			
		||||
        "type": "fly-a-plane"
 | 
			
		||||
    },
 | 
			
		||||
    "color": "#A6BBCF",
 | 
			
		||||
    "icon": "font-awesome/fa-space-shuttle",
 | 
			
		||||
    "status": {
 | 
			
		||||
        "x": 500,
 | 
			
		||||
        "y": 300,
 | 
			
		||||
        "wires": [
 | 
			
		||||
            {
 | 
			
		||||
                "id": "8252d1cc.54f94",
 | 
			
		||||
                "port": 0
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    "flow": [
 | 
			
		||||
        {
 | 
			
		||||
            "id": "2f1d674f.a02d28",
 | 
			
		||||
            "type": "function",
 | 
			
		||||
            "z": "caf258cc.4e2c48",
 | 
			
		||||
            "name": "",
 | 
			
		||||
            "func": "node.error(\"subflow error \"+msg.payload,msg);\nmsg.payload = {\n    FOO: env.get(\"FOO\"),\n    BAR: env.get(\"BAR\"),\n    WILMA: env.get(\"WILMA\"),\n    BARRY: env.get(\"BARRY\")\n}\nnode.warn(\"warning\");\n\nreturn msg;",
 | 
			
		||||
            "outputs": 1,
 | 
			
		||||
            "noerr": 0,
 | 
			
		||||
            "initialize": "",
 | 
			
		||||
            "finalize": "",
 | 
			
		||||
            "x": 240,
 | 
			
		||||
            "y": 100,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                [
 | 
			
		||||
                    "1497236e.07f12d"
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "id": "f4334f5f.4905c",
 | 
			
		||||
            "type": "catch",
 | 
			
		||||
            "z": "caf258cc.4e2c48",
 | 
			
		||||
            "name": "",
 | 
			
		||||
            "scope": null,
 | 
			
		||||
            "uncaught": false,
 | 
			
		||||
            "x": 220,
 | 
			
		||||
            "y": 200,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                [
 | 
			
		||||
                    "8252d1cc.54f94"
 | 
			
		||||
                ]
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "id": "8252d1cc.54f94",
 | 
			
		||||
            "type": "change",
 | 
			
		||||
            "z": "caf258cc.4e2c48",
 | 
			
		||||
            "name": "",
 | 
			
		||||
            "rules": [
 | 
			
		||||
                {
 | 
			
		||||
                    "t": "set",
 | 
			
		||||
                    "p": "payload",
 | 
			
		||||
                    "pt": "msg",
 | 
			
		||||
                    "to": "error.message",
 | 
			
		||||
                    "tot": "msg"
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "action": "",
 | 
			
		||||
            "property": "",
 | 
			
		||||
            "from": "",
 | 
			
		||||
            "to": "",
 | 
			
		||||
            "reg": false,
 | 
			
		||||
            "x": 350,
 | 
			
		||||
            "y": 300,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                []
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "id": "1497236e.07f12d",
 | 
			
		||||
            "type": "random",
 | 
			
		||||
            "z": "caf258cc.4e2c48",
 | 
			
		||||
            "name": "",
 | 
			
		||||
            "low": "1",
 | 
			
		||||
            "high": "10",
 | 
			
		||||
            "inte": "true",
 | 
			
		||||
            "property": "random",
 | 
			
		||||
            "x": 420,
 | 
			
		||||
            "y": 100,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                []
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            "id": "876fc49e.f15268",
 | 
			
		||||
            "type": "subflow:caf258cc.4e2c48",
 | 
			
		||||
            "z": "d607ce33.4fa5a",
 | 
			
		||||
            "name": "",
 | 
			
		||||
            "x": 200,
 | 
			
		||||
            "y": 760,
 | 
			
		||||
            "wires": [
 | 
			
		||||
                [],
 | 
			
		||||
                []
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -131,6 +131,7 @@ describe('nodes/registry/installer', function() {
 | 
			
		|||
        it("rejects when update requested to existing version", function(done) {
 | 
			
		||||
            sinon.stub(typeRegistry,"getModuleInfo", function() {
 | 
			
		||||
                return {
 | 
			
		||||
                    user: true,
 | 
			
		||||
                    version: "0.1.1"
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			@ -142,6 +143,7 @@ describe('nodes/registry/installer', function() {
 | 
			
		|||
        it("rejects when update requested to existing version and url", function(done) {
 | 
			
		||||
            sinon.stub(typeRegistry,"getModuleInfo", function() {
 | 
			
		||||
                return {
 | 
			
		||||
                    user: true,
 | 
			
		||||
                    version: "0.1.1"
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -414,60 +414,61 @@ describe("red/nodes/registry/loader",function() {
 | 
			
		|||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("load core node files scanned by lfs - missing html file", function(done) {
 | 
			
		||||
            stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
 | 
			
		||||
                var result = {};
 | 
			
		||||
                result["node-red"] = {
 | 
			
		||||
                    "name": "node-red",
 | 
			
		||||
                    "version": "1.2.3",
 | 
			
		||||
                    "nodes": {
 | 
			
		||||
                        "DuffNode": {
 | 
			
		||||
                            "file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
 | 
			
		||||
                            "module": "node-red",
 | 
			
		||||
                            "name": "DuffNode"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
                return result;
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
 | 
			
		||||
            stubs.push(sinon.stub(registry,"addModule", function(){ return }));
 | 
			
		||||
            // This module isn't already loaded
 | 
			
		||||
            stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
 | 
			
		||||
 | 
			
		||||
            stubs.push(sinon.stub(nodes,"registerType"));
 | 
			
		||||
            loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
 | 
			
		||||
            loader.load().then(function(result) {
 | 
			
		||||
 | 
			
		||||
                registry.addModule.called.should.be.true();
 | 
			
		||||
                var module = registry.addModule.lastCall.args[0];
 | 
			
		||||
                module.should.have.property("name","node-red");
 | 
			
		||||
                module.should.have.property("version","1.2.3");
 | 
			
		||||
                module.should.have.property("nodes");
 | 
			
		||||
                module.nodes.should.have.property("DuffNode");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("id","node-red/DuffNode");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("module","node-red");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("name","DuffNode");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("file");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("template");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("enabled",true);
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("loaded",false);
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("types");
 | 
			
		||||
                module.nodes.DuffNode.types.should.have.a.length(0);
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("config","");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("help",{});
 | 
			
		||||
                module.nodes.DuffNode.should.have.property("namespace","node-red");
 | 
			
		||||
                module.nodes.DuffNode.should.have.property('err');
 | 
			
		||||
                module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist");
 | 
			
		||||
 | 
			
		||||
                nodes.registerType.called.should.be.false();
 | 
			
		||||
 | 
			
		||||
                done();
 | 
			
		||||
            }).catch(function(err) {
 | 
			
		||||
                done(err);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        // it("load core node files scanned by lfs - missing html file", function(done) {
 | 
			
		||||
        //     // This is now an okay situation
 | 
			
		||||
        //     stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
 | 
			
		||||
        //         var result = {};
 | 
			
		||||
        //         result["node-red"] = {
 | 
			
		||||
        //             "name": "node-red",
 | 
			
		||||
        //             "version": "1.2.3",
 | 
			
		||||
        //             "nodes": {
 | 
			
		||||
        //                 "DuffNode": {
 | 
			
		||||
        //                     "file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
 | 
			
		||||
        //                     "module": "node-red",
 | 
			
		||||
        //                     "name": "DuffNode"
 | 
			
		||||
        //                 }
 | 
			
		||||
        //             }
 | 
			
		||||
        //         };
 | 
			
		||||
        //         return result;
 | 
			
		||||
        //     }));
 | 
			
		||||
        //
 | 
			
		||||
        //     stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
 | 
			
		||||
        //     stubs.push(sinon.stub(registry,"addModule", function(){ return }));
 | 
			
		||||
        //     // This module isn't already loaded
 | 
			
		||||
        //     stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
 | 
			
		||||
        //
 | 
			
		||||
        //     stubs.push(sinon.stub(nodes,"registerType"));
 | 
			
		||||
        //     loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
 | 
			
		||||
        //     loader.load().then(function(result) {
 | 
			
		||||
        //
 | 
			
		||||
        //         registry.addModule.called.should.be.true();
 | 
			
		||||
        //         var module = registry.addModule.lastCall.args[0];
 | 
			
		||||
        //         module.should.have.property("name","node-red");
 | 
			
		||||
        //         module.should.have.property("version","1.2.3");
 | 
			
		||||
        //         module.should.have.property("nodes");
 | 
			
		||||
        //         module.nodes.should.have.property("DuffNode");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("id","node-red/DuffNode");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("module","node-red");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("name","DuffNode");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("file");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("template");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("enabled",true);
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("loaded",false);
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("types");
 | 
			
		||||
        //         module.nodes.DuffNode.types.should.have.a.length(0);
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("config","");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("help",{});
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property("namespace","node-red");
 | 
			
		||||
        //         module.nodes.DuffNode.should.have.property('err');
 | 
			
		||||
        //         module.nodes.DuffNode.err.should.endWith("DuffNode.html does not exist");
 | 
			
		||||
        //
 | 
			
		||||
        //         nodes.registerType.called.should.be.false();
 | 
			
		||||
        //
 | 
			
		||||
        //         done();
 | 
			
		||||
        //     }).catch(function(err) {
 | 
			
		||||
        //         done(err);
 | 
			
		||||
        //     });
 | 
			
		||||
        // });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("#addModule",function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -527,7 +528,7 @@ describe("red/nodes/registry/loader",function() {
 | 
			
		|||
            stubs.push(sinon.stub(nodes,"registerType"));
 | 
			
		||||
            loader.init({nodes:nodes,log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
 | 
			
		||||
            loader.addModule("TestNodeModule").then(function(result) {
 | 
			
		||||
                result.should.eql("a node list");
 | 
			
		||||
                result.should.eql("TestNodeModule");
 | 
			
		||||
 | 
			
		||||
                registry.addModule.called.should.be.true();
 | 
			
		||||
                var module = registry.addModule.lastCall.args[0];
 | 
			
		||||
| 
						 | 
				
			
			@ -584,7 +585,7 @@ describe("red/nodes/registry/loader",function() {
 | 
			
		|||
            stubs.push(sinon.stub(nodes,"registerType"));
 | 
			
		||||
            loader.init({log:{"_":function(){},warn:function(){}},nodes:nodes,version: function() { return "0.12.0"}, settings:{available:function(){return true;}}});
 | 
			
		||||
            loader.addModule("TestNodeModule").then(function(result) {
 | 
			
		||||
                result.should.eql("a node list");
 | 
			
		||||
                result.should.eql("TestNodeModule");
 | 
			
		||||
                registry.addModule.called.should.be.false();
 | 
			
		||||
                nodes.registerType.called.should.be.false();
 | 
			
		||||
                done();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
describe("red/nodes/registry/subflow",function() {
 | 
			
		||||
    it.skip("NEEDS TESTS");
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -653,42 +653,30 @@ describe('Node', function() {
 | 
			
		|||
 | 
			
		||||
    describe('#log', function() {
 | 
			
		||||
        it('produces a log message', function(done) {
 | 
			
		||||
            var n = new RedNode({id:'123',type:'abc',z:'789'});
 | 
			
		||||
            var n = new RedNode({id:'123',type:'abc',z:'789', _flow: {log:function(msg) { loginfo = msg;}}});
 | 
			
		||||
            var loginfo = {};
 | 
			
		||||
            sinon.stub(Log, 'log', function(msg) {
 | 
			
		||||
                loginfo = msg;
 | 
			
		||||
            });
 | 
			
		||||
            n.log("a log message");
 | 
			
		||||
            should.deepEqual({level:Log.INFO, id:n.id,
 | 
			
		||||
                               type:n.type, msg:"a log message",z:'789'}, loginfo);
 | 
			
		||||
            Log.log.restore();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
        it('produces a log message with a name', function(done) {
 | 
			
		||||
            var n = new RedNode({id:'123', type:'abc', name:"barney", z:'789'});
 | 
			
		||||
            var n = new RedNode({id:'123', type:'abc', name:"barney", z:'789', _flow: {log:function(msg) { loginfo = msg;}}});
 | 
			
		||||
            var loginfo = {};
 | 
			
		||||
            sinon.stub(Log, 'log', function(msg) {
 | 
			
		||||
                loginfo = msg;
 | 
			
		||||
            });
 | 
			
		||||
            n.log("a log message");
 | 
			
		||||
            should.deepEqual({level:Log.INFO, id:n.id, name: "barney",
 | 
			
		||||
                              type:n.type, msg:"a log message",z:'789'}, loginfo);
 | 
			
		||||
            Log.log.restore();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('#warn', function() {
 | 
			
		||||
        it('produces a warning message', function(done) {
 | 
			
		||||
            var n = new RedNode({id:'123',type:'abc',z:'789'});
 | 
			
		||||
            var n = new RedNode({id:'123',type:'abc',z:'789', _flow: {log:function(msg) { loginfo = msg;}}});
 | 
			
		||||
            var loginfo = {};
 | 
			
		||||
            sinon.stub(Log, 'log', function(msg) {
 | 
			
		||||
                loginfo = msg;
 | 
			
		||||
            });
 | 
			
		||||
            n.warn("a warning");
 | 
			
		||||
            should.deepEqual({level:Log.WARN, id:n.id,
 | 
			
		||||
                              type:n.type, msg:"a warning",z:'789'}, loginfo);
 | 
			
		||||
            Log.log.restore();
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -696,7 +684,8 @@ describe('Node', function() {
 | 
			
		|||
    describe('#error', function() {
 | 
			
		||||
        it('handles a null error message', function(done) {
 | 
			
		||||
            var flow = {
 | 
			
		||||
                handleError: sinon.stub()
 | 
			
		||||
                handleError: sinon.stub(),
 | 
			
		||||
                log:sinon.stub()
 | 
			
		||||
            }
 | 
			
		||||
            var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'});
 | 
			
		||||
            var message = {a:1};
 | 
			
		||||
| 
						 | 
				
			
			@ -712,7 +701,8 @@ describe('Node', function() {
 | 
			
		|||
 | 
			
		||||
        it('produces an error message', function(done) {
 | 
			
		||||
            var flow = {
 | 
			
		||||
                handleError: sinon.stub()
 | 
			
		||||
                handleError: sinon.stub(),
 | 
			
		||||
                log:sinon.stub()
 | 
			
		||||
            }
 | 
			
		||||
            var n = new RedNode({_flow:flow, id:'123',type:'abc',z:'789'});
 | 
			
		||||
            var message = {a:2};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue