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: 73d997046c
**
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
This commit is contained in:
commit
b4045e997f
|
@ -57,6 +57,7 @@ entry.
|
|||
- Initialize Sidekiq with the list of queues used by GitLab
|
||||
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
|
||||
- Shortened merge request modal to let clipboard button not overlap
|
||||
- Adds JavaScript validation for group path editing field
|
||||
- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
|
||||
- Improve search query parameter naming in /admin/users !7115 (YarNayar)
|
||||
- Fix table pagination to be responsive
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 = {}));
|
|
@ -1,131 +1,9 @@
|
|||
/* eslint-disable */
|
||||
|
||||
//= require gl_field_error
|
||||
|
||||
((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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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';
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
= render 'devise/shared/tab_single', tab_title: 'Sign in preview'
|
||||
.login-box
|
||||
%form.show-gl-field-errors
|
||||
%form.gl-show-field-errors
|
||||
.form-group
|
||||
= label_tag :login
|
||||
= text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= render 'devise/shared/tab_single', tab_title: 'Resend confirmation instructions'
|
||||
.login-box
|
||||
.login-body
|
||||
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
|
||||
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
.form-group
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= render 'devise/shared/tab_single', tab_title:'Change your password'
|
||||
.login-box
|
||||
.login-body
|
||||
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f|
|
||||
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'gl-show-field-errors' }) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
= f.hidden_field :reset_password_token
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= render 'devise/shared/tab_single', tab_title: 'Reset Password'
|
||||
.login-box
|
||||
.login-body
|
||||
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
|
||||
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
.form-group
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f|
|
||||
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
|
||||
%div.form-group
|
||||
= f.label "Username or email", for: :login
|
||||
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do
|
||||
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'gl-show-field-errors') do
|
||||
.form-group
|
||||
= label_tag :username, 'Username or email'
|
||||
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do
|
||||
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
|
||||
.form-group
|
||||
= label_tag :username, "#{server['label']} Username"
|
||||
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
.login-box
|
||||
.login-body
|
||||
- if @user.two_factor_otp_enabled?
|
||||
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f|
|
||||
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user gl-show-field-errors' }) do |f|
|
||||
- resource_params = params[resource_name].presence || params
|
||||
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
|
||||
%div
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
|
||||
.login-body
|
||||
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user show-gl-field-errors", "aria-live" => "assertive" }) do |f|
|
||||
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
%div.form-group
|
||||
|
@ -8,7 +8,7 @@
|
|||
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
|
||||
%div.username.form-group
|
||||
= f.label :username
|
||||
= f.text_field :username, class: "form-control middle no-gl-field-error", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
|
||||
= f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true, title: 'Please create a username with only alphanumeric characters.'
|
||||
%p.validation-error.hide Username is already taken.
|
||||
%p.validation-success.hide Username is available.
|
||||
%p.validation-pending.hide Checking username availability...
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= render 'devise/shared/tab_single', tab_title: 'Resend unlock instructions'
|
||||
.login-box
|
||||
.login-body
|
||||
= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
|
||||
= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
|
||||
.devise-errors
|
||||
= devise_error_messages!
|
||||
.form-group.append-bottom-20
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
.panel-heading
|
||||
Group settings
|
||||
.panel-body
|
||||
= form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f|
|
||||
= form_for @group, html: { multipart: true, class: "form-horizontal gl-show-field-errors" }, authenticity_token: true do |f|
|
||||
= form_errors(@group)
|
||||
= render 'shared/group_form', f: f
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
New Group
|
||||
%hr
|
||||
|
||||
= form_for @group, html: { class: 'group-form form-horizontal' } do |f|
|
||||
= form_for @group, html: { class: 'group-form form-horizontal gl-show-field-errors' } do |f|
|
||||
= form_errors(@group)
|
||||
= render 'shared/group_form', f: f, autofocus: true
|
||||
|
||||
|
|
|
@ -9,11 +9,13 @@
|
|||
= f.label :path, class: 'control-label' do
|
||||
Group path
|
||||
.col-sm-10
|
||||
.input-group
|
||||
.input-group.gl-field-error-anchor
|
||||
.input-group-addon
|
||||
= root_url
|
||||
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
|
||||
autofocus: local_assigns[:autofocus] || false
|
||||
autofocus: local_assigns[:autofocus] || false, pattern: "[a-zA-Z0-9-_]+",
|
||||
required: true, title: 'Please choose a group name with no special characters.'
|
||||
|
||||
- if @group.persisted?
|
||||
.alert.alert-warning.prepend-top-10
|
||||
%ul
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
%form.show-gl-field-errors{action: 'submit', method: 'post'}
|
||||
%form.gl-show-field-errors{action: 'submit', method: 'post'}
|
||||
.form-group
|
||||
%input.required-text{required: true, type: 'text'} Text
|
||||
.form-group
|
||||
|
@ -10,6 +10,6 @@
|
|||
.form-group
|
||||
%input.hidden{ type:'hidden' }
|
||||
.form-group
|
||||
%input.custom.no-gl-field-errors{ type:'text' } Custom, do not validate
|
||||
%input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate
|
||||
.form-group
|
||||
%input.submit{type: 'submit'} Submit
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
describe('GL Style Field Errors', function() {
|
||||
beforeEach(function() {
|
||||
fixture.load('gl_field_errors.html');
|
||||
const $form = this.$form = $('form.show-gl-field-errors');
|
||||
const $form = this.$form = $('form.gl-show-field-errors');
|
||||
this.fieldErrors = new global.GlFieldErrors($form);
|
||||
});
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
|||
});
|
||||
|
||||
it('should ignore elements with custom error handling', function() {
|
||||
const customErrorFlag = 'no-gl-field-errors';
|
||||
const customErrorFlag = 'gl-field-error-ignore';
|
||||
const customErrorElem = $(`.${customErrorFlag}`);
|
||||
|
||||
expect(customErrorElem.length).toBe(1);
|
||||
|
|
Loading…
Reference in New Issue