mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-23 19:02:29 +03:00
🎨 Redesigned user authentication pages (#2286)
Refs https://www.notion.so/ghost/Invite-staff-users-steps-in-setup-guide-367737e13d97450a98a0f39ec6b68181 * Simplified the selfhoster setup flow to one setup page only * Redesigned the reset password pages and the signup page for new staff members Co-authored-by: Daniel Lockyer <hi@daniellockyer.com>
This commit is contained in:
parent
e46a406645
commit
3ae3e8142a
@ -1175,3 +1175,18 @@ remove|ember-template-lint|no-invalid-interactive|165|59|165|59|df9c2bf70a166edc
|
|||||||
remove|ember-template-lint|no-invalid-interactive|221|163|221|163|a5145bca59feb20e1078505aea2fd35758795d52|1646611200000|1649199600000|1651791600000|app/components/modal-portal-settings.hbs
|
remove|ember-template-lint|no-invalid-interactive|221|163|221|163|a5145bca59feb20e1078505aea2fd35758795d52|1646611200000|1649199600000|1651791600000|app/components/modal-portal-settings.hbs
|
||||||
remove|ember-template-lint|no-invalid-interactive|235|60|235|60|50dc8bff0ff82f6924e528d71f114326cfed17ab|1646611200000|1649199600000|1651791600000|app/components/modal-portal-settings.hbs
|
remove|ember-template-lint|no-invalid-interactive|235|60|235|60|50dc8bff0ff82f6924e528d71f114326cfed17ab|1646611200000|1649199600000|1651791600000|app/components/modal-portal-settings.hbs
|
||||||
remove|ember-template-lint|no-invalid-interactive|280|59|280|59|76d4aa1f6519b5257d8809ab05cf5d34319bcac3|1646611200000|1649199600000|1651791600000|app/components/modal-portal-settings.hbs
|
remove|ember-template-lint|no-invalid-interactive|280|59|280|59|76d4aa1f6519b5257d8809ab05cf5d34319bcac3|1646611200000|1649199600000|1651791600000|app/components/modal-portal-settings.hbs
|
||||||
|
remove|ember-template-lint|style-concatenation|24|46|24|46|f0aa84093e7f6b05f31e553bbb8042e19011c841|1646611200000|1649199600000|1651791600000|app/templates/signin.hbs
|
||||||
|
remove|ember-template-lint|no-action|10|78|10|78|95d57bb1a3a47d94a196cb0c0daa117fa681892a|1646611200000|1649199600000|1651791600000|app/templates/signup.hbs
|
||||||
|
add|ember-template-lint|no-action|21|31|21|31|3aa834e53af871a821ebb1b47f7a04f925c2b593|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|22|35|22|35|ae65e93ed2b79a307e0eba50b154fe84ee1ae210|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|39|31|39|31|eefdee43411bdd45c2786b9e826e6b550fd6212d|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|40|35|40|35|a8e4bd4c57a8f2df65749b0cfa4349da22b2a0aa|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|58|31|58|31|1709109776bf3fc47aaecc21e6d3ec8a0489ad6b|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|59|35|59|35|8000241a81189d3876d264331ec0c9b6476df076|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|77|31|77|31|6756e119daad4aa143724e9b56a6aef744d65251|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-action|78|35|78|35|26b1d89c0e5bbcd629a215903c0819d35f798aa6|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-passed-in-event-handlers|21|24|21|24|f0b7babba7593639d68dadae2f19e7144dd8c63e|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-passed-in-event-handlers|39|24|39|24|2692530760cb1f7156dcf9570aacf0f8baca2770|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-passed-in-event-handlers|58|24|58|24|ce0d7e2e732b22ce3643fb9b1ac6ecef07c275ff|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-passed-in-event-handlers|77|24|77|24|80681fcec2258c3d81a231bbd635aa1f03ff1452|1646697600000|1649286000000|1651878000000|app/templates/setup.hbs
|
||||||
|
add|ember-template-lint|no-duplicate-landmark-elements|16|16|16|16|1661d2edb187b634c8187e5ecb0db15a4c7262fc|1646697600000|1649286000000|1651878000000|app/templates/signin.hbs
|
||||||
|
@ -3,7 +3,7 @@ module.exports = {
|
|||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
'no-forbidden-elements': ['meta', 'html', 'script'],
|
'no-forbidden-elements': ['meta', 'html', 'script'],
|
||||||
'no-implicit-this': {allow: ['now']},
|
'no-implicit-this': {allow: ['now', 'site-icon-style']},
|
||||||
'no-inline-styles': false
|
'no-inline-styles': false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<LinkTo @route={{this.route}} @alternateActive={{action "setActive"}} class={{@linkClasses}}>{{this.title}}{{yield}}</LinkTo>
|
|
@ -1,24 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
import classic from 'ember-classic-decorator';
|
|
||||||
import {action} from '@ember/object';
|
|
||||||
import {classNameBindings, tagName} from '@ember-decorators/component';
|
|
||||||
import {schedule} from '@ember/runloop';
|
|
||||||
|
|
||||||
@classic
|
|
||||||
@classNameBindings('active')
|
|
||||||
@tagName('li')
|
|
||||||
export default class GhActivatingListItem extends Component {
|
|
||||||
active = false;
|
|
||||||
linkClasses = null;
|
|
||||||
|
|
||||||
@action
|
|
||||||
setActive(value) {
|
|
||||||
schedule('afterRender', this, function () {
|
|
||||||
this.set('active', value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
click() {
|
|
||||||
this.element.querySelector('a').blur();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +1,198 @@
|
|||||||
import classic from 'ember-classic-decorator';
|
import classic from 'ember-classic-decorator';
|
||||||
import {computed} from '@ember/object';
|
|
||||||
import {match} from '@ember/object/computed';
|
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
/* eslint-disable camelcase, ghost/ember/alias-model-in-controller */
|
||||||
import Controller from '@ember/controller';
|
import Controller, {inject as controller} from '@ember/controller';
|
||||||
|
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||||
|
import {action, get} from '@ember/object';
|
||||||
|
import {htmlSafe} from '@ember/template';
|
||||||
|
import {isInvalidError} from 'ember-ajax/errors';
|
||||||
|
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||||
|
import {task} from 'ember-concurrency';
|
||||||
|
|
||||||
@classic
|
@classic
|
||||||
export default class SetupController extends Controller {
|
export default class SetupController extends Controller.extend(ValidationEngine) {
|
||||||
|
@controller application;
|
||||||
|
|
||||||
|
@service ajax;
|
||||||
|
@service config;
|
||||||
@service ghostPaths;
|
@service ghostPaths;
|
||||||
|
@service notifications;
|
||||||
@service router;
|
@service router;
|
||||||
|
@service session;
|
||||||
|
|
||||||
@match('router.currentRouteName', /^setup\.(two|three)$/)
|
// ValidationEngine settings
|
||||||
showBackLink;
|
validationType = 'setup';
|
||||||
|
|
||||||
@computed('router.currentRouteName')
|
blogCreated = false;
|
||||||
get backRoute() {
|
blogTitle = null;
|
||||||
let currentRoute = this.router.currentRouteName;
|
email = '';
|
||||||
|
flowErrors = '';
|
||||||
|
profileImage = null;
|
||||||
|
name = null;
|
||||||
|
password = null;
|
||||||
|
|
||||||
return currentRoute === 'setup.two' ? 'setup.one' : 'setup.two';
|
@action
|
||||||
|
setup() {
|
||||||
|
this.setupTask.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
preValidate(model) {
|
||||||
|
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
||||||
|
if (this.get(model)) {
|
||||||
|
return this.validate({property: model});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setImage(image) {
|
||||||
|
this.set('profileImage', image);
|
||||||
|
}
|
||||||
|
|
||||||
|
@task(function* () {
|
||||||
|
return yield this._passwordSetup();
|
||||||
|
})
|
||||||
|
setupTask;
|
||||||
|
|
||||||
|
@task(function* (authStrategy, authentication) {
|
||||||
|
// we don't want to redirect after sign-in during setup
|
||||||
|
this.session.skipAuthSuccessHandler = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let authResult = yield this.session
|
||||||
|
.authenticate(authStrategy, ...authentication);
|
||||||
|
|
||||||
|
this.errors.remove('session');
|
||||||
|
|
||||||
|
return authResult;
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.payload && error.payload.errors) {
|
||||||
|
if (isVersionMismatchError(error)) {
|
||||||
|
return this.notifications.showAPIError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.payload.errors.forEach((err) => {
|
||||||
|
err.message = htmlSafe(err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set('flowErrors', error.payload.errors[0].message.string);
|
||||||
|
} else {
|
||||||
|
// Connection errors don't return proper status message, only req.body
|
||||||
|
this.notifications.showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
authenticate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the given data image, then sends the changed user image property to the server
|
||||||
|
* @param {Object} user User object, returned from the 'setup' api call
|
||||||
|
* @return {RSVP.Promise} A promise that takes care of both calls
|
||||||
|
*/
|
||||||
|
_sendImage(user) {
|
||||||
|
let formData = new FormData();
|
||||||
|
let imageFile = this.profileImage;
|
||||||
|
let uploadUrl = this.get('ghostPaths.url').api('images', 'upload');
|
||||||
|
|
||||||
|
formData.append('file', imageFile, imageFile.name);
|
||||||
|
formData.append('purpose', 'profile_image');
|
||||||
|
|
||||||
|
return this.ajax.post(uploadUrl, {
|
||||||
|
data: formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
dataType: 'text'
|
||||||
|
}).then((response) => {
|
||||||
|
let [image] = get(JSON.parse(response), 'images');
|
||||||
|
let imageUrl = image.url;
|
||||||
|
let usersUrl = this.get('ghostPaths.url').api('users', user.id.toString());
|
||||||
|
user.profile_image = imageUrl;
|
||||||
|
|
||||||
|
return this.ajax.put(usersUrl, {
|
||||||
|
data: {
|
||||||
|
users: [user]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_passwordSetup() {
|
||||||
|
let setupProperties = ['blogTitle', 'name', 'email', 'password'];
|
||||||
|
let data = this.getProperties(setupProperties);
|
||||||
|
let config = this.config;
|
||||||
|
let method = this.blogCreated ? 'put' : 'post';
|
||||||
|
|
||||||
|
this.set('flowErrors', '');
|
||||||
|
|
||||||
|
this.hasValidated.addObjects(setupProperties);
|
||||||
|
|
||||||
|
return this.validate().then(() => {
|
||||||
|
let authUrl = this.get('ghostPaths.url').api('authentication', 'setup');
|
||||||
|
|
||||||
|
return this.ajax[method](authUrl, {
|
||||||
|
data: {
|
||||||
|
setup: [{
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
password: data.password,
|
||||||
|
blogTitle: data.blogTitle
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
config.set('blogTitle', data.blogTitle);
|
||||||
|
|
||||||
|
// don't try to login again if we are already logged in
|
||||||
|
if (this.get('session.isAuthenticated')) {
|
||||||
|
return this._afterAuthentication(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't call the success handler, otherwise we will be redirected to admin
|
||||||
|
this.session.skipAuthSuccessHandler = true;
|
||||||
|
|
||||||
|
return this.session.authenticate('authenticator:cookie', data.email, data.password).then(() => {
|
||||||
|
this.set('blogCreated', true);
|
||||||
|
return this.session.populateUser().then(() => {
|
||||||
|
return this._afterAuthentication(result);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this._handleAuthenticationError(error);
|
||||||
|
});
|
||||||
|
}).catch((error) => {
|
||||||
|
this._handleSaveError(error);
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
this.set('flowErrors', 'Please fill out every field correctly to set up your blog.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleSaveError(resp) {
|
||||||
|
if (isInvalidError(resp)) {
|
||||||
|
let [error] = resp.payload.errors;
|
||||||
|
this.set('flowErrors', [error.message, error.context].join(' '));
|
||||||
|
} else {
|
||||||
|
this.notifications.showAPIError(resp, {key: 'setup.blog-details'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleAuthenticationError(error) {
|
||||||
|
if (error && error.payload && error.payload.errors) {
|
||||||
|
let [apiError] = error.payload.errors;
|
||||||
|
this.set('flowErrors', [apiError.message, apiError.context].join(' '));
|
||||||
|
} else {
|
||||||
|
// Connection errors don't return proper status message, only req.body
|
||||||
|
this.notifications.showAlert('There was a problem on the server.', {type: 'error', key: 'setup.authenticate.failed'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_afterAuthentication(result) {
|
||||||
|
if (this.profileImage) {
|
||||||
|
return this._sendImage(result.users[0])
|
||||||
|
.then(() => (this.router.transitionTo('dashboard')))
|
||||||
|
.catch((resp) => {
|
||||||
|
this.notifications.showAPIError(resp, {key: 'setup.blog-details'});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.router.transitionTo('dashboard');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,253 +0,0 @@
|
|||||||
/* eslint-disable ghost/ember/alias-model-in-controller */
|
|
||||||
import Controller, {inject as controller} from '@ember/controller';
|
|
||||||
// TODO: remove usage of Ember Data's private `Errors` class when refactoring validations
|
|
||||||
// eslint-disable-next-line
|
|
||||||
import DS from 'ember-data';
|
|
||||||
import Ember from 'ember';
|
|
||||||
import RSVP from 'rsvp';
|
|
||||||
import validator from 'validator';
|
|
||||||
import {alias} from '@ember/object/computed';
|
|
||||||
import {computed} from '@ember/object';
|
|
||||||
import {A as emberA} from '@ember/array';
|
|
||||||
import {htmlSafe} from '@ember/template';
|
|
||||||
import {isInvalidError} from 'ember-ajax/errors';
|
|
||||||
import {run} from '@ember/runloop';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
import {task, timeout} from 'ember-concurrency';
|
|
||||||
|
|
||||||
const {Errors} = DS;
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
two: controller('setup/two'),
|
|
||||||
|
|
||||||
modals: service(),
|
|
||||||
notifications: service(),
|
|
||||||
router: service(),
|
|
||||||
session: service(),
|
|
||||||
|
|
||||||
users: '',
|
|
||||||
|
|
||||||
errors: Errors.create(),
|
|
||||||
hasValidated: emberA(),
|
|
||||||
ownerEmail: alias('two.email'),
|
|
||||||
|
|
||||||
usersArray: computed('users', function () {
|
|
||||||
let errors = this.errors;
|
|
||||||
let users = this.users.split('\n').filter(function (email) {
|
|
||||||
return email.trim().length > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// remove "no users to invite" error if we have users
|
|
||||||
if (users.uniq().length > 0 && errors.get('users.length') === 1) {
|
|
||||||
if (errors.get('users.firstObject').message.match(/no users/i)) {
|
|
||||||
errors.remove('users');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return users.uniq();
|
|
||||||
}),
|
|
||||||
|
|
||||||
validUsersArray: computed('usersArray', 'ownerEmail', function () {
|
|
||||||
let ownerEmail = this.ownerEmail;
|
|
||||||
|
|
||||||
return this.usersArray.filter(function (user) {
|
|
||||||
return validator.isEmail(user || '') && user !== ownerEmail;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
invalidUsersArray: computed('usersArray', 'ownerEmail', function () {
|
|
||||||
let ownerEmail = this.ownerEmail;
|
|
||||||
|
|
||||||
return this.usersArray.reject(user => validator.isEmail(user || '') || user === ownerEmail);
|
|
||||||
}),
|
|
||||||
|
|
||||||
validationResult: computed('invalidUsersArray', function () {
|
|
||||||
let errors = [];
|
|
||||||
|
|
||||||
this.invalidUsersArray.forEach((user) => {
|
|
||||||
errors.push({
|
|
||||||
user,
|
|
||||||
error: 'email'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (errors.length === 0) {
|
|
||||||
// ensure we aren't highlighting fields when everything is fine
|
|
||||||
this.errors.clear();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
buttonText: computed('errors.users', 'validUsersArray', 'invalidUsersArray', function () {
|
|
||||||
let usersError = this.get('errors.users.firstObject.message');
|
|
||||||
let validNum = this.validUsersArray.length;
|
|
||||||
let invalidNum = this.invalidUsersArray.length;
|
|
||||||
let userCount;
|
|
||||||
|
|
||||||
if (usersError && usersError.match(/no users/i)) {
|
|
||||||
return usersError;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidNum > 0) {
|
|
||||||
userCount = invalidNum === 1 ? 'email address' : 'email addresses';
|
|
||||||
return `${invalidNum} invalid ${userCount}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validNum > 0) {
|
|
||||||
userCount = validNum === 1 ? 'user' : 'users';
|
|
||||||
userCount = `${validNum} ${userCount}`;
|
|
||||||
} else {
|
|
||||||
userCount = 'some users';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Invite ${userCount}`;
|
|
||||||
}),
|
|
||||||
|
|
||||||
buttonClass: computed('validationResult', 'usersArray.length', function () {
|
|
||||||
if (this.validationResult === true && this.get('usersArray.length') > 0) {
|
|
||||||
return 'gh-btn-green';
|
|
||||||
} else {
|
|
||||||
return 'gh-btn-minor';
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
authorRole: computed(function () {
|
|
||||||
return this.store.findAll('role', {reload: true}).then(roles => roles.findBy('name', 'Author'));
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
validate() {
|
|
||||||
this.validate();
|
|
||||||
},
|
|
||||||
|
|
||||||
invite() {
|
|
||||||
this.invite.perform();
|
|
||||||
},
|
|
||||||
|
|
||||||
skipInvite() {
|
|
||||||
this.session.loadServerNotifications();
|
|
||||||
this.modals.open('modals/get-started');
|
|
||||||
this.router.transitionTo('home');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
validate() {
|
|
||||||
let errors = this.errors;
|
|
||||||
let validationResult = this.validationResult;
|
|
||||||
let property = 'users';
|
|
||||||
|
|
||||||
errors.clear();
|
|
||||||
|
|
||||||
// If property isn't in the `hasValidated` array, add it to mark that this field can show a validation result
|
|
||||||
this.hasValidated.addObject(property);
|
|
||||||
|
|
||||||
if (validationResult === true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
validationResult.forEach((error) => {
|
|
||||||
// Only one error type here so far, but one day the errors might be more detailed
|
|
||||||
switch (error.error) {
|
|
||||||
case 'email':
|
|
||||||
errors.add(property, `${error.user} is not a valid email.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
_transitionAfterSubmission() {
|
|
||||||
if (!this._hasTransitioned) {
|
|
||||||
this._hasTransitioned = true;
|
|
||||||
this.modals.open('modals/get-started');
|
|
||||||
this.router.transitionTo('home');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
invite: task(function* () {
|
|
||||||
let users = this.validUsersArray;
|
|
||||||
|
|
||||||
if (this.validate() && users.length > 0) {
|
|
||||||
this._hasTransitioned = false;
|
|
||||||
|
|
||||||
this._slowSubmissionTimeout.perform();
|
|
||||||
|
|
||||||
let authorRole = yield this.authorRole;
|
|
||||||
let invites = yield this._saveInvites(authorRole);
|
|
||||||
|
|
||||||
this._slowSubmissionTimeout.cancelAll();
|
|
||||||
|
|
||||||
this._showNotifications(invites);
|
|
||||||
|
|
||||||
run.schedule('actions', this, function () {
|
|
||||||
this.session.loadServerNotifications();
|
|
||||||
this._transitionAfterSubmission();
|
|
||||||
});
|
|
||||||
} else if (users.length === 0) {
|
|
||||||
this.errors.add('users', 'No users to invite');
|
|
||||||
}
|
|
||||||
}).drop(),
|
|
||||||
|
|
||||||
_slowSubmissionTimeout: task(function* () {
|
|
||||||
yield timeout(4000);
|
|
||||||
this._transitionAfterSubmission();
|
|
||||||
}).drop(),
|
|
||||||
|
|
||||||
_saveInvites(authorRole) {
|
|
||||||
let users = this.validUsersArray;
|
|
||||||
|
|
||||||
return RSVP.Promise.all(
|
|
||||||
users.map((user) => {
|
|
||||||
let invite = this.store.createRecord('invite', {
|
|
||||||
email: user,
|
|
||||||
role: authorRole
|
|
||||||
});
|
|
||||||
|
|
||||||
return invite.save().then(() => ({
|
|
||||||
email: user,
|
|
||||||
success: invite.get('status') === 'sent'
|
|
||||||
})).catch(error => ({
|
|
||||||
error,
|
|
||||||
email: user,
|
|
||||||
success: false
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
_showNotifications(invites) {
|
|
||||||
let notifications = this.notifications;
|
|
||||||
let erroredEmails = [];
|
|
||||||
let successCount = 0;
|
|
||||||
let invitationsString, message;
|
|
||||||
|
|
||||||
invites.forEach((invite) => {
|
|
||||||
if (invite.success) {
|
|
||||||
successCount += 1;
|
|
||||||
} else if (isInvalidError(invite.error)) {
|
|
||||||
message = `${invite.email} was invalid: ${invite.error.payload.errors[0].message}`;
|
|
||||||
notifications.showAlert(message, {type: 'error', delayed: true, key: `signup.send-invitations.${invite.email}`});
|
|
||||||
} else {
|
|
||||||
erroredEmails.push(invite.email);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (erroredEmails.length > 0) {
|
|
||||||
invitationsString = erroredEmails.length > 1 ? ' invitations: ' : ' invitation: ';
|
|
||||||
message = `Failed to send ${erroredEmails.length} ${invitationsString}`;
|
|
||||||
message += Ember.Handlebars.Utils.escapeExpression(erroredEmails.join(', '));
|
|
||||||
message += '. Please check your email configuration, see <a href=\'https://ghost.org/docs/config/#mail\' target=\'_blank\'>https://ghost.org/docs/config/#mail</a> for instructions';
|
|
||||||
|
|
||||||
message = htmlSafe(message);
|
|
||||||
notifications.showAlert(message, {type: 'error', delayed: successCount > 0, key: 'signup.send-invitations.failed'});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successCount > 0) {
|
|
||||||
// pluralize
|
|
||||||
invitationsString = successCount > 1 ? 'invitations' : 'invitation';
|
|
||||||
notifications.showAlert(`${successCount} ${invitationsString} sent!`, {type: 'success', delayed: true, key: 'signup.send-invitations.success'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,196 +0,0 @@
|
|||||||
import classic from 'ember-classic-decorator';
|
|
||||||
import {inject as service} from '@ember/service';
|
|
||||||
/* eslint-disable camelcase, ghost/ember/alias-model-in-controller */
|
|
||||||
import Controller, {inject as controller} from '@ember/controller';
|
|
||||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
|
||||||
import {action, get} from '@ember/object';
|
|
||||||
import {htmlSafe} from '@ember/template';
|
|
||||||
import {isInvalidError} from 'ember-ajax/errors';
|
|
||||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
|
||||||
import {task} from 'ember-concurrency';
|
|
||||||
|
|
||||||
@classic
|
|
||||||
export default class TwoController extends Controller.extend(ValidationEngine) {
|
|
||||||
@controller application;
|
|
||||||
|
|
||||||
@service ajax;
|
|
||||||
@service config;
|
|
||||||
@service ghostPaths;
|
|
||||||
@service notifications;
|
|
||||||
@service router;
|
|
||||||
@service session;
|
|
||||||
|
|
||||||
// ValidationEngine settings
|
|
||||||
validationType = 'setup';
|
|
||||||
|
|
||||||
blogCreated = false;
|
|
||||||
blogTitle = null;
|
|
||||||
email = '';
|
|
||||||
flowErrors = '';
|
|
||||||
profileImage = null;
|
|
||||||
name = null;
|
|
||||||
password = null;
|
|
||||||
|
|
||||||
@action
|
|
||||||
setup() {
|
|
||||||
this.setupTask.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
preValidate(model) {
|
|
||||||
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
|
||||||
if (this.get(model)) {
|
|
||||||
return this.validate({property: model});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setImage(image) {
|
|
||||||
this.set('profileImage', image);
|
|
||||||
}
|
|
||||||
|
|
||||||
@task(function* () {
|
|
||||||
return yield this._passwordSetup();
|
|
||||||
})
|
|
||||||
setupTask;
|
|
||||||
|
|
||||||
@task(function* (authStrategy, authentication) {
|
|
||||||
// we don't want to redirect after sign-in during setup
|
|
||||||
this.session.skipAuthSuccessHandler = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
let authResult = yield this.session
|
|
||||||
.authenticate(authStrategy, ...authentication);
|
|
||||||
|
|
||||||
this.errors.remove('session');
|
|
||||||
|
|
||||||
return authResult;
|
|
||||||
} catch (error) {
|
|
||||||
if (error && error.payload && error.payload.errors) {
|
|
||||||
if (isVersionMismatchError(error)) {
|
|
||||||
return this.notifications.showAPIError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
error.payload.errors.forEach((err) => {
|
|
||||||
err.message = htmlSafe(err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set('flowErrors', error.payload.errors[0].message.string);
|
|
||||||
} else {
|
|
||||||
// Connection errors don't return proper status message, only req.body
|
|
||||||
this.notifications.showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
authenticate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads the given data image, then sends the changed user image property to the server
|
|
||||||
* @param {Object} user User object, returned from the 'setup' api call
|
|
||||||
* @return {RSVP.Promise} A promise that takes care of both calls
|
|
||||||
*/
|
|
||||||
_sendImage(user) {
|
|
||||||
let formData = new FormData();
|
|
||||||
let imageFile = this.profileImage;
|
|
||||||
let uploadUrl = this.get('ghostPaths.url').api('images', 'upload');
|
|
||||||
|
|
||||||
formData.append('file', imageFile, imageFile.name);
|
|
||||||
formData.append('purpose', 'profile_image');
|
|
||||||
|
|
||||||
return this.ajax.post(uploadUrl, {
|
|
||||||
data: formData,
|
|
||||||
processData: false,
|
|
||||||
contentType: false,
|
|
||||||
dataType: 'text'
|
|
||||||
}).then((response) => {
|
|
||||||
let [image] = get(JSON.parse(response), 'images');
|
|
||||||
let imageUrl = image.url;
|
|
||||||
let usersUrl = this.get('ghostPaths.url').api('users', user.id.toString());
|
|
||||||
user.profile_image = imageUrl;
|
|
||||||
|
|
||||||
return this.ajax.put(usersUrl, {
|
|
||||||
data: {
|
|
||||||
users: [user]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_passwordSetup() {
|
|
||||||
let setupProperties = ['blogTitle', 'name', 'email', 'password'];
|
|
||||||
let data = this.getProperties(setupProperties);
|
|
||||||
let config = this.config;
|
|
||||||
let method = this.blogCreated ? 'put' : 'post';
|
|
||||||
|
|
||||||
this.set('flowErrors', '');
|
|
||||||
|
|
||||||
this.hasValidated.addObjects(setupProperties);
|
|
||||||
|
|
||||||
return this.validate().then(() => {
|
|
||||||
let authUrl = this.get('ghostPaths.url').api('authentication', 'setup');
|
|
||||||
|
|
||||||
return this.ajax[method](authUrl, {
|
|
||||||
data: {
|
|
||||||
setup: [{
|
|
||||||
name: data.name,
|
|
||||||
email: data.email,
|
|
||||||
password: data.password,
|
|
||||||
blogTitle: data.blogTitle
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}).then((result) => {
|
|
||||||
config.set('blogTitle', data.blogTitle);
|
|
||||||
|
|
||||||
// don't try to login again if we are already logged in
|
|
||||||
if (this.get('session.isAuthenticated')) {
|
|
||||||
return this._afterAuthentication(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't call the success handler, otherwise we will be redirected to admin
|
|
||||||
this.session.skipAuthSuccessHandler = true;
|
|
||||||
|
|
||||||
return this.session.authenticate('authenticator:cookie', data.email, data.password).then(() => {
|
|
||||||
this.set('blogCreated', true);
|
|
||||||
return this._afterAuthentication(result);
|
|
||||||
}).catch((error) => {
|
|
||||||
this._handleAuthenticationError(error);
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
this._handleSaveError(error);
|
|
||||||
});
|
|
||||||
}).catch(() => {
|
|
||||||
this.set('flowErrors', 'Please fill out the form to setup your blog.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleSaveError(resp) {
|
|
||||||
if (isInvalidError(resp)) {
|
|
||||||
let [error] = resp.payload.errors;
|
|
||||||
this.set('flowErrors', [error.message, error.context].join(' '));
|
|
||||||
} else {
|
|
||||||
this.notifications.showAPIError(resp, {key: 'setup.blog-details'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleAuthenticationError(error) {
|
|
||||||
if (error && error.payload && error.payload.errors) {
|
|
||||||
let [apiError] = error.payload.errors;
|
|
||||||
this.set('flowErrors', [apiError.message, apiError.context].join(' '));
|
|
||||||
} else {
|
|
||||||
// Connection errors don't return proper status message, only req.body
|
|
||||||
this.notifications.showAlert('There was a problem on the server.', {type: 'error', key: 'setup.authenticate.failed'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_afterAuthentication(result) {
|
|
||||||
if (this.profileImage) {
|
|
||||||
return this._sendImage(result.users[0])
|
|
||||||
.then(() => (this.router.transitionTo('setup.three')))
|
|
||||||
.catch((resp) => {
|
|
||||||
this.notifications.showAPIError(resp, {key: 'setup.blog-details'});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.router.transitionTo('setup.three');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -48,18 +48,6 @@ export default class SigninController extends Controller.extend(ValidationEngine
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed('config.icon')
|
|
||||||
get siteIconStyle() {
|
|
||||||
let icon = this.get('config.icon');
|
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
return htmlSafe(`background-image: url(${icon})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
icon = 'https://static.ghost.org/v4.0.0/images/ghost-orb-2.png';
|
|
||||||
return htmlSafe(`background-image: url(${icon})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
authenticate() {
|
authenticate() {
|
||||||
return this.validateAndAuthenticate.perform();
|
return this.validateAndAuthenticate.perform();
|
||||||
|
14
ghost/admin/app/helpers/site-icon-style.js
Normal file
14
ghost/admin/app/helpers/site-icon-style.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import classic from 'ember-classic-decorator';
|
||||||
|
import {htmlSafe} from '@ember/template';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
@classic
|
||||||
|
export default class SiteIconStyleHelper extends Helper {
|
||||||
|
@service config;
|
||||||
|
|
||||||
|
compute() {
|
||||||
|
const icon = this.get('config.icon') || 'https://static.ghost.org/v4.0.0/images/ghost-orb-2.png';
|
||||||
|
return htmlSafe(`background-image: url(${icon})`);
|
||||||
|
}
|
||||||
|
}
|
@ -10,11 +10,7 @@ const Router = EmberRouter.extend({
|
|||||||
Router.map(function () {
|
Router.map(function () {
|
||||||
this.route('home', {path: '/'});
|
this.route('home', {path: '/'});
|
||||||
|
|
||||||
this.route('setup', function () {
|
this.route('setup');
|
||||||
this.route('one');
|
|
||||||
this.route('two');
|
|
||||||
this.route('three');
|
|
||||||
});
|
|
||||||
this.route('setup.done', {path: '/setup/done'});
|
this.route('setup.done', {path: '/setup/done'});
|
||||||
|
|
||||||
this.route('signin');
|
this.route('signin');
|
||||||
|
@ -26,7 +26,7 @@ export default class SetupRoute extends Route {
|
|||||||
if (setup.status) {
|
if (setup.status) {
|
||||||
return this.transitionTo('signin');
|
return this.transitionTo('signin');
|
||||||
} else {
|
} else {
|
||||||
let controller = this.controllerFor('setup/two');
|
let controller = this.controllerFor('setup');
|
||||||
if (setup.title) {
|
if (setup.title) {
|
||||||
controller.set('blogTitle', setup.title.replace(/'/gim, '\''));
|
controller.set('blogTitle', setup.title.replace(/'/gim, '\''));
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ export default class SetupRoute extends Route {
|
|||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
super.deactivate(...arguments);
|
super.deactivate(...arguments);
|
||||||
this.controllerFor('setup/two').set('password', '');
|
this.controllerFor('setup').set('password', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
buildRouteInfoMetadata() {
|
buildRouteInfoMetadata() {
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default class ThreeRoute extends Route {
|
|
||||||
beforeModel() {
|
|
||||||
super.beforeModel(...arguments);
|
|
||||||
if (!this.controllerFor('setup.two').get('blogCreated')) {
|
|
||||||
this.transitionTo('setup.two');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,19 +2,17 @@
|
|||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
.gh-signin, .gh-auth-email {
|
.gh-signin, .gh-auth-email {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 30px auto;
|
|
||||||
padding: 40px;
|
|
||||||
max-width: 620px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-flow-content header,
|
||||||
.gh-signin header {
|
.gh-signin header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-flow-content .gh-site-icon,
|
||||||
.gh-signin .gh-site-icon {
|
.gh-signin .gh-site-icon {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
width: 70px;
|
width: 70px;
|
||||||
@ -46,7 +44,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gh-signin .gh-btn-login,
|
.gh-signin .gh-btn-login,
|
||||||
.gh-signin .gh-btn-reset {
|
.gh-signin .gh-btn-reset,
|
||||||
|
.gh-setup .gh-btn-signup,
|
||||||
|
.gh-signup .gh-btn-signup {
|
||||||
height: 54px;
|
height: 54px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
line-height: 54px;
|
line-height: 54px;
|
||||||
@ -58,7 +58,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gh-signin .gh-btn-login span,
|
.gh-signin .gh-btn-login span,
|
||||||
.gh-signin .gh-btn-reset span {
|
.gh-signin .gh-btn-reset span,
|
||||||
|
.gh-setup .gh-btn-signup span,
|
||||||
|
.gh-signup .gh-btn-signup span {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@ -129,140 +131,18 @@
|
|||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-signin input:focus {
|
.gh-signin .gh-input:focus:not(.gh-signin .gh-input.reset-password:focus) {
|
||||||
border-color: var(--midgrey) !important;
|
border-color: var(--midgrey) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Email notification */
|
/* Email notification */
|
||||||
/* ---------------------------------------------------------- */
|
/* ---------------------------------------------------------- */
|
||||||
.gh-auth-animation-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--lightgrey);
|
|
||||||
animation: 0.5s forwards 0.6s containerFadeIn;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes containerFadeIn {
|
.gh-auth-email header {
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-email-animation {
|
|
||||||
position: relative;
|
|
||||||
width: 122px;
|
|
||||||
height: 125px;
|
|
||||||
margin-bottom: -24px;
|
|
||||||
animation: 0.5s forwards 0.6s envelopeFadeIn;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes envelopeFadeIn {
|
|
||||||
0% {
|
|
||||||
transform: translateY(-6px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-email-animation .gh-auth-envelope-back {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-email-animation .gh-auth-envelope-front {
|
|
||||||
position: absolute;
|
|
||||||
top: 48px;
|
|
||||||
left: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-email-animation .gh-auth-paper {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: flex-start;
|
|
||||||
position: absolute;
|
|
||||||
top: 40px;
|
|
||||||
left: 15px;
|
|
||||||
width: 90px;
|
|
||||||
height: 82px;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #C5D2D9;
|
|
||||||
border-radius: 4px;
|
|
||||||
animation: 1.2s ease forwards 1.15s paperIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-email-animation .gh-auth-lock {
|
|
||||||
margin-top: 15px;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
opacity: 0;
|
|
||||||
animation: 0.45s forwards 1.35s lockIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes paperIn {
|
|
||||||
0% {
|
|
||||||
transform: scale(1,1) translateY(0);
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
transform: scale(1.05,.95) translateY(0);
|
|
||||||
}
|
|
||||||
30% {
|
|
||||||
transform: scale(.95,1.05) translateY(-32px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1,1) translateY(-27px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1,1) translateY(-27px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes lockIn {
|
|
||||||
0% {
|
|
||||||
transform: scale(1) translateY(2px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
transform: scale(1.1) translateY(-2px);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1) translateY(0px);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-lock-body {
|
|
||||||
margin-top: 48px;
|
|
||||||
animation: 0.5s forwards 0.2s bodyFadeIn;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-auth-lock-body p {
|
|
||||||
color: var(--midgrey);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 1.4em;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes bodyFadeIn {
|
.gh-auth-email p {
|
||||||
0% {
|
color: var(--midgrey);
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -7,7 +7,16 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background: linear-gradient(135deg, #fff, #f4f4f4);
|
background: linear-gradient(315deg,#efefef,#fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-setup {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: unset;
|
||||||
|
overflow-y: unset;
|
||||||
|
padding: 0 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-head {
|
.gh-flow-head {
|
||||||
@ -28,163 +37,56 @@
|
|||||||
padding-bottom: 8vh;
|
padding-bottom: 8vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-back {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 0 0 3%;
|
|
||||||
padding: 2px 9px 2px 5px;
|
|
||||||
border: transparent 1px solid;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #7d878a;
|
|
||||||
font-weight: 300;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-back svg {
|
|
||||||
margin-right: 4px;
|
|
||||||
height: 12px;
|
|
||||||
line-height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-back svg path {
|
|
||||||
stroke: #7d878a;
|
|
||||||
stroke-width: 1.2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-back:hover {
|
|
||||||
border: #dae1e3 1px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav {
|
|
||||||
position: relative;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav ol {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0;
|
|
||||||
width: 160px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav li {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .divider {
|
|
||||||
align-self: center;
|
|
||||||
width: 22px;
|
|
||||||
height: 2px;
|
|
||||||
background-image: linear-gradient(to right, var(--green) 33%, rgba(255, 255, 255, 0) 0%);
|
|
||||||
background-position: bottom;
|
|
||||||
background-size: 6px 2px;
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active ~ .divider {
|
|
||||||
background-image: linear-gradient(to right, #e3e3e3 33%, rgba(255, 255, 255, 0) 0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .step {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border: transparent 2px solid;
|
|
||||||
background: var(--green);
|
|
||||||
border-radius: 100%;
|
|
||||||
color: #fff;
|
|
||||||
vertical-align: middle;
|
|
||||||
text-align: center;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .step .num {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .step svg {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
fill: #fff;
|
|
||||||
stroke: #fff;
|
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .step svg path {
|
|
||||||
stroke: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active ~ li:not(divider) .step {
|
|
||||||
border: #e3e3e3 2px solid;
|
|
||||||
background: transparent;
|
|
||||||
color: #cdcdcd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active ~ li:not(divider) .step .num {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active ~ li:not(divider) .step svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active .step {
|
|
||||||
border: var(--green) 2px solid;
|
|
||||||
background: transparent;
|
|
||||||
color: color-mod(var(--green) lightness(-10%));
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active .step .num {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .active .step svg {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-nav .done {
|
|
||||||
border: none;
|
|
||||||
background: var(--green);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.gh-flow-content {
|
.gh-flow-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: 700px;
|
max-width: 520px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: var(--midgrey);
|
margin: 4.8rem 0 8rem;
|
||||||
text-align: center;
|
color: var(--darkgrey);
|
||||||
font-size: 1.9rem;
|
font-size: 1.9rem;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 400px) {
|
||||||
.gh-flow-content {
|
.gh-flow-content {
|
||||||
|
margin: 4rem 0 6rem;
|
||||||
font-size: 4vw;
|
font-size: 4vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content header {
|
.gh-setup .gh-flow-content header {
|
||||||
margin: 0 auto;
|
margin: 0 0 1rem;
|
||||||
max-width: 520px;
|
}
|
||||||
|
|
||||||
|
.gh-setup .gh-flow-content header svg {
|
||||||
|
width: 7.2rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-setup .gh-flow-content h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-setup .gh-flow-content p {
|
||||||
|
color: var(--midgrey);
|
||||||
|
font-size: 1.9rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.gh-setup .gh-flow-content p {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content h1 {
|
.gh-flow-content h1 {
|
||||||
font-size: 4.2rem;
|
margin-bottom: 40px;
|
||||||
font-weight: 300;
|
font-size: 4.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
@ -198,44 +100,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content em {
|
.gh-flow-content em {
|
||||||
color: var(--blue);
|
color: var(--black);
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .gh-flow-screenshot {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0;
|
|
||||||
height: 45vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-content .gh-flow-screenshot img {
|
|
||||||
position: relative;
|
|
||||||
left: -3%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 860px) {
|
|
||||||
.gh-flow-content .gh-flow-screenshot img {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.gh-flow-content .gh-flow-screenshot {
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-content .gh-btn {
|
.gh-flow-content .gh-btn {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 40px auto 0;
|
margin: 40px auto 0;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-setup .gh-flow-content .gh-btn {
|
||||||
|
height: 52px;
|
||||||
|
max-width: unset;
|
||||||
|
margin: 40px 0 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-setup .gh-flow-content .gh-btn span {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.gh-flow-content .login span {
|
.gh-flow-content .login span {
|
||||||
height: 37px !important;
|
height: 37px !important;
|
||||||
line-height: 37px !important;
|
line-height: 37px !important;
|
||||||
@ -273,11 +158,6 @@
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .gh-input:focus {
|
|
||||||
box-shadow: none;
|
|
||||||
border-color: color-mod(var(--midlightgrey) l(+10%));
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-content .gh-flow-skip {
|
.gh-flow-content .gh-flow-skip {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@ -298,11 +178,6 @@
|
|||||||
box-shadow: 0 20px 45px -10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 20px 45px -10px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-create .gh-btn-create-account span {
|
|
||||||
height: 37px;
|
|
||||||
line-height: 37px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gh-flow-content .account-image {
|
.gh-flow-content .account-image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -50px;
|
top: -50px;
|
||||||
@ -395,9 +270,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .form-group label {
|
.gh-flow-content .form-group label {
|
||||||
margin: 0;
|
margin: 0 0 .3em;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 400;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .form-group a {
|
.gh-flow-content .form-group a {
|
||||||
@ -405,10 +280,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content input {
|
.gh-flow-content input {
|
||||||
padding: 10px 10px 10px 30px;
|
height: 48px;
|
||||||
font-size: 1.4rem;
|
padding: 12px 16px;
|
||||||
line-height: 1.4em;
|
font-size: 1.7rem;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-flow-content .gh-input:focus {
|
||||||
|
border-color: var(--green);
|
||||||
|
box-shadow: 0 0 0 3px rgba(26,170,96,.15);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .pw-strength {
|
.gh-flow-content .pw-strength {
|
||||||
@ -490,14 +372,14 @@
|
|||||||
line-height: 1.8rem;
|
line-height: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .response {
|
.gh-flow-content .response,
|
||||||
|
.gh-setup .gh-flow-content .response {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: -25px;
|
bottom: -24px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #a6b0b3;
|
color: #a6b0b3;
|
||||||
text-align: right;
|
font-size: 1.35rem;
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content form:not(.gh-signin) .success .gh-input-icon svg {
|
.gh-flow-content form:not(.gh-signin) .success .gh-input-icon svg {
|
||||||
@ -511,21 +393,32 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .error input {
|
.gh-flow-content .error input,
|
||||||
|
.gh-flow-content .error input:focus {
|
||||||
border-color: var(--red);
|
border-color: var(--red);
|
||||||
box-shadow: none;
|
box-shadow: 0 0 0 3px rgba(239,24,24,.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .error .gh-input-icon svg {
|
.gh-flow-content .error .gh-input-icon svg {
|
||||||
fill: var(--red);
|
fill: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .error .response {
|
.gh-flow-content .error .response,
|
||||||
|
.gh-setup .gh-flow-content .error .response {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-flow-content .main-error {
|
.gh-flow-content .main-error,
|
||||||
margin-top: 5px;
|
.gh-setup .gh-flow-content .main-error {
|
||||||
color: var(--red);
|
margin-top: 8px;
|
||||||
font-size: 1.3rem;
|
color: var(--midgrey);
|
||||||
|
font-size: 1.35rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gh-setup .gh-flow-form .gh-btn-red,
|
||||||
|
.gh-setup .gh-flow-form .gh-btn-red:active,
|
||||||
|
.gh-signup .gh-btn-red,
|
||||||
|
.gh-signup .gh-btn-red:active {
|
||||||
|
background: var(--black) !important;
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,12 @@ input[type=number] {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-moz-placeholder {
|
||||||
|
color: var(--midlightgrey);
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.error .response {
|
.error .response {
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,17 @@
|
|||||||
<div class="gh-flow-content-wrap">
|
<div class="gh-flow-content-wrap">
|
||||||
<section class="gh-flow-content fade-in">
|
<section class="gh-flow-content fade-in">
|
||||||
<form id="reset" class="gh-signin" method="post" novalidate="novalidate" {{action "submit" on="submit"}}>
|
<form id="reset" class="gh-signin" method="post" novalidate="novalidate" {{action "submit" on="submit"}}>
|
||||||
|
<header>
|
||||||
|
<div class="gh-site-icon" style={{site-icon-style}}></div>
|
||||||
|
<h1>Reset your password.</h1>
|
||||||
|
</header>
|
||||||
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="newPassword">
|
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="newPassword">
|
||||||
<span class="gh-input-icon gh-icon-lock">
|
<span class="gh-input-icon gh-icon-lock">
|
||||||
<GhTextInput
|
<GhTextInput
|
||||||
@type="password"
|
@type="password"
|
||||||
@name="newpassword"
|
@name="newpassword"
|
||||||
@placeholder="New password"
|
@placeholder="New password"
|
||||||
@class="password"
|
@class="password reset-password"
|
||||||
@autocorrect="off"
|
@autocorrect="off"
|
||||||
@shouldFocus={{true}}
|
@shouldFocus={{true}}
|
||||||
@value={{readonly this.newPassword}}
|
@value={{readonly this.newPassword}}
|
||||||
@ -21,14 +25,14 @@
|
|||||||
@type="password"
|
@type="password"
|
||||||
@name="ne2password"
|
@name="ne2password"
|
||||||
@placeholder="Confirm new password"
|
@placeholder="Confirm new password"
|
||||||
@class="password"
|
@class="password reset-password"
|
||||||
@autocorrect="off"
|
@autocorrect="off"
|
||||||
@value={{readonly this.ne2Password}}
|
@value={{readonly this.ne2Password}}
|
||||||
@input={{action (mut this.ne2Password) value="target.value"}} />
|
@input={{action (mut this.ne2Password) value="target.value"}} />
|
||||||
</span>
|
</span>
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
|
|
||||||
<GhTaskButton @buttonText="Save new password" @task={{this.resetPassword}} @class="gh-btn gh-btn-blue gh-btn-reset gh-btn-block gh-btn-icon" @type="submit" @autoWidth="false" />
|
<GhTaskButton @buttonText="Save new password" @task={{this.resetPassword}} @class="gh-btn gh-btn-reset gh-btn-block gh-btn-icon" @type="submit" @autoWidth="false" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p class="main-error">{{this.flowErrors}} </p>
|
<p class="main-error">{{this.flowErrors}} </p>
|
||||||
|
@ -1,27 +1,96 @@
|
|||||||
<div class="gh-flow">
|
<section class="gh-flow gh-setup">
|
||||||
<header class="gh-flow-head">
|
<div class="gh-flow-content">
|
||||||
<nav class="gh-flow-nav">
|
<header>
|
||||||
{{#if this.showBackLink}}
|
{{svg-jar "ghost-orb" alt="Ghost"}}
|
||||||
<LinkTo @route={{this.backRoute}} class="gh-flow-back">{{svg-jar "arrow-left-small"}} Back</LinkTo>
|
<h1>Welcome to Ghost.</h1>
|
||||||
{{/if}}
|
<p>All over the world, people have started 3,000,000+ incredible sites with Ghost. Today, we’re starting yours.</p>
|
||||||
<ol>
|
</header>
|
||||||
<GhActivatingListItem @route="setup.one" @linkClasses="step">
|
|
||||||
{{svg-jar "check-circle"}}<span class="num">1</span>
|
<form id="setup" class="gh-flow-form">
|
||||||
</GhActivatingListItem>
|
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="blogTitle">
|
||||||
<li class="divider"></li>
|
<label for="blog-title">Site title</label>
|
||||||
<GhActivatingListItem @route="setup.two" @linkClasses="step">
|
<span class="gh-input-icon gh-icon-content">
|
||||||
{{svg-jar "check-circle"}}<span class="num">2</span>
|
<GhTrimFocusInput
|
||||||
</GhActivatingListItem>
|
@tabindex="1"
|
||||||
<li class="divider"></li>
|
@type="text"
|
||||||
<GhActivatingListItem @route="setup.three" @linkClasses="step">
|
@id="blog-title"
|
||||||
{{svg-jar "check-circle"}}<span class="num">3</span>
|
@name="blog-title"
|
||||||
</GhActivatingListItem>
|
@placeholder="The Daily Awesome"
|
||||||
</ol>
|
@autocorrect="off"
|
||||||
</nav>
|
@value={{readonly this.blogTitle}}
|
||||||
</header>
|
@input={{action (mut this.blogTitle) value="target.value"}}
|
||||||
<div class="gh-flow-content-wrap">
|
@focus-out={{action "preValidate" "blogTitle"}}
|
||||||
<section class="gh-flow-content">
|
data-test-blog-title-input={{true}} />
|
||||||
{{outlet}}
|
</span>
|
||||||
</section>
|
<GhErrorMessage @errors={{this.errors}} @property="blogTitle" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="name">
|
||||||
|
<label for="name">Full name</label>
|
||||||
|
<span class="gh-input-icon gh-icon-user">
|
||||||
|
<GhTextInput
|
||||||
|
@tabindex="2"
|
||||||
|
@id="name"
|
||||||
|
@name="name"
|
||||||
|
@placeholder="Jamie Larson"
|
||||||
|
@autocorrect="off"
|
||||||
|
@autocomplete="name"
|
||||||
|
@value={{readonly this.name}}
|
||||||
|
@input={{action (mut this.name) value="target.value"}}
|
||||||
|
@focus-out={{action "preValidate" "name"}}
|
||||||
|
data-test-name-input={{true}} />
|
||||||
|
</span>
|
||||||
|
<GhErrorMessage @errors={{this.errors}} @property="name" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="email">
|
||||||
|
<label for="email">Email address</label>
|
||||||
|
<span class="gh-input-icon gh-icon-mail">
|
||||||
|
<GhTextInput
|
||||||
|
@tabindex="3"
|
||||||
|
@type="email"
|
||||||
|
@id="email"
|
||||||
|
@name="email"
|
||||||
|
@placeholder="jamie@example.com"
|
||||||
|
@autocorrect="off"
|
||||||
|
@autocomplete="username email"
|
||||||
|
@value={{readonly this.email}}
|
||||||
|
@input={{action (mut this.email) value="target.value"}}
|
||||||
|
@focus-out={{action "preValidate" "email"}}
|
||||||
|
data-test-email-input={{true}} />
|
||||||
|
</span>
|
||||||
|
<GhErrorMessage @errors={{this.errors}} @property="email" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="password">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<span class="gh-input-icon gh-icon-lock">
|
||||||
|
<GhTextInput
|
||||||
|
@tabindex="4"
|
||||||
|
@type="password"
|
||||||
|
@id="password"
|
||||||
|
@name="password"
|
||||||
|
@placeholder="At least 10 characters"
|
||||||
|
@autocorrect="off"
|
||||||
|
@autocomplete="new-password"
|
||||||
|
@value={{readonly this.password}}
|
||||||
|
@input={{action (mut this.password) value="target.value"}}
|
||||||
|
@focus-out={{action "preValidate" "password"}}
|
||||||
|
data-test-password-input={{true}} />
|
||||||
|
</span>
|
||||||
|
<GhErrorMessage @errors={{this.errors}} @property="password" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhTaskButton @task={{this.setupTask}} @type="submit" @tabindex="5" @data-test-button="setup" @class="gh-btn gh-btn-black gh-btn-signup gh-btn-block gh-btn-icon" as |task|>
|
||||||
|
{{#if task.isRunning}}
|
||||||
|
<span>{{svg-jar "spinner" class="gh-icon-spinner gh-btn-icon-no-margin"}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span>Create account & start publishing →</span>
|
||||||
|
{{/if}}
|
||||||
|
</GhTaskButton>
|
||||||
|
</form>
|
||||||
|
{{#if this.flowErrors}}
|
||||||
|
<p class="main-error">{{this.flowErrors}} </p>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
@ -1,12 +0,0 @@
|
|||||||
<header>
|
|
||||||
<h1>Welcome to <strong>Ghost</strong>!</h1>
|
|
||||||
<p>All over the world, people have started <em>2,000,000+</em> incredible sites with Ghost. Today, we’re starting yours.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<figure class="gh-flow-screenshot">
|
|
||||||
<img src="assets/img/install-welcome.png" alt="Ghost screenshot" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<LinkTo @route="setup.two" class="gh-btn gh-btn-green gh-btn-lg gh-btn-icon gh-btn-icon-right">
|
|
||||||
<span>Create your account →</span>
|
|
||||||
</LinkTo>
|
|
@ -1,40 +0,0 @@
|
|||||||
<header>
|
|
||||||
<h1>Invite staff users</h1>
|
|
||||||
<p>Ghost works best when shared with others. Collaborate, get feedback on your posts & work together on ideas.</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div><img class="gh-flow-faces" src="assets/img/users.png" alt="" /></div>
|
|
||||||
|
|
||||||
<form class="gh-flow-invite" {{action "invite" on="submit"}}>
|
|
||||||
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="users">
|
|
||||||
<label for="users">Enter one email address per line, we’ll handle the rest! {{svg-jar "email"}}</label>
|
|
||||||
<GhTextarea
|
|
||||||
@name="users"
|
|
||||||
@required="required"
|
|
||||||
@value={{readonly this.users}}
|
|
||||||
@input={{action (mut this.users) value="target.value"}}
|
|
||||||
@focus-out={{action "validate"}}
|
|
||||||
/>
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
<GhTaskButton
|
|
||||||
@task={{this.invite}}
|
|
||||||
@type="submit"
|
|
||||||
@class="gh-btn gh-btn-default gh-btn-lg gh-btn-block {{this.buttonClass}}"
|
|
||||||
@successClass=""
|
|
||||||
@failureClass=""
|
|
||||||
as |task|
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{{#if task.isRunning}}
|
|
||||||
{{svg-jar "spinner" class="no-margin"}}
|
|
||||||
{{else}}
|
|
||||||
{{this.buttonText}}
|
|
||||||
{{/if}}
|
|
||||||
</span>
|
|
||||||
</GhTaskButton>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<button class="gh-flow-skip" type="button" {{action "skipInvite"}}>
|
|
||||||
I'll do this later, take me to my site!
|
|
||||||
</button>
|
|
@ -1,95 +0,0 @@
|
|||||||
<header>
|
|
||||||
<h1>Create your account</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form id="setup" class="gh-flow-create">
|
|
||||||
<GhProfileImage @email={{this.email}} @setImage={{action "setImage"}} />
|
|
||||||
|
|
||||||
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="blogTitle">
|
|
||||||
<label for="blog-title">Site title</label>
|
|
||||||
<span class="gh-input-icon gh-icon-content">
|
|
||||||
{{svg-jar "content"}}
|
|
||||||
<GhTrimFocusInput
|
|
||||||
@tabindex="1"
|
|
||||||
@type="text"
|
|
||||||
@id="blog-title"
|
|
||||||
@name="blog-title"
|
|
||||||
@placeholder="Eg. The Daily Awesome"
|
|
||||||
@autocorrect="off"
|
|
||||||
@value={{readonly this.blogTitle}}
|
|
||||||
@input={{action (mut this.blogTitle) value="target.value"}}
|
|
||||||
@focus-out={{action "preValidate" "blogTitle"}}
|
|
||||||
data-test-blog-title-input={{true}} />
|
|
||||||
</span>
|
|
||||||
<GhErrorMessage @errors={{this.errors}} @property="blogTitle" />
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="name">
|
|
||||||
<label for="name">Full name</label>
|
|
||||||
<span class="gh-input-icon gh-icon-user">
|
|
||||||
{{svg-jar "user-circle"}}
|
|
||||||
<GhTextInput
|
|
||||||
@tabindex="2"
|
|
||||||
@id="name"
|
|
||||||
@name="name"
|
|
||||||
@placeholder="Eg. John H. Watson"
|
|
||||||
@autocorrect="off"
|
|
||||||
@autocomplete="name"
|
|
||||||
@value={{readonly this.name}}
|
|
||||||
@input={{action (mut this.name) value="target.value"}}
|
|
||||||
@focus-out={{action "preValidate" "name"}}
|
|
||||||
data-test-name-input={{true}} />
|
|
||||||
</span>
|
|
||||||
<GhErrorMessage @errors={{this.errors}} @property="name" />
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="email">
|
|
||||||
<label for="email">Email address</label>
|
|
||||||
<span class="gh-input-icon gh-icon-mail">
|
|
||||||
{{svg-jar "email"}}
|
|
||||||
<GhTextInput
|
|
||||||
@tabindex="3"
|
|
||||||
@type="email"
|
|
||||||
@id="email"
|
|
||||||
@name="email"
|
|
||||||
@placeholder="Eg. john@example.com"
|
|
||||||
@autocorrect="off"
|
|
||||||
@autocomplete="username email"
|
|
||||||
@value={{readonly this.email}}
|
|
||||||
@input={{action (mut this.email) value="target.value"}}
|
|
||||||
@focus-out={{action "preValidate" "email"}}
|
|
||||||
data-test-email-input={{true}} />
|
|
||||||
</span>
|
|
||||||
<GhErrorMessage @errors={{this.errors}} @property="email" />
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
<GhFormGroup @errors={{this.errors}} @hasValidated={{this.hasValidated}} @property="password">
|
|
||||||
<label for="password">Password</label>
|
|
||||||
<span class="gh-input-icon gh-icon-lock">
|
|
||||||
{{svg-jar "lock"}}
|
|
||||||
<GhTextInput
|
|
||||||
@tabindex="4"
|
|
||||||
@type="password"
|
|
||||||
@id="password"
|
|
||||||
@name="password"
|
|
||||||
@placeholder="At least 10 characters"
|
|
||||||
@autocorrect="off"
|
|
||||||
@autocomplete="new-password"
|
|
||||||
@value={{readonly this.password}}
|
|
||||||
@input={{action (mut this.password) value="target.value"}}
|
|
||||||
@focus-out={{action "preValidate" "password"}}
|
|
||||||
data-test-password-input={{true}} />
|
|
||||||
</span>
|
|
||||||
<GhErrorMessage @errors={{this.errors}} @property="password" />
|
|
||||||
</GhFormGroup>
|
|
||||||
|
|
||||||
<GhTaskButton @task={{this.setupTask}} @type="submit" @tabindex="5" @class="gh-btn gh-btn-green gh-btn-lg gh-btn-block gh-btn-icon" as |task|>
|
|
||||||
{{#if task.isRunning}}
|
|
||||||
<span>{{svg-jar "spinner" class="gh-icon-spinner gh-btn-icon-no-margin"}}</span>
|
|
||||||
{{else}}
|
|
||||||
<span>Last step: Invite staff users →</span>
|
|
||||||
{{/if}}
|
|
||||||
</GhTaskButton>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<p class="main-error">{{this.flowErrors}} </p>
|
|
@ -3,25 +3,18 @@
|
|||||||
<section class="gh-flow-content">
|
<section class="gh-flow-content">
|
||||||
{{#if this.passwordResetEmailSent}}
|
{{#if this.passwordResetEmailSent}}
|
||||||
<div class="gh-auth-email">
|
<div class="gh-auth-email">
|
||||||
<div class="gh-auth-animation-container">
|
<header>
|
||||||
<div class="gh-auth-email-animation">
|
<div class="gh-site-icon" style={{site-icon-style}}></div>
|
||||||
{{svg-jar "locked-email-back" class="gh-auth-envelope-back"}}
|
<h1>Update your password.</h1>
|
||||||
{{svg-jar "locked-email-front" class="gh-auth-envelope-front"}}
|
|
||||||
<div class="gh-auth-paper">
|
|
||||||
{{svg-jar "locked-email-lock" class="gh-auth-lock"}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="gh-auth-lock-body">
|
|
||||||
<p>
|
<p>
|
||||||
For security, you need to create a new password. An email has been sent to you with instructions!
|
For security, you need to create a new password. An email has been sent to you with instructions!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form id="login" method="post" class="gh-signin" novalidate="novalidate" {{action "authenticate" on="submit"}}>
|
<form id="login" method="post" class="gh-signin" novalidate="novalidate" {{action "authenticate" on="submit"}}>
|
||||||
<header>
|
<header>
|
||||||
<div class="gh-site-icon" style="{{this.siteIconStyle}}"></div>
|
<div class="gh-site-icon" style={{site-icon-style}}></div>
|
||||||
<h1>Sign in to {{this.config.blogTitle}}.</h1>
|
<h1>Sign in to {{this.config.blogTitle}}.</h1>
|
||||||
</header>
|
</header>
|
||||||
{{#if this.config.oauth}}
|
{{#if this.config.oauth}}
|
||||||
|
@ -3,22 +3,20 @@
|
|||||||
<div class="gh-flow-content-wrap">
|
<div class="gh-flow-content-wrap">
|
||||||
<section class="gh-flow-content">
|
<section class="gh-flow-content">
|
||||||
<header>
|
<header>
|
||||||
<h1>Create your account</h1>
|
<div class="gh-site-icon" style={{site-icon-style}}></div>
|
||||||
|
<h1>Create your account.</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form id="signup" class="gh-flow-create" method="post" novalidate="novalidate" onsubmit={{action "submit"}}>
|
<form id="signup" class="gh-signup" method="post" novalidate="novalidate" onsubmit={{action "submit"}}>
|
||||||
<GhProfileImage @email={{this.signupDetails.email}} @setImage={{action "setImage"}} />
|
|
||||||
|
|
||||||
<GhFormGroup @errors={{this.signupDetails.errors}} @hasValidated={{this.signupDetails.hasValidated}} @property="name">
|
<GhFormGroup @errors={{this.signupDetails.errors}} @hasValidated={{this.signupDetails.hasValidated}} @property="name">
|
||||||
<label for="name">Full name</label>
|
<label for="name">Full name</label>
|
||||||
<span class="gh-input-icon gh-icon-user">
|
<span class="gh-input-icon gh-icon-user">
|
||||||
{{svg-jar "user-circle"}}
|
|
||||||
<GhTrimFocusInput
|
<GhTrimFocusInput
|
||||||
@tabindex="1"
|
@tabindex="1"
|
||||||
@type="text"
|
@type="text"
|
||||||
@id="display-name"
|
@id="display-name"
|
||||||
@name="display-name"
|
@name="display-name"
|
||||||
@placeholder="Eg. John H. Watson"
|
@placeholder="Jamie Larson"
|
||||||
@autocorrect="off"
|
@autocorrect="off"
|
||||||
@autocomplete="name"
|
@autocomplete="name"
|
||||||
@value={{readonly this.signupDetails.name}}
|
@value={{readonly this.signupDetails.name}}
|
||||||
@ -33,13 +31,12 @@
|
|||||||
<GhFormGroup @errors={{this.signupDetails.errors}} @hasValidated={{this.signupDetails.hasValidated}} @property="email">
|
<GhFormGroup @errors={{this.signupDetails.errors}} @hasValidated={{this.signupDetails.hasValidated}} @property="email">
|
||||||
<label for="email">Email address</label>
|
<label for="email">Email address</label>
|
||||||
<span class="gh-input-icon gh-icon-mail">
|
<span class="gh-input-icon gh-icon-mail">
|
||||||
{{svg-jar "email"}}
|
|
||||||
<GhTextInput
|
<GhTextInput
|
||||||
@tabindex="2"
|
@tabindex="2"
|
||||||
@type="text"
|
@type="text"
|
||||||
@id="username"
|
@id="username"
|
||||||
@name="username"
|
@name="username"
|
||||||
@placeholder="Eg. john@example.com"
|
@placeholder="jamie@example.com"
|
||||||
@autocorrect="off"
|
@autocorrect="off"
|
||||||
@autocomplete="username email"
|
@autocomplete="username email"
|
||||||
@value={{readonly this.signupDetails.email}}
|
@value={{readonly this.signupDetails.email}}
|
||||||
@ -54,7 +51,6 @@
|
|||||||
<GhFormGroup @errors={{this.signupDetails.errors}} @hasValidated={{this.signupDetails.hasValidated}} @property="password">
|
<GhFormGroup @errors={{this.signupDetails.errors}} @hasValidated={{this.signupDetails.hasValidated}} @property="password">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<span class="gh-input-icon gh-icon-lock">
|
<span class="gh-input-icon gh-icon-lock">
|
||||||
{{svg-jar "lock"}}
|
|
||||||
<GhTextInput
|
<GhTextInput
|
||||||
@tabindex="3"
|
@tabindex="3"
|
||||||
@type="password"
|
@type="password"
|
||||||
@ -72,11 +68,12 @@
|
|||||||
<GhErrorMessage @errors={{this.signupDetails.errors}} @property="password" />
|
<GhErrorMessage @errors={{this.signupDetails.errors}} @property="password" />
|
||||||
</GhFormGroup>
|
</GhFormGroup>
|
||||||
|
|
||||||
<GhTaskButton @buttonText="Create Account" @type="submit" @form="signup" @defaultClick={{true}} @runningText="Creating"
|
<GhTaskButton @buttonText="Create Account →" @type="submit" @form="signup" @defaultClick={{true}} @runningText="Creating"
|
||||||
@task={{this.signup}} @class="gh-btn-create-account gh-btn gh-btn-green gh-btn-lg gh-btn-block gh-btn-icon" @tabindex="3" />
|
@task={{this.signup}} @data-test-button="signup" @class="gh-btn gh-btn-black gh-btn-signup gh-btn-block gh-btn-icon" @tabindex="3" />
|
||||||
</form>
|
</form>
|
||||||
|
{{#if this.flowErrors}}
|
||||||
<p class="main-error">{{if this.flowErrors this.flowErrors}} </p>
|
<p class="main-error">{{this.flowErrors}} </p>
|
||||||
|
{{/if}}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
<svg width="122" height="125" viewBox="0 0 122 125" xmlns="http://www.w3.org/2000/svg"><title>envelope-back</title><path d="M3.953 39.164L54.375 2.172c3.947-2.896 9.303-2.896 13.25 0l50.422 36.992c2.484 1.823 3.953 4.728 3.953 7.82v68.336c0 5.346-4.312 9.68-9.632 9.68H9.632C4.312 125 0 120.666 0 115.32V46.983c0-3.09 1.469-5.996 3.953-7.819z" fill="#C5D2D9" fill-rule="evenodd"/></svg>
|
|
Before Width: | Height: | Size: 386 B |
@ -1 +0,0 @@
|
|||||||
<svg width="122" height="77" viewBox="0 0 122 77" xmlns="http://www.w3.org/2000/svg"><title>envelope-front</title><g stroke="#C5D2D9" fill="none" fill-rule="evenodd"><path d="M.5 3.023v64.28c0 2.519 1.002 4.802 2.642 6.463 1.634 1.655 3.9 2.693 6.438 2.734h102.431c1.888-.005 3.641-.592 5.093-1.593l-5.64-3.368C44.768 31.699 9.41 10.472 5.388 7.859 3.085 6.365 1.463 4.75.5 3.024z" fill="#F2F4F7"/><path d="M121.491 3.111c-.97 1.746-2.609 3.38-4.94 4.89-3.73 2.418-34.423 20.824-92.079 55.216L4.895 74.89c1.428.99 3.162 1.578 5.127 1.61l102.41-.001c2.325-.006 4.447-.894 6.052-2.35 1.616-1.466 2.71-3.508 2.962-5.806l.045-65.233z" fill="#F8FAFC"/></g></svg>
|
|
Before Width: | Height: | Size: 657 B |
@ -1 +0,0 @@
|
|||||||
<svg width="40" height="40" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 16.25h22.5c1.667 0 2.5.833 2.5 2.5v17.5c0 1.667-.833 2.5-2.5 2.5H8.75c-1.667 0-2.5-.833-2.5-2.5v-17.5c0-1.667.833-2.5 2.5-2.5zm2.5 0V10c0-3.368 1.458-5.894 4.375-7.578 2.917-1.684 5.833-1.684 8.75 0S28.75 6.632 28.75 10v6.25M20 25v5" stroke="#3EB0EF" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
Before Width: | Height: | Size: 454 B |
Binary file not shown.
Before Width: | Height: | Size: 6.4 KiB |
@ -24,7 +24,7 @@ describe('Acceptance: Authentication', function () {
|
|||||||
});
|
});
|
||||||
it('redirects to setup when setup isn\'t complete', async function () {
|
it('redirects to setup when setup isn\'t complete', async function () {
|
||||||
await visit('settings/labs');
|
await visit('settings/labs');
|
||||||
expect(currentURL()).to.equal('/setup/one');
|
expect(currentURL()).to.equal('/setup');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import {Response} from 'miragejs';
|
import {Response} from 'miragejs';
|
||||||
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';
|
||||||
import {beforeEach, describe, it} from 'mocha';
|
import {beforeEach, describe, it} from 'mocha';
|
||||||
import {blur, click, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
|
import {click, currentURL, fillIn, find, findAll} from '@ember/test-helpers';
|
||||||
import {enableLabsFlag} from '../helpers/labs-flag';
|
import {enableLabsFlag} from '../helpers/labs-flag';
|
||||||
import {expect} from 'chai';
|
import {expect} from 'chai';
|
||||||
import {setupApplicationTest} from 'ember-mocha';
|
import {setupApplicationTest} from 'ember-mocha';
|
||||||
@ -23,13 +22,7 @@ describe('Acceptance: Setup', function () {
|
|||||||
|
|
||||||
await authenticateSession();
|
await authenticateSession();
|
||||||
|
|
||||||
await visit('/setup/one');
|
await visit('/setup');
|
||||||
expect(currentURL()).to.equal('/site');
|
|
||||||
|
|
||||||
await visit('/setup/two');
|
|
||||||
expect(currentURL()).to.equal('/site');
|
|
||||||
|
|
||||||
await visit('/setup/three');
|
|
||||||
expect(currentURL()).to.equal('/site');
|
expect(currentURL()).to.equal('/site');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,30 +60,13 @@ describe('Acceptance: Setup', function () {
|
|||||||
|
|
||||||
await visit('/setup');
|
await visit('/setup');
|
||||||
|
|
||||||
// it redirects to step one
|
|
||||||
expect(currentURL(), 'url after accessing /setup')
|
|
||||||
.to.equal('/setup/one');
|
|
||||||
|
|
||||||
// it highlights first step
|
|
||||||
let stepIcons = findAll('.gh-flow-nav .step');
|
|
||||||
expect(stepIcons.length, 'sanity check: three steps').to.equal(3);
|
|
||||||
expect(stepIcons[0], 'first step').to.have.class('active');
|
|
||||||
expect(stepIcons[1], 'second step').to.not.have.class('active');
|
|
||||||
expect(stepIcons[2], 'third step').to.not.have.class('active');
|
|
||||||
|
|
||||||
await click('.gh-btn-green');
|
|
||||||
|
|
||||||
// it transitions to step two
|
|
||||||
expect(currentURL(), 'url after clicking "Create your account"')
|
|
||||||
.to.equal('/setup/two');
|
|
||||||
|
|
||||||
// email field is focused by default
|
// email field is focused by default
|
||||||
// NOTE: $('x').is(':focus') doesn't work in phantomjs CLI runner
|
// NOTE: $('x').is(':focus') doesn't work in phantomjs CLI runner
|
||||||
// https://github.com/ariya/phantomjs/issues/10427
|
// https://github.com/ariya/phantomjs/issues/10427
|
||||||
expect(findAll('[data-test-blog-title-input]')[0] === document.activeElement, 'blog title has focus')
|
expect(findAll('[data-test-blog-title-input]')[0] === document.activeElement, 'blog title has focus')
|
||||||
.to.be.true;
|
.to.be.true;
|
||||||
|
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="setup"]');
|
||||||
|
|
||||||
// it marks fields as invalid
|
// it marks fields as invalid
|
||||||
expect(findAll('.form-group.error').length, 'number of invalid fields')
|
expect(findAll('.form-group.error').length, 'number of invalid fields')
|
||||||
@ -109,39 +85,14 @@ describe('Acceptance: Setup', function () {
|
|||||||
await fillIn('[data-test-name-input]', 'Test User');
|
await fillIn('[data-test-name-input]', 'Test User');
|
||||||
await fillIn('[data-test-password-input]', 'thisissupersafe');
|
await fillIn('[data-test-password-input]', 'thisissupersafe');
|
||||||
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="setup"]');
|
||||||
|
|
||||||
// it transitions to step 3
|
// it redirects to the dashboard
|
||||||
expect(currentURL(), 'url after submitting step two')
|
expect(currentURL(), 'url after submitting account details')
|
||||||
.to.equal('/setup/three');
|
|
||||||
|
|
||||||
// submit button is "disabled"
|
|
||||||
expect(find('button[type="submit"]').classList.contains('gh-btn-green'), 'invite button with no emails is white')
|
|
||||||
.to.be.false;
|
|
||||||
|
|
||||||
// fill in a valid email
|
|
||||||
await fillIn('[name="users"]', 'new-user@example.com');
|
|
||||||
|
|
||||||
// submit button is "enabled"
|
|
||||||
expect(find('button[type="submit"]').classList.contains('gh-btn-green'), 'invite button is green with valid email address')
|
|
||||||
.to.be.true;
|
|
||||||
|
|
||||||
// submit the invite form
|
|
||||||
await click('button[type="submit"]');
|
|
||||||
|
|
||||||
// it redirects to the home / "content" screen
|
|
||||||
expect(currentURL(), 'url after submitting invites')
|
|
||||||
.to.equal('/dashboard');
|
.to.equal('/dashboard');
|
||||||
|
|
||||||
// it displays success alert
|
|
||||||
expect(findAll('.gh-alert-green').length, 'number of success alerts')
|
|
||||||
.to.equal(1);
|
|
||||||
|
|
||||||
// it opens get-started modal
|
|
||||||
expect(find('[data-test-modal="get-started"]')).to.exist;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles validation errors in step 2', async function () {
|
it('handles validation errors in setup', async function () {
|
||||||
let postCount = 0;
|
let postCount = 0;
|
||||||
|
|
||||||
await invalidateSession();
|
await invalidateSession();
|
||||||
@ -168,8 +119,8 @@ describe('Acceptance: Setup', function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await visit('/setup/two');
|
await visit('/setup');
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="setup"]');
|
||||||
|
|
||||||
// non-server validation
|
// non-server validation
|
||||||
expect(find('.main-error').textContent.trim(), 'error text')
|
expect(find('.main-error').textContent.trim(), 'error text')
|
||||||
@ -181,22 +132,22 @@ describe('Acceptance: Setup', function () {
|
|||||||
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
||||||
|
|
||||||
// first post - simulated validation error
|
// first post - simulated validation error
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="setup"]');
|
||||||
|
|
||||||
expect(find('.main-error').textContent.trim(), 'error text')
|
expect(find('.main-error').textContent.trim(), 'error text')
|
||||||
.to.equal('Server response message');
|
.to.equal('Server response message');
|
||||||
|
|
||||||
// second post - simulated server error
|
// second post - simulated server error
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="setup"]');
|
||||||
|
|
||||||
expect(find('.main-error').textContent.trim(), 'error text')
|
expect(findAll('.main-error').length, 'main error is not displayed')
|
||||||
.to.be.empty;
|
.to.equal(0);
|
||||||
|
|
||||||
expect(findAll('.gh-alert-red').length, 'number of alerts')
|
expect(findAll('.gh-alert-red').length, 'number of alerts')
|
||||||
.to.equal(1);
|
.to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles invalid origin error on step 2', async function () {
|
it('handles invalid origin error on setup', async function () {
|
||||||
// mimick the API response for an invalid origin
|
// mimick the API response for an invalid origin
|
||||||
this.server.post('/session', function () {
|
this.server.post('/session', function () {
|
||||||
return new Response(401, {}, {
|
return new Response(401, {}, {
|
||||||
@ -212,149 +163,20 @@ describe('Acceptance: Setup', function () {
|
|||||||
await invalidateSession();
|
await invalidateSession();
|
||||||
this.server.loadFixtures('roles');
|
this.server.loadFixtures('roles');
|
||||||
|
|
||||||
await visit('/setup/two');
|
await visit('/setup');
|
||||||
await fillIn('[data-test-email-input]', 'test@example.com');
|
await fillIn('[data-test-email-input]', 'test@example.com');
|
||||||
await fillIn('[data-test-name-input]', 'Test User');
|
await fillIn('[data-test-name-input]', 'Test User');
|
||||||
await fillIn('[data-test-password-input]', 'thisissupersafe');
|
await fillIn('[data-test-password-input]', 'thisissupersafe');
|
||||||
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="setup"]');
|
||||||
|
|
||||||
// button should not be spinning
|
// button should not be spinning
|
||||||
expect(findAll('.gh-btn-green .spinner').length, 'button has spinner')
|
expect(findAll('.gh-btn-signup .spinner').length, 'button has spinner')
|
||||||
.to.equal(0);
|
.to.equal(0);
|
||||||
// we should show an error message
|
// we should show an error message
|
||||||
expect(find('.main-error').textContent, 'error text')
|
expect(find('.main-error').textContent, 'error text')
|
||||||
.to.have.string('Access Denied from url: unknown.com. Please use the url configured in config.js.');
|
.to.have.string('Access Denied from url: unknown.com. Please use the url configured in config.js.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles validation errors in step 3', async function () {
|
|
||||||
let input = '[name="users"]';
|
|
||||||
let postCount = 0;
|
|
||||||
let button, formGroup;
|
|
||||||
|
|
||||||
await invalidateSession();
|
|
||||||
this.server.loadFixtures('roles');
|
|
||||||
|
|
||||||
this.server.post('/invites/', function ({invites}) {
|
|
||||||
let attrs = this.normalizedRequestAttrs();
|
|
||||||
|
|
||||||
postCount += 1;
|
|
||||||
|
|
||||||
// invalid
|
|
||||||
if (postCount === 1) {
|
|
||||||
return new Response(422, {}, {
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
type: 'ValidationError',
|
|
||||||
message: 'Dummy validation error'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: duplicated from mirage/config/invites - extract method?
|
|
||||||
attrs.token = `${invites.all().models.length}-token`;
|
|
||||||
attrs.expires = moment.utc().add(1, 'day').valueOf();
|
|
||||||
attrs.createdAt = moment.utc().format();
|
|
||||||
attrs.createdBy = 1;
|
|
||||||
attrs.updatedAt = moment.utc().format();
|
|
||||||
attrs.updatedBy = 1;
|
|
||||||
attrs.status = 'sent';
|
|
||||||
|
|
||||||
return invites.create(attrs);
|
|
||||||
});
|
|
||||||
|
|
||||||
// complete step 2 so we can access step 3
|
|
||||||
await visit('/setup/two');
|
|
||||||
await fillIn('[data-test-email-input]', 'test@example.com');
|
|
||||||
await fillIn('[data-test-name-input]', 'Test User');
|
|
||||||
await fillIn('[data-test-password-input]', 'thisissupersafe');
|
|
||||||
await fillIn('[data-test-blog-title-input]', 'Blog Title');
|
|
||||||
await click('.gh-btn-green');
|
|
||||||
|
|
||||||
// default field/button state
|
|
||||||
formGroup = find('.gh-flow-invite .form-group');
|
|
||||||
button = find('.gh-flow-invite button[type="submit"]');
|
|
||||||
|
|
||||||
expect(formGroup, 'default field has error class')
|
|
||||||
.to.not.have.class('error');
|
|
||||||
|
|
||||||
expect(button.textContent, 'default button text')
|
|
||||||
.to.have.string('Invite some users');
|
|
||||||
|
|
||||||
expect(button, 'default button is disabled')
|
|
||||||
.to.have.class('gh-btn-minor');
|
|
||||||
|
|
||||||
// no users submitted state
|
|
||||||
await click('.gh-flow-invite button[type="submit"]');
|
|
||||||
|
|
||||||
expect(formGroup, 'no users submitted field has error class')
|
|
||||||
.to.have.class('error');
|
|
||||||
|
|
||||||
expect(button.textContent, 'no users submitted button text')
|
|
||||||
.to.have.string('No users to invite');
|
|
||||||
|
|
||||||
expect(button, 'no users submitted button is disabled')
|
|
||||||
.to.have.class('gh-btn-minor');
|
|
||||||
|
|
||||||
// single invalid email
|
|
||||||
await fillIn(input, 'invalid email');
|
|
||||||
await blur(input);
|
|
||||||
|
|
||||||
expect(formGroup, 'invalid field has error class')
|
|
||||||
.to.have.class('error');
|
|
||||||
|
|
||||||
expect(button.textContent, 'single invalid button text')
|
|
||||||
.to.have.string('1 invalid email address');
|
|
||||||
|
|
||||||
expect(button, 'invalid email button is disabled')
|
|
||||||
.to.have.class('gh-btn-minor');
|
|
||||||
|
|
||||||
// multiple invalid emails
|
|
||||||
await fillIn(input, 'invalid email\nanother invalid address');
|
|
||||||
await blur(input);
|
|
||||||
|
|
||||||
expect(button.textContent, 'multiple invalid button text')
|
|
||||||
.to.have.string('2 invalid email addresses');
|
|
||||||
|
|
||||||
// single valid email
|
|
||||||
await fillIn(input, 'invited@example.com');
|
|
||||||
await blur(input);
|
|
||||||
|
|
||||||
expect(formGroup, 'valid field has error class')
|
|
||||||
.to.not.have.class('error');
|
|
||||||
|
|
||||||
expect(button.textContent, 'single valid button text')
|
|
||||||
.to.have.string('Invite 1 user');
|
|
||||||
|
|
||||||
expect(button, 'valid email button is enabled')
|
|
||||||
.to.have.class('gh-btn-green');
|
|
||||||
|
|
||||||
// multiple valid emails
|
|
||||||
await fillIn(input, 'invited1@example.com\ninvited2@example.com');
|
|
||||||
await blur(input);
|
|
||||||
|
|
||||||
expect(button.textContent, 'multiple valid button text')
|
|
||||||
.to.have.string('Invite 2 users');
|
|
||||||
|
|
||||||
// submit invitations with simulated failure on 1 invite
|
|
||||||
await click('.gh-btn-green');
|
|
||||||
|
|
||||||
// it redirects to the home / "content" screen
|
|
||||||
expect(currentURL(), 'url after submitting invites')
|
|
||||||
.to.equal('/dashboard');
|
|
||||||
|
|
||||||
// it displays success alert
|
|
||||||
expect(findAll('.gh-alert-green').length, 'number of success alerts')
|
|
||||||
.to.equal(1);
|
|
||||||
|
|
||||||
// it displays failure alert
|
|
||||||
expect(findAll('.gh-alert-red').length, 'number of failure alerts')
|
|
||||||
.to.equal(1);
|
|
||||||
|
|
||||||
// it opens get-started modal
|
|
||||||
expect(find('[data-test-modal="get-started"]')).to.exist;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('?firstStart=true', function () {
|
describe('?firstStart=true', function () {
|
||||||
|
@ -165,7 +165,7 @@ describe('Acceptance: Signup', function () {
|
|||||||
).to.equal('');
|
).to.equal('');
|
||||||
|
|
||||||
// submitting sends correct details and redirects to content screen
|
// submitting sends correct details and redirects to content screen
|
||||||
await click('.gh-btn-green');
|
await click('[data-test-button="signup"]');
|
||||||
|
|
||||||
expect(currentRouteName()).to.equal('site');
|
expect(currentRouteName()).to.equal('site');
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user