Merge branch 'dev' into resyn-dev

This commit is contained in:
Nick O'Leary
2024-04-03 13:57:10 +01:00
committed by GitHub
231 changed files with 47719 additions and 3558 deletions

View File

@@ -174,12 +174,24 @@
this.uiContainer.width(m[1]);
}
if (this.options.sortable) {
var isCanceled = false; // Flag to track if an item has been canceled from being dropped into a different list
var noDrop = false; // Flag to track if an item is being dragged into a different list
var handle = (typeof this.options.sortable === 'string')?
this.options.sortable :
".red-ui-editableList-item-handle";
var sortOptions = {
axis: "y",
update: function( event, ui ) {
// dont trigger update if the item is being canceled
const targetList = $(event.target);
const draggedItem = ui.item;
const draggedItemParent = draggedItem.parent();
if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
noDrop = true;
}
if (isCanceled || noDrop) {
return;
}
if (that.options.sortItems) {
that.options.sortItems(that.items());
}
@@ -189,8 +201,32 @@
tolerance: "pointer",
forcePlaceholderSize:true,
placeholder: "red-ui-editabelList-item-placeholder",
start: function(e, ui){
ui.placeholder.height(ui.item.height()-4);
start: function (event, ui) {
isCanceled = false;
ui.placeholder.height(ui.item.height() - 4);
ui.item.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
},
stop: function (event, ui) {
ui.item.css('cursor', 'auto');
},
receive: function (event, ui) {
if (ui.item.hasClass("red-ui-editableList-item-constrained")) {
isCanceled = true;
$(ui.sender).sortable('cancel');
}
},
over: function (event, ui) {
// if the dragged item is constrained, prevent it from being dropped into a different list
const targetList = $(event.target);
const draggedItem = ui.item;
const draggedItemParent = draggedItem.parent();
if (!targetList.is(draggedItemParent) && draggedItem.hasClass("red-ui-editableList-item-constrained")) {
noDrop = true;
draggedItem.css('cursor', 'no-drop'); // TODO: this doesn't seem to work, use a class instead?
} else {
noDrop = false;
draggedItem.css('cursor', 'grabbing'); // TODO: this doesn't seem to work, use a class instead?
}
}
};
if (this.options.connectWith) {

View File

@@ -54,25 +54,26 @@
return icon;
}
var autoComplete = function(options) {
function getMatch(value, searchValue) {
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const len = idx > -1 ? searchValue.length : 0;
return {
index: idx,
found: idx > -1,
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
function getMatch(value, searchValue) {
const idx = value.toLowerCase().indexOf(searchValue.toLowerCase());
const len = idx > -1 ? searchValue.length : 0;
return {
index: idx,
found: idx > -1,
pre: value.substring(0,idx),
match: value.substring(idx,idx+len),
post: value.substring(idx+len),
}
}
function generateSpans(match) {
const els = [];
if(match.pre) { els.push($('<span/>').text(match.pre)); }
if(match.match) { els.push($('<span/>',{style:"font-weight: bold; color: var(--red-ui-text-color-link);"}).text(match.match)); }
if(match.post) { els.push($('<span/>').text(match.post)); }
return els;
}
const msgAutoComplete = function(options) {
return function(val) {
var matches = [];
options.forEach(opt => {
@@ -102,6 +103,197 @@
}
}
function getEnvVars (obj, envVars = {}) {
contextKnownKeys.env = contextKnownKeys.env || {}
if (contextKnownKeys.env[obj.id]) {
return contextKnownKeys.env[obj.id]
}
let parent
if (obj.type === 'tab' || obj.type === 'subflow') {
RED.nodes.eachConfig(function (conf) {
if (conf.type === "global-config") {
parent = conf;
}
})
} else if (obj.g) {
parent = RED.nodes.group(obj.g)
} else if (obj.z) {
parent = RED.nodes.workspace(obj.z) || RED.nodes.subflow(obj.z)
}
if (parent) {
getEnvVars(parent, envVars)
}
if (obj.env) {
obj.env.forEach(env => {
envVars[env.name] = obj
})
}
contextKnownKeys.env[obj.id] = envVars
return envVars
}
const envAutoComplete = function (val) {
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (!editingNode) {
return []
}
const envVarsMap = getEnvVars(editingNode)
const envVars = Object.keys(envVarsMap)
const matches = []
const i = val.lastIndexOf('${')
let searchKey = val
let isSubkey = false
if (i > -1) {
if (val.lastIndexOf('}') < i) {
searchKey = val.substring(i+2)
isSubkey = true
}
}
envVars.forEach(v => {
let valMatch = getMatch(v, searchKey);
if (valMatch.found) {
const optSrc = envVarsMap[v]
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
if (optSrc) {
const optEl = $('<div>').css({ "font-size": "0.8em" });
let label
if (optSrc.type === 'global-config') {
label = RED._('sidebar.context.global')
} else if (optSrc.type === 'group') {
label = RED.utils.getNodeLabel(optSrc) || (RED._('sidebar.info.group') + ': '+optSrc.id)
} else {
label = RED.utils.getNodeLabel(optSrc) || optSrc.id
}
optEl.append(generateSpans({ match: label }));
optEl.appendTo(element);
}
matches.push({
value: isSubkey ? val + v + '}' : v,
label: element,
i: valMatch.index
});
}
})
matches.sort(function(A,B){return A.i-B.i})
return matches
}
let contextKnownKeys = {}
let contextCache = {}
if (RED.events) {
RED.events.on("editor:close", function () {
contextCache = {}
contextKnownKeys = {}
});
}
const contextAutoComplete = function() {
const that = this
const getContextKeysFromRuntime = function(scope, store, searchKey, done) {
contextKnownKeys[scope] = contextKnownKeys[scope] || {}
contextKnownKeys[scope][store] = contextKnownKeys[scope][store] || new Set()
if (searchKey.length > 0) {
try {
RED.utils.normalisePropertyExpression(searchKey)
} catch (err) {
// Not a valid context key, so don't try looking up
done()
return
}
}
const url = `context/${scope}/${encodeURIComponent(searchKey)}?store=${store}&keysOnly`
if (contextCache[url]) {
// console.log('CACHED', url)
done()
} else {
// console.log('GET', url)
$.getJSON(url, function(data) {
// console.log(data)
contextCache[url] = true
const result = data[store] || {}
const keys = result.keys || []
const keyPrefix = searchKey + (searchKey.length > 0 ? '.' : '')
keys.forEach(key => {
if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(key)) {
contextKnownKeys[scope][store].add(keyPrefix + key)
} else {
contextKnownKeys[scope][store].add(searchKey + "[\""+key.replace(/"/,"\\\"")+"\"]")
}
})
done()
})
}
}
const getContextKeys = function(key, done) {
const keyParts = key.split('.')
const partialKey = keyParts.pop()
let scope = that.propertyType
if (scope === 'flow') {
// Get the flow id of the node we're editing
const editStack = RED.editor.getEditStack()
if (editStack.length === 0) {
done([])
return
}
const editingNode = editStack.pop()
if (editingNode.z) {
scope = `${scope}/${editingNode.z}`
} else {
done([])
return
}
}
const store = (contextStoreOptions.length === 1) ? contextStoreOptions[0].value : that.optionValue
const searchKey = keyParts.join('.')
getContextKeysFromRuntime(scope, store, searchKey, function() {
if (contextKnownKeys[scope][store].has(key) || key.endsWith(']')) {
getContextKeysFromRuntime(scope, store, key, function() {
done(contextKnownKeys[scope][store])
})
}
done(contextKnownKeys[scope][store])
})
}
return function(val, done) {
getContextKeys(val, function (keys) {
const matches = []
keys.forEach(v => {
let optVal = v
let valMatch = getMatch(optVal, val);
if (!valMatch.found && val.length > 0 && val.endsWith('.')) {
// Search key ends in '.' - but doesn't match. Check again
// with [" at the end instead so we match bracket notation
valMatch = getMatch(optVal, val.substring(0, val.length - 1) + '["')
}
if (valMatch.found) {
const element = $('<div>',{style: "display: flex"});
const valEl = $('<div/>',{style:"font-family: var(--red-ui-monospace-font); white-space:nowrap; overflow: hidden; flex-grow:1"});
valEl.append(generateSpans(valMatch))
valEl.appendTo(element)
matches.push({
value: optVal,
label: element,
});
}
})
matches.sort(function(a, b) { return a.value.localeCompare(b.value) });
done(matches);
})
}
}
// This is a hand-generated list of completions for the core nodes (based on the node help html).
var msgCompletions = [
{ value: "payload" },
@@ -166,20 +358,22 @@
{ value: "_session", source: ["websocket out","tcp out"] },
]
var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)},
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)},
flow: {value:"flow",label:"flow.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport,
valueLabel: contextLabel
valueLabel: contextLabel,
autoComplete: contextAutoComplete
},
global: {value:"global",label:"global.",hasValue:true,
options:[],
validate:RED.utils.validatePropertyExpression,
parse: contextParse,
export: contextExport,
valueLabel: contextLabel
valueLabel: contextLabel,
autoComplete: contextAutoComplete
},
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"},
num: {value:"num",label:"number",icon:"red/images/typedInput/09.svg",validate: function(v) {
@@ -214,7 +408,25 @@
}
},
re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"},
date: {value:"date",label:"timestamp",icon:"fa fa-clock-o",hasValue:false},
date: {
value:"date",
label:"timestamp",
icon:"fa fa-clock-o",
options:[
{
label: 'milliseconds since epoch',
value: ''
},
{
label: 'YYYY-MM-DDTHH:mm:ss.sssZ',
value: 'iso'
},
{
label: 'JavaScript Date Object',
value: 'object'
}
]
},
jsonata: {
value: "jsonata",
label: "expression",
@@ -251,7 +463,8 @@
env: {
value: "env",
label: "env variable",
icon: "red/images/typedInput/env.svg"
icon: "red/images/typedInput/env.svg",
autoComplete: envAutoComplete
},
node: {
value: "node",
@@ -383,18 +596,75 @@
eyeButton.show();
}
}
},
'conf-types': {
value: "conf-types",
label: "config",
icon: "fa fa-cog",
// hasValue: false,
valueLabel: function (container, value) {
// get the selected option (for access to the "name" and "module" properties)
const _options = this._optionsCache || this.typeList.find(opt => opt.value === value)?.options || []
const selectedOption = _options.find(opt => opt.value === value) || {
title: '',
name: '',
module: ''
}
container.attr("title", selectedOption.title) // set tooltip to the full path/id of the module/node
container.text(selectedOption.name) // apply the "name" of the selected option
// set "line-height" such as to make the "name" appear further up, giving room for the "module" to be displayed below the value
container.css("line-height", "1.4em")
// add the module name in smaller, lighter font below the value
$('<div></div>').text(selectedOption.module).css({
// "font-family": "var(--red-ui-monospace-font)",
color: "var(--red-ui-tertiary-text-color)",
"font-size": "0.8em",
"line-height": "1em",
opacity: 0.8
}).appendTo(container);
},
// hasValue: false,
options: function () {
if (this._optionsCache) {
return this._optionsCache
}
const configNodes = RED.nodes.registry.getNodeDefinitions({configOnly: true, filter: (def) => def.type !== "global-config"}).map((def) => {
// create a container with with 2 rows (row 1 for the name, row 2 for the module name in smaller, lighter font)
const container = $('<div style="display: flex; flex-direction: column; justify-content: space-between; row-gap: 1px;">')
const row1Name = $('<div>').text(def.type)
const row2Module = $('<div style="font-size: 0.8em; color: var(--red-ui-tertiary-text-color);">').text(def.set.module)
container.append(row1Name, row2Module)
return {
value: def.type,
name: def.type,
enabled: def.set.enabled ?? true,
local: def.set.local,
title: def.set.id, // tooltip e.g. "node-red-contrib-foo/bar"
module: def.set.module,
icon: container[0].outerHTML.trim(), // the typeInput will interpret this as html text and render it in the anchor
}
})
this._optionsCache = configNodes
return configNodes
}
}
};
// For a type with options, check value is a valid selection
// If !opt.multiple, returns the valid option object
// if opt.multiple, returns an array of valid option objects
// If not valid, returns null;
function isOptionValueValid(opt, currentVal) {
let _options = opt.options
if (typeof _options === "function") {
_options = _options.call(this)
}
if (!opt.multiple) {
for (var i=0;i<opt.options.length;i++) {
op = opt.options[i];
for (var i=0;i<_options.length;i++) {
op = _options[i];
if (typeof op === "string" && op === currentVal) {
return {value:currentVal}
} else if (op.value === currentVal) {
@@ -411,8 +681,8 @@
currentValues[v] = true;
}
});
for (var i=0;i<opt.options.length;i++) {
op = opt.options[i];
for (var i=0;i<_options.length;i++) {
op = _options[i];
var val = typeof op === "string" ? op : op.value;
if (currentValues.hasOwnProperty(val)) {
delete currentValues[val];
@@ -427,6 +697,7 @@
}
var nlsd = false;
let contextStoreOptions;
$.widget( "nodered.typedInput", {
_create: function() {
@@ -438,7 +709,7 @@
}
}
var contextStores = RED.settings.context.stores;
var contextOptions = contextStores.map(function(store) {
contextStoreOptions = contextStores.map(function(store) {
return {value:store,label: store, icon:'<i class="red-ui-typedInput-icon fa fa-database"></i>'}
}).sort(function(A,B) {
if (A.value === RED.settings.context.default) {
@@ -449,13 +720,17 @@
return A.value.localeCompare(B.value);
}
})
if (contextOptions.length < 2) {
if (contextStoreOptions.length < 2) {
allOptions.flow.options = [];
allOptions.global.options = [];
} else {
allOptions.flow.options = contextOptions;
allOptions.global.options = contextOptions;
allOptions.flow.options = contextStoreOptions;
allOptions.global.options = contextStoreOptions;
}
// Translate timestamp options
allOptions.date.options.forEach(opt => {
opt.label = RED._("typedInput.date.format." + (opt.value || 'timestamp'), {defaultValue: opt.label})
})
}
nlsd = true;
var that = this;
@@ -544,7 +819,7 @@
that.element.trigger('paste',evt);
});
this.input.on('keydown', function(evt) {
if (that.typeMap[that.propertyType].autoComplete) {
if (that.typeMap[that.propertyType].autoComplete || that.input.hasClass('red-ui-autoComplete')) {
return
}
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
@@ -838,7 +1113,9 @@
if (this.optionMenu) {
this.optionMenu.remove();
}
this.menu.remove();
if (this.menu) {
this.menu.remove();
}
this.uiSelect.remove();
},
types: function(types) {
@@ -871,7 +1148,7 @@
this.menu = this._createMenu(this.typeList,{},function(v) { that.type(v) });
if (currentType && !this.typeMap.hasOwnProperty(currentType)) {
if (!firstCall) {
this.type(this.typeList[0].value);
this.type(this.typeList[0]?.value || ""); // permit empty typeList
}
} else {
this.propertyType = null;
@@ -908,6 +1185,11 @@
var selectedOption = [];
var valueToCheck = value;
if (opt.options) {
let _options = opt.options
if (typeof opt.options === "function") {
_options = opt.options.call(this)
}
if (opt.hasValue && opt.parse) {
var parts = opt.parse(value);
if (this.options.debug) { console.log(this.identifier,"new parse",parts) }
@@ -921,8 +1203,8 @@
checkValues = valueToCheck.split(",");
}
checkValues.forEach(function(valueToCheck) {
for (var i=0;i<opt.options.length;i++) {
var op = opt.options[i];
for (var i=0;i<_options.length;i++) {
var op = _options[i];
if (typeof op === "string") {
if (op === valueToCheck || op === ""+valueToCheck) {
selectedOption.push(that.activeOptions[op]);
@@ -957,7 +1239,7 @@
},
type: function(type) {
if (!arguments.length) {
return this.propertyType;
return this.propertyType || this.options?.default || '';
} else {
var that = this;
if (this.options.debug) { console.log(this.identifier,"----- SET TYPE -----",type) }
@@ -967,6 +1249,9 @@
// If previousType is !null, then this is a change of the type, rather than the initialisation
var previousType = this.typeMap[this.propertyType];
previousValue = this.input.val();
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
if (previousType && this.typeChanged) {
if (this.options.debug) { console.log(this.identifier,"typeChanged",{previousType,previousValue}) }
@@ -1013,7 +1298,9 @@
this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||""))
}
if (previousType.autoComplete) {
this.input.autoComplete("destroy");
if (this.input.hasClass('red-ui-autoComplete')) {
this.input.autoComplete("destroy");
}
}
}
this.propertyType = type;
@@ -1053,6 +1340,10 @@
this.optionMenu = null;
}
if (opt.options) {
let _options = opt.options
if (typeof _options === "function") {
_options = opt.options.call(this);
}
if (this.optionExpandButton) {
this.optionExpandButton.hide();
this.optionExpandButton.shown = false;
@@ -1069,7 +1360,7 @@
this.valueLabelContainer.hide();
}
this.activeOptions = {};
opt.options.forEach(function(o) {
_options.forEach(function(o) {
if (typeof o === 'string') {
that.activeOptions[o] = {label:o,value:o};
} else {
@@ -1089,7 +1380,7 @@
if (validValues) {
that._updateOptionSelectLabel(validValues)
} else {
op = opt.options[0];
op = _options[0] || {value:""}; // permit zero options
if (typeof op === "string") {
this.value(op);
that._updateOptionSelectLabel({value:op});
@@ -1108,7 +1399,7 @@
that._updateOptionSelectLabel(validValues);
}
} else {
var selectedOption = this.optionValue||opt.options[0];
var selectedOption = this.optionValue||_options[0];
if (opt.parse) {
var selectedOptionObj = typeof selectedOption === "string"?{value:selectedOption}:selectedOption
var parts = opt.parse(this.input.val(),selectedOptionObj);
@@ -1141,8 +1432,18 @@
} else {
this.optionSelectTrigger.hide();
}
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: searchFunction,
minLength: 0
})
}
}
this.optionMenu = this._createMenu(opt.options,opt,function(v){
this.optionMenu = this._createMenu(_options,opt,function(v){
if (!opt.multiple) {
that._updateOptionSelectLabel(that.activeOptions[v]);
if (!opt.hasValue) {
@@ -1183,8 +1484,12 @@
this.valueLabelContainer.hide();
this.elementDiv.show();
if (opt.autoComplete) {
let searchFunction = opt.autoComplete
if (searchFunction.length === 0) {
searchFunction = opt.autoComplete.call(this)
}
this.input.autoComplete({
search: opt.autoComplete,
search: searchFunction,
minLength: 0
})
}

View File

@@ -326,47 +326,78 @@ RED.editor = (function() {
/**
* Create a config-node select box for this property
* @param node - the node being edited
* @param property - the name of the field
* @param type - the type of the config-node
* @param {Object} node - the node being edited
* @param {String} property - the name of the node property
* @param {String} type - the type of the config-node
* @param {"node-config-input"|"node-input"|"node-input-subflow-env"} prefix - the prefix to use in the input element ids
* @param {Function} [filter] - a function to filter the list of config nodes
* @param {Object} [env] - the environment variable object (only used for subflow env vars)
*/
function prepareConfigNodeSelect(node,property,type,prefix,filter) {
var input = $("#"+prefix+"-"+property);
if (input.length === 0 ) {
function prepareConfigNodeSelect(node, property, type, prefix, filter, env) {
let nodeValue
if (prefix === 'node-input-subflow-env') {
nodeValue = env?.value
} else {
nodeValue = node[property]
}
const buttonId = `${prefix}-lookup-${property}`
const selectId = prefix + '-' + property
const input = $(`#${selectId}`);
if (input.length === 0) {
return;
}
var newWidth = input.width();
var attrStyle = input.attr('style');
var m;
const attrStyle = input.attr('style');
let newWidth;
let m;
if ((m = /(^|\s|;)width\s*:\s*([^;]+)/i.exec(attrStyle)) !== null) {
newWidth = m[2].trim();
} else {
newWidth = "70%";
}
var outerWrap = $("<div></div>").css({
const outerWrap = $("<div></div>").css({
width: newWidth,
display:'inline-flex'
display: 'inline-flex'
});
var select = $('<select id="'+prefix+'-'+property+'"></select>').appendTo(outerWrap);
const select = $('<select id="' + selectId + '"></select>').appendTo(outerWrap);
input.replaceWith(outerWrap);
// set the style attr directly - using width() on FF causes a value of 114%...
select.css({
'flex-grow': 1
});
updateConfigNodeSelect(property,type,node[property],prefix,filter);
$('<a id="'+prefix+'-lookup-'+property+'" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
.css({"margin-left":"10px"})
updateConfigNodeSelect(property, type, nodeValue, prefix, filter);
const disableButton = function(disabled) {
btn.prop( "disabled", !!disabled)
btn.toggleClass("disabled", !!disabled)
}
// create the edit button
const btn = $('<a id="' + buttonId + '" class="red-ui-button"><i class="fa fa-pencil"></i></a>')
.css({ "margin-left": "10px" })
.appendTo(outerWrap);
$('#'+prefix+'-lookup-'+property).on("click", function(e) {
showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node);
// add the click handler
btn.on("click", function (e) {
const selectedOpt = select.find(":selected")
if (selectedOpt.data('env')) { return } // don't show the dialog for env vars items (MVP. Future enhancement: lookup the env, if present, show the associated edit dialog)
if (btn.prop("disabled")) { return }
showEditConfigNodeDialog(property, type, selectedOpt.val(), prefix, node);
e.preventDefault();
});
// dont permit the user to click the button if the selected option is an env var
select.on("change", function () {
const selectedOpt = select.find(":selected")
if (selectedOpt?.data('env')) {
disableButton(true)
} else {
disableButton(false)
}
});
var label = "";
var configNode = RED.nodes.node(node[property]);
var node_def = RED.nodes.getType(type);
var configNode = RED.nodes.node(nodeValue);
if (configNode) {
label = RED.utils.getNodeLabel(configNode,configNode.id);
label = RED.utils.getNodeLabel(configNode, configNode.id);
}
input.val(label);
}
@@ -768,12 +799,9 @@ RED.editor = (function() {
}
function defaultConfigNodeSort(A,B) {
if (A.__label__ < B.__label__) {
return -1;
} else if (A.__label__ > B.__label__) {
return 1;
}
return 0;
// sort case insensitive so that `[env] node-name` items are at the top and
// not mixed inbetween the the lower and upper case items
return (A.__label__ || '').localeCompare((B.__label__ || ''), undefined, {sensitivity: 'base'})
}
function updateConfigNodeSelect(name,type,value,prefix,filter) {
@@ -788,7 +816,7 @@ RED.editor = (function() {
}
$("#"+prefix+"-"+name).val(value);
} else {
let inclSubflowEnvvars = false
var select = $("#"+prefix+"-"+name);
var node_def = RED.nodes.getType(type);
select.children().remove();
@@ -796,6 +824,7 @@ RED.editor = (function() {
var activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
if (!activeWorkspace) {
activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
inclSubflowEnvvars = true
}
var configNodes = [];
@@ -811,6 +840,31 @@ RED.editor = (function() {
}
}
});
// as includeSubflowEnvvars is true, this is a subflow.
// include any 'conf-types' env vars as a list of avaiable configs
// in the config dropdown as `[env] node-name`
if (inclSubflowEnvvars && activeWorkspace.env) {
const parentEnv = activeWorkspace.env.filter(env => env.ui?.type === 'conf-types' && env.type === type)
if (parentEnv && parentEnv.length > 0) {
const locale = RED.i18n.lang()
for (let i = 0; i < parentEnv.length; i++) {
const tenv = parentEnv[i]
const ui = tenv.ui || {}
const labels = ui.label || {}
const labelText = RED.editor.envVarList.lookupLabel(labels, labels["en-US"] || tenv.name, locale)
const config = {
env: tenv,
id: '${' + parentEnv[0].name + '}',
type: type,
label: labelText,
__label__: `[env] ${labelText}`
}
configNodes.push(config)
}
}
}
var configSortFn = defaultConfigNodeSort;
if (typeof node_def.sort == "function") {
configSortFn = node_def.sort;
@@ -822,7 +876,10 @@ RED.editor = (function() {
}
configNodes.forEach(function(cn) {
$('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
const option = $('<option value="'+cn.id+'"'+(value==cn.id?" selected":"")+'></option>').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select);
if (cn.env) {
option.data('env', cn.env) // set a data attribute to indicate this is an env var (to inhibit the edit button)
}
delete cn.__label__;
});
@@ -1483,9 +1540,16 @@ RED.editor = (function() {
}
RED.tray.close(function() {
var filter = null;
if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
filter = function(n) {
return editContext._def.defaults[configProperty].filter.call(editContext,n);
// when editing a config via subflow edit panel, the `configProperty` will not
// necessarily be a property of the editContext._def.defaults object
// Also, when editing via dashboard sidebar, editContext can be null
// so we need to guard both scenarios
if (editContext?._def) {
const isSubflow = (editContext._def.type === 'subflow' || /subflow:.*/.test(editContext._def.type))
if (editContext && !isSubflow && typeof editContext._def.defaults?.[configProperty]?.filter === 'function') {
filter = function(n) {
return editContext._def.defaults[configProperty].filter.call(editContext,n);
}
}
}
updateConfigNodeSelect(configProperty,configType,editing_config_node.id,prefix,filter);
@@ -1546,7 +1610,7 @@ RED.editor = (function() {
RED.history.push(historyEvent);
RED.tray.close(function() {
var filter = null;
if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
if (editContext && typeof editContext._def.defaults[configProperty]?.filter === 'function') {
filter = function(n) {
return editContext._def.defaults[configProperty].filter.call(editContext,n);
}
@@ -2087,6 +2151,7 @@ RED.editor = (function() {
}
},
editBuffer: function(options) { showTypeEditor("_buffer", options) },
getEditStack: function () { return [...editStack] },
buildEditForm: buildEditForm,
validateNode: validateNode,
updateNodeProperties: updateNodeProperties,
@@ -2131,6 +2196,7 @@ RED.editor = (function() {
filteredEditPanes[type] = filter
}
editPanes[type] = definition;
}
},
prepareConfigNodeSelect: prepareConfigNodeSelect,
}
})();

View File

@@ -585,7 +585,7 @@ RED.editor.codeEditor.monaco = (function() {
createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range),
createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range),
createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range),
createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME|}");','Get env variable value',range),
createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME,NR_SUBFLOW_NAME,NR_SUBFLOW_ID,NR_SUBFLOW_PATH|}");','Get env variable value',range),
createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});',
["```typescript",
"RED.util.cloneMessage<T extends registry.NodeMessage>(msg: T): T",

View File

@@ -1,8 +1,9 @@
RED.editor.envVarList = (function() {
var currentLocale = 'en-US';
var DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
var DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
const DEFAULT_ENV_TYPE_LIST = ['str','num','bool','json','bin','env'];
const DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES = ['str','num','bool','json','bin','env','conf-types'];
const DEFAULT_ENV_TYPE_LIST_INC_CRED = ['str','num','bool','json','bin','env','cred','jsonata'];
/**
* Create env var edit interface
@@ -10,8 +11,8 @@ RED.editor.envVarList = (function() {
* @param node - subflow node
*/
function buildPropertiesList(envContainer, node) {
var isTemplateNode = (node.type === "subflow");
if(RED.editor.envVarList.debug) { console.log('envVarList: buildPropertiesList', envContainer, node) }
const isTemplateNode = (node.type === "subflow");
envContainer
.css({
@@ -83,7 +84,14 @@ RED.editor.envVarList = (function() {
// if `opt.ui` does not exist, then apply defaults. If these
// defaults do not change then they will get stripped off
// before saving.
if (opt.type === 'cred') {
if (opt.type === 'conf-types') {
opt.ui = opt.ui || {
icon: "fa fa-cog",
type: "conf-types",
opts: {opts:[]}
}
opt.ui.type = "conf-types";
} else if (opt.type === 'cred') {
opt.ui = opt.ui || {
icon: "",
type: "cred"
@@ -119,7 +127,7 @@ RED.editor.envVarList = (function() {
}
});
buildEnvEditRow(uiRow, opt.ui, nameField, valueField);
buildEnvEditRow(uiRow, opt, nameField, valueField);
nameField.trigger('change');
}
},
@@ -181,21 +189,23 @@ RED.editor.envVarList = (function() {
* @param nameField - name field of env var
* @param valueField - value field of env var
*/
function buildEnvEditRow(container, ui, nameField, valueField) {
function buildEnvEditRow(container, opt, nameField, valueField) {
const ui = opt.ui
if(RED.editor.envVarList.debug) { console.log('envVarList: buildEnvEditRow', container, ui, nameField, valueField) }
container.addClass("red-ui-editor-subflow-env-ui-row")
var topRow = $('<div></div>').appendTo(container);
$('<div></div>').appendTo(topRow);
$('<div>').text(RED._("editor.icon")).appendTo(topRow);
$('<div>').text(RED._("editor.label")).appendTo(topRow);
$('<div>').text(RED._("editor.inputType")).appendTo(topRow);
$('<div class="red-env-ui-input-type-col">').text(RED._("editor.inputType")).appendTo(topRow);
var row = $('<div></div>').appendTo(container);
$('<div><i class="red-ui-editableList-item-handle fa fa-bars"></i></div>').appendTo(row);
var typeOptions = {
'input': {types:DEFAULT_ENV_TYPE_LIST},
'select': {opts:[]},
'spinner': {},
'cred': {}
'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
'select': {opts:[]},
'spinner': {},
'cred': {}
};
if (ui.opts) {
typeOptions[ui.type] = ui.opts;
@@ -260,15 +270,16 @@ RED.editor.envVarList = (function() {
labelInput.attr("placeholder",$(this).val())
});
var inputCell = $('<div></div>').appendTo(row);
var inputCellInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
var inputCell = $('<div class="red-env-ui-input-type-col"></div>').appendTo(row);
var uiInputTypeInput = $('<input type="text">').css("width","100%").appendTo(inputCell);
if (ui.type === "input") {
inputCellInput.val(ui.opts.types.join(","));
uiInputTypeInput.val(ui.opts.types.join(","));
}
var checkbox;
var selectBox;
inputCellInput.typedInput({
// the options presented in the UI section for an "input" type selection
uiInputTypeInput.typedInput({
types: [
{
value:"input",
@@ -429,7 +440,7 @@ RED.editor.envVarList = (function() {
}
});
ui.opts.opts = vals;
inputCellInput.typedInput('value',Date.now())
uiInputTypeInput.typedInput('value',Date.now())
}
}
}
@@ -496,12 +507,13 @@ RED.editor.envVarList = (function() {
} else {
delete ui.opts.max;
}
inputCellInput.typedInput('value',Date.now())
uiInputTypeInput.typedInput('value',Date.now())
}
}
}
}
},
'conf-types',
{
value:"none",
label:RED._("editor.inputs.none"), icon:"fa fa-times",hasValue:false
@@ -519,14 +531,20 @@ RED.editor.envVarList = (function() {
// In the case of 'input' type, the typedInput uses the multiple-option
// mode. Its value needs to be set to a comma-separately list of the
// selected options.
inputCellInput.typedInput('value',ui.opts.types.join(","))
uiInputTypeInput.typedInput('value',ui.opts.types.join(","))
} else if (ui.type === 'conf-types') {
// In the case of 'conf-types' type, the typedInput will be populated
// with a list of all config nodes types installed.
// Restore the value to the last selected type
uiInputTypeInput.typedInput('value', opt.type)
} else {
// No other type cares about `value`, but doing this will
// force a refresh of the label now that `ui.opts` has
// been updated.
inputCellInput.typedInput('value',Date.now())
uiInputTypeInput.typedInput('value',Date.now())
}
if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:typedinputtypechange. ui.type = ' + ui.type) }
switch (ui.type) {
case 'input':
valueField.typedInput('types',ui.opts.types);
@@ -544,7 +562,7 @@ RED.editor.envVarList = (function() {
valueField.typedInput('types',['cred']);
break;
default:
valueField.typedInput('types',DEFAULT_ENV_TYPE_LIST)
valueField.typedInput('types', DEFAULT_ENV_TYPE_LIST);
}
if (ui.type === 'checkbox') {
valueField.typedInput('type','bool');
@@ -556,8 +574,46 @@ RED.editor.envVarList = (function() {
}
}).on("change", function(evt,type) {
if (ui.type === 'input') {
var types = inputCellInput.typedInput('value');
const selectedType = $(this).typedInput('type') // the UI typedInput type
if(RED.editor.envVarList.debug) { console.log('envVarList: inputCellInput on:change. selectedType = ' + selectedType) }
if (selectedType === 'conf-types') {
const selectedConfigType = $(this).typedInput('value') || opt.type
let activeWorkspace = RED.nodes.workspace(RED.workspaces.active());
if (!activeWorkspace) {
activeWorkspace = RED.nodes.subflow(RED.workspaces.active());
}
// get a list of all config nodes matching the selectedValue
const configNodes = [];
RED.nodes.eachConfig(function(config) {
if (config.type == selectedConfigType && (!config.z || config.z === activeWorkspace.id)) {
const modulePath = config._def?.set?.id || ''
let label = RED.utils.getNodeLabel(config, config.id) || config.id;
label += config.d ? ' ['+RED._('workspace.disabled')+']' : '';
const _config = {
_type: selectedConfigType,
value: config.id,
label: label,
title: modulePath ? modulePath + ' - ' + label : label,
enabled: config.d !== true,
disabled: config.d === true,
}
configNodes.push(_config);
}
});
const tiTypes = {
value: selectedConfigType,
label: "config",
icon: "fa fa-cog",
options: configNodes,
}
valueField.typedInput('types', [tiTypes]);
valueField.typedInput('type', selectedConfigType);
valueField.typedInput('value', opt.value);
} else if (ui.type === 'input') {
var types = uiInputTypeInput.typedInput('value');
ui.opts.types = (types === "") ? ["str"] : types.split(",");
valueField.typedInput('types',ui.opts.types);
}
@@ -569,7 +625,7 @@ RED.editor.envVarList = (function() {
})
// Set the input to the right type. This will trigger the 'typedinputtypechange'
// event handler (just above ^^) to update the value if needed
inputCellInput.typedInput('type',ui.type)
uiInputTypeInput.typedInput('type',ui.type)
}
function setLocale(l, list) {

View File

@@ -153,10 +153,6 @@ RED.envVar = (function() {
}
function init(done) {
if (!RED.user.hasPermission("settings.write")) {
RED.notify(RED._("user.errors.settings"),"error");
return;
}
RED.userSettings.add({
id:'envvar',
title: RED._("env-var.environment"),

View File

@@ -248,86 +248,106 @@ RED.palette.editor = (function() {
var moduleInfo = nodeEntries[module].info;
var nodeEntry = nodeEntries[module].elements;
if (nodeEntry) {
var activeTypeCount = 0;
var typeCount = 0;
var errorCount = 0;
nodeEntry.errorList.empty();
nodeEntries[module].totalUseCount = 0;
nodeEntries[module].setUseCount = {};
if (moduleInfo.plugin) {
nodeEntry.enableButton.hide();
nodeEntry.removeButton.show();
for (var setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
var inUseCount = 0;
var set = moduleInfo.sets[setName];
var setElements = nodeEntry.sets[setName];
if (set.err) {
errorCount++;
var errMessage = set.err;
if (set.err.message) {
errMessage = set.err.message;
} else if (set.err.code) {
errMessage = set.err.code;
let pluginCount = 0;
for (let setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
let set = moduleInfo.sets[setName];
if (set.plugins) {
pluginCount += set.plugins.length;
}
$("<li>").text(errMessage).appendTo(nodeEntry.errorList);
}
if (set.enabled) {
activeTypeCount += set.types.length;
}
typeCount += set.types.length;
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
var t = moduleInfo.sets[setName].types[i];
inUseCount += (typesInUse[t]||0);
var swatch = setElements.swatches[t];
}
nodeEntry.setCount.text(RED._('palette.editor.pluginCount',{count:pluginCount,label:pluginCount}));
} else {
var activeTypeCount = 0;
var typeCount = 0;
var errorCount = 0;
nodeEntry.errorList.empty();
nodeEntries[module].totalUseCount = 0;
nodeEntries[module].setUseCount = {};
for (var setName in moduleInfo.sets) {
if (moduleInfo.sets.hasOwnProperty(setName)) {
var inUseCount = 0;
const set = moduleInfo.sets[setName];
const setElements = nodeEntry.sets[setName]
if (set.err) {
errorCount++;
var errMessage = set.err;
if (set.err.message) {
errMessage = set.err.message;
} else if (set.err.code) {
errMessage = set.err.code;
}
$("<li>").text(errMessage).appendTo(nodeEntry.errorList);
}
if (set.enabled) {
var def = RED.nodes.getType(t);
if (def && def.color) {
swatch.css({background:RED.utils.getNodeColor(t,def)});
swatch.css({border: "1px solid "+getContrastingBorder(swatch.css('backgroundColor'))})
activeTypeCount += set.types.length;
}
typeCount += set.types.length;
for (var i=0;i<moduleInfo.sets[setName].types.length;i++) {
var t = moduleInfo.sets[setName].types[i];
inUseCount += (typesInUse[t]||0);
if (setElements && set.enabled) {
var def = RED.nodes.getType(t);
if (def && def.color) {
setElements.swatches[t].css({background:RED.utils.getNodeColor(t,def)});
setElements.swatches[t].css({border: "1px solid "+getContrastingBorder(setElements.swatches[t].css('backgroundColor'))})
}
}
}
}
nodeEntries[module].setUseCount[setName] = inUseCount;
nodeEntries[module].totalUseCount += inUseCount;
nodeEntries[module].setUseCount[setName] = inUseCount;
nodeEntries[module].totalUseCount += inUseCount;
if (inUseCount > 0) {
setElements.enableButton.text(RED._('palette.editor.inuse'));
setElements.enableButton.addClass('disabled');
} else {
setElements.enableButton.removeClass('disabled');
if (set.enabled) {
setElements.enableButton.text(RED._('palette.editor.disable'));
} else {
setElements.enableButton.text(RED._('palette.editor.enable'));
if (setElements) {
if (inUseCount > 0) {
setElements.enableButton.text(RED._('palette.editor.inuse'));
setElements.enableButton.addClass('disabled');
} else {
setElements.enableButton.removeClass('disabled');
if (set.enabled) {
setElements.enableButton.text(RED._('palette.editor.disable'));
} else {
setElements.enableButton.text(RED._('palette.editor.enable'));
}
}
setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
}
}
setElements.setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
}
}
if (errorCount === 0) {
nodeEntry.errorRow.hide()
} else {
nodeEntry.errorRow.show();
}
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
if (nodeEntries[module].totalUseCount > 0) {
nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
nodeEntry.enableButton.addClass('disabled');
nodeEntry.removeButton.hide();
} else {
nodeEntry.enableButton.removeClass('disabled');
if (moduleInfo.local) {
nodeEntry.removeButton.css('display', 'inline-block');
}
if (activeTypeCount === 0) {
nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
if (errorCount === 0) {
nodeEntry.errorRow.hide()
} else {
nodeEntry.enableButton.text(RED._('palette.editor.disableall'));
nodeEntry.errorRow.show();
}
var nodeCount = (activeTypeCount === typeCount)?typeCount:activeTypeCount+" / "+typeCount;
nodeEntry.setCount.text(RED._('palette.editor.nodeCount',{count:typeCount,label:nodeCount}));
if (nodeEntries[module].totalUseCount > 0) {
nodeEntry.enableButton.text(RED._('palette.editor.inuse'));
nodeEntry.enableButton.addClass('disabled');
nodeEntry.removeButton.hide();
} else {
nodeEntry.enableButton.removeClass('disabled');
if (moduleInfo.local) {
nodeEntry.removeButton.css('display', 'inline-block');
}
if (activeTypeCount === 0) {
nodeEntry.enableButton.text(RED._('palette.editor.enableall'));
} else {
nodeEntry.enableButton.text(RED._('palette.editor.disableall'));
}
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
}
nodeEntry.container.toggleClass("disabled",(activeTypeCount === 0));
}
}
if (moduleInfo.pending_version) {
@@ -678,6 +698,33 @@ RED.palette.editor = (function() {
}
}
})
RED.events.on("registry:plugin-module-added", function(module) {
if (!nodeEntries.hasOwnProperty(module)) {
nodeEntries[module] = {info:RED.plugins.getModule(module)};
var index = [module];
for (var s in nodeEntries[module].info.sets) {
if (nodeEntries[module].info.sets.hasOwnProperty(s)) {
index.push(s);
index = index.concat(nodeEntries[module].info.sets[s].types)
}
}
nodeEntries[module].index = index.join(",").toLowerCase();
nodeList.editableList('addItem', nodeEntries[module]);
} else {
_refreshNodeModule(module);
}
for (var i=0;i<filteredList.length;i++) {
if (filteredList[i].info.id === module) {
var installButton = filteredList[i].elements.installButton;
installButton.addClass('disabled');
installButton.text(RED._('palette.editor.installed'));
break;
}
}
});
}
var settingsPane;
@@ -804,6 +851,7 @@ RED.palette.editor = (function() {
errorRow: errorRow,
errorList: errorList,
setCount: setCount,
setButton: setButton,
container: container,
shade: shade,
versionSpan: versionSpan,
@@ -814,49 +862,88 @@ RED.palette.editor = (function() {
if (container.hasClass('expanded')) {
container.removeClass('expanded');
contentRow.slideUp();
setTimeout(() => {
contentRow.empty()
}, 200)
object.elements.sets = {}
} else {
container.addClass('expanded');
populateSetList()
contentRow.slideDown();
}
})
var setList = Object.keys(entry.sets)
setList.sort(function(A,B) {
return A.toLowerCase().localeCompare(B.toLowerCase());
});
setList.forEach(function(setName) {
var set = entry.sets[setName];
var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
var typeSwatches = {};
set.types.forEach(function(t) {
var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
$('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
})
var enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
enableButton.on("click", function(evt) {
evt.preventDefault();
if (object.setUseCount[setName] === 0) {
var currentSet = RED.nodes.registry.getNodeSet(set.id);
shade.show();
var newState = !currentSet.enabled
changeNodeState(set.id,newState,shade,function(xhr){
if (xhr) {
if (xhr.responseJSON) {
RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
const populateSetList = function () {
var setList = Object.keys(entry.sets)
setList.sort(function(A,B) {
return A.toLowerCase().localeCompare(B.toLowerCase());
});
setList.forEach(function(setName) {
var set = entry.sets[setName];
var setRow = $('<div>',{class:"red-ui-palette-module-set"}).appendTo(contentRow);
var buttonGroup = $('<div>',{class:"red-ui-palette-module-set-button-group"}).appendTo(setRow);
var typeSwatches = {};
let enableButton;
if (set.types) {
set.types.forEach(function(t) {
var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
typeSwatches[t] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
if (set.enabled) {
var def = RED.nodes.getType(t);
if (def && def.color) {
typeSwatches[t].css({background:RED.utils.getNodeColor(t,def)});
typeSwatches[t].css({border: "1px solid "+getContrastingBorder(typeSwatches[t].css('backgroundColor'))})
}
}
});
}
})
$('<span>',{class:"red-ui-palette-module-type-node"}).text(t).appendTo(typeDiv);
})
enableButton = $('<a href="#" class="red-ui-button red-ui-button-small"></a>').appendTo(buttonGroup);
enableButton.on("click", function(evt) {
evt.preventDefault();
if (object.setUseCount[setName] === 0) {
var currentSet = RED.nodes.registry.getNodeSet(set.id);
shade.show();
var newState = !currentSet.enabled
changeNodeState(set.id,newState,shade,function(xhr){
if (xhr) {
if (xhr.responseJSON) {
RED.notify(RED._('palette.editor.errors.'+(newState?'enable':'disable')+'Failed',{module: id,message:xhr.responseJSON.message}));
}
}
});
}
})
object.elements.sets[set.name] = {
setRow: setRow,
enableButton: enableButton,
swatches: typeSwatches
};
});
if (object.setUseCount[setName] > 0) {
enableButton.text(RED._('palette.editor.inuse'));
enableButton.addClass('disabled');
} else {
enableButton.removeClass('disabled');
if (set.enabled) {
enableButton.text(RED._('palette.editor.disable'));
} else {
enableButton.text(RED._('palette.editor.enable'));
}
}
setRow.toggleClass("red-ui-palette-module-set-disabled",!set.enabled);
}
if (set.plugins) {
set.plugins.forEach(function(p) {
var typeDiv = $('<div>',{class:"red-ui-palette-module-type"}).appendTo(setRow);
// typeSwatches[p.id] = $('<span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
$('<span><i class="fa fa-puzzle-piece" aria-hidden="true"></i> </span>',{class:"red-ui-palette-module-type-swatch"}).appendTo(typeDiv);
$('<span>',{class:"red-ui-palette-module-type-node"}).text(p.id).appendTo(typeDiv);
})
}
object.elements.sets[set.name] = {
setRow: setRow,
enableButton: enableButton,
swatches: typeSwatches
};
});
}
enableButton.on("click", function(evt) {
evt.preventDefault();
if (object.totalUseCount === 0) {
@@ -1226,7 +1313,55 @@ RED.palette.editor = (function() {
}
}
]
}); }
});
}
} else {
// dedicated list management for plugins
if (entry.plugin) {
let e = nodeEntries[entry.name];
if (e) {
nodeList.editableList('removeItem', e);
delete nodeEntries[entry.name];
}
// We assume that a plugin that implements onremove
// cleans the editor accordingly of its left-overs.
let found_onremove = true;
let keys = Object.keys(entry.sets);
keys.forEach((key) => {
let set = entry.sets[key];
for (let i=0; i<set.plugins?.length; i++) {
let plgn = RED.plugins.getPlugin(set.plugins[i].id);
if (plgn && plgn.onremove && typeof plgn.onremove === 'function') {
plgn.onremove();
} else {
if (plgn && plgn.onadd && typeof plgn.onadd === 'function') {
// if there's no 'onadd', there shouldn't be any left-overs
found_onremove = false;
}
}
}
});
if (!found_onremove) {
let removeNotify = RED.notify("Removed plugin " + entry.name + ". Please reload the editor to clear left-overs.",{
modal: true,
fixed: true,
type: 'warning',
buttons: [
{
text: "Understood",
class:"primary",
click: function(e) {
removeNotify.close();
}
}
]
});
}
}
}
})
notification.close();

View File

@@ -35,6 +35,10 @@ RED.palette = (function() {
var categoryContainers = {};
var sidebarControls;
let paletteState = { filter: "", collapsed: [] };
let filterRefreshTimeout
function createCategory(originalCategory,rootCategory,category,ns) {
if ($("#red-ui-palette-base-category-"+rootCategory).length === 0) {
createCategoryContainer(originalCategory,rootCategory, ns+":palette.label."+rootCategory);
@@ -60,20 +64,57 @@ RED.palette = (function() {
catDiv.data('label',label);
categoryContainers[category] = {
container: catDiv,
close: function() {
hide: function (instant) {
if (instant) {
catDiv.hide()
} else {
catDiv.slideUp()
}
},
show: function () {
catDiv.show()
},
isOpen: function () {
return !!catDiv.hasClass("red-ui-palette-open")
},
getNodeCount: function (visibleOnly) {
const nodes = catDiv.find(".red-ui-palette-node")
if (visibleOnly) {
return nodes.filter(function() { return $(this).css('display') !== 'none'}).length
} else {
return nodes.length
}
},
close: function(instant, skipSaveState) {
catDiv.removeClass("red-ui-palette-open");
catDiv.addClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideUp();
if (instant) {
$("#red-ui-palette-base-category-"+category).hide();
} else {
$("#red-ui-palette-base-category-"+category).slideUp();
}
$("#red-ui-palette-header-"+category+" i").removeClass("expanded");
if (!skipSaveState) {
if (!paletteState.collapsed.includes(category)) {
paletteState.collapsed.push(category);
savePaletteState();
}
}
},
open: function() {
open: function(skipSaveState) {
catDiv.addClass("red-ui-palette-open");
catDiv.removeClass("red-ui-palette-closed");
$("#red-ui-palette-base-category-"+category).slideDown();
$("#red-ui-palette-header-"+category+" i").addClass("expanded");
if (!skipSaveState) {
if (paletteState.collapsed.includes(category)) {
paletteState.collapsed.splice(paletteState.collapsed.indexOf(category), 1);
savePaletteState();
}
}
},
toggle: function() {
if (catDiv.hasClass("red-ui-palette-open")) {
if (categoryContainers[category].isOpen()) {
categoryContainers[category].close();
} else {
categoryContainers[category].open();
@@ -415,8 +456,16 @@ RED.palette = (function() {
var categoryNode = $("#red-ui-palette-container-"+rootCategory);
if (categoryNode.find(".red-ui-palette-node").length === 1) {
categoryContainers[rootCategory].open();
if (!paletteState?.collapsed?.includes(rootCategory)) {
categoryContainers[rootCategory].open();
} else {
categoryContainers[rootCategory].close(true);
}
}
clearTimeout(filterRefreshTimeout)
filterRefreshTimeout = setTimeout(() => {
refreshFilter()
}, 200)
}
}
@@ -516,7 +565,8 @@ RED.palette = (function() {
paletteNode.css("backgroundColor", sf.color);
}
function filterChange(val) {
function refreshFilter() {
const val = $("#red-ui-palette-search input").val()
var re = new RegExp(val.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'),'i');
$("#red-ui-palette-container .red-ui-palette-node").each(function(i,el) {
var currentLabel = $(el).attr("data-palette-label");
@@ -528,16 +578,26 @@ RED.palette = (function() {
}
});
for (var category in categoryContainers) {
for (let category in categoryContainers) {
if (categoryContainers.hasOwnProperty(category)) {
if (categoryContainers[category].container
.find(".red-ui-palette-node")
.filter(function() { return $(this).css('display') !== 'none'}).length === 0) {
categoryContainers[category].close();
categoryContainers[category].container.slideUp();
const categorySection = categoryContainers[category]
if (categorySection.getNodeCount(true) === 0) {
categorySection.hide()
} else {
categoryContainers[category].open();
categoryContainers[category].container.show();
categorySection.show()
if (val) {
// There is a filter being applied and it has matched
// something in this category - show the contents
categorySection.open(true)
} else {
// No filter. Only show the category if it isn't in lastState
if (!paletteState.collapsed.includes(category)) {
categorySection.open(true)
} else if (categorySection.isOpen()) {
// This section should be collapsed but isn't - so make it so
categorySection.close(true, true)
}
}
}
}
}
@@ -553,6 +613,9 @@ RED.palette = (function() {
$("#red-ui-palette > .red-ui-palette-spinner").show();
RED.events.on('logout', function () {
RED.settings.removeLocal('palette-state')
})
RED.events.on('registry:node-type-added', function(nodeType) {
var def = RED.nodes.getType(nodeType);
@@ -596,14 +659,14 @@ RED.palette = (function() {
RED.events.on("subflows:change",refreshSubflow);
$("#red-ui-palette-search input").searchBox({
delay: 100,
change: function() {
filterChange($(this).val());
refreshFilter();
paletteState.filter = $(this).val();
savePaletteState();
}
})
});
sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
@@ -669,7 +732,23 @@ RED.palette = (function() {
togglePalette(state);
}
});
try {
paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
if (paletteState.filter) {
// Apply the category filter
$("#red-ui-palette-search input").searchBox("value", paletteState.filter);
}
} catch (error) {
console.error("Unexpected error loading palette state from localStorage: ", error);
}
setTimeout(() => {
// Lazily tidy up any categories that haven't been reloaded
paletteState.collapsed = paletteState.collapsed.filter(category => !!categoryContainers[category])
savePaletteState()
}, 10000)
}
function togglePalette(state) {
if (!state) {
$("#red-ui-main-container").addClass("red-ui-palette-closed");
@@ -689,6 +768,15 @@ RED.palette = (function() {
})
return categories;
}
function savePaletteState() {
try {
RED.settings.setLocal("palette-state", JSON.stringify(paletteState));
} catch (error) {
console.error("Unexpected error saving palette state to localStorage: ", error);
}
}
return {
init: init,
add:addNodeType,

View File

@@ -909,17 +909,19 @@ RED.subflow = (function() {
/**
* Create interface for controlling env var UI definition
* Build the edit dialog for a subflow template (creating/modifying a subflow template)
* @param {Object} uiContainer - the jQuery container for the environment variable list
* @param {Object} node - the subflow template node
*/
function buildEnvControl(envList,node) {
function buildEnvControl(uiContainer,node) {
var tabs = RED.tabs.create({
id: "subflow-env-tabs",
onchange: function(tab) {
if (tab.id === "subflow-env-tab-preview") {
var inputContainer = $("#subflow-input-ui");
var list = envList.editableList("items");
var list = uiContainer.editableList("items");
var exportedEnv = exportEnvList(list, true);
buildEnvUI(inputContainer, exportedEnv,node);
buildEnvUI(inputContainer, exportedEnv, node);
}
$("#subflow-env-tabs-content").children().hide();
$("#" + tab.id).show();
@@ -957,12 +959,33 @@ RED.subflow = (function() {
RED.editor.envVarList.setLocale(locale);
}
function buildEnvUIRow(row, tenv, ui, node) {
/**
* Build a UI row for a subflow instance environment variable
* Also used to build the UI row for subflow template preview
* @param {JQuery} row - A form row element
* @param {Object} tenv - A template environment variable
* @param {String} tenv.name - The name of the environment variable
* @param {String} tenv.type - The type of the environment variable
* @param {String} tenv.value - The value set for this environment variable
* @param {Object} tenv.parent - The parent environment variable
* @param {String} tenv.parent.value - The value set for the parent environment variable
* @param {String} tenv.parent.type - The type of the parent environment variable
* @param {Object} tenv.ui - The UI configuration for the environment variable
* @param {String} tenv.ui.icon - The icon for the environment variable
* @param {Object} tenv.ui.label - The label for the environment variable
* @param {String} tenv.ui.type - The type of the UI control for the environment variable
* @param {Object} node - The subflow instance node
*/
function buildEnvUIRow(row, tenv, node) {
if(RED.subflow.debug) { console.log("buildEnvUIRow", tenv) }
const ui = tenv.ui || {}
ui.label = ui.label||{};
if ((tenv.type === "cred" || (tenv.parent && tenv.parent.type === "cred")) && !ui.type) {
ui.type = "cred";
ui.opts = {};
} else if (tenv.type === "conf-types") {
ui.type = "conf-types"
ui.opts = { types: ['conf-types'] }
} else if (!ui.type) {
ui.type = "input";
ui.opts = { types: RED.editor.envVarList.DEFAULT_ENV_TYPE_LIST }
@@ -1006,9 +1029,10 @@ RED.subflow = (function() {
if (tenv.hasOwnProperty('type')) {
val.type = tenv.type;
}
const elId = getSubflowEnvPropertyName(tenv.name)
switch(ui.type) {
case "input":
input = $('<input type="text">').css('width','70%').appendTo(row);
input = $('<input type="text">').css('width','70%').attr('id', elId).appendTo(row);
if (ui.opts.types && ui.opts.types.length > 0) {
var inputType = val.type;
if (ui.opts.types.indexOf(inputType) === -1) {
@@ -1035,7 +1059,7 @@ RED.subflow = (function() {
}
break;
case "select":
input = $('<select>').css('width','70%').appendTo(row);
input = $('<select>').css('width','70%').attr('id', elId).appendTo(row);
if (ui.opts.opts) {
ui.opts.opts.forEach(function(o) {
$('<option>').val(o.v).text(RED.editor.envVarList.lookupLabel(o.l, o.l['en-US']||o.v, locale)).appendTo(input);
@@ -1046,7 +1070,7 @@ RED.subflow = (function() {
case "checkbox":
label.css("cursor","default");
var cblabel = $('<label>').css('width','70%').appendTo(row);
input = $('<input type="checkbox">').css({
input = $('<input type="checkbox">').attr('id', elId).css({
marginTop: 0,
width: 'auto',
height: '34px'
@@ -1064,7 +1088,7 @@ RED.subflow = (function() {
input.prop("checked",boolVal);
break;
case "spinner":
input = $('<input>').css('width','70%').appendTo(row);
input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
var spinnerOpts = {};
if (ui.opts.hasOwnProperty('min')) {
spinnerOpts.min = ui.opts.min;
@@ -1093,18 +1117,25 @@ RED.subflow = (function() {
default: 'cred'
})
break;
}
if (input) {
input.attr('id',getSubflowEnvPropertyName(tenv.name))
case "conf-types":
// let clsId = 'config-node-input-' + val.type + '-' + val.value + '-' + Math.floor(Math.random() * 100000);
// clsId = clsId.replace(/\W/g, '-');
// input = $('<input>').css('width','70%').addClass(clsId).attr('id', elId).appendTo(row);
input = $('<input>').css('width','70%').attr('id', elId).appendTo(row);
const _type = tenv.parent?.type || tenv.type;
RED.editor.prepareConfigNodeSelect(node, tenv.name, _type, 'node-input-subflow-env', null, tenv);
break;
}
}
/**
* Create environment variable input UI
* Build the edit form for a subflow instance
* Also used to build the preview form in the subflow template edit dialog
* @param uiContainer - container for UI
* @param envList - env var definitions of template
*/
function buildEnvUI(uiContainer, envList, node) {
if(RED.subflow.debug) { console.log("buildEnvUI",envList) }
uiContainer.empty();
for (var i = 0; i < envList.length; i++) {
var tenv = envList[i];
@@ -1112,7 +1143,7 @@ RED.subflow = (function() {
continue;
}
var row = $("<div/>", { class: "form-row" }).appendTo(uiContainer);
buildEnvUIRow(row,tenv, tenv.ui || {}, node);
buildEnvUIRow(row, tenv, node);
}
}
// buildEnvUI
@@ -1185,6 +1216,9 @@ RED.subflow = (function() {
delete ui.opts
}
break;
case "conf-types":
delete ui.opts;
break;
default:
delete ui.opts;
}
@@ -1207,8 +1241,9 @@ RED.subflow = (function() {
if (/^subflow:/.test(node.type)) {
var subflowDef = RED.nodes.subflow(node.type.substring(8));
if (subflowDef.env) {
subflowDef.env.forEach(function(env) {
subflowDef.env.forEach(function(env, i) {
var item = {
index: i,
name:env.name,
parent: {
type: env.type,
@@ -1245,14 +1280,20 @@ RED.subflow = (function() {
var nodePropValue = nodeProp;
if (prop.ui && prop.ui.type === "cred") {
nodePropType = "cred";
} else if (prop.ui && prop.ui.type === "conf-types") {
nodePropType = prop.value.type
} 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;
if (nodeProp) {
nodePropType = nodeProp.type;
nodePropValue = nodeProp.value;
} else {
nodePropType = 'str'
}
}
}
var item = {
@@ -1273,6 +1314,7 @@ RED.subflow = (function() {
}
function exportSubflowInstanceEnv(node) {
if(RED.subflow.debug) { console.log("exportSubflowInstanceEnv",node) }
var env = [];
// First, get the values for the SubflowTemplate defined properties
// - these are the ones with custom UI elements
@@ -1319,6 +1361,9 @@ RED.subflow = (function() {
item.type = 'bool';
item.value = ""+input.prop("checked");
break;
case "conf-types":
item.value = input.val()
item.type = data.parent.value;
}
if (ui.type === "cred" || item.type !== data.parent.type || item.value !== data.parent.value) {
env.push(item);
@@ -1332,8 +1377,15 @@ RED.subflow = (function() {
return 'node-input-subflow-env-'+name.replace(/[^a-z0-9-_]/ig,"_");
}
// Called by subflow.oneditprepare for both instances and templates
/**
* Build the subflow edit form
* Called by subflow.oneditprepare for both instances and templates
* @param {"subflow"|"subflow-template"} type - the type of subflow being edited
* @param {Object} node - the node being edited
*/
function buildEditForm(type,node) {
if(RED.subflow.debug) { console.log("buildEditForm",type,node) }
if (type === "subflow-template") {
// This is the tabbed UI that offers the env list - with UI options
// plus the preview tab

View File

@@ -435,10 +435,15 @@ RED.tourGuide = (function() {
function listTour() {
return [
{
id: "4_0",
label: "4.0",
path: "./tours/welcome.js"
},
{
id: "3_1",
label: "3.1",
path: "./tours/welcome.js"
path: "./tours/3.1/welcome.js"
},
{
id: "3_0",

View File

@@ -483,6 +483,16 @@ RED.utils = (function() {
$('<span class="red-ui-debug-msg-type-string-swatch"></span>').css('backgroundColor',obj).appendTo(e);
}
let n = RED.nodes.node(obj) ?? RED.nodes.workspace(obj);
if (n) {
if (options.nodeSelector && "function" == typeof options.nodeSelector) {
e.css('cursor', 'pointer').on("click", function(evt) {
evt.preventDefault();
options.nodeSelector(n.id);
})
}
}
} else if (typeof obj === 'number') {
e = $('<span class="red-ui-debug-msg-type-number"></span>').appendTo(entryObj);
@@ -589,6 +599,7 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@@ -619,6 +630,7 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}
@@ -675,6 +687,7 @@ RED.utils = (function() {
exposeApi: exposeApi,
// tools: tools // Do not pass tools down as we
// keep them attached to the top-level header
nodeSelector: options.nodeSelector,
}
).appendTo(row);
}

View File

@@ -6252,6 +6252,10 @@ RED.view = (function() {
}
})
}
if (selection.links) {
selectedLinks.clear();
selection.links.forEach(selectedLinks.add);
}
}
}
updateSelection();