diff --git a/editor/images/typedInput/env.png b/editor/images/typedInput/env.png new file mode 100644 index 000000000..0ea51da00 Binary files /dev/null and b/editor/images/typedInput/env.png differ diff --git a/editor/js/red.js b/editor/js/red.js index a56f6732a..fa6f62083 100644 --- a/editor/js/red.js +++ b/editor/js/red.js @@ -433,8 +433,8 @@ var RED = (function() { {id:"menu-item-import-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-import-dialog"}, {id:"menu-item-import-library",label:RED._("menu.label.library"),options:[]} ]}); - menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),disabled:true,options:[ - {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),disabled:true,onselect:"core:show-export-dialog"}, + menuOptions.push({id:"menu-item-export",label:RED._("menu.label.export"),options:[ + {id:"menu-item-export-clipboard",label:RED._("menu.label.clipboard"),onselect:"core:show-export-dialog"}, {id:"menu-item-export-library",label:RED._("menu.label.library"),disabled:true,onselect:"core:library-export"} ]}); menuOptions.push(null); diff --git a/editor/js/ui/clipboard.js b/editor/js/ui/clipboard.js index 2bdd20052..0d7519e10 100644 --- a/editor/js/ui/clipboard.js +++ b/editor/js/ui/clipboard.js @@ -309,18 +309,6 @@ RED.clipboard = (function() { $('').appendTo("body"); - RED.events.on("view:selection-changed",function(selection) { - if (!selection.nodes) { - RED.menu.setDisabled("menu-item-export",true); - RED.menu.setDisabled("menu-item-export-clipboard",true); - RED.menu.setDisabled("menu-item-export-library",true); - } else { - RED.menu.setDisabled("menu-item-export",false); - RED.menu.setDisabled("menu-item-export-clipboard",false); - RED.menu.setDisabled("menu-item-export-library",false); - } - }); - RED.actions.add("core:show-export-dialog",exportNodes); RED.actions.add("core:show-import-dialog",importNodes); diff --git a/editor/js/ui/common/popover.js b/editor/js/ui/common/popover.js index 6a0bc9fdf..0170f7070 100644 --- a/editor/js/ui/common/popover.js +++ b/editor/js/ui/common/popover.js @@ -19,12 +19,14 @@ RED.popover = (function() { "default": { top: 10, leftRight: 17, - leftLeft: 25 + leftLeft: 25, + leftBottom: 8, }, "small": { top: 5, leftRight: 17, - leftLeft: 16 + leftLeft: 16, + leftBottom: 3, } } function createPopover(options) { @@ -69,6 +71,8 @@ RED.popover = (function() { div.css({top: targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top,left:targetPos.left+targetWidth+deltaSizes[size].leftRight}); } else if (direction === 'left') { div.css({top: targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top,left:targetPos.left-deltaSizes[size].leftLeft-divWidth}); + } else if (direction === 'bottom') { + div.css({top: targetPos.top+targetHeight+deltaSizes[size].top,left:targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftBottom}); } if (instant) { div.show(); diff --git a/editor/js/ui/common/tabs.js b/editor/js/ui/common/tabs.js index f66b95421..23b69f5d1 100644 --- a/editor/js/ui/common/tabs.js +++ b/editor/js/ui/common/tabs.js @@ -19,8 +19,10 @@ RED.tabs = (function() { function createTabs(options) { var tabs = {}; + var pinnedTabsCount = 0; var currentTabWidth; var currentActiveTabWidth = 0; + var collapsibleMenu; var ul = options.element || $("#"+options.id); var wrapper = ul.wrap( "
" ).parent(); @@ -50,6 +52,55 @@ RED.tabs = (function() { scrollRight = $('
').appendTo(wrapper).find("a"); scrollRight.on('mousedown',function(evt) { scrollEventHandler(evt,'+=150') }).on('click',function(evt){ evt.preventDefault();}); } + + if (options.collapsible) { + // var dropDown = $('
',{class:"red-ui-tabs-select"}).appendTo(wrapper); + // ul.hide(); + wrapper.addClass("red-ui-tabs-collapsible"); + + var collapsedButtonsRow = $('').appendTo(wrapper); + + var selectButton = $('').appendTo(collapsedButtonsRow); + selectButton.addClass("red-ui-tab-link-button-menu") + selectButton.click(function(evt) { + evt.preventDefault(); + if (!collapsibleMenu) { + var pinnedOptions = []; + var options = []; + ul.children().each(function(i,el) { + var id = $(el).data('tabId'); + var opt = { + id:"red-ui-tabs-menu-option-"+id, + label: tabs[id].name, + onselect: function() { + activateTab(id); + } + }; + if (tabs[id].pinned) { + pinnedOptions.push(opt); + } else { + options.push(opt); + } + }); + options = pinnedOptions.concat(options); + collapsibleMenu = RED.menu.init({id:"debug-message-option-menu",options: options}); + collapsibleMenu.css({ + position: "absolute" + }) + collapsibleMenu.on('mouseleave', function(){ $(this).hide() }); + collapsibleMenu.on('mouseup', function() { $(this).hide() }); + collapsibleMenu.appendTo("body"); + var elementPos = selectButton.offset(); + collapsibleMenu.css({ + top: (elementPos.top+selectButton.height()-20)+"px", + left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px" + }) + } + collapsibleMenu.toggle(); + }) + + } + function scrollEventHandler(evt,dir) { evt.preventDefault(); if ($(this).hasClass('disabled')) { @@ -118,6 +169,9 @@ RED.tabs = (function() { ul.children().removeClass("active"); ul.children().css({"transition": "width 100ms"}); link.parent().addClass("active"); + var parentId = link.parent().attr('id'); + wrapper.find(".red-ui-tab-link-button").removeClass("active"); + $("#"+parentId+"-link-button").addClass("active"); if (options.scrollable) { var pos = link.parent().position().left; if (pos-21 < 0) { @@ -155,41 +209,70 @@ RED.tabs = (function() { var tabs = ul.find("li.red-ui-tab"); var width = wrapper.width(); var tabCount = tabs.size(); - var tabWidth = (width-12-(tabCount*6))/tabCount; - currentTabWidth = (100*tabWidth/width)+"%"; - currentActiveTabWidth = currentTabWidth+"%"; - if (options.scrollable) { - tabWidth = Math.max(tabWidth,140); - currentTabWidth = tabWidth+"px"; - currentActiveTabWidth = 0; - var listWidth = Math.max(wrapper.width(),12+(tabWidth+6)*tabCount); - ul.width(listWidth); - updateScroll(); - } else if (options.hasOwnProperty("minimumActiveTabWidth")) { - if (tabWidth < options.minimumActiveTabWidth) { - tabCount -= 1; - tabWidth = (width-12-options.minimumActiveTabWidth-(tabCount*6))/tabCount; - currentTabWidth = (100*tabWidth/width)+"%"; - currentActiveTabWidth = options.minimumActiveTabWidth+"px"; + var tabWidth; + + if (options.collapsible) { + tabWidth = width - collapsedButtonsRow.width()-10; + if (tabWidth < 198) { + var delta = 198 - tabWidth; + var b = collapsedButtonsRow.find("a:last").prev(); + while (b.is(":not(:visible)")) { + b = b.prev(); + } + if (!b.hasClass("red-ui-tab-link-button-pinned")) { + b.hide(); + } + tabWidth = width - collapsedButtonsRow.width()-10; } else { - currentActiveTabWidth = 0; + var space = width - 198 - collapsedButtonsRow.width(); + if (space > 40) { + collapsedButtonsRow.find("a:not(:visible):first").show(); + tabWidth = width - collapsedButtonsRow.width()-10; + } } - } - tabs.css({width:currentTabWidth}); - if (tabWidth < 50) { - ul.find(".red-ui-tab-close").hide(); - ul.find(".red-ui-tab-icon").hide(); - ul.find(".red-ui-tab-label").css({paddingLeft:Math.min(12,Math.max(0,tabWidth-38))+"px"}) + tabs.css({width:tabWidth}); + } else { - ul.find(".red-ui-tab-close").show(); - ul.find(".red-ui-tab-icon").show(); - ul.find(".red-ui-tab-label").css({paddingLeft:""}) - } - if (currentActiveTabWidth !== 0) { - ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); - ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); - ul.find("li.red-ui-tab.active .red-ui-tab-icon").show(); - ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""}) + var tabWidth = (width-12-(tabCount*6))/tabCount; + currentTabWidth = (100*tabWidth/width)+"%"; + currentActiveTabWidth = currentTabWidth+"%"; + if (options.scrollable) { + tabWidth = Math.max(tabWidth,140); + currentTabWidth = tabWidth+"px"; + currentActiveTabWidth = 0; + var listWidth = Math.max(wrapper.width(),12+(tabWidth+6)*tabCount); + ul.width(listWidth); + updateScroll(); + } else if (options.hasOwnProperty("minimumActiveTabWidth")) { + if (tabWidth < options.minimumActiveTabWidth) { + tabCount -= 1; + tabWidth = (width-12-options.minimumActiveTabWidth-(tabCount*6))/tabCount; + currentTabWidth = (100*tabWidth/width)+"%"; + currentActiveTabWidth = options.minimumActiveTabWidth+"px"; + } else { + currentActiveTabWidth = 0; + } + } + if (options.collapsible) { + console.log(currentTabWidth); + } + + tabs.css({width:currentTabWidth}); + if (tabWidth < 50) { + ul.find(".red-ui-tab-close").hide(); + ul.find(".red-ui-tab-icon").hide(); + ul.find(".red-ui-tab-label").css({paddingLeft:Math.min(12,Math.max(0,tabWidth-38))+"px"}) + } else { + ul.find(".red-ui-tab-close").show(); + ul.find(".red-ui-tab-icon").show(); + ul.find(".red-ui-tab-label").css({paddingLeft:""}) + } + if (currentActiveTabWidth !== 0) { + ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); + ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); + ul.find("li.red-ui-tab.active .red-ui-tab-icon").show(); + ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""}) + } } } @@ -210,11 +293,15 @@ RED.tabs = (function() { activateTab(tab.find("a")); } li.remove(); + if (tabs[id].pinned) { + pinnedTabsCount--; + } if (options.onremove) { options.onremove(tabs[id]); } delete tabs[id]; updateTabWidths(); + collapsibleMenu = null; } return { @@ -223,13 +310,55 @@ RED.tabs = (function() { var li = $("
  • ",{class:"red-ui-tab"}).appendTo(ul); li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-"))); li.data("tabId",tab.id); + + if (options.maximumTabWidth) { + li.css("maxWidth",options.maximumTabWidth+"px"); + } var link = $("",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li); if (tab.icon) { $('').appendTo(link); + } else if (tab.iconClass) { + $('',{class:"red-ui-tab-icon "+tab.iconClass}).appendTo(link); } var span = $('',{class:"bidiAware"}).text(tab.label).appendTo(link); span.attr('dir', RED.text.bidi.resolveBaseTextDir(tab.label)); + if (options.collapsible) { + li.addClass("red-ui-tab-pinned"); + var pinnedLink = $(''); + if (tab.pinned) { + if (pinnedTabsCount === 0) { + pinnedLink.prependTo(collapsedButtonsRow) + } else { + pinnedLink.insertAfter(collapsedButtonsRow.find("a.red-ui-tab-link-button-pinned:last")); + } + } else { + pinnedLink.insertBefore(collapsedButtonsRow.find("a:last")); + } + pinnedLink.attr('id',li.attr('id')+"-link-button"); + if (tab.iconClass) { + $('',{class:tab.iconClass}).appendTo(pinnedLink); + } else { + $('',{class:"fa fa-lemon-o"}).appendTo(pinnedLink); + } + pinnedLink.click(function(evt) { + evt.preventDefault(); + activateTab(tab.id); + }); + if (tab.pinned) { + pinnedLink.addClass("red-ui-tab-link-button-pinned"); + pinnedTabsCount++; + } + RED.popover.create({ + target:$(pinnedLink), + trigger: "hover", + size: "small", + direction: "bottom", + content: tab.name, + delay: { show: 550, hide: 10 } + }); + + } link.on("click",onTabClick); link.on("dblclick",onTabDblClick); if (tab.closeable) { @@ -326,6 +455,7 @@ RED.tabs = (function() { } }) } + collapsibleMenu = null; }, removeTab: removeTab, activateTab: activateTab, diff --git a/editor/js/ui/common/typedInput.js b/editor/js/ui/common/typedInput.js index 3b5a0d62c..f5227fd68 100644 --- a/editor/js/ui/common/typedInput.js +++ b/editor/js/ui/common/typedInput.js @@ -76,6 +76,11 @@ } }) } + }, + env: { + value: "env", + label: "env variable", + icon: "red/images/typedInput/env.png" } }; var nlsd = false; diff --git a/editor/js/ui/library.js b/editor/js/ui/library.js index edcfc4974..f99cdf0a8 100644 --- a/editor/js/ui/library.js +++ b/editor/js/ui/library.js @@ -425,12 +425,8 @@ RED.library = (function() { RED.events.on("view:selection-changed",function(selection) { if (!selection.nodes) { - RED.menu.setDisabled("menu-item-export",true); - RED.menu.setDisabled("menu-item-export-clipboard",true); RED.menu.setDisabled("menu-item-export-library",true); } else { - RED.menu.setDisabled("menu-item-export",false); - RED.menu.setDisabled("menu-item-export-clipboard",false); RED.menu.setDisabled("menu-item-export-library",false); } }); diff --git a/editor/js/ui/projects/tab-versionControl.js b/editor/js/ui/projects/tab-versionControl.js index 44b602019..4560a56e5 100644 --- a/editor/js/ui/projects/tab-versionControl.js +++ b/editor/js/ui/projects/tab-versionControl.js @@ -1003,6 +1003,8 @@ RED.sidebar.versionControl = (function() { name: "Project History", content: sidebarContent, enableOnEdit: false, + pinned: true, + iconClass: "fa fa-code-fork", onchange: function() { setTimeout(function() { sections.resize(); diff --git a/editor/js/ui/sidebar.js b/editor/js/ui/sidebar.js index 222217668..d9f302885 100644 --- a/editor/js/ui/sidebar.js +++ b/editor/js/ui/sidebar.js @@ -35,7 +35,8 @@ RED.sidebar = (function() { tab.onremove.call(tab); } }, - minimumActiveTabWidth: 70 + // minimumActiveTabWidth: 70, + collapsible: true // scrollable: true }); @@ -59,6 +60,8 @@ RED.sidebar = (function() { options = title; } + delete options.closeable; + options.wrapper = $('
    ',{style:"height:100%"}).appendTo("#sidebar-content") options.wrapper.append(options.content); options.wrapper.hide(); @@ -82,6 +85,8 @@ RED.sidebar = (function() { group: "sidebar-tabs" }); + options.iconClass = options.iconClass || "fa fa-square-o" + knownTabs[options.id] = options; if (options.visible !== false) { diff --git a/editor/js/ui/tab-config.js b/editor/js/ui/tab-config.js index 9a4619b28..00caea6cd 100644 --- a/editor/js/ui/tab-config.js +++ b/editor/js/ui/tab-config.js @@ -221,8 +221,7 @@ RED.sidebar.config = (function() { name: RED._("sidebar.config.name"), content: content, toolbar: toolbar, - closeable: true, - visible: false, + iconClass: "fa fa-cog", onchange: function() { refreshConfigNodeList(); } }); RED.actions.add("core:show-config-tab",function() {RED.sidebar.show('config')}); diff --git a/editor/js/ui/tab-info.js b/editor/js/ui/tab-info.js index b4cf87d46..340a6bd74 100644 --- a/editor/js/ui/tab-info.js +++ b/editor/js/ui/tab-info.js @@ -83,7 +83,9 @@ RED.sidebar.info = (function() { id: "info", label: RED._("sidebar.info.label"), name: RED._("sidebar.info.name"), + iconClass: "fa fa-info", content: content, + pinned: true, enableOnEdit: true }); if (tips.enabled()) { diff --git a/editor/sass/popover.scss b/editor/sass/popover.scss index d3c077e6d..691acd5c8 100644 --- a/editor/sass/popover.scss +++ b/editor/sass/popover.scss @@ -30,7 +30,6 @@ } .red-ui-popover:after, .red-ui-popover:before { - top: 50%; border: solid transparent; content: " "; height: 0; @@ -39,12 +38,18 @@ pointer-events: none; } .red-ui-popover.red-ui-popover-right:after, .red-ui-popover.red-ui-popover-right:before { + top: 50%; right: 100%; } .red-ui-popover.red-ui-popover-left:after, .red-ui-popover.red-ui-popover-left:before { + top: 50%; left: 100%; } +.red-ui-popover.red-ui-popover-bottom:after, .red-ui-popover.red-ui-popover-bottom:before { + bottom: 100%; + left: 50%; +} .red-ui-popover.red-ui-popover-right:after { border-color: rgba(136, 183, 213, 0); @@ -72,6 +77,21 @@ margin-top: -11px; } + +.red-ui-popover.red-ui-popover-bottom:after { + border-color: rgba(136, 183, 213, 0); + border-bottom-color: #fff; + border-width: 10px; + margin-left: -10px; +} +.red-ui-popover.red-ui-popover-bottom:before { + border-color: rgba(194, 225, 245, 0); + border-bottom-color: $primary-border-color; + border-width: 11px; + margin-left: -11px; +} + + .red-ui-popover-size-small { font-size: 11px; padding: 5px; @@ -93,4 +113,12 @@ border-width: 6px; margin-top: -6px; } + &.red-ui-popover-bottom:after { + border-width: 5px; + margin-left: -5px; + } + &.red-ui-popover-bottom:before { + border-width: 6px; + margin-left: -6px; + } } diff --git a/editor/sass/tabs.scss b/editor/sass/tabs.scss index 4b2dc7f5d..488f4f3c9 100644 --- a/editor/sass/tabs.scss +++ b/editor/sass/tabs.scss @@ -93,7 +93,7 @@ color: $workspace-button-color-hover; } } - .red-ui-tab-icon { + img.red-ui-tab-icon { opacity: 0.2; } } @@ -113,6 +113,21 @@ &.red-ui-tabs-add.red-ui-tabs-scrollable { padding-right: 59px; } + &.red-ui-tabs-collapsible { + li:not(.active) { + display: none; + &.red-ui-tab-pinned { + a { + padding-left: 0; + text-align: center; + } + span { + display: none; + } + width: 32px; + } + } + } &.red-ui-tabs-vertical { box-sizing: border-box; @@ -157,6 +172,15 @@ } } } + .red-ui-tabs-select { + position: absolute; + top:0; + bottom: 0; + left: 0; + right: 0; + opacity: 0.4; + background: red; + } } .red-ui-tab-button { position: absolute; @@ -180,7 +204,33 @@ z-index: 2; } } - +.red-ui-tab-link-buttons { + position: absolute; + box-sizing: border-box; + top: 0; + right: 0; + height: 35px; + background: #fff; + border-bottom: 1px solid $primary-border-color; + z-index: 2; + a { + @include workspace-button; + line-height: 26px; + height: 28px; + width: 28px; + margin: 4px 3px 3px; + border: 1px solid $primary-border-color; + z-index: 2; + &.red-ui-tab-link-button { + &:not(.active) { + background: #eee; + } + } + &.red-ui-tab-link-button-menu { + border-color: white; + } + } +} .red-ui-tab-scroll { width: 21px; top: 0; @@ -216,7 +266,7 @@ right: 38px; } -.red-ui-tab-icon { +img.red-ui-tab-icon { margin-left: -8px; margin-right: 3px; margin-top: -2px; @@ -225,6 +275,11 @@ height: 20px; vertical-align: middle; } +i.red-ui-tab-icon { + opacity: 0.7; + width: 18px; + height: 20px; +} .red-ui-tabs-badges { position: absolute; diff --git a/editor/vendor/jsonata/formatter.js b/editor/vendor/jsonata/formatter.js index 9b0f544c8..6ff26accb 100644 --- a/editor/vendor/jsonata/formatter.js +++ b/editor/vendor/jsonata/formatter.js @@ -117,6 +117,7 @@ '$contains':{ args:[ 'str', 'pattern' ]}, '$count':{ args:[ 'array' ]}, '$each':{ args:[ 'object', 'function' ]}, + '$env': { args:[ 'arg' ]}, '$exists':{ args:[ 'arg' ]}, '$filter':{ args:[ 'array', 'function' ]}, '$floor':{ args:[ 'number' ]}, diff --git a/nodes/core/core/20-inject.html b/nodes/core/core/20-inject.html index 4090fdbbd..6f8ebcf69 100644 --- a/nodes/core/core/20-inject.html +++ b/nodes/core/core/20-inject.html @@ -263,7 +263,7 @@ If you want every 20 minutes from now - use the "interval" option.

    $("#node-input-payload").typedInput({ default: 'str', typeField: $("#node-input-payloadType"), - types:['flow','global','str','num','bool','json','bin','date'] + types:['flow','global','str','num','bool','json','bin','date','env'] }); $("#inject-time-type-select").change(function() { diff --git a/nodes/core/core/58-debug.html b/nodes/core/core/58-debug.html index 7317d7064..95b55d7f5 100644 --- a/nodes/core/core/58-debug.html +++ b/nodes/core/core/58-debug.html @@ -154,7 +154,9 @@ name: this._("debug.sidebar.name"), content: uiComponents.content, toolbar: uiComponents.footer, - enableOnEdit: true + enableOnEdit: true, + pinned: true, + iconClass: "fa fa-wrench" }); RED.actions.add("core:show-debug-tab",function() { RED.sidebar.show('debug'); }); diff --git a/nodes/core/logic/10-switch.html b/nodes/core/logic/10-switch.html index ffa5a48e5..f4ffc7dea 100644 --- a/nodes/core/logic/10-switch.html +++ b/nodes/core/logic/10-switch.html @@ -230,12 +230,12 @@ selectField.append($("").val(operators[d].v).text(/^switch/.test(operators[d].t)?node._(operators[d].t):operators[d].t)); } } - var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata',previousValueType]}); - var numValueField = $('',{class:"node-input-rule-num-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['flow','global','num','jsonata']}); + var valueField = $('',{class:"node-input-rule-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'str',types:['msg','flow','global','str','num','jsonata','env',previousValueType]}); + var numValueField = $('',{class:"node-input-rule-num-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['flow','global','num','jsonata','env']}); var expValueField = $('',{class:"node-input-rule-exp-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'jsonata',types:['jsonata']}); - var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); + var btwnValueField = $('',{class:"node-input-rule-btwn-value",type:"text",style:"margin-left: 5px;"}).appendTo(row).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata','env',previousValueType]}); var btwnAndLabel = $('',{class:"node-input-rule-btwn-label"}).text(" "+andLabel+" ").appendTo(row3); - var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata',previousValueType]}); + var btwnValue2Field = $('',{class:"node-input-rule-btwn-value2",type:"text",style:"margin-left:2px;"}).appendTo(row3).typedInput({default:'num',types:['msg','flow','global','str','num','jsonata','env',previousValueType]}); var typeValueField = $('',{class:"node-input-rule-type-value",type:"text",style:"margin-left: 5px;"}).appendTo(row) .typedInput({default:'string',types:[ {value:"string",label:"string",hasValue:false}, diff --git a/nodes/core/logic/15-change.html b/nodes/core/logic/15-change.html index 999c0ed71..83bdecf9e 100644 --- a/nodes/core/logic/15-change.html +++ b/nodes/core/logic/15-change.html @@ -146,7 +146,7 @@ .appendTo(row2); var propertyValue = $('',{class:"node-input-rule-property-value",type:"text"}) .appendTo(row2) - .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin','date','jsonata']}); + .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin','date','jsonata','env']}); var row3_1 = $('
    ').appendTo(row3); $('
    ',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) @@ -154,7 +154,7 @@ .appendTo(row3_1); var fromValue = $('',{class:"node-input-rule-property-search-value",type:"text"}) .appendTo(row3_1) - .typedInput({default:'str',types:['msg','flow','global','str','re','num','bool']}); + .typedInput({default:'str',types:['msg','flow','global','str','re','num','bool','env']}); var row3_2 = $('
    ',{style:"margin-top:8px;"}).appendTo(row3); $('
    ',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) @@ -162,7 +162,7 @@ .appendTo(row3_2); var toValue = $('',{class:"node-input-rule-property-replace-value",type:"text"}) .appendTo(row3_2) - .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin']}); + .typedInput({default:'str',types:['msg','flow','global','str','num','bool','json','bin','env']}); $('
    ',{style:"display:inline-block;text-align:right; width:120px; padding-right:10px; box-sizing:border-box;"}) .text(to) diff --git a/nodes/core/logic/15-change.js b/nodes/core/logic/15-change.js index ae50394ed..c7c05118e 100644 --- a/nodes/core/logic/15-change.js +++ b/nodes/core/logic/15-change.js @@ -93,6 +93,8 @@ module.exports = function(RED) { valid = false; this.error(RED._("change.errors.invalid-expr",{error:e.message})); } + } else if (rule.tot === 'env') { + rule.to = RED.util.evaluateNodeProperty(rule.to,'env'); } } diff --git a/red/api/editor/locales/en-US/jsonata.json b/red/api/editor/locales/en-US/jsonata.json index 14c7f7cd1..c839c759e 100644 --- a/red/api/editor/locales/en-US/jsonata.json +++ b/red/api/editor/locales/en-US/jsonata.json @@ -190,11 +190,11 @@ }, "$flowContext": { "args": "string", - "desc": "Retrieves a flow context property." + "desc": "Retrieves a flow context property.\n\nThis is a Node-RED defined function." }, "$globalContext": { "args": "string", - "desc": "Retrieves a global context property." + "desc": "Retrieves a global context property.\n\nThis is a Node-RED defined function." }, "$pad": { "args": "string, width [, char]", @@ -215,5 +215,9 @@ "$toMillis": { "args": "timestamp", "desc": "Convert a `timestamp` string in the ISO 8601 format to the number of milliseconds since the Unix Epoch (1 January, 1970 UTC) as a number. An error is thrown if the string is not in the correct format." + }, + "$env": { + "args": "arg", + "desc": "Returns the value of an environment variable.\n\nThis is a Node-RED defined function." } } diff --git a/red/runtime-registry/loader.js b/red/runtime-registry/loader.js index f0bd86587..4ef841b48 100644 --- a/red/runtime-registry/loader.js +++ b/red/runtime-registry/loader.js @@ -64,6 +64,17 @@ function copyObjectProperties(src,dst,copyList,blockList) { } } } +function requireModule(name) { + var moduleInfo = registry.getModuleInfo(name); + if (moduleInfo && moduleInfo.path) { + var relPath = path.relative(__dirname, moduleInfo.path); + return require(relPath); + } else { + var err = new Error(`Cannot find module '${name}'`); + err.code = "MODULE_NOT_FOUND"; + throw err; + } +} function createNodeApi(node) { var red = { @@ -73,6 +84,7 @@ function createNodeApi(node) { events: runtime.events, util: runtime.util, version: runtime.version, + require: requireModule, comms: { publish: function(topic,data,retain) { runtime.events.emit("comms",{ diff --git a/red/runtime-registry/localfilesystem.js b/red/runtime-registry/localfilesystem.js index 877491f2f..409b46425 100644 --- a/red/runtime-registry/localfilesystem.js +++ b/red/runtime-registry/localfilesystem.js @@ -88,7 +88,7 @@ function getLocalNodeFiles(dir) { try { files = fs.readdirSync(dir); } catch(err) { - return result; + return {files: [], icons: []}; } files.sort(); files.forEach(function(fn) { @@ -296,6 +296,7 @@ function getNodeFiles(disableNodePathScan) { nodeList[moduleFile.package.name] = { name: moduleFile.package.name, version: moduleFile.package.version, + path: moduleFile.dir, local: moduleFile.local||false, nodes: {}, icons: nodeModuleFiles.icons, diff --git a/red/runtime-registry/registry.js b/red/runtime-registry/registry.js index fa5fc6ae3..65f3251f1 100644 --- a/red/runtime-registry/registry.js +++ b/red/runtime-registry/registry.js @@ -339,6 +339,7 @@ function getModuleInfo(module) { name: module, version: moduleConfigs[module].version, local: moduleConfigs[module].local, + path: moduleConfigs[module].path, nodes: [] }; for (var i = 0; i < nodes.length; ++i) { @@ -560,30 +561,6 @@ var icon_paths = { var iconCache = {}; var defaultIcon = path.resolve(__dirname + '/../../public/icons/arrow-in.png'); -function nodeIconDir(dir) { - icon_paths[dir.name] = icon_paths[dir.name] || []; - icon_paths[dir.name].push(path.resolve(dir.path)); - - if (dir.icons) { - if (!moduleConfigs[dir.name]) { - moduleConfigs[dir.name] = { - name: dir.name, - nodes: {}, - icons: [] - }; - } - var module = moduleConfigs[dir.name]; - if (module.icons === undefined) { - module.icons = []; - } - dir.icons.forEach(function(icon) { - if (module.icons.indexOf(icon) === -1) { - module.icons.push(icon); - } - }); - } -} - function getNodeIconPath(module,icon) { if (/\.\./.test(icon)) { throw new Error(); @@ -624,7 +601,6 @@ function getNodeIcons() { } } } - return iconList; } diff --git a/red/runtime/nodes/flows/util.js b/red/runtime/nodes/flows/util.js index cda79adc6..512eb19a7 100644 --- a/red/runtime/nodes/flows/util.js +++ b/red/runtime/nodes/flows/util.js @@ -37,7 +37,8 @@ function diffNodes(oldNode,newNode) { return false; } -var EnvVarPropertyRE = /^\$\((\S+)\)$/; +var EnvVarPropertyRE_old = /^\$\((\S+)\)$/; +var EnvVarPropertyRE = /^\${(\S+)}$/; function mapEnvVarProperties(obj,prop) { if (Buffer.isBuffer(obj[prop])) { @@ -47,11 +48,9 @@ function mapEnvVarProperties(obj,prop) { mapEnvVarProperties(obj[prop],i); } } else if (typeof obj[prop] === 'string') { - var m; - if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) { - if (process.env.hasOwnProperty(m[1])) { - obj[prop] = process.env[m[1]]; - } + if (obj[prop][0] === "$" && (EnvVarPropertyRE_old.test(obj[prop]) || EnvVarPropertyRE.test(obj[prop])) ) { + var envVar = obj[prop].substring(2,obj[prop].length-1); + obj[prop] = process.env.hasOwnProperty(envVar)?process.env[envVar]:obj[prop]; } } else { for (var p in obj[prop]) { diff --git a/red/runtime/storage/localfilesystem/index.js b/red/runtime/storage/localfilesystem/index.js index af9d103fb..510ed261b 100644 --- a/red/runtime/storage/localfilesystem/index.js +++ b/red/runtime/storage/localfilesystem/index.js @@ -73,7 +73,8 @@ var localfilesystem = { var defaultPackage = { "name": "node-red-project", "description": "A Node-RED Project", - "version": "0.0.1" + "version": "0.0.1", + "private": true }; return util.writeFile(packageFile,JSON.stringify(defaultPackage,"",4)); } diff --git a/red/runtime/util.js b/red/runtime/util.js index 88c06a029..257e87777 100644 --- a/red/runtime/util.js +++ b/red/runtime/util.js @@ -303,6 +303,23 @@ function setMessageProperty(msg,prop,value,createMissing) { } } +function evaluteEnvProperty(value) { + if (/^\${[^}]+}$/.test(value)) { + // ${ENV_VAR} + value = value.substring(2,value.length-1); + value = process.env.hasOwnProperty(value)?process.env[value]:"" + } else if (!/\${\S+}/.test(value)) { + // ENV_VAR + value = process.env.hasOwnProperty(value)?process.env[value]:"" + } else { + // FOO${ENV_VAR}BAR + value = value.replace(/\${([^}]+)}/g, function(match, v) { + return process.env.hasOwnProperty(v)?process.env[v]:"" + }); + } + return value; +} + function evaluateNodeProperty(value, type, node, msg) { if (type === 'str') { return ""+value; @@ -328,6 +345,8 @@ function evaluateNodeProperty(value, type, node, msg) { } else if (type === 'jsonata') { var expr = prepareJSONataExpression(value,node); return evaluateJSONataExpression(expr,msg); + } else if (type === 'env') { + return evaluteEnvProperty(value); } return value; } @@ -340,6 +359,9 @@ function prepareJSONataExpression(value,node) { expr.assign('globalContext',function(val) { return node.context().global.get(val); }); + expr.assign('env', function(val) { + return process.env[val]; + }) expr.registerFunction('clone', cloneMessage, '<(oa)-:o>'); expr._legacyMode = /(^|[^a-zA-Z0-9_'"])msg([^a-zA-Z0-9_'"]|$)/.test(value); return expr; diff --git a/test/nodes/core/logic/15-change_spec.js b/test/nodes/core/logic/15-change_spec.js index f5ca0f3d2..e444b2663 100644 --- a/test/nodes/core/logic/15-change_spec.js +++ b/test/nodes/core/logic/15-change_spec.js @@ -390,7 +390,7 @@ describe('change Node', function() { changeNode1.receive({payload:""}); }); }); - + it('sets the value of the message property to the current timestamp', function(done) { var flow = [{"id":"changeNode1","type":"change","rules":[{"t":"set","p":"ts","pt":"msg","to":"","tot":"date"}],"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -409,6 +409,33 @@ describe('change Node', function() { }); }); + describe('env var', function() { + before(function() { + process.env.NR_TEST_A = 'foo'; + }) + after(function() { + delete process.env.NR_TEST_A; + }) + it('sets the value using env property', function(done) { + var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"set","p":"payload","pt":"msg","to":"NR_TEST_A","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("foo"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"123",topic:"ABC"}); + }); + }); + }); + + it('changes the value using jsonata', function(done) { var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"set","p":"payload","to":"$length(payload)","tot":"jsonata"}],"name":"changeNode","wires":[["helperNode1"]]}, {id:"helperNode1", type:"helper", wires:[]}]; @@ -872,6 +899,33 @@ describe('change Node', function() { changeNode1.receive({payload:""}); }); }); + + describe('env var', function() { + before(function() { + process.env.NR_TEST_A = 'foo'; + }) + after(function() { + delete process.env.NR_TEST_A; + }) + it('changes the value using env property', function(done) { + var flow = [{"id":"changeNode1","type":"change",rules:[{"t":"change","p":"payload","from":"topic","to":"NR_TEST_A","fromt":"msg","tot":"env"}],"name":"changeNode","wires":[["helperNode1"]]}, + {id:"helperNode1", type:"helper", wires:[]}]; + helper.load(changeNode, flow, function() { + var changeNode1 = helper.getNode("changeNode1"); + var helperNode1 = helper.getNode("helperNode1"); + helperNode1.on("input", function(msg) { + try { + msg.payload.should.equal("abcfooabc"); + done(); + } catch(err) { + done(err); + } + }); + changeNode1.receive({payload:"abcABCabc",topic:"ABC"}); + }); + }); + }); + }); describe("#delete", function() { diff --git a/test/red/runtime/nodes/flows/util_spec.js b/test/red/runtime/nodes/flows/util_spec.js index dda43ce9a..774e786ad 100644 --- a/test/red/runtime/nodes/flows/util_spec.js +++ b/test/red/runtime/nodes/flows/util_spec.js @@ -35,10 +35,17 @@ describe('flows/util', function() { }); describe('#mapEnvVarProperties',function() { - it('handles ENV substitutions in an object', function() { + before(function() { process.env.foo1 = "bar1"; process.env.foo2 = "bar2"; process.env.foo3 = "bar3"; + }) + after(function() { + delete process.env.foo1; + delete process.env.foo2; + delete process.env.foo3; + }) + it('handles ENV substitutions in an object - $()', function() { var foo = {a:"$(foo1)",b:"$(foo2)",c:{d:"$(foo3)"}}; for (var p in foo) { if (foo.hasOwnProperty(p)) { @@ -47,6 +54,15 @@ describe('flows/util', function() { } foo.should.eql({ a: 'bar1', b: 'bar2', c: { d: 'bar3' } } ); }); + it('handles ENV substitutions in an object - ${}', function() { + var foo = {a:"${foo1}",b:"${foo2}",c:{d:"${foo3}"}}; + for (var p in foo) { + if (foo.hasOwnProperty(p)) { + flowUtil.mapEnvVarProperties(foo,p); + } + } + foo.should.eql({ a: 'bar1', b: 'bar2', c: { d: 'bar3' } } ); + }); }); describe('#diffNodes',function() { diff --git a/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/arrow-in.png b/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/arrow-in.png new file mode 100644 index 000000000..e38f39146 Binary files /dev/null and b/test/red/runtime/nodes/resources/local/NestedDirectoryNode/NestedNode/icons/arrow-in.png differ diff --git a/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png b/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png new file mode 100644 index 000000000..59a29af14 --- /dev/null +++ b/test/red/runtime/nodes/resources/local/TestNodeModule/node_modules/TestNodeModule/icons/arrow-in.png @@ -0,0 +1,3 @@ +This file exists just to ensure the 'icons' directory is in the repository. +TODO: a future test needs to ensure the right icon files are loaded - this + directory can be used for that diff --git a/test/red/runtime/util_spec.js b/test/red/runtime/util_spec.js index f542228a2..1b7438efd 100644 --- a/test/red/runtime/util_spec.js +++ b/test/red/runtime/util_spec.js @@ -307,6 +307,38 @@ describe("red/util", function() { },{}); result.should.eql("123"); }); + describe('environment variable', function() { + before(function() { + process.env.NR_TEST_A = "foo"; + process.env.NR_TEST_B = "${NR_TEST_A}"; + }) + after(function() { + delete process.env.NR_TEST_A; + delete process.env.NR_TEST_B; + }) + + it('returns an environment variable - NR_TEST_A', function() { + var result = util.evaluateNodeProperty('NR_TEST_A','env'); + result.should.eql('foo'); + }); + it('returns an environment variable - ${NR_TEST_A}', function() { + var result = util.evaluateNodeProperty('${NR_TEST_A}','env'); + result.should.eql('foo'); + }); + it('returns an environment variable - ${NR_TEST_A', function() { + var result = util.evaluateNodeProperty('${NR_TEST_A','env'); + result.should.eql(''); + }); + it('returns an environment variable - foo${NR_TEST_A}bar', function() { + var result = util.evaluateNodeProperty('123${NR_TEST_A}456','env'); + result.should.eql('123foo456'); + }); + it('returns an environment variable - foo${NR_TEST_B}bar', function() { + var result = util.evaluateNodeProperty('123${NR_TEST_B}456','env'); + result.should.eql('123${NR_TEST_A}456'); + }); + + }); }); describe('normalisePropertyExpression', function() {