permit config selection in subflow and subflow template

This commit is contained in:
Steve-Mcl 2024-03-01 11:07:26 +00:00
parent d509c1a57c
commit db249356e6
3 changed files with 240 additions and 74 deletions

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__;
});
@ -1478,11 +1535,18 @@ RED.editor = (function() {
}
RED.tray.close(function() {
var filter = null;
if (editContext && typeof editContext._def.defaults[configProperty].filter === 'function') {
// 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);
});
}
@ -1541,7 +1605,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);
}
@ -2127,6 +2191,7 @@ RED.editor = (function() {
filteredEditPanes[type] = filter
}
editPanes[type] = definition;
}
},
prepareConfigNodeSelect: prepareConfigNodeSelect,
}
})();

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,18 +189,20 @@ 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},
'input': {types:DEFAULT_ENV_TYPE_LIST_INC_CONFTYPES},
'select': {opts:[]},
'spinner': {},
'cred': {}
@ -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 || true) { 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

@ -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,8 @@ RED.subflow = (function() {
delete ui.opts
}
break;
case "conf-types":
delete ui.opts; // CHECK ME DOESNT BREAK STUFF
default:
delete ui.opts;
}
@ -1207,8 +1240,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,
@ -1273,6 +1307,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 +1354,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 +1370,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