Replace validation notifications with inline validations

issue #5409 & #5336

- update settings/general
- update signin
- update signup
- update edit user
- update reset password
- update setup/three
- remove `formatErrors` function from validationEngine mixin (it's no longer needed as inline validations should handle this instead)
This commit is contained in:
Kevin Ansfield 2015-07-07 18:14:23 +01:00
parent 1ba6bcc131
commit d4c8892ad5
18 changed files with 147 additions and 154 deletions

View File

@ -13,19 +13,18 @@ var TrimFocusInput = Ember.TextField.extend({
return false;
}),
didInsertElement: function () {
focusField: Ember.on('didInsertElement', function () {
// This fix is required until Mobile Safari has reliable
// autofocus, select() or focus() support
if (this.get('focus') && !device.ios()) {
this.$().val(this.$().val()).focus();
}
},
}),
focusOut: function () {
trimValue: Ember.on('focusOut', function () {
var text = this.$().val();
this.$().val(text.trim());
}
})
});
export default TrimFocusInput;

View File

@ -63,6 +63,10 @@ export default Ember.Controller.extend({
}),
actions: {
validate: function () {
this.get('model').validate(arguments);
},
save: function () {
var notifications = this.get('notifications'),
config = this.get('config');
@ -71,8 +75,10 @@ export default Ember.Controller.extend({
config.set('blogTitle', model.get('title'));
return model;
}).catch(function (errors) {
notifications.showErrors(errors);
}).catch(function (error) {
if (error) {
notifications.showAPIError(error);
}
});
},

View File

@ -1,7 +1,9 @@
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Controller.extend({
notifications: Ember.inject.service(),
errors: DS.Errors.create(),
users: '',
usersArray: Ember.computed('users', function () {
var users = this.get('users').split('\n').filter(function (email) {
@ -62,10 +64,11 @@ export default Ember.Controller.extend({
var self = this,
validationErrors = this.get('validateUsers'),
users = this.get('usersArray'),
errorMessages,
notifications = this.get('notifications'),
invitationsString;
this.get('errors').clear();
if (validationErrors === true && users.length > 0) {
this.get('authorRole').then(function (authorRole) {
Ember.RSVP.Promise.all(
@ -117,19 +120,15 @@ export default Ember.Controller.extend({
});
});
} else if (users.length === 0) {
// TODO: switch to inline-validation
notifications.showAlert('No users to invite.', {type: 'error'});
this.get('errors').add('users', 'No users to invite.');
} else {
errorMessages = validationErrors.map(function (error) {
validationErrors.forEach(function (error) {
// Only one error type here so far, but one day the errors might be more detailed
switch (error.error) {
case 'email':
return {message: error.user + ' is not a valid email.'};
self.get('errors').add('users', error.user + ' is not a valid email.');
}
});
// TODO: switch to inline-validation
notifications.showErrors(errorMessages);
}
}
}

View File

@ -3,13 +3,14 @@ import ValidationEngine from 'ghost/mixins/validation-engine';
import {request as ajax} from 'ic-ajax';
export default Ember.Controller.extend(ValidationEngine, {
validationType: 'signin',
submitting: false,
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),
// ValidationEngine settings
validationType: 'signin',
actions: {
authenticate: function () {
var model = this.get('model'),
@ -30,12 +31,12 @@ export default Ember.Controller.extend(ValidationEngine, {
// browsers and password managers that don't send proper events on autofill
$('#login').find('input').trigger('change');
this.validate({format: false}).then(function () {
this.validate().then(function () {
self.get('notifications').closeNotifications();
self.send('authenticate');
}).catch(function (errors) {
if (errors) {
self.get('notifications').showErrors(errors);
}).catch(function (error) {
if (error) {
self.get('notifications').showAPIError(error);
}
});
},
@ -45,27 +46,24 @@ export default Ember.Controller.extend(ValidationEngine, {
notifications = this.get('notifications'),
self = this;
if (!email) {
// TODO: Switch to in-line validation
return notifications.showNotification('Enter email address to reset password.', {type: 'error'});
}
this.validate({property: 'identification'}).then(function () {
self.set('submitting', true);
self.set('submitting', true);
ajax({
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'),
type: 'POST',
data: {
passwordreset: [{
email: email
}]
}
}).then(function () {
self.set('submitting', false);
notifications.showAlert('Please check your email for instructions.', {type: 'info'});
}).catch(function (resp) {
self.set('submitting', false);
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'});
ajax({
url: self.get('ghostPaths.url').api('authentication', 'passwordreset'),
type: 'POST',
data: {
passwordreset: [{
email: email
}]
}
}).then(function () {
self.set('submitting', false);
notifications.showAlert('Please check your email for instructions.', {type: 'info'});
}).catch(function (resp) {
self.set('submitting', false);
notifications.showAPIError(resp, {defaultErrorText: 'There was a problem with the reset, please try again.'});
});
});
}
}

View File

@ -20,8 +20,8 @@ export default Ember.Controller.extend(ValidationEngine, {
notifications.closeNotifications();
this.toggleProperty('submitting');
this.validate({format: false}).then(function () {
this.validate().then(function () {
this.toggleProperty('submitting');
ajax({
url: self.get('ghostPaths.url').api('authentication', 'invitation'),
type: 'POST',
@ -43,10 +43,9 @@ export default Ember.Controller.extend(ValidationEngine, {
self.toggleProperty('submitting');
notifications.showAPIError(resp);
});
}).catch(function (errors) {
self.toggleProperty('submitting');
if (errors) {
notifications.showErrors(errors);
}).catch(function (error) {
if (error) {
notifications.showAPIError(error);
}
});
}

View File

@ -2,8 +2,12 @@ import Ember from 'ember';
import SlugGenerator from 'ghost/models/slug-generator';
import isNumber from 'ghost/utils/isNumber';
import boundOneWay from 'ghost/utils/bound-one-way';
import ValidationEngine from 'ghost/mixins/validation-engine';
export default Ember.Controller.extend(ValidationEngine, {
// ValidationEngine settings
validationType: 'user',
export default Ember.Controller.extend({
ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(),

View File

@ -15,49 +15,6 @@ import TagSettingsValidator from 'ghost/validators/tag-settings';
// our extensions to the validator library
ValidatorExtensions.init();
// This is here because it is used by some things that format errors from api responses
// This function should be removed in the notifications refactor
// format errors to be used in `notifications.showErrors`.
// result is [{message: 'concatenated error messages'}]
function formatErrors(errors, opts) {
var message = 'There was an error';
opts = opts || {};
if (opts.wasSave && opts.validationType) {
message += ' saving this ' + opts.validationType;
}
if (Ember.isArray(errors)) {
// get the validator's error messages from the array.
// normalize array members to map to strings.
message = errors.map(function (error) {
var errorMessage;
if (typeof error === 'string') {
errorMessage = error;
} else {
errorMessage = error.message;
}
return Ember.Handlebars.Utils.escapeExpression(errorMessage);
}).join('<br />').htmlSafe();
} else if (errors instanceof Error) {
message += errors.message || '.';
} else if (typeof errors === 'object') {
// Get messages from server response
message += ': ' + getRequestErrorMessage(errors, true);
} else if (typeof errors === 'string') {
message += ': ' + errors;
} else {
message += '.';
}
// set format for notifications.showErrors
message = [{message: message}];
return message;
}
/**
* The class that gets this mixin will receive these properties and functions.
* It will be able to validate any properties on itself (or the model it passes to validate())
@ -163,15 +120,10 @@ export default Ember.Mixin.create({
return this.validate(options).then(function () {
return _super.call(self, options);
}).catch(function (result) {
// server save failed - validate() would have given back an array
if (!Ember.isArray(result)) {
if (options.format !== false) {
// concatenate all errors into an array with a single object: [{message: 'concatted message'}]
result = formatErrors(result, options);
} else {
// return the array of errors from the server
result = getRequestErrorMessage(result);
}
// server save failed or validator type doesn't exist
if (result && !Ember.isArray(result)) {
// return the array of errors from the server
result = getRequestErrorMessage(result);
}
return Ember.RSVP.reject(result);

View File

@ -377,6 +377,7 @@
}
.gh-flow-content .gh-flow-invite {
position: relative;
margin: 0 auto;
max-width: 400px;
width: 100%;

View File

@ -2,12 +2,21 @@
<div class="gh-flow-content-wrap">
<section class="gh-flow-content fade-in">
<form id="reset" class="gh-signin" method="post" novalidate="novalidate" {{action "submit" on="submit"}}>
<div class="form-group">
{{input value=newPassword class="gh-input password" type="password" placeholder="Password" name="newpassword" autofocus="autofocus" }}
</div>
<div class="form-group">
{{input value=ne2Password class="gh-input password" type="password" placeholder="Confirm Password" name="ne2password" }}
</div>
{{#gh-form-group errors=errors property="newPassword"}}
{{input value=newPassword class="gh-input password" type="password" placeholder="Password" name="newpassword" autofocus="autofocus" focusOut=(action "validate" "newPassword")}}
<div class="pw-strength">
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot <!--pw-strength-activedot-->"></div>
</div>
{{gh-error-message errors=errors property="newPassword"}}
{{/gh-form-group}}
{{#gh-form-group errors=errors property="ne2Password"}}
{{input value=ne2Password class="gh-input password" type="password" placeholder="Confirm Password" name="ne2password" focusOut=(action "validate" "ne2Password")}}
{{gh-error-message errors=errors property="ne2Password"}}
{{/gh-form-group}}
<button class="btn btn-blue btn-block" type="submit" disabled={{submitting}}>Reset Password</button>
</form>
</section>

View File

@ -10,21 +10,22 @@
<form id="settings-general" novalidate="novalidate">
<fieldset>
<div class="form-group">
{{#gh-form-group errors=model.errors property="title"}}
<label for="blog-title">Blog Title</label>
{{input id="blog-title" class="gh-input" name="general[title]" type="text" value=model.title}}
{{gh-input id="blog-title" class="gh-input" name="general[title]" type="text" value=model.title focusOut=(action "validate" "title")}}
{{gh-error-message errors=model.errors property="title"}}
<p>The name of your blog</p>
</div>
{{/gh-form-group}}
<div class="form-group description-container">
{{#gh-form-group class="description-container" errors=model.errors property="description"}}
<label for="blog-description">Blog Description</label>
{{textarea id="blog-description" class="gh-input" name="general[description]" value=model.description}}
{{gh-textarea id="blog-description" class="gh-input" name="general[description]" value=model.description focusOut=(action "validate" "description")}}
{{gh-error-message errors=model.errors property="description"}}
<p>
Describe what your blog is about
{{gh-count-characters model.description}}
</p>
</div>
{{/gh-form-group}}
</fieldset>
<div class="form-group">
@ -52,7 +53,7 @@
<div class="form-group">
<label for="postsPerPage">Posts per page</label>
{{! `pattern` brings up numeric keypad allowing any number of digits}}
{{input id="postsPerPage" class="gh-input" name="general[postsPerPage]" focus-out="checkPostsPerPage" value=model.postsPerPage min="1" max="1000" type="number" pattern="[0-9]*"}}
{{gh-input id="postsPerPage" class="gh-input" name="general[postsPerPage]" focus-out="checkPostsPerPage" value=model.postsPerPage min="1" max="1000" type="number" pattern="[0-9]*"}}
<p>How many posts should be displayed on each page</p>
</div>
@ -92,10 +93,11 @@
</div>
{{#if model.isPrivate}}
<div class="form-group">
{{input name="general[password]" type="text" value=model.password}}
{{#gh-form-group errors=model.errors property="password"}}
{{gh-input name="general[password]" type="text" value=model.password focusOut=(action "validate" "password")}}
{{gh-error-message errors=model.errors property="password"}}
<p>This password will be needed to access your blog. All search engine optimization and social features are now disabled. This password is stored in plaintext.</p>
</div>
{{/gh-form-group}}
{{/if}}
</fieldset>
</form>

View File

@ -8,6 +8,7 @@
<form class="gh-flow-invite">
<label>Enter one email address per line, well handle the rest! <i class="icon-mail"></i></label>
{{textarea class="gh-input" name="users" value=users required="required"}}
{{gh-error-message errors=errors property="users"}}
</form>
<button {{action 'invite'}} class="btn btn-default btn-lg btn-block {{buttonClass}}">

View File

@ -2,17 +2,19 @@
<div class="gh-flow-content-wrap">
<section class="gh-flow-content">
<form id="login" class="gh-signin" method="post" novalidate="novalidate" {{action "validateAndAuthenticate" on="submit"}}>
<div class="form-group">
{{#gh-form-group errors=model.errors property="identification"}}
<span class="input-icon icon-mail">
{{gh-trim-focus-input class="gh-input email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" value=model.identification}}
{{gh-trim-focus-input class="gh-input email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" value=model.identification focusOut=(action "validate" "identification")}}
</span>
</div>
<div class="form-group">
{{gh-error-message errors=model.errors property="identification"}}
{{/gh-form-group}}
{{#gh-form-group errors=model.errors property="password"}}
<span class="input-icon icon-lock forgotten-wrap">
{{input class="gh-input password" type="password" placeholder="Password" name="password" tabindex="2" value=model.password}}
{{input class="gh-input password" type="password" placeholder="Password" name="password" tabindex="2" value=model.password focusOut=(action "validate" "password")}}
<button type="button" {{action "forgotten"}} class="forgotten-link btn btn-link" tabindex="4" disabled={{submitting}}>Forgot?</button>
</span>
</div>
{{gh-error-message errors=model.errors property="password"}}
{{/gh-form-group}}
<button id="login-button" class="login btn btn-blue btn-block" type="submit" tabindex="3" disabled={{submitting}}>Sign in</button>
</form>

View File

@ -14,26 +14,28 @@
<figure class="account-image">
<div id="account-image" class="img" style="background-image: url(http://www.gravatar.com/avatar/75e958a6674a7d68fe0d575fb235116c?d=404&s=250)">
<!-- fallback to: Ghost/core/shared/img/ghosticon.jpg -->
<span class="sr-only">User imge</span>
<span class="sr-only">User image</span>
</div>
<a class="edit-account-image" href="#"><i class="icon-photos "><span class="sr-only">Upload an image</span></i></a>
</figure>
<div class="form-group">
{{#gh-form-group errors=model.errors property="email"}}
<label for="email-address">Email address</label>
<span class="input-icon icon-mail">
{{input class="gh-input" type="email" name="email" autocorrect="off" value=model.email }}
{{gh-input type="email" name="email" placeholder="Eg. john@example.com" class="gh-input" autofocus="autofocus" autocorrect="off" value=model.email focusOut=(action "validate" "email")}}
</span>
</div>
<div class="form-group">
{{gh-error-message errors=model.errors property="email"}}
{{/gh-form-group}}
{{#gh-form-group errors=model.errors property="name"}}
<label for="full-name">Full name</label>
<span class="input-icon icon-user">
{{gh-trim-focus-input class="gh-input" type="text" name="name" autofocus="autofocus" autocorrect="off" value=model.name }}
{{gh-input type="text" name="name" placeholder="Eg. John H. Watson" class="gh-input" autofocus="autofocus" autocorrect="off" value=model.name focusOut=(action "validate" "name")}}
</span>
</div>
<div class="form-group">
{{gh-error-message errors=model.errors property="name"}}
{{/gh-form-group}}
{{#gh-form-group errors=model.errors property="password"}}
<label for="password">Password</label>
<span class="input-icon icon-lock">
{{input class="gh-input" type="password" name="password" autofocus="autofocus" autocorrect="off" value=model.password }}
{{input class="gh-input" type="password" name="password" autofocus="autofocus" autocorrect="off" value=model.password focusOut=(action "validate" "password")}}
<div class="pw-strength">
<div class="pw-strength-dot"></div>
<div class="pw-strength-dot"></div>
@ -42,7 +44,8 @@
<div class="pw-strength-dot <!--pw-strength-activedot-->"></div>
</div>
</span>
</div>
{{gh-error-message errors=model.errors property="password"}}
{{/gh-form-group}}
</form>
<button type="submit" class="btn btn-green btn-lg btn-block" {{action "signup"}} disabled={{submitting}}>Create Account</button>

View File

@ -53,32 +53,39 @@
<button type="button" {{action "openModal" "upload" user "image"}} class="edit-user-image js-modal-image">Edit Picture</button>
</figure>
<div class="form-group first-form-group">
{{#gh-form-group class="first-form-group" errors=user.errors property="name"}}
<label for="user-name">Full Name</label>
{{input value=user.name id="user-name" class="gh-input user-name" placeholder="Full Name" autocorrect="off"}}
<p>Use your real name so people can recognise you</p>
</div>
{{input value=user.name id="user-name" class="gh-input user-name" placeholder="Full Name" autocorrect="off" focusOut=(action "validate" "name")}}
{{#if user.errors.name}}
{{gh-error-message errors=user.errors property="name"}}
{{else}}
<p>Use your real name so people can recognise you</p>
{{/if}}
{{/gh-form-group}}
</fieldset>
<fieldset class="user-details-bottom">
<div class="form-group">
{{#gh-form-group errors=user.errors property="slug"}}
<label for="user-slug">Slug</label>
{{gh-input class="gh-input user-name" id="user-slug" value=slugValue name="user" focus-out="updateSlug" placeholder="Slug" selectOnClick="true" autocorrect="off"}}
<p>{{gh-blog-url}}/author/{{slugValue}}</p>
</div>
{{gh-error-message errors=user.errors property="slug"}}
{{/gh-form-group}}
<div class="form-group">
{{#gh-form-group errors=user.errors property="email"}}
<label for="user-email">Email</label>
{{!-- Administrators only see text of Owner's email address but not input --}}
{{#unless isAdminUserOnOwnerProfile}}
{{input type="email" value=user.email id="user-email" class="gh-input" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off"}}
{{input type="email" value=user.email id="user-email" name="email" class="gh-input" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "email")}}
{{gh-error-message errors=user.errors property="email"}}
{{else}}
<span>{{user.email}}</span>
{{/unless}}
<p>Used for notifications</p>
</div>
{{/gh-form-group}}
{{#if rolesDropdownIsVisible}}
<div class="form-group">
<label for="user-role">Role</label>
@ -94,26 +101,30 @@
<p>What permissions should this user have?</p>
</div>
{{/if}}
<div class="form-group">
{{#gh-form-group errors=user.errors property="location"}}
<label for="user-location">Location</label>
{{input type="text" value=user.location id="user-location" class="gh-input"}}
{{input type="text" value=user.location id="user-location" class="gh-input" focusOut=(action "validate" "location")}}
{{gh-error-message errors=user.errors property="location"}}
<p>Where in the world do you live?</p>
</div>
{{/gh-form-group}}
<div class="form-group">
{{#gh-form-group errors=user.errors property="website"}}
<label for="user-website">Website</label>
{{input type="url" value=user.website id="user-website" class="gh-input" autocapitalize="off" autocorrect="off" autocomplete="off"}}
{{input type="url" value=user.website id="user-website" class="gh-input" autocapitalize="off" autocorrect="off" autocomplete="off" focusOut=(action "validate" "website")}}
{{gh-error-message errors=user.errors property="website"}}
<p>Have a website or blog other than this one? Link it!</p>
</div>
{{/gh-form-group}}
<div class="form-group bio-container">
{{#gh-form-group class="bio-container" errors=user.errors property="bio"}}
<label for="user-bio">Bio</label>
{{textarea id="user-bio" class="gh-input" value=user.bio}}
{{textarea id="user-bio" class="gh-input" value=user.bio focusOut=(action "validate" "bio")}}
{{gh-error-message errors=user.errors property="bio"}}
<p>
Write about you, in 200 characters or less.
{{gh-count-characters user.bio}}
</p>
</div>
{{/gh-form-group}}
<hr />

View File

@ -5,7 +5,10 @@ var ResetValidator = BaseValidator.create({
var p1 = model.get('newPassword'),
p2 = model.get('ne2Password');
if (!validator.isLength(p1, 8)) {
if (validator.empty(p1)) {
model.get('errors').add('newPassword', 'Please enter a password.');
this.invalidate();
} else if (!validator.isLength(p1, 8)) {
model.get('errors').add('newPassword', 'The password is not long enough.');
this.invalidate();
} else if (!validator.equals(p1, p2)) {

View File

@ -20,7 +20,7 @@ var SettingValidator = BaseValidator.create({
},
password: function (model) {
var isPrivate = model.get('isPrivate'),
password = this.get('password');
password = model.get('password');
if (isPrivate && password === '') {
model.get('errors').add('password', 'Password must be supplied');

View File

@ -7,6 +7,7 @@ var SigninValidator = BaseValidator.create({
if (validator.empty(id)) {
model.get('errors').add('identification', 'Please enter an email');
this.invalidate();
} else if (!validator.isEmail(id)) {
model.get('errors').add('identification', 'Invalid email');
this.invalidate();

View File

@ -10,7 +10,10 @@ var UserValidator = BaseValidator.create({
var name = model.get('name');
if (this.isActive(model)) {
if (!validator.isLength(name, 0, 150)) {
if (validator.empty(name)) {
model.get('errors').add('name', 'Please enter a name.');
this.invalidate();
} else if (!validator.isLength(name, 0, 150)) {
model.get('errors').add('name', 'Name is too long');
this.invalidate();
}