From 5b6d07a44f01109836de66463a304de164e4758c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Lu=C5=88=C3=A1k?= Date: Wed, 12 Apr 2023 15:33:52 +0200 Subject: [PATCH 1/2] [New #153] Add basic validation logic --- src/components/Answer.jsx | 1 - src/components/DefaultInput.jsx | 41 +++++- src/components/Question.jsx | 2 +- src/components/answer/InputAnswer.jsx | 10 +- src/constants/Constants.js | 12 ++ src/model/ValidatorFactory.js | 189 ++++++++++++++++++-------- src/stories/assets/form/form1.json | 2 +- src/stories/assets/form/form2.json | 21 ++- src/styles/s-forms.css | 15 ++ src/util/FormUtils.js | 3 + 10 files changed, 231 insertions(+), 65 deletions(-) diff --git a/src/components/Answer.jsx b/src/components/Answer.jsx index da608b85..f85ae7cf 100644 --- a/src/components/Answer.jsx +++ b/src/components/Answer.jsx @@ -114,7 +114,6 @@ const Answer = (props) => { answer={props.answer} /> ); - return null; }; const _renderRegularInput = (value, label, title) => { diff --git a/src/components/DefaultInput.jsx b/src/components/DefaultInput.jsx index 8809e2ff..9f757f5e 100644 --- a/src/components/DefaultInput.jsx +++ b/src/components/DefaultInput.jsx @@ -131,8 +131,34 @@ export default class DefaultInput extends React.Component { ); } - _renderHelp() { - return this.props.help ? {this.props.help} : null; + _renderHelp(classname = "") { + return this.props.help ? ( + {this.props.help} + ) : null; + } + + _hasValidationWarning() { + return this.props.validation && this.props.validation === "warning"; + } + + _hasValidationError() { + return this.props.validation && this.props.validation === "error"; + } + + _hasValidationSuccess() { + return this.props.validation && this.props.validation === "success"; + } + + _getValidationClassname() { + if (this.props.validation && this.props.validation === "error") { + return "is-invalid"; + } + + if (this.props.validation && this.props.validation === "warning") { + return "is-warning"; + } + + return ""; } _renderInput() { @@ -142,13 +168,20 @@ export default class DefaultInput extends React.Component { {this._renderLabel()} (this.input = c)} as="input" {...this.props} onChange={(e) => this.saveCursorPosition(e)} /> - {this.props.validation && } - {this._renderHelp()} + {(this._hasValidationSuccess() || this._hasValidationWarning()) && + this._renderHelp(this._getValidationClassname())} + {this._hasValidationError() && ( + + {this.props.help} + + )} + {!this.props.validation && this._renderHelp()} ); } diff --git a/src/components/Question.jsx b/src/components/Question.jsx index c70c369d..8d5945e0 100644 --- a/src/components/Question.jsx +++ b/src/components/Question.jsx @@ -25,7 +25,7 @@ export default class Question extends React.Component { super(props); JsonLdObjectMap.putObject(props.question["@id"], props.question); this.state = { - validator: null, + validator: {}, expanded: !FormUtils.isCollapsed(props.question), showIcon: false, }; diff --git a/src/components/answer/InputAnswer.jsx b/src/components/answer/InputAnswer.jsx index e9e3eeae..b887b156 100644 --- a/src/components/answer/InputAnswer.jsx +++ b/src/components/answer/InputAnswer.jsx @@ -79,8 +79,16 @@ class InputPropertiesResolver { } props.disabled = componentsOptions.readOnly || FormUtils.isDisabled(question); + if (question[Constants.HAS_VALID_ANSWER] === false) { - props.validation = "error"; + if ( + question[Constants.HAS_VALIDATION_SEVERITY] === + Constants.VALIDATION_SEVERITY.WARNING + ) { + props.validation = "warning"; + } else { + props.validation = "error"; + } props.help = question[Constants.HAS_VALIDATION_MESSAGE]; } diff --git a/src/constants/Constants.js b/src/constants/Constants.js index 4f7c15e3..93097b91 100644 --- a/src/constants/Constants.js +++ b/src/constants/Constants.js @@ -84,6 +84,7 @@ export default class Constants { BOOLEAN: "http://www.w3.org/2001/XMLSchema#boolean", }; static STEP = "http://onto.fel.cvut.cz/ontologies/form/step"; + static PATTERN = "http://onto.fel.cvut.cz/ontologies/form/pattern"; static ACCEPTS_ANSWER_VALUE = "http://onto.fel.cvut.cz/ontologies/form/accepts-answer-value"; static ACCEPTS = "http://onto.fel.cvut.cz/ontologies/form/accepts"; @@ -111,6 +112,10 @@ export default class Constants { "http://onto.fel.cvut.cz/ontologies/form/negative-condition"; static REQUIRES_ANSWER = "http://onto.fel.cvut.cz/ontologies/form/requires-answer"; + static USED_ONLY_FOR_COMPLETENESS = + "http://onto.fel.cvut.cz/ontologies/form/used-only-for-completeness"; + static REQUIRES_ANSWER_DESCRIPTION_IF = + "http://onto.fel.cvut.cz/ontologies/form/requires-answer-description-if"; static REQUIRES_ANSWER_IF = "http://onto.fel.cvut.cz/ontologies/form/requires-answer-if"; static HAS_PRECEDING_QUESTION = @@ -143,6 +148,8 @@ export default class Constants { "http://onto.fel.cvut.cz/ontologies/form/not-answered-question"; static ANSWERED_QUESTION = "http://onto.fel.cvut.cz/ontologies/form/answered-question"; + static HAS_VALIDATION_SEVERITY = + "http://onto.fel.cvut.cz/ontologies/form/has-validation-severity"; static RDFS_LABEL = JsonLdUtils.RDFS_LABEL; static RDFS_COMMENT = JsonLdUtils.RDFS_COMMENT; @@ -193,4 +200,9 @@ export default class Constants { label: "English", }, }; + + static VALIDATION_SEVERITY = { + ERROR: "error", + WARNING: "warning", + }; } diff --git a/src/model/ValidatorFactory.js b/src/model/ValidatorFactory.js index 896c2f8b..35852ed3 100644 --- a/src/model/ValidatorFactory.js +++ b/src/model/ValidatorFactory.js @@ -9,69 +9,148 @@ import FormUtils from "../util/FormUtils"; export default class ValidatorFactory { static createValidator(question, intl) { - if (question[Constants.REQUIRES_ANSWER]) { - if (FormUtils.isCheckbox(question)) { - //TODO revise - return ValidatorFactory._generateRequiresAnswerCheckBoxValidator( - question, - intl - ); + const validators = [ + this._patternValidator, + this._checkboxValidator, + this._requiredValidator, + this._completenessValidator, + ]; + + return (answer) => { + if (FormUtils.hasValidationLogic(question)) { + const answerValue = this._getAnswerValue(answer); + return this._validateAnswer(question, intl, answerValue, validators); } - return ValidatorFactory._generateRequiresAnswerValidator(question, intl); - } else { - return () => { - const result = {}; - result[Constants.HAS_VALID_ANSWER] = true; - delete result[Constants.HAS_VALIDATION_MESSAGE]; + }; + } + + static _isQuestionAnswered(answerValue) { + return ( + answerValue !== null && answerValue !== undefined && answerValue !== "" + ); + } + + static _isCheckboxAnswered(answerValue) { + return ( + answerValue !== null && + answerValue !== undefined && + answerValue !== "" && + answerValue !== false + ); + } + + static _validateAnswer(question, intl, answerValue, validators) { + const result = {}; + for (const validator of validators) { + const validationResult = validator(question, intl, answerValue); + if (!validationResult.isValid) { + result[Constants.HAS_VALID_ANSWER] = false; + result[Constants.HAS_VALIDATION_MESSAGE] = validationResult.message; + result[Constants.HAS_VALIDATION_SEVERITY] = + validationResult.validationSeverity; return result; - }; + } + if (result[Constants.HAS_VALID_ANSWER] === false) { + break; + } } + result[Constants.HAS_VALID_ANSWER] = true; + return result; } - static _generateRequiresAnswerValidator(question, intl) { - return (answer) => { - let val = null; - if (answer[Constants.HAS_DATA_VALUE]) { - val = JsonLdUtils.getJsonAttValue(answer, Constants.HAS_DATA_VALUE); - } else if (answer[Constants.HAS_OBJECT_VALUE]) { - val = JsonLdUtils.getJsonAttValue( - answer, - Constants.HAS_OBJECT_VALUE, - "@id" - ); + static _patternValidator(question, intl, answerValue) { + if (answerValue && answerValue.length > 0) { + if (question[Constants.PATTERN]) { + let pattern = question[Constants.PATTERN]; + const regExp = new RegExp(pattern); + const isValid = + regExp.test(answerValue) || + !ValidatorFactory._isQuestionAnswered(answerValue); + if (!isValid) { + return { + isValid: false, + validationSeverity: Constants.VALIDATION_SEVERITY.ERROR, + message: question[Constants.HAS_VALIDATION_MESSAGE] + ? question[Constants.HAS_VALIDATION_MESSAGE] + : "Please enter a valid answer to " + + JsonLdUtils.getLocalized( + question[JsonLdUtils.RDFS_LABEL], + intl + ), + }; + } } - const isValid = val !== null && val !== undefined && val !== ""; - const result = {}; - result[Constants.HAS_VALID_ANSWER] = isValid; - result[Constants.HAS_VALIDATION_MESSAGE] = isValid - ? null - : JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) + - " is missing a value."; - return result; - }; + } + return { isValid: true }; } - static _generateRequiresAnswerCheckBoxValidator(question, intl) { - return (answer) => { - let val = null; - if (answer[Constants.HAS_DATA_VALUE]) { - val = JsonLdUtils.getJsonAttValue(answer, Constants.HAS_DATA_VALUE); - } else if (answer[Constants.HAS_OBJECT_VALUE]) { - val = JsonLdUtils.getJsonAttValue( - answer, - Constants.HAS_OBJECT_VALUE, - "@id" - ); + static _requiredValidator(question, intl, answerValue) { + if ( + question[Constants.REQUIRES_ANSWER] && + !question[Constants.USED_ONLY_FOR_COMPLETENESS] + ) { + const isValid = ValidatorFactory._isQuestionAnswered(answerValue); + if (!isValid) { + return { + isValid: false, + validationSeverity: Constants.VALIDATION_SEVERITY.ERROR, + message: + JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) + + " is required", + }; } - const isValid = - val !== null && val !== undefined && val !== "" && val !== false; - const result = {}; - result[Constants.HAS_VALID_ANSWER] = isValid; - result[Constants.HAS_VALIDATION_MESSAGE] = isValid - ? null - : JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) + - " must be checked."; - return result; - }; + } + return { isValid: true }; + } + + static _checkboxValidator(question, intl, answerValue) { + if (FormUtils.isCheckbox(question)) { + if (question[Constants.REQUIRES_ANSWER]) { + const isValid = ValidatorFactory._isCheckboxAnswered(answerValue); + if (!isValid) { + return { + isValid: false, + validationSeverity: Constants.VALIDATION_SEVERITY.ERROR, + message: + JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) + + " must be checked", + }; + } + } + } + return { isValid: true }; + } + + static _completenessValidator(question, intl, answerValue) { + if ( + question[Constants.REQUIRES_ANSWER] && + question[Constants.USED_ONLY_FOR_COMPLETENESS] + ) { + const isValid = ValidatorFactory._isQuestionAnswered(answerValue); + if (!isValid) { + return { + isValid: false, + validationSeverity: Constants.VALIDATION_SEVERITY.WARNING, + message: + JsonLdUtils.getLocalized(question[JsonLdUtils.RDFS_LABEL], intl) + + " should be filled to complete the form.", + }; + } + } + return { isValid: true }; + } + + static _getAnswerValue(answer) { + let val = null; + if (answer[Constants.HAS_DATA_VALUE]) { + val = JsonLdUtils.getJsonAttValue(answer, Constants.HAS_DATA_VALUE); + } else if (answer[Constants.HAS_OBJECT_VALUE]) { + val = JsonLdUtils.getJsonAttValue( + answer, + Constants.HAS_OBJECT_VALUE, + "@id" + ); + } + return val; } } diff --git a/src/stories/assets/form/form1.json b/src/stories/assets/form/form1.json index 0fcbf9a9..4259c8eb 100644 --- a/src/stories/assets/form/form1.json +++ b/src/stories/assets/form/form1.json @@ -556,7 +556,7 @@ "has-layout-class": ["section", "checkbox", "answerable"], "@id": "answerable-section-4808", "has_related_question": ["aircraft-name-9553", "aircraft-number-2375"], - "@type": "doc:question" + "@type": "doc:question", }, { "label": "Category 1", diff --git a/src/stories/assets/form/form2.json b/src/stories/assets/form/form2.json index b8a44bb4..b9fc620f 100644 --- a/src/stories/assets/form/form2.json +++ b/src/stories/assets/form/form2.json @@ -99,6 +99,15 @@ }, "provides-dereferenceable-answer-values": { "@id": "http://onto.fel.cvut.cz/ontologies/form/provides-dereferenceable-answer-values" + }, + "used-only-for-completeness": { + "@id": "http://onto.fel.cvut.cz/ontologies/form/used-only-for-completeness" + }, + "has-validation-message": { + "@id": "http://onto.fel.cvut.cz/ontologies/form/has-validation-message" + }, + "pattern": { + "@id": "http://onto.fel.cvut.cz/ontologies/form/pattern" } }, "@graph": [ @@ -159,7 +168,11 @@ "@type": "doc:question", "has_related_question": [], "has-layout-class": "text", - "label": "First name" + "label": "First name (test completeness and invalid values)", + "requires-answer": true, + "used-only-for-completeness": true, + "pattern": "^[A-Za-z]+$", + "description": "This question requires answer only to complete the form (i.e. it is not necessary for saving the form). When you loose focus of the field, the validation is triggered. It has custom validation message which will be triggered if you write name that is not compliant with pattern \"^[A-Za-z]+$\"" }, { "@id": "last-name-1495", @@ -167,7 +180,11 @@ "has_related_question": [], "has-layout-class": "text", "has-preceding-question": "first-name-6663", - "label": "Last name" + "label": "Last name (testing required and invalid values)", + "requires-answer": true, + "pattern": "^[A-Za-z]+$", + "has-validation-message": "Answer can only use characters.", + "description": "This question requires answer to save the form. When you loose focus of the field, the validation is triggered. It has custom validation message which will be triggered if you write name that is not compliant with pattern \"^[A-Za-z]+$\"" }, { "@id": "job-9002", diff --git a/src/styles/s-forms.css b/src/styles/s-forms.css index 04ad2b8f..8d8b7b87 100644 --- a/src/styles/s-forms.css +++ b/src/styles/s-forms.css @@ -315,6 +315,21 @@ input:disabled { pointer-events: auto; } +.is-warning { + border-color: #dc9135; +} + +.is-warning:focus { + box-shadow: 0 0 0 0.2rem rgba(203, 127, 16, 0.25); + border-color: #dc9135; +} + +.is-warning.form-text { + color: #dc9135; + border: none; + box-shadow: none; +} + @keyframes emphasiseOnRelevant { 0% { display: none; diff --git a/src/util/FormUtils.js b/src/util/FormUtils.js index 68af3598..84bd8ab0 100644 --- a/src/util/FormUtils.js +++ b/src/util/FormUtils.js @@ -219,6 +219,9 @@ export default class FormUtils { if (question[Constants.REQUIRES_ANSWER_IF]) { return true; } + if (question[Constants.PATTERN]) { + return true; + } return false; } From 6a690b7841f9aff499a37b1f9005cf2a6a15bf0d Mon Sep 17 00:00:00 2001 From: Max Chopart Date: Mon, 4 Mar 2024 16:20:47 +0100 Subject: [PATCH 2/2] [Fix] Fixed form1.json parse error --- src/stories/assets/form/form1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/assets/form/form1.json b/src/stories/assets/form/form1.json index 4259c8eb..0fcbf9a9 100644 --- a/src/stories/assets/form/form1.json +++ b/src/stories/assets/form/form1.json @@ -556,7 +556,7 @@ "has-layout-class": ["section", "checkbox", "answerable"], "@id": "answerable-section-4808", "has_related_question": ["aircraft-name-9553", "aircraft-number-2375"], - "@type": "doc:question", + "@type": "doc:question" }, { "label": "Category 1",