diff options
author | Alfredo Sumaran <alfredo@gitlab.com> | 2016-11-03 20:37:00 +0300 |
---|---|---|
committer | Alfredo Sumaran <alfredo@gitlab.com> | 2016-11-03 20:37:00 +0300 |
commit | b4045e997feae2043965706ae9f89ce6221a3792 (patch) | |
tree | 41d9fc9aa03482c1297b2a96a0bad7ec5abd9f62 /app/assets | |
parent | 651bdd7c34c9ebf686b8357d8fc3ca4d0a58792e (diff) | |
parent | b8b3f919fd37abddf082e0af22bc28b0c3d7c1b2 (diff) |
Merge branch 'group-name-validation' into 'master'
Adds JavaScript validation for group path editing
## What does this MR do?
- Prevents group_edit form submission when special characters are included in the new group name
- Enhances gl_field_errors to support this use case and be more re-usable.
## Are there points in the code the reviewer needs to double check?
**The implementation of validation is here: https://gitlab.com/gitlab-org/gitlab-ce/commit/73d997046cac376ccdbcaac6f183002bc19b9a1f**
The rest of the diff for this MR is augmenting gl_field_errors
## Why was this MR needed?
- Currently we allow submission and a 500 error is returned.
## Screenshots (if relevant)
![2016-10-21_14.11.21](/uploads/2bef5764d3f2429dd0f900661153eef7/2016-10-21_14.11.21.gif)
## Does this MR meet the acceptance criteria?
- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md) entry added
- [x] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
- [x] All builds are passing
- [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
- [x] Branch has no merge conflicts with `master` (if it does - rebase it please)
- [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
## What are the relevant issue numbers?
Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/23564, https://gitlab.com/gitlab-org/gitlab-ce/issues/19756, https://gitlab.com/gitlab-org/gitlab-ce/issues/19736, https://gitlab.com/gitlab-org/gitlab-ce/issues/23922
See merge request !7041
Diffstat (limited to 'app/assets')
-rw-r--r-- | app/assets/javascripts/dispatcher.js.es6 | 2 | ||||
-rw-r--r-- | app/assets/javascripts/gl_field_error.js.es6 | 164 | ||||
-rw-r--r-- | app/assets/javascripts/gl_field_errors.js.es6 | 130 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/forms.scss | 32 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/login.scss | 30 |
5 files changed, 203 insertions, 155 deletions
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index ff8b8f6d0ae..8e4fd1f19ba 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -299,7 +299,7 @@ }; Dispatcher.prototype.initFieldErrors = function() { - $('.show-gl-field-errors').each((i, form) => { + $('.gl-show-field-errors').each((i, form) => { new gl.GlFieldErrors(form); }); }; diff --git a/app/assets/javascripts/gl_field_error.js.es6 b/app/assets/javascripts/gl_field_error.js.es6 new file mode 100644 index 00000000000..f7cbecc0385 --- /dev/null +++ b/app/assets/javascripts/gl_field_error.js.es6 @@ -0,0 +1,164 @@ +/* eslint-disable no-param-reassign */ +((global) => { + /* + * This class overrides the browser's validation error bubbles, displaying custom + * error messages for invalid fields instead. To begin validating any form, add the + * class `gl-show-field-errors` to the form element, and ensure error messages are + * declared in each inputs' `title` attribute. If no title is declared for an invalid + * field the user attempts to submit, "This field is required." will be shown by default. + * + * Opt not to validate certain fields by adding the class `gl-field-error-ignore` to the input. + * + * Set a custom error anchor for error message to be injected after with the + * class `gl-field-error-anchor` + * + * Examples: + * + * Basic: + * + * <form class='gl-show-field-errors'> + * <input type='text' name='username' title='Username is required.'/> + * </form> + * + * Ignore specific inputs (e.g. UsernameValidator): + * + * <form class='gl-show-field-errors'> + * <div class="form-group> + * <input type='text' class='gl-field-errors-ignore' pattern='[a-zA-Z0-9-_]+'/> + * </div> + * <div class="form-group"> + * <input type='text' name='username' title='Username is required.'/> + * </div> + * </form> + * + * Custom Error Anchor (allows error message to be injected after specified element): + * + * <form class='gl-show-field-errors'> + * <div class="form-group gl-field-error-anchor"> + * <input type='text' name='username' title='Username is required.'/> + * // Error message typically injected here + * </div> + * // Error message now injected here + * </form> + * + * */ + + /* + * Regex Patterns in use: + * + * Only alphanumeric: : "[a-zA-Z0-9]+" + * No special characters : "[a-zA-Z0-9-_]+", + * + * */ + + const errorMessageClass = 'gl-field-error'; + const inputErrorClass = 'gl-field-error-outline'; + const errorAnchorSelector = '.gl-field-error-anchor'; + const ignoreInputSelector = '.gl-field-error-ignore'; + + class GlFieldError { + constructor({ input, formErrors }) { + this.inputElement = $(input); + this.inputDomElement = this.inputElement.get(0); + this.form = formErrors; + this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; + this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${this.errorMessage}</p>`); + + this.state = { + valid: false, + empty: true, + }; + + this.initFieldValidation(); + } + + initFieldValidation() { + const customErrorAnchor = this.inputElement.parents(errorAnchorSelector); + const errorAnchor = customErrorAnchor.length ? customErrorAnchor : this.inputElement; + + // hidden when injected into DOM + errorAnchor.after(this.fieldErrorElement); + this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); + this.scopedSiblings = this.safelySelectSiblings(); + } + + safelySelectSiblings() { + // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled + const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreInputSelector})`); + const parentContainer = this.inputElement.parent('.form-group'); + + // Only select siblings when they're scoped within a form-group with one input + const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; + + return safelyScoped ? unignoredSiblings : this.fieldErrorElement; + } + + renderValidity() { + this.renderClear(); + + if (this.state.valid) { + this.renderValid(); + } else if (this.state.empty) { + this.renderEmpty(); + } else if (!this.state.valid) { + this.renderInvalid(); + } + } + + handleInvalidSubmit(event) { + event.preventDefault(); + const currentValue = this.accessCurrentValue(); + this.state.valid = false; + this.state.empty = currentValue === ''; + + this.renderValidity(); + this.form.focusOnFirstInvalid.apply(this.form); + // For UX, wait til after first invalid submission to check each keyup + this.inputElement.off('keyup.fieldValidator') + .on('keyup.fieldValidator', this.updateValidity.bind(this)); + } + + /* Get or set current input value */ + accessCurrentValue(newVal) { + return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); + } + + getInputValidity() { + return this.inputDomElement.validity.valid; + } + + updateValidity() { + const inputVal = this.accessCurrentValue(); + this.state.empty = !inputVal.length; + this.state.valid = this.getInputValidity(); + this.renderValidity(); + } + + renderValid() { + return this.renderClear(); + } + + renderEmpty() { + return this.renderInvalid(); + } + + renderInvalid() { + this.inputElement.addClass(inputErrorClass); + this.scopedSiblings.hide(); + return this.fieldErrorElement.show(); + } + + renderClear() { + const inputVal = this.accessCurrentValue(); + if (!inputVal.split(' ').length) { + const trimmedInput = inputVal.trim(); + this.accessCurrentValue(trimmedInput); + } + this.inputElement.removeClass(inputErrorClass); + this.scopedSiblings.hide(); + this.fieldErrorElement.hide(); + } + } + + global.GlFieldError = GlFieldError; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index be6c3ec274f..6ce392d2a5b 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,131 +1,9 @@ /* eslint-disable */ -((global) => { - /* - * This class overrides the browser's validation error bubbles, displaying custom - * error messages for invalid fields instead. To begin validating any form, add the - * class `show-gl-field-errors` to the form element, and ensure error messages are - * declared in each inputs' title attribute. - * - * Example: - * - * <form class='show-gl-field-errors'> - * <input type='text' name='username' title='Username is required.'/> - *</form> - * - * */ - - const errorMessageClass = 'gl-field-error'; - const inputErrorClass = 'gl-field-error-outline'; - - class GlFieldError { - constructor({ input, formErrors }) { - this.inputElement = $(input); - this.inputDomElement = this.inputElement.get(0); - this.form = formErrors; - this.errorMessage = this.inputElement.attr('title') || 'This field is required.'; - this.fieldErrorElement = $(`<p class='${errorMessageClass} hide'>${ this.errorMessage }</p>`); - - this.state = { - valid: false, - empty: true - }; - - this.initFieldValidation(); - } - - initFieldValidation() { - // hidden when injected into DOM - this.inputElement.after(this.fieldErrorElement); - this.inputElement.off('invalid').on('invalid', this.handleInvalidSubmit.bind(this)); - this.scopedSiblings = this.safelySelectSiblings(); - } - - safelySelectSiblings() { - // Apply `ignoreSelector` in markup to siblings whose visibility should not be toggled with input validity - const ignoreSelector = '.validation-ignore'; - const unignoredSiblings = this.inputElement.siblings(`p:not(${ignoreSelector})`); - const parentContainer = this.inputElement.parent('.form-group'); - // Only select siblings when they're scoped within a form-group with one input - const safelyScoped = parentContainer.length && parentContainer.find('input').length === 1; - - return safelyScoped ? unignoredSiblings : this.fieldErrorElement; - } - - renderValidity() { - this.renderClear(); - - if (this.state.valid) { - return this.renderValid(); - } - - if (this.state.empty) { - return this.renderEmpty(); - } - - if (!this.state.valid) { - return this.renderInvalid(); - } - - } +//= require gl_field_error - handleInvalidSubmit(event) { - event.preventDefault(); - const currentValue = this.accessCurrentValue(); - this.state.valid = false; - this.state.empty = currentValue === ''; - - this.renderValidity(); - this.form.focusOnFirstInvalid.apply(this.form); - // For UX, wait til after first invalid submission to check each keyup - this.inputElement.off('keyup.field_validator') - .on('keyup.field_validator', this.updateValidity.bind(this)); - - } - - /* Get or set current input value */ - accessCurrentValue(newVal) { - return newVal ? this.inputElement.val(newVal) : this.inputElement.val(); - } - - getInputValidity() { - return this.inputDomElement.validity.valid; - } - - updateValidity() { - const inputVal = this.accessCurrentValue(); - this.state.empty = !inputVal.length; - this.state.valid = this.getInputValidity(); - this.renderValidity(); - } - - renderValid() { - return this.renderClear(); - } - - renderEmpty() { - return this.renderInvalid(); - } - - renderInvalid() { - this.inputElement.addClass(inputErrorClass); - this.scopedSiblings.hide(); - return this.fieldErrorElement.show(); - } - - renderClear() { - const inputVal = this.accessCurrentValue(); - if (!inputVal.split(' ').length) { - const trimmedInput = inputVal.trim(); - this.accessCurrentValue(trimmedInput); - } - this.inputElement.removeClass(inputErrorClass); - this.scopedSiblings.hide(); - this.fieldErrorElement.hide(); - } - } - - const customValidationFlag = 'no-gl-field-errors'; +((global) => { + const customValidationFlag = 'gl-field-error-ignore'; class GlFieldErrors { constructor(form) { @@ -144,7 +22,7 @@ this.state.inputs = this.form.find(validateSelectors).toArray() .filter((input) => !input.classList.contains(customValidationFlag)) - .map((input) => new GlFieldError({ input, formErrors: this })); + .map((input) => new global.GlFieldError({ input, formErrors: this })); this.form.on('submit', this.catchInvalidFormSubmit); } diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 761c07384f4..f0727e9688a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -136,3 +136,35 @@ label { color: $red-normal; } +.gl-show-field-errors { + .gl-field-success-outline { + border: 1px solid $green-normal; + + &:focus { + box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; + border: 0 none; + } + } + + .gl-field-error-outline { + border: 1px solid $red-normal; + + &:focus { + box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); + border: 0 none; + } + } + + .gl-field-success-message { + color: $green-normal; + } + + .gl-field-error-message { + color: $red-normal; + } + + .gl-field-hint { + color: $gl-text-color; + } +} + diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index a2f5c6c6bd3..10f67b47998 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -75,43 +75,17 @@ .login-body { font-size: 13px; - input + p { margin-top: 5px; } - .gl-field-success-outline { - border: 1px solid $green-normal; - - &:focus { - box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 $green-normal; - border: 0 none; - } - } - - .gl-field-error-outline { - border: 1px solid $red-normal; - - &:focus { - box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px rgba(0, 0, 0, 0.075) inset, 0 0 4px 0 rgba(210, 40, 82, 0.6); - border: 0 none; - } - } - - .username .validation-success, - .gl-field-success-message { + .username .validation-success { color: $green-normal; } - .username .validation-error, - .gl-field-error-message { + .username .validation-error { color: $red-normal; } - - .gl-field-hint { - color: $gl-text-color; - } - } } |