diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 7c4964ca8..47355565f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -358,61 +358,64 @@ { value: "_session", source: ["websocket out","tcp out"] }, ] var allOptions = { - msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)}, - flow: {value:"flow",label:"flow.",hasValue:true, - options:[], - validate:RED.utils.validatePropertyExpression, + 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, autoComplete: contextAutoComplete }, - global: {value:"global",label:"global.",hasValue:true, - options:[], - validate:RED.utils.validatePropertyExpression, + global: { + value: "global", label: "global.", hasValue: true, + options: [], + validate: RED.utils.validatePropertyExpression, parse: contextParse, export: contextExport, 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) { - return (true === RED.utils.validateTypedProperty(v, "num")); + 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, o) { + return RED.utils.validateTypedProperty(v, "num", o); } }, - bool: {value:"bool",label:"boolean",icon:"red/images/typedInput/bool.svg",options:["true","false"]}, + bool: { value: "bool", label: "boolean", icon: "red/images/typedInput/bool.svg", options: ["true", "false"] }, json: { - value:"json", - label:"JSON", - icon:"red/images/typedInput/json.svg", - validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}, - expand: function() { + value: "json", + label: "JSON", + icon: "red/images/typedInput/json.svg", + validate: function (v, o) { + return RED.utils.validateTypedProperty(v, "json", o); + }, + expand: function () { var that = this; var value = this.value(); try { - value = JSON.stringify(JSON.parse(value),null,4); - } catch(err) { + value = JSON.stringify(JSON.parse(value), null, 4); + } catch (err) { } RED.editor.editJSON({ value: value, stateId: RED.editor.generateViewStateId("typedInput", that, "json"), focus: true, - complete: function(v) { + complete: function (v) { var value = v; try { value = JSON.stringify(JSON.parse(v)); - } catch(err) { + } catch (err) { } that.value(value); } }) } }, - re: {value:"re",label:"regular expression",icon:"red/images/typedInput/re.svg"}, + re: { value: "re", label: "regular expression", icon: "red/images/typedInput/re.svg" }, date: { - value:"date", - label:"timestamp", - icon:"fa fa-clock-o", - options:[ + value: "date", + label: "timestamp", + icon: "fa fa-clock-o", + options: [ { label: 'milliseconds since epoch', value: '' @@ -431,15 +434,17 @@ value: "jsonata", label: "expression", icon: "red/images/typedInput/expr.svg", - validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}, - expand:function() { + validate: function (v, o) { + return RED.utils.validateTypedProperty(v, "jsonata", o); + }, + expand: function () { var that = this; RED.editor.editExpression({ - value: this.value().replace(/\t/g,"\n"), + value: this.value().replace(/\t/g, "\n"), stateId: RED.editor.generateViewStateId("typedInput", that, "jsonata"), focus: true, - complete: function(v) { - that.value(v.replace(/\n/g,"\t")); + complete: function (v) { + that.value(v.replace(/\n/g, "\t")); } }) } @@ -448,13 +453,13 @@ value: "bin", label: "buffer", icon: "red/images/typedInput/bin.svg", - expand: function() { + expand: function () { var that = this; RED.editor.editBuffer({ value: this.value(), stateId: RED.editor.generateViewStateId("typedInput", that, "bin"), focus: true, - complete: function(v) { + complete: function (v) { that.value(v); } }) @@ -470,9 +475,9 @@ value: "node", label: "node", icon: "red/images/typedInput/target.svg", - valueLabel: function(container,value) { + valueLabel: function (container, value) { var node = RED.nodes.node(value); - var nodeDiv = $('
',{class:"red-ui-search-result-node"}).css({ + var nodeDiv = $('
', { class: "red-ui-search-result-node" }).css({ "margin-top": "2px", "margin-left": "3px" }).appendTo(container); @@ -481,117 +486,117 @@ "margin-left": "6px" }).appendTo(container); if (node) { - var colour = RED.utils.getNodeColor(node.type,node._def); - var icon_url = RED.utils.getNodeIcon(node._def,node); + var colour = RED.utils.getNodeColor(node.type, node._def); + var icon_url = RED.utils.getNodeIcon(node._def, node); if (node.type === 'tab') { colour = "#C0DEED"; } - nodeDiv.css('backgroundColor',colour); - var iconContainer = $('
',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); + nodeDiv.css('backgroundColor', colour); + var iconContainer = $('
', { class: "red-ui-palette-icon-container" }).appendTo(nodeDiv); RED.utils.createIconElement(icon_url, iconContainer, true); - var l = RED.utils.getNodeLabel(node,node.id); + var l = RED.utils.getNodeLabel(node, node.id); nodeLabel.text(l); } else { nodeDiv.css({ 'backgroundColor': '#eee', - 'border-style' : 'dashed' + 'border-style': 'dashed' }); } }, - expand: function() { + expand: function () { var that = this; RED.tray.hide(); RED.view.selectNodes({ single: true, selected: [that.value()], - onselect: function(selection) { + onselect: function (selection) { that.value(selection.id); RED.tray.show(); }, - oncancel: function() { + oncancel: function () { RED.tray.show(); } }) } }, - cred:{ - value:"cred", - label:"credential", - icon:"fa fa-lock", + cred: { + value: "cred", + label: "credential", + icon: "fa fa-lock", inputType: "password", - valueLabel: function(container,value) { + valueLabel: function (container, value) { var that = this; - container.css("pointer-events","none"); - container.css("flex-grow",0); + container.css("pointer-events", "none"); + container.css("flex-grow", 0); this.elementDiv.hide(); var buttons = $('
').css({ position: "absolute", - right:"6px", + right: "6px", top: "6px", - "pointer-events":"all" + "pointer-events": "all" }).appendTo(container); var eyeButton = $('').css({ - width:"20px" - }).appendTo(buttons).on("click", function(evt) { + width: "20px" + }).appendTo(buttons).on("click", function (evt) { evt.preventDefault(); var cursorPosition = that.input[0].selectionStart; var currentType = that.input.attr("type"); if (currentType === "text") { - that.input.attr("type","password"); + that.input.attr("type", "password"); eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); - setTimeout(function() { + setTimeout(function () { that.input.focus(); that.input[0].setSelectionRange(cursorPosition, cursorPosition); - },50); + }, 50); } else { - that.input.attr("type","text"); + that.input.attr("type", "text"); eyeCon.removeClass("fa-eye").addClass("fa-eye-slash"); - setTimeout(function() { + setTimeout(function () { that.input.focus(); that.input[0].setSelectionRange(cursorPosition, cursorPosition); - },50); + }, 50); } }).hide(); - var eyeCon = $('').css("margin-left","-2px").appendTo(eyeButton); + var eyeCon = $('').css("margin-left", "-2px").appendTo(eyeButton); if (value === "__PWRD__") { var innerContainer = $('
').css({ - padding:"6px 6px", - borderRadius:"4px" + padding: "6px 6px", + borderRadius: "4px" }).addClass("red-ui-typedInput-value-label-inactive").appendTo(container); - var editButton = $('').appendTo(buttons).on("click", function(evt) { + var editButton = $('').appendTo(buttons).on("click", function (evt) { evt.preventDefault(); innerContainer.hide(); - container.css("background","none"); - container.css("pointer-events","none"); + container.css("background", "none"); + container.css("pointer-events", "none"); that.input.val(""); that.element.val(""); that.elementDiv.show(); editButton.hide(); cancelButton.show(); eyeButton.show(); - setTimeout(function() { + setTimeout(function () { that.input.focus(); - },50); + }, 50); }); - var cancelButton = $('').css("margin-left","3px").appendTo(buttons).on("click", function(evt) { + var cancelButton = $('').css("margin-left", "3px").appendTo(buttons).on("click", function (evt) { evt.preventDefault(); innerContainer.show(); - container.css("background",""); + container.css("background", ""); that.input.val("__PWRD__"); that.element.val("__PWRD__"); that.elementDiv.hide(); editButton.show(); cancelButton.hide(); eyeButton.hide(); - that.input.attr("type","password"); + that.input.attr("type", "password"); eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); }).hide(); } else { - container.css("background","none"); - container.css("pointer-events","none"); + container.css("background", "none"); + container.css("pointer-events", "none"); this.elementDiv.show(); eyeButton.show(); } @@ -1538,26 +1543,48 @@ } } }, - validate: function() { - var result; - var value = this.value(); - var type = this.type(); + validate: function(options) { + let valid = true; + const value = this.value(); + const type = this.type(); if (this.typeMap[type] && this.typeMap[type].validate) { - var val = this.typeMap[type].validate; - if (typeof val === 'function') { - result = val(value); + const validate = this.typeMap[type].validate; + if (typeof validate === 'function') { + valid = validate(value, {}); } else { - result = val.test(value); + // Regex + valid = validate.test(value); + if (!valid) { + valid = RED._("validator.errors.invalid-regexp"); + } + } + } + if ((typeof valid === "string") || !valid) { + this.element.addClass("input-error"); + this.uiSelect.addClass("input-error"); + if (typeof valid === "string") { + let tooltip = this.element.data("tooltip"); + if (tooltip) { + tooltip.setContent(valid); + } else { + tooltip = RED.popover.tooltip(this.elementDiv, valid); + this.element.data("tooltip", tooltip); + } } } else { - result = true; + this.element.removeClass("input-error"); + this.uiSelect.removeClass("input-error"); + const tooltip = this.element.data("tooltip"); + if (tooltip) { + this.element.data("tooltip", null); + tooltip.delete(); + } } - if (result) { - this.uiSelect.removeClass('input-error'); - } else { - this.uiSelect.addClass('input-error'); + if (options?.returnErrorMessage === true) { + return valid; } - return result; + // Must return a boolean for no 3.x validator + return (typeof valid === "string") ? false : valid; }, show: function() { this.uiSelect.show(); 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 90570a969..f11dd45d9 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 @@ -190,7 +190,10 @@ RED.editor = (function() { const input = $("#"+prefix+"-"+property); const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0; if (isTypedInput) { - valid = input.typedInput("validate"); + valid = input.typedInput("validate", { returnErrorMessage: true }); + if (typeof valid === "string") { + return label ? label + ": " + valid : valid; + } } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index 94bf19718..cfc7f65de 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -901,11 +901,25 @@ RED.utils = (function() { return parts; } - function validatePropertyExpression(str) { + /** + * Validate a property expression + * @param {*} str - the property value + * @returns {boolean|string} whether the node proprty is valid. `true`: valid `false|String`: invalid + */ + function validatePropertyExpression(str, opt) { try { - var parts = normalisePropertyExpression(str); + const parts = normalisePropertyExpression(str); return true; } catch(err) { + // If the validator has opt, it is a 3.x validator that + // can return a String to mean 'invalid' and provide a reason + if (opt) { + if (opt.label) { + return opt.label + ': ' + err.message; + } + return err.message; + } + // Otherwise, a 2.x returns a false value return false; } } @@ -923,22 +937,24 @@ RED.utils = (function() { // Allow ${ENV_VAR} value return true } - let error + let error; if (propertyType === 'json') { try { JSON.parse(propertyValue); } catch(err) { error = RED._("validator.errors.invalid-json", { error: err.message - }) + }); } } else if (propertyType === 'msg' || propertyType === 'flow' || propertyType === 'global' ) { - if (!RED.utils.validatePropertyExpression(propertyValue)) { - error = RED._("validator.errors.invalid-prop") + // To avoid double label + const valid = RED.utils.validatePropertyExpression(propertyValue, opt ? {} : null); + if (valid !== true) { + error = opt ? valid : RED._("validator.errors.invalid-prop"); } } else if (propertyType === 'num') { if (!/^NaN$|^[+-]?[0-9]*\.?[0-9]*([eE][-+]?[0-9]+)?$|^[+-]?(0b|0B)[01]+$|^[+-]?(0o|0O)[0-7]+$|^[+-]?(0x|0X)[0-9a-fA-F]+$/.test(propertyValue)) { - error = RED._("validator.errors.invalid-num") + error = RED._("validator.errors.invalid-num"); } } else if (propertyType === 'jsonata') { try { @@ -946,16 +962,16 @@ RED.utils = (function() { } catch(err) { error = RED._("validator.errors.invalid-expr", { error: err.message - }) + }); } } if (error) { if (opt && opt.label) { - return opt.label+': '+error + return opt.label + ': ' + error; } - return error + return error; } - return true + return true; } function getMessageProperty(msg,expr) {