Merge pull request #4747 from GogoVega/tooltip-input-validation

Add tooltip and message validation to `typedInput`
This commit is contained in:
Nick O'Leary 2024-06-10 20:44:34 +01:00 committed by GitHub
commit 3952a23ba3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 145 additions and 99 deletions

View File

@ -358,61 +358,64 @@
{ value: "_session", source: ["websocket out","tcp out"] }, { value: "_session", source: ["websocket out","tcp out"] },
] ]
var allOptions = { var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions)}, msg: { value: "msg", label: "msg.", validate: RED.utils.validatePropertyExpression, autoComplete: msgAutoComplete(msgCompletions) },
flow: {value:"flow",label:"flow.",hasValue:true, flow: { value: "flow", label: "flow.", hasValue: true,
options:[], options: [],
validate:RED.utils.validatePropertyExpression, validate: RED.utils.validatePropertyExpression,
parse: contextParse, parse: contextParse,
export: contextExport, export: contextExport,
valueLabel: contextLabel, valueLabel: contextLabel,
autoComplete: contextAutoComplete autoComplete: contextAutoComplete
}, },
global: {value:"global",label:"global.",hasValue:true, global: {
options:[], value: "global", label: "global.", hasValue: true,
validate:RED.utils.validatePropertyExpression, options: [],
validate: RED.utils.validatePropertyExpression,
parse: contextParse, parse: contextParse,
export: contextExport, export: contextExport,
valueLabel: contextLabel, valueLabel: contextLabel,
autoComplete: contextAutoComplete autoComplete: contextAutoComplete
}, },
str: {value:"str",label:"string",icon:"red/images/typedInput/az.svg"}, 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) { num: { value: "num", label: "number", icon: "red/images/typedInput/09.svg", validate: function (v, o) {
return (true === RED.utils.validateTypedProperty(v, "num")); 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: { json: {
value:"json", value: "json",
label:"JSON", label: "JSON",
icon:"red/images/typedInput/json.svg", icon: "red/images/typedInput/json.svg",
validate: function(v) { try{JSON.parse(v);return true;}catch(e){return false;}}, validate: function (v, o) {
expand: function() { return RED.utils.validateTypedProperty(v, "json", o);
},
expand: function () {
var that = this; var that = this;
var value = this.value(); var value = this.value();
try { try {
value = JSON.stringify(JSON.parse(value),null,4); value = JSON.stringify(JSON.parse(value), null, 4);
} catch(err) { } catch (err) {
} }
RED.editor.editJSON({ RED.editor.editJSON({
value: value, value: value,
stateId: RED.editor.generateViewStateId("typedInput", that, "json"), stateId: RED.editor.generateViewStateId("typedInput", that, "json"),
focus: true, focus: true,
complete: function(v) { complete: function (v) {
var value = v; var value = v;
try { try {
value = JSON.stringify(JSON.parse(v)); value = JSON.stringify(JSON.parse(v));
} catch(err) { } catch (err) {
} }
that.value(value); 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: { date: {
value:"date", value: "date",
label:"timestamp", label: "timestamp",
icon:"fa fa-clock-o", icon: "fa fa-clock-o",
options:[ options: [
{ {
label: 'milliseconds since epoch', label: 'milliseconds since epoch',
value: '' value: ''
@ -431,15 +434,17 @@
value: "jsonata", value: "jsonata",
label: "expression", label: "expression",
icon: "red/images/typedInput/expr.svg", icon: "red/images/typedInput/expr.svg",
validate: function(v) { try{jsonata(v);return true;}catch(e){return false;}}, validate: function (v, o) {
expand:function() { return RED.utils.validateTypedProperty(v, "jsonata", o);
},
expand: function () {
var that = this; var that = this;
RED.editor.editExpression({ RED.editor.editExpression({
value: this.value().replace(/\t/g,"\n"), value: this.value().replace(/\t/g, "\n"),
stateId: RED.editor.generateViewStateId("typedInput", that, "jsonata"), stateId: RED.editor.generateViewStateId("typedInput", that, "jsonata"),
focus: true, focus: true,
complete: function(v) { complete: function (v) {
that.value(v.replace(/\n/g,"\t")); that.value(v.replace(/\n/g, "\t"));
} }
}) })
} }
@ -448,13 +453,13 @@
value: "bin", value: "bin",
label: "buffer", label: "buffer",
icon: "red/images/typedInput/bin.svg", icon: "red/images/typedInput/bin.svg",
expand: function() { expand: function () {
var that = this; var that = this;
RED.editor.editBuffer({ RED.editor.editBuffer({
value: this.value(), value: this.value(),
stateId: RED.editor.generateViewStateId("typedInput", that, "bin"), stateId: RED.editor.generateViewStateId("typedInput", that, "bin"),
focus: true, focus: true,
complete: function(v) { complete: function (v) {
that.value(v); that.value(v);
} }
}) })
@ -470,9 +475,9 @@
value: "node", value: "node",
label: "node", label: "node",
icon: "red/images/typedInput/target.svg", icon: "red/images/typedInput/target.svg",
valueLabel: function(container,value) { valueLabel: function (container, value) {
var node = RED.nodes.node(value); var node = RED.nodes.node(value);
var nodeDiv = $('<div>',{class:"red-ui-search-result-node"}).css({ var nodeDiv = $('<div>', { class: "red-ui-search-result-node" }).css({
"margin-top": "2px", "margin-top": "2px",
"margin-left": "3px" "margin-left": "3px"
}).appendTo(container); }).appendTo(container);
@ -481,117 +486,117 @@
"margin-left": "6px" "margin-left": "6px"
}).appendTo(container); }).appendTo(container);
if (node) { if (node) {
var colour = RED.utils.getNodeColor(node.type,node._def); var colour = RED.utils.getNodeColor(node.type, node._def);
var icon_url = RED.utils.getNodeIcon(node._def,node); var icon_url = RED.utils.getNodeIcon(node._def, node);
if (node.type === 'tab') { if (node.type === 'tab') {
colour = "#C0DEED"; colour = "#C0DEED";
} }
nodeDiv.css('backgroundColor',colour); nodeDiv.css('backgroundColor', colour);
var iconContainer = $('<div/>',{class:"red-ui-palette-icon-container"}).appendTo(nodeDiv); var iconContainer = $('<div/>', { class: "red-ui-palette-icon-container" }).appendTo(nodeDiv);
RED.utils.createIconElement(icon_url, iconContainer, true); 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); nodeLabel.text(l);
} else { } else {
nodeDiv.css({ nodeDiv.css({
'backgroundColor': '#eee', 'backgroundColor': '#eee',
'border-style' : 'dashed' 'border-style': 'dashed'
}); });
} }
}, },
expand: function() { expand: function () {
var that = this; var that = this;
RED.tray.hide(); RED.tray.hide();
RED.view.selectNodes({ RED.view.selectNodes({
single: true, single: true,
selected: [that.value()], selected: [that.value()],
onselect: function(selection) { onselect: function (selection) {
that.value(selection.id); that.value(selection.id);
RED.tray.show(); RED.tray.show();
}, },
oncancel: function() { oncancel: function () {
RED.tray.show(); RED.tray.show();
} }
}) })
} }
}, },
cred:{ cred: {
value:"cred", value: "cred",
label:"credential", label: "credential",
icon:"fa fa-lock", icon: "fa fa-lock",
inputType: "password", inputType: "password",
valueLabel: function(container,value) { valueLabel: function (container, value) {
var that = this; var that = this;
container.css("pointer-events","none"); container.css("pointer-events", "none");
container.css("flex-grow",0); container.css("flex-grow", 0);
this.elementDiv.hide(); this.elementDiv.hide();
var buttons = $('<div>').css({ var buttons = $('<div>').css({
position: "absolute", position: "absolute",
right:"6px", right: "6px",
top: "6px", top: "6px",
"pointer-events":"all" "pointer-events": "all"
}).appendTo(container); }).appendTo(container);
var eyeButton = $('<button type="button" class="red-ui-button red-ui-button-small"></button>').css({ var eyeButton = $('<button type="button" class="red-ui-button red-ui-button-small"></button>').css({
width:"20px" width: "20px"
}).appendTo(buttons).on("click", function(evt) { }).appendTo(buttons).on("click", function (evt) {
evt.preventDefault(); evt.preventDefault();
var cursorPosition = that.input[0].selectionStart; var cursorPosition = that.input[0].selectionStart;
var currentType = that.input.attr("type"); var currentType = that.input.attr("type");
if (currentType === "text") { if (currentType === "text") {
that.input.attr("type","password"); that.input.attr("type", "password");
eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); eyeCon.removeClass("fa-eye-slash").addClass("fa-eye");
setTimeout(function() { setTimeout(function () {
that.input.focus(); that.input.focus();
that.input[0].setSelectionRange(cursorPosition, cursorPosition); that.input[0].setSelectionRange(cursorPosition, cursorPosition);
},50); }, 50);
} else { } else {
that.input.attr("type","text"); that.input.attr("type", "text");
eyeCon.removeClass("fa-eye").addClass("fa-eye-slash"); eyeCon.removeClass("fa-eye").addClass("fa-eye-slash");
setTimeout(function() { setTimeout(function () {
that.input.focus(); that.input.focus();
that.input[0].setSelectionRange(cursorPosition, cursorPosition); that.input[0].setSelectionRange(cursorPosition, cursorPosition);
},50); }, 50);
} }
}).hide(); }).hide();
var eyeCon = $('<i class="fa fa-eye"></i>').css("margin-left","-2px").appendTo(eyeButton); var eyeCon = $('<i class="fa fa-eye"></i>').css("margin-left", "-2px").appendTo(eyeButton);
if (value === "__PWRD__") { if (value === "__PWRD__") {
var innerContainer = $('<div><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i></div>').css({ var innerContainer = $('<div><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i><i class="fa fa-asterisk"></i></div>').css({
padding:"6px 6px", padding: "6px 6px",
borderRadius:"4px" borderRadius: "4px"
}).addClass("red-ui-typedInput-value-label-inactive").appendTo(container); }).addClass("red-ui-typedInput-value-label-inactive").appendTo(container);
var editButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-pencil"></i></button>').appendTo(buttons).on("click", function(evt) { var editButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-pencil"></i></button>').appendTo(buttons).on("click", function (evt) {
evt.preventDefault(); evt.preventDefault();
innerContainer.hide(); innerContainer.hide();
container.css("background","none"); container.css("background", "none");
container.css("pointer-events","none"); container.css("pointer-events", "none");
that.input.val(""); that.input.val("");
that.element.val(""); that.element.val("");
that.elementDiv.show(); that.elementDiv.show();
editButton.hide(); editButton.hide();
cancelButton.show(); cancelButton.show();
eyeButton.show(); eyeButton.show();
setTimeout(function() { setTimeout(function () {
that.input.focus(); that.input.focus();
},50); }, 50);
}); });
var cancelButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').css("margin-left","3px").appendTo(buttons).on("click", function(evt) { var cancelButton = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-times"></i></button>').css("margin-left", "3px").appendTo(buttons).on("click", function (evt) {
evt.preventDefault(); evt.preventDefault();
innerContainer.show(); innerContainer.show();
container.css("background",""); container.css("background", "");
that.input.val("__PWRD__"); that.input.val("__PWRD__");
that.element.val("__PWRD__"); that.element.val("__PWRD__");
that.elementDiv.hide(); that.elementDiv.hide();
editButton.show(); editButton.show();
cancelButton.hide(); cancelButton.hide();
eyeButton.hide(); eyeButton.hide();
that.input.attr("type","password"); that.input.attr("type", "password");
eyeCon.removeClass("fa-eye-slash").addClass("fa-eye"); eyeCon.removeClass("fa-eye-slash").addClass("fa-eye");
}).hide(); }).hide();
} else { } else {
container.css("background","none"); container.css("background", "none");
container.css("pointer-events","none"); container.css("pointer-events", "none");
this.elementDiv.show(); this.elementDiv.show();
eyeButton.show(); eyeButton.show();
} }
@ -1538,26 +1543,48 @@
} }
} }
}, },
validate: function() { validate: function(options) {
var result; let valid = true;
var value = this.value(); const value = this.value();
var type = this.type(); const type = this.type();
if (this.typeMap[type] && this.typeMap[type].validate) { if (this.typeMap[type] && this.typeMap[type].validate) {
var val = this.typeMap[type].validate; const validate = this.typeMap[type].validate;
if (typeof val === 'function') { if (typeof validate === 'function') {
result = val(value); valid = validate(value, {});
} else { } 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 { } 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) { if (options?.returnErrorMessage === true) {
this.uiSelect.removeClass('input-error'); return valid;
} else {
this.uiSelect.addClass('input-error');
} }
return result; // Must return a boolean for no 3.x validator
return (typeof valid === "string") ? false : valid;
}, },
show: function() { show: function() {
this.uiSelect.show(); this.uiSelect.show();

View File

@ -190,7 +190,10 @@ RED.editor = (function() {
const input = $("#"+prefix+"-"+property); const input = $("#"+prefix+"-"+property);
const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0; const isTypedInput = input.length > 0 && input.next(".red-ui-typedInput-container").length > 0;
if (isTypedInput) { if (isTypedInput) {
valid = input.typedInput("validate"); valid = input.typedInput("validate", { returnErrorMessage: true });
if (typeof valid === "string") {
return label ? label + ": " + valid : valid;
}
} }
} }
} }

View File

@ -901,11 +901,25 @@ RED.utils = (function() {
return parts; 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 { try {
var parts = normalisePropertyExpression(str); const parts = normalisePropertyExpression(str);
return true; return true;
} catch(err) { } 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; return false;
} }
} }
@ -923,22 +937,24 @@ RED.utils = (function() {
// Allow ${ENV_VAR} value // Allow ${ENV_VAR} value
return true return true
} }
let error let error;
if (propertyType === 'json') { if (propertyType === 'json') {
try { try {
JSON.parse(propertyValue); JSON.parse(propertyValue);
} catch(err) { } catch(err) {
error = RED._("validator.errors.invalid-json", { error = RED._("validator.errors.invalid-json", {
error: err.message error: err.message
}) });
} }
} else if (propertyType === 'msg' || propertyType === 'flow' || propertyType === 'global' ) { } else if (propertyType === 'msg' || propertyType === 'flow' || propertyType === 'global' ) {
if (!RED.utils.validatePropertyExpression(propertyValue)) { // To avoid double label
error = RED._("validator.errors.invalid-prop") const valid = RED.utils.validatePropertyExpression(propertyValue, opt ? {} : null);
if (valid !== true) {
error = opt ? valid : RED._("validator.errors.invalid-prop");
} }
} else if (propertyType === 'num') { } 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)) { 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') { } else if (propertyType === 'jsonata') {
try { try {
@ -946,16 +962,16 @@ RED.utils = (function() {
} catch(err) { } catch(err) {
error = RED._("validator.errors.invalid-expr", { error = RED._("validator.errors.invalid-expr", {
error: err.message error: err.message
}) });
} }
} }
if (error) { if (error) {
if (opt && opt.label) { if (opt && opt.label) {
return opt.label+': '+error return opt.label + ': ' + error;
} }
return error return error;
} }
return true return true;
} }
function getMessageProperty(msg,expr) { function getMessageProperty(msg,expr) {