From d509c1a57c0e71462cbbba52dca22c60cada3968 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Fri, 1 Mar 2024 11:06:27 +0000 Subject: [PATCH 1/7] add "conf-types" to typedInput --- .../editor-client/locales/en-US/editor.json | 3 +- .../@node-red/editor-client/src/js/nodes.js | 25 +++++ .../src/js/ui/common/typedInput.js | 94 ++++++++++++++++--- 3 files changed, 108 insertions(+), 14 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 1836b6a9e..0faa11151 100644 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -924,7 +924,8 @@ "date": "timestamp", "jsonata": "expression", "env": "env variable", - "cred": "credential" + "cred": "credential", + "conf-types": "config node" } }, "editableList": { diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index bf1faf0a4..63f0fae23 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -91,6 +91,31 @@ RED.nodes = (function() { getNodeTypes: function() { return Object.keys(nodeDefinitions); }, + /** + * Get an array of node definitions + * @param {Object} options - options object + * @param {boolean} [options.configOnly] - if true, only return config nodes + * @param {function} [options.filter] - a filter function to apply to the list of nodes + * @returns array of node definitions + */ + getNodeDefinitions: function(options) { + const result = [] + const configOnly = (options && options.configOnly) + const filter = (options && options.filter) + const keys = Object.keys(nodeDefinitions) + for (const key of keys) { + const def = nodeDefinitions[key] + if(!def) { continue } + if (configOnly && def.category !== "config") { + continue + } + if (filter && !filter(nodeDefinitions[key])) { + continue + } + result.push(nodeDefinitions[key]) + } + return result + }, setNodeList: function(list) { nodeList = []; for(var i=0;i 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 + $('
').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 = $('
') + const row1Name = $('
').text(def.set.name) + const row2Module = $('
').text(def.set.module) + container.append(row1Name, row2Module) + + return { + value: def.type, + name: def.set.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 Date: Fri, 1 Mar 2024 11:07:26 +0000 Subject: [PATCH 2/7] permit config selection in subflow and subflow template --- .../editor-client/src/js/ui/editor.js | 131 +++++++++++++----- .../src/js/ui/editors/envVarList.js | 104 ++++++++++---- .../editor-client/src/js/ui/subflow.js | 79 ++++++++--- 3 files changed, 240 insertions(+), 74 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js index 9fd4ba01e..5b5f45572 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editor.js @@ -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 = $("
").css({ + const outerWrap = $("
").css({ width: newWidth, - display:'inline-flex' + display: 'inline-flex' }); - var select = $('').appendTo(outerWrap); + const 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); - $('') - .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 = $('') + .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) { - $('').text(RED.text.bidi.enforceTextDirectionWithUCC(cn.__label__)).appendTo(select); + const 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,9 +1535,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); @@ -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, } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js index ba71e651f..514e7ff94 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/envVarList.js @@ -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 = $('
').appendTo(container); $('
').appendTo(topRow); $('
').text(RED._("editor.icon")).appendTo(topRow); $('
').text(RED._("editor.label")).appendTo(topRow); - $('
').text(RED._("editor.inputType")).appendTo(topRow); + $('
').text(RED._("editor.inputType")).appendTo(topRow); var row = $('
').appendTo(container); $('
').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 = $('
').appendTo(row); - var inputCellInput = $('').css("width","100%").appendTo(inputCell); + var inputCell = $('
').appendTo(row); + var uiInputTypeInput = $('').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) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index a06f8bca4..2d0d97fca 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -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 = $('').css('width','70%').appendTo(row); + input = $('').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 = $('').css('width','70%').attr('id', elId).appendTo(row); if (ui.opts.opts) { ui.opts.opts.forEach(function(o) { $('