mirror of
https://github.com/TryGhost/Ghost-Admin.git
synced 2023-12-14 02:33:04 +01:00
💄 refactor setup screens to use ember-concurrency (#644)
refs TryGhost/Ghost#7865 💄 refactor signup to use ember-concurrency refs https://github.com/TryGhost/Ghost/issues/7865 - moves authentication actions from `signup` route to controller - refactors authentication and signup logic into EC tasks - replaces use of `gh-spin-button` with `gh-task-button` in signup template 💄 refactor setup screens to use ember-concurrency refs https://github.com/TryGhost/Ghost/issues/7865 - moves authentication actions from `setup/two` route to controller - refactors authentication and setup logic into EC tasks - replaces use of `gh-spin-button` with `gh-task-button` - fixes some styling issues with the new SVG icons - adds `app/styles/patterns/icons.css` back to contain per-icon overrides and animations (some SVGs use fills and others use strokes so we sometimes have conflicting styles)
This commit is contained in:
parent
ae1b5ee519
commit
c1a9726f1b
16 changed files with 417 additions and 284 deletions
|
@ -7,6 +7,7 @@ import injectController from 'ember-controller/inject';
|
|||
import {htmlSafe} from 'ember-string';
|
||||
import run from 'ember-runloop';
|
||||
import DS from 'ember-data';
|
||||
import {task, timeout} from 'ember-concurrency';
|
||||
|
||||
const {Errors} = DS;
|
||||
|
||||
|
@ -18,7 +19,6 @@ export default Controller.extend({
|
|||
hasValidated: emberA(),
|
||||
users: '',
|
||||
ownerEmail: alias('two.email'),
|
||||
submitting: false,
|
||||
|
||||
usersArray: computed('users', function () {
|
||||
let errors = this.get('errors');
|
||||
|
@ -142,87 +142,99 @@ export default Controller.extend({
|
|||
}
|
||||
},
|
||||
|
||||
invite: task(function* () {
|
||||
let users = this.get('usersArray');
|
||||
|
||||
if (this.validate() && users.length > 0) {
|
||||
this._hasTransitioned = false;
|
||||
|
||||
this.get('_slowSubmissionTimeout').perform();
|
||||
|
||||
let authorRole = yield this.get('authorRole');
|
||||
let invites = yield this._saveInvites(authorRole);
|
||||
|
||||
this.get('_slowSubmissionTimeout').cancelAll();
|
||||
|
||||
this._showNotifications(invites);
|
||||
|
||||
run.schedule('actions', this, function () {
|
||||
this.send('loadServerNotifications');
|
||||
this._transitionAfterSubmission();
|
||||
});
|
||||
|
||||
} else if (users.length === 0) {
|
||||
this.get('errors').add('users', 'No users to invite');
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
_slowSubmissionTimeout: task(function* () {
|
||||
yield timeout(4000);
|
||||
this._transitionAfterSubmission();
|
||||
}).drop(),
|
||||
|
||||
_saveInvites(authorRole) {
|
||||
let users = this.get('usersArray');
|
||||
|
||||
return RSVP.Promise.all(
|
||||
users.map((user) => {
|
||||
let invite = this.store.createRecord('invite', {
|
||||
email: user,
|
||||
role: authorRole
|
||||
});
|
||||
|
||||
return invite.save().then(() => {
|
||||
return {
|
||||
email: user,
|
||||
success: invite.get('status') === 'sent'
|
||||
};
|
||||
}).catch(() => {
|
||||
return {
|
||||
email: user,
|
||||
success: false
|
||||
};
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_showNotifications(invites) {
|
||||
let notifications = this.get('notifications');
|
||||
let erroredEmails = [];
|
||||
let successCount = 0;
|
||||
let invitationsString, message;
|
||||
|
||||
invites.forEach((invite) => {
|
||||
if (invite.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
erroredEmails.push(invite.email);
|
||||
}
|
||||
});
|
||||
|
||||
if (erroredEmails.length > 0) {
|
||||
invitationsString = erroredEmails.length > 1 ? ' invitations: ' : ' invitation: ';
|
||||
message = `Failed to send ${erroredEmails.length} ${invitationsString}`;
|
||||
message += erroredEmails.join(', ');
|
||||
message += ". Please check your email configuration, see <a href=\'http://support.ghost.org/mail\' target=\'_blank\'>http://support.ghost.org/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'});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
validate() {
|
||||
this.validate();
|
||||
},
|
||||
|
||||
invite() {
|
||||
let users = this.get('usersArray');
|
||||
let notifications = this.get('notifications');
|
||||
let invitationsString, submissionTimeout;
|
||||
|
||||
if (this.validate() && users.length > 0) {
|
||||
this.set('submitting', true);
|
||||
this._hasTransitioned = false;
|
||||
|
||||
// wait for 4 seconds, otherwise transition anyway
|
||||
submissionTimeout = run.later(this, function () {
|
||||
this._transitionAfterSubmission();
|
||||
}, 4000);
|
||||
|
||||
this.get('authorRole').then((authorRole) => {
|
||||
RSVP.Promise.all(
|
||||
users.map((user) => {
|
||||
let invite = this.store.createRecord('invite', {
|
||||
email: user,
|
||||
role: authorRole
|
||||
});
|
||||
|
||||
return invite.save().then(() => {
|
||||
return {
|
||||
email: user,
|
||||
success: invite.get('status') === 'sent'
|
||||
};
|
||||
}).catch(() => {
|
||||
return {
|
||||
email: user,
|
||||
success: false
|
||||
};
|
||||
});
|
||||
})
|
||||
).then((invites) => {
|
||||
let erroredEmails = [];
|
||||
let successCount = 0;
|
||||
let message;
|
||||
|
||||
run.cancel(submissionTimeout);
|
||||
|
||||
invites.forEach((invite) => {
|
||||
if (invite.success) {
|
||||
successCount++;
|
||||
} else {
|
||||
erroredEmails.push(invite.email);
|
||||
}
|
||||
});
|
||||
|
||||
if (erroredEmails.length > 0) {
|
||||
invitationsString = erroredEmails.length > 1 ? ' invitations: ' : ' invitation: ';
|
||||
message = `Failed to send ${erroredEmails.length} ${invitationsString}`;
|
||||
message += erroredEmails.join(', ');
|
||||
message += ". Please check your email configuration, see <a href=\'http://support.ghost.org/mail\' target=\'_blank\'>http://support.ghost.org/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'});
|
||||
}
|
||||
|
||||
this.set('submitting', false);
|
||||
|
||||
run.schedule('actions', this, function () {
|
||||
this.send('loadServerNotifications');
|
||||
this._transitionAfterSubmission();
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (users.length === 0) {
|
||||
this.get('errors').add('users', 'No users to invite');
|
||||
}
|
||||
this.get('invite').perform();
|
||||
},
|
||||
|
||||
skipInvite() {
|
||||
|
|
|
@ -1,40 +1,99 @@
|
|||
import Controller from 'ember-controller';
|
||||
import RSVP from 'rsvp';
|
||||
import injectService from 'ember-service/inject';
|
||||
import injectController from 'ember-controller/inject';
|
||||
import {isInvalidError} from 'ember-ajax/errors';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import injectController from 'ember-controller/inject';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {isInvalidError} from 'ember-ajax/errors';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
const {Promise} = RSVP;
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
size: 90,
|
||||
blogTitle: null,
|
||||
name: null,
|
||||
email: '',
|
||||
password: null,
|
||||
image: null,
|
||||
blogCreated: false,
|
||||
submitting: false,
|
||||
flowErrors: '',
|
||||
|
||||
ghostPaths: injectService(),
|
||||
notifications: injectService(),
|
||||
ajax: injectService(),
|
||||
application: injectController(),
|
||||
config: injectService(),
|
||||
ghostPaths: injectService(),
|
||||
notifications: injectService(),
|
||||
session: injectService(),
|
||||
settings: injectService(),
|
||||
ajax: injectService(),
|
||||
torii: injectService(),
|
||||
|
||||
// ValidationEngine settings
|
||||
validationType: 'setup',
|
||||
|
||||
blogCreated: false,
|
||||
blogTitle: null,
|
||||
email: '',
|
||||
flowErrors: '',
|
||||
image: null,
|
||||
name: null,
|
||||
password: null,
|
||||
|
||||
setup: task(function* () {
|
||||
if (this.get('config.ghostOAuth')) {
|
||||
return yield this._oauthSetup();
|
||||
} else {
|
||||
return yield this._passwordSetup();
|
||||
}
|
||||
}),
|
||||
|
||||
// TODO: remove duplication with controllers/signin
|
||||
authenticateWithGhostOrg: task(function* () {
|
||||
let authStrategy = 'authenticator:oauth2-ghost';
|
||||
|
||||
this.set('flowErrors', '');
|
||||
|
||||
try {
|
||||
let authentication = yield this.get('torii')
|
||||
.open('ghost-oauth2', {type: 'setup'});
|
||||
|
||||
yield this.get('authenticate').perform(authStrategy, [authentication]);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.set('flowErrors', 'Authentication with Ghost.org denied or failed');
|
||||
throw error;
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
authenticate: task(function* (authStrategy, authentication) {
|
||||
// we don't want to redirect after sign-in during setup
|
||||
this.set('session.skipAuthSuccessHandler', true);
|
||||
|
||||
try {
|
||||
let authResult = yield this.get('session')
|
||||
.authenticate(authStrategy, ...authentication);
|
||||
|
||||
this.get('errors').remove('session');
|
||||
|
||||
return authResult;
|
||||
|
||||
} catch (error) {
|
||||
if (error && error.errors) {
|
||||
if (isVersionMismatchError(error)) {
|
||||
return this.get('notifications').showAPIError(error);
|
||||
}
|
||||
|
||||
error.errors.forEach((err) => {
|
||||
err.message = err.message.htmlSafe();
|
||||
});
|
||||
|
||||
this.set('flowErrors', error.errors[0].message.string);
|
||||
} else {
|
||||
// Connection errors don't return proper status message, only req.body
|
||||
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* 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 {Ember.RSVP.Promise} A promise that takes care of both calls
|
||||
*/
|
||||
sendImage(user) {
|
||||
_sendImage(user) {
|
||||
let image = this.get('image');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -54,57 +113,12 @@ export default Controller.extend(ValidationEngine, {
|
|||
});
|
||||
},
|
||||
|
||||
_handleSaveError(resp) {
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
if (isInvalidError(resp)) {
|
||||
this.set('flowErrors', resp.errors[0].message);
|
||||
} else {
|
||||
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
|
||||
}
|
||||
},
|
||||
|
||||
_handleAuthenticationError(error) {
|
||||
this.toggleProperty('submitting');
|
||||
if (error && error.errors) {
|
||||
this.set('flowErrors', error.errors[0].message);
|
||||
} else {
|
||||
// Connection errors don't return proper status message, only req.body
|
||||
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'setup.authenticate.failed'});
|
||||
}
|
||||
},
|
||||
|
||||
afterAuthentication(result) {
|
||||
if (this.get('image')) {
|
||||
return this.sendImage(result.users[0])
|
||||
.then(() => {
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
// fetch settings for synchronous access before transitioning
|
||||
return this.get('settings').fetch().then(() => {
|
||||
return this.transitionToRoute('setup.three');
|
||||
});
|
||||
}).catch((resp) => {
|
||||
this.toggleProperty('submitting');
|
||||
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
|
||||
});
|
||||
} else {
|
||||
this.toggleProperty('submitting');
|
||||
|
||||
// fetch settings for synchronous access before transitioning
|
||||
return this.get('settings').fetch().then(() => {
|
||||
return this.transitionToRoute('setup.three');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_passwordSetup() {
|
||||
let setupProperties = ['blogTitle', 'name', 'email', 'password'];
|
||||
let data = this.getProperties(setupProperties);
|
||||
let config = this.get('config');
|
||||
let method = this.get('blogCreated') ? 'put' : 'post';
|
||||
|
||||
this.toggleProperty('submitting');
|
||||
this.set('flowErrors', '');
|
||||
|
||||
this.get('hasValidated').addObjects(setupProperties);
|
||||
|
@ -126,7 +140,7 @@ export default Controller.extend(ValidationEngine, {
|
|||
|
||||
// don't try to login again if we are already logged in
|
||||
if (this.get('session.isAuthenticated')) {
|
||||
return this.afterAuthentication(result);
|
||||
return this._afterAuthentication(result);
|
||||
}
|
||||
|
||||
// Don't call the success handler, otherwise we will be redirected to admin
|
||||
|
@ -134,7 +148,7 @@ export default Controller.extend(ValidationEngine, {
|
|||
|
||||
return this.get('session').authenticate('authenticator:oauth2', this.get('email'), this.get('password')).then(() => {
|
||||
this.set('blogCreated', true);
|
||||
return this.afterAuthentication(result);
|
||||
return this._afterAuthentication(result);
|
||||
}).catch((error) => {
|
||||
this._handleAuthenticationError(error);
|
||||
}).finally(() => {
|
||||
|
@ -144,12 +158,11 @@ export default Controller.extend(ValidationEngine, {
|
|||
this._handleSaveError(error);
|
||||
});
|
||||
}).catch(() => {
|
||||
this.toggleProperty('submitting');
|
||||
this.set('flowErrors', 'Please fill out the form to setup your blog.');
|
||||
});
|
||||
},
|
||||
|
||||
// TODO: for OAuth ghost is in the "setup completed" step as soon
|
||||
// NOTE: for OAuth ghost is in the "setup completed" step as soon
|
||||
// as a user has been authenticated so we need to use the standard settings
|
||||
// update to set the blog title before redirecting
|
||||
_oauthSetup() {
|
||||
|
@ -172,20 +185,61 @@ export default Controller.extend(ValidationEngine, {
|
|||
// this.blogCreated is used by step 3 to check if step 2
|
||||
// has been completed
|
||||
this.set('blogCreated', true);
|
||||
return this.afterAuthentication(settings);
|
||||
return this._afterAuthentication(settings);
|
||||
})
|
||||
.catch((error) => {
|
||||
this._handleSaveError(error);
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.toggleProperty('submitting');
|
||||
this.set('session.skipAuthSuccessHandler', undefined);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_handleSaveError(resp) {
|
||||
if (isInvalidError(resp)) {
|
||||
this.set('flowErrors', resp.errors[0].message);
|
||||
} else {
|
||||
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
|
||||
}
|
||||
},
|
||||
|
||||
_handleAuthenticationError(error) {
|
||||
if (error && error.errors) {
|
||||
this.set('flowErrors', error.errors[0].message);
|
||||
} else {
|
||||
// Connection errors don't return proper status message, only req.body
|
||||
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'setup.authenticate.failed'});
|
||||
}
|
||||
},
|
||||
|
||||
_afterAuthentication(result) {
|
||||
if (this.get('image')) {
|
||||
return this._sendImage(result.users[0])
|
||||
.then(() => {
|
||||
|
||||
// fetch settings for synchronous access before transitioning
|
||||
return this.get('settings').fetch().then(() => {
|
||||
return this.transitionToRoute('setup.three');
|
||||
});
|
||||
}).catch((resp) => {
|
||||
this.get('notifications').showAPIError(resp, {key: 'setup.blog-details'});
|
||||
});
|
||||
} else {
|
||||
|
||||
// fetch settings for synchronous access before transitioning
|
||||
return this.get('settings').fetch().then(() => {
|
||||
return this.transitionToRoute('setup.three');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
setup() {
|
||||
this.get('setup').perform();
|
||||
},
|
||||
|
||||
preValidate(model) {
|
||||
// Only triggers validation if a value has been entered, preventing empty errors on focusOut
|
||||
if (this.get(model)) {
|
||||
|
@ -193,14 +247,6 @@ export default Controller.extend(ValidationEngine, {
|
|||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
if (this.get('config.ghostOAuth')) {
|
||||
return this._oauthSetup();
|
||||
} else {
|
||||
return this._passwordSetup();
|
||||
}
|
||||
},
|
||||
|
||||
setImage(image) {
|
||||
this.set('image', image);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import Controller from 'ember-controller';
|
||||
import injectService from 'ember-service/inject';
|
||||
import injectController from 'ember-controller/inject';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
import {task} from 'ember-concurrency';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
|
||||
import injectController from 'ember-controller/inject';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {isEmberArray} from 'ember-array/utils';
|
||||
import {isVersionMismatchError} from 'ghost-admin/services/ajax';
|
||||
import {task} from 'ember-concurrency';
|
||||
|
||||
export default Controller.extend(ValidationEngine, {
|
||||
submitting: false,
|
||||
|
@ -87,6 +87,7 @@ export default Controller.extend(ValidationEngine, {
|
|||
}
|
||||
}).drop(),
|
||||
|
||||
// TODO: remove duplication with controllers/setup/two
|
||||
authenticateWithGhostOrg: task(function* () {
|
||||
let authStrategy = 'authenticator:oauth2-ghost';
|
||||
|
||||
|
@ -100,6 +101,7 @@ export default Controller.extend(ValidationEngine, {
|
|||
|
||||
} catch (error) {
|
||||
this.set('flowErrors', 'Authentication with Ghost.org denied or failed');
|
||||
throw error;
|
||||
}
|
||||
}).drop(),
|
||||
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
import Route from 'ember-route';
|
||||
import injectService from 'ember-service/inject';
|
||||
import {
|
||||
isVersionMismatchError
|
||||
} from 'ghost-admin/services/ajax';
|
||||
|
||||
export default Route.extend({
|
||||
|
||||
session: injectService(),
|
||||
notifications: injectService(),
|
||||
|
||||
actions: {
|
||||
// TODO: reduce duplication with setup/signin/signup routes
|
||||
authenticateWithGhostOrg() {
|
||||
let authStrategy = 'authenticator:oauth2-ghost';
|
||||
|
||||
this.toggleProperty('controller.loggingIn');
|
||||
this.set('controller.flowErrors', '');
|
||||
|
||||
return this.get('torii')
|
||||
.open('ghost-oauth2', {type: 'setup'})
|
||||
.then((authentication) => {
|
||||
return this.send('authenticate', authStrategy, [authentication]);
|
||||
})
|
||||
.catch(() => {
|
||||
this.toggleProperty('controller.loggingIn');
|
||||
this.set('controller.flowErrors', 'Authentication with Ghost.org denied or failed');
|
||||
});
|
||||
},
|
||||
|
||||
authenticate(strategy, authentication) {
|
||||
// we don't want to redirect after sign-in during setup
|
||||
this.set('session.skipAuthSuccessHandler', true);
|
||||
|
||||
// Authentication transitions to posts.index, we can leave spinner running unless there is an error
|
||||
return this.get('session')
|
||||
.authenticate(strategy, ...authentication)
|
||||
.then(() => {
|
||||
this.get('controller.errors').remove('session');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error && error.errors) {
|
||||
if (isVersionMismatchError(error)) {
|
||||
return this.get('notifications').showAPIError(error);
|
||||
}
|
||||
|
||||
error.errors.forEach((err) => {
|
||||
err.message = err.message.htmlSafe();
|
||||
});
|
||||
|
||||
this.set('controller.flowErrors', error.errors[0].message.string);
|
||||
} else {
|
||||
// Connection errors don't return proper status message, only req.body
|
||||
this.get('notifications').showAlert('There was a problem on the server.', {type: 'error', key: 'session.authenticate.failed'});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.toggleProperty('controller.loggingIn');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -7,6 +7,7 @@
|
|||
/* ---------------------------------------------------------- */
|
||||
@import "patterns/global.css";
|
||||
@import "patterns/_shame.css";
|
||||
@import "patterns/icons.css";
|
||||
@import "patterns/forms.css";
|
||||
@import "patterns/buttons.css";
|
||||
@import "patterns/labels.css";
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
/* ---------------------------------------------------------- */
|
||||
@import "patterns/global.css";
|
||||
@import "patterns/_shame.css";
|
||||
@import "patterns/icons.css";
|
||||
@import "patterns/forms.css";
|
||||
@import "patterns/buttons.css";
|
||||
@import "patterns/labels.css";
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
.settings-menu-header .close svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
fill: --var(darkgrey);
|
||||
fill: var(--darkgrey);
|
||||
}
|
||||
|
||||
.settings-menu-header.subview h4 {
|
||||
|
|
|
@ -114,6 +114,8 @@
|
|||
width: 26px;
|
||||
height: 26px;
|
||||
fill: #fff;
|
||||
stroke: #fff;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.gh-flow-nav .active ~ li:not(divider) .step {
|
||||
|
@ -405,7 +407,7 @@
|
|||
}
|
||||
|
||||
.gh-flow-invite textarea {
|
||||
background: url(img/invite-placeholder.png) 8px 10px no-repeat;
|
||||
background: url(img/invite-placeholder.png) 10px 8px no-repeat;
|
||||
background-size: 202px 48px;
|
||||
box-shadow: none; /* Remove some default styling for Firefox (required attribute) */
|
||||
}
|
||||
|
|
|
@ -302,7 +302,8 @@ fieldset[disabled] .gh-btn {
|
|||
fill: #fff;
|
||||
}
|
||||
|
||||
.gh-btn-icon-right svg {
|
||||
.gh-btn-icon-right svg,
|
||||
svg.gh-btn-icon-right {
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
@ -311,43 +312,10 @@ fieldset[disabled] .gh-btn {
|
|||
stroke: #fff;
|
||||
}
|
||||
|
||||
.gh-btn svg.no-margin {
|
||||
.gh-btn-icon-no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
/* Animated button icons
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
/* Success icon */
|
||||
|
||||
path.animated-check-circle {
|
||||
stroke: white;
|
||||
stroke-dashoffset: 300;
|
||||
stroke-dasharray: 300;
|
||||
animation: dash 4s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 300;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Failure icon */
|
||||
|
||||
svg.retry-animated {
|
||||
animation: rotate-360 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes rotate-360 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
/* Loading Button Spinner
|
||||
|
|
43
app/styles/patterns/icons.css
Normal file
43
app/styles/patterns/icons.css
Normal file
|
@ -0,0 +1,43 @@
|
|||
/* Overrides for SVG icons that need different fill/stroke styles
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
.gh-icon-user {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.gh-icon-spinner {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
/* Animated icons
|
||||
/* ---------------------------------------------------------- */
|
||||
|
||||
/* Success icon */
|
||||
|
||||
path.animated-check-circle {
|
||||
stroke: white;
|
||||
stroke-dashoffset: 300;
|
||||
stroke-dasharray: 300;
|
||||
animation: dash 4s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 300;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Failure icon */
|
||||
|
||||
svg.retry-animated {
|
||||
animation: rotate-360 0.5s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes rotate-360 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
)}}
|
||||
{{else}}
|
||||
<span>
|
||||
{{#if isRunning}}{{inline-svg "spinner"}}{{/if}}
|
||||
{{#if isRunning}}{{inline-svg "spinner" class="gh-icon-spinner"}}{{/if}}
|
||||
{{if (or isIdle isRunning) buttonText}}
|
||||
{{#if isSuccess}}{{inline-svg "check-circle"}} {{successText}}{{/if}}
|
||||
{{#if isFailure}}{{inline-svg "retry"}} {{failureText}}{{/if}}
|
||||
|
|
|
@ -5,13 +5,28 @@
|
|||
|
||||
<div><img class="gh-flow-faces" src="assets/img/users.png" alt="" /></div>
|
||||
|
||||
<form class="gh-flow-invite">
|
||||
<form class="gh-flow-invite" {{action "invite" on="submit"}}>
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="users"}}
|
||||
<label for="users">Enter one email address per line, we’ll handle the rest! {{inline-svg "email"}}</label>
|
||||
{{gh-textarea users name="users" required="required" focusOut=(action "validate") update=(action (mut users))}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
{{#gh-spin-button type="submit" action="invite" classNameBindings=":gh-btn :gh-btn-default :gh-btn-lg :gh-btn-block buttonClass" submitting=submitting autoWidth="false"}}<span>{{buttonText}}</span>{{/gh-spin-button}}
|
||||
{{#gh-task-button
|
||||
task=invite
|
||||
type="submit"
|
||||
classNameBindings=":gh-btn :gh-btn-default :gh-btn-lg :gh-btn-block buttonClass"
|
||||
successClass=""
|
||||
failureClass=""
|
||||
as |task|
|
||||
}}
|
||||
<span>
|
||||
{{#if task.isRunning}}
|
||||
{{inline-svg "spinner" class="no-margin"}}
|
||||
{{else}}
|
||||
{{buttonText}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/gh-task-button}}
|
||||
</form>
|
||||
|
||||
<button class="gh-flow-skip" {{action "skipInvite"}}>
|
||||
|
|
|
@ -5,13 +5,23 @@
|
|||
|
||||
<form id="setup" class="gh-flow-create" {{action "setup" on="submit"}}>
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="session"}}
|
||||
{{#gh-spin-button class="login gh-btn gh-btn-blue gh-btn-block" type="button" action="authenticateWithGhostOrg" tabindex="3" submitting=loggingIn autoWidth="false"}}
|
||||
{{#if session.isAuthenticated}}
|
||||
<span>Connected: {{session.user.email}}</span>
|
||||
{{#gh-task-button
|
||||
task=authenticateWithGhostOrg
|
||||
class="login gh-btn gh-btn-blue gh-btn-block gh-btn-icon"
|
||||
type="button"
|
||||
successClass=""
|
||||
as |task|
|
||||
}}
|
||||
{{#if task.isRunning}}
|
||||
<span>{{inline-svg "spinner" class="gh-icon-spinner gh-btn-icon-no-margin"}}</span>
|
||||
{{else}}
|
||||
<span>Sign in with Ghost</span>
|
||||
{{#if session.isAuthenticated}}
|
||||
<span>Connected: {{session.user.email}}</span>
|
||||
{{else}}
|
||||
<span>Sign in with Ghost</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/gh-spin-button}}
|
||||
{{/gh-task-button}}
|
||||
{{gh-error-message errors=errors property="session"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
|
@ -19,15 +29,35 @@
|
|||
<label for="blog-title">Blog title</label>
|
||||
<span class="gh-input-icon gh-icon-content">
|
||||
{{inline-svg "content"}}
|
||||
{{gh-input blogTitle tabindex="4" type="text" name="blog-title" placeholder="Eg. The Daily Awesome" autocorrect="off" focusOut=(action "preValidate" "blogTitle") update=(action (mut blogTitle)) onenter=(action "setup")}}
|
||||
{{gh-input blogTitle
|
||||
tabindex="4"
|
||||
type="text"
|
||||
name="blog-title"
|
||||
placeholder="Eg. The Daily Awesome"
|
||||
autocorrect="off"
|
||||
focusOut=(action "preValidate" "blogTitle")
|
||||
update=(action (mut blogTitle))
|
||||
onenter=(action "setup")}}
|
||||
</span>
|
||||
{{gh-error-message errors=errors property="blogTitle"}}
|
||||
{{/gh-form-group}}
|
||||
</form>
|
||||
|
||||
{{#gh-spin-button type="submit" tabindex="5" class="gh-btn gh-btn-green gh-btn-lg gh-btn-block gh-btn-icon gh-btn-icon-right" action=(action 'setup') disabled=submitDisabled submitting=submitting autoWidth="false"}}
|
||||
<span>Last step: Invite your team {{inline-svg "arrow-right-small"}}</span>
|
||||
{{/gh-spin-button}}
|
||||
{{#gh-task-button
|
||||
task=setup
|
||||
type="submit"
|
||||
tabindex="5"
|
||||
class="gh-btn gh-btn-green gh-btn-lg gh-btn-block gh-btn-icon"
|
||||
disabled=submitDisabled
|
||||
data-test-submit-button=true
|
||||
as |task|
|
||||
}}
|
||||
{{#if task.isRunning}}
|
||||
<span>{{inline-svg "spinner" class="gh-icon-spinner gh-btn-icon-no-margin"}}</span>
|
||||
{{else}}
|
||||
<span>Last step: Invite your team {{inline-svg "arrow-right-small" class="gh-btn-icon-right"}}</span>
|
||||
{{/if}}
|
||||
{{/gh-task-button}}
|
||||
{{else}}
|
||||
|
||||
<header>
|
||||
|
@ -39,42 +69,85 @@
|
|||
<input style="display:none;" type="text" name="fakeusernameremembered"/>
|
||||
<input style="display:none;" type="password" name="fakepasswordremembered"/>
|
||||
|
||||
{{gh-profile-image email=email setImage="setImage"}}
|
||||
{{gh-profile-image email=email setImage=(action "setImage")}}
|
||||
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="email"}}
|
||||
<label for="email-address">Email address</label>
|
||||
<span class="gh-input-icon gh-icon-mail">
|
||||
{{inline-svg "email"}}
|
||||
{{gh-trim-focus-input email tabindex="1" type="email" name="email" placeholder="Eg. john@example.com" autocorrect="off" focusOut=(action "preValidate" "email") update=(action (mut email))}}
|
||||
{{gh-trim-focus-input email
|
||||
tabindex="1"
|
||||
ype="email"
|
||||
name="email"
|
||||
placeholder="Eg. john@example.com"
|
||||
autocorrect="off"
|
||||
focusOut=(action "preValidate" "email")
|
||||
update=(action (mut email))}}
|
||||
</span>
|
||||
{{gh-error-message errors=errors property="email"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="name"}}
|
||||
<label for="full-name">Full name</label>
|
||||
<span class="gh-input-icon gh-icon-user">
|
||||
{{inline-svg "user-circle"}}
|
||||
{{gh-input name tabindex="2" type="text" name="name" placeholder="Eg. John H. Watson" autocorrect="off" focusOut=(action "preValidate" "name") update=(action (mut name))}}
|
||||
{{gh-input name
|
||||
tabindex="2"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Eg. John H. Watson"
|
||||
autocorrect="off"
|
||||
focusOut=(action "preValidate" "name")
|
||||
update=(action (mut name))}}
|
||||
</span>
|
||||
{{gh-error-message errors=errors property="name"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="password"}}
|
||||
<label for="password">Password</label>
|
||||
<span class="gh-input-icon gh-icon-lock">
|
||||
{{inline-svg "lock"}}
|
||||
{{gh-input password tabindex="3" type="password" name="password" placeholder="At least 8 characters" autocorrect="off" focusOut=(action "preValidate" "password") update=(action (mut password))}}
|
||||
{{gh-input password
|
||||
tabindex="3"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="At least 8 characters"
|
||||
autocorrect="off"
|
||||
focusOut=(action "preValidate" "password")
|
||||
update=(action (mut password))}}
|
||||
</span>
|
||||
{{gh-error-message errors=errors property="password"}}
|
||||
{{/gh-form-group}}
|
||||
|
||||
{{#gh-form-group errors=errors hasValidated=hasValidated property="blogTitle"}}
|
||||
<label for="blog-title">Blog title</label>
|
||||
<span class="gh-input-icon gh-icon-content">
|
||||
{{inline-svg "content"}}
|
||||
{{gh-input blogTitle tabindex="4" type="text" name="blog-title" placeholder="Eg. The Daily Awesome" autocorrect="off" focusOut=(action "preValidate" "blogTitle") update=(action (mut blogTitle))}}
|
||||
{{gh-input blogTitle
|
||||
tabindex="4"
|
||||
type="text"
|
||||
name="blog-title"
|
||||
placeholder="Eg. The Daily Awesome"
|
||||
autocorrect="off"
|
||||
focusOut=(action "preValidate" "blogTitle")
|
||||
update=(action (mut blogTitle))}}
|
||||
</span>
|
||||
{{gh-error-message errors=errors property="blogTitle"}}
|
||||
{{/gh-form-group}}
|
||||
{{#gh-spin-button type="submit" tabindex="5" class="gh-btn gh-btn-green gh-btn-lg gh-btn-block gh-btn-icon gh-btn-icon-right" action="setup" submitting=submitting autoWidth="false"}}
|
||||
<span>Last step: Invite your team {{inline-svg "arrow-right-small"}}</span>
|
||||
{{/gh-spin-button}}
|
||||
|
||||
{{#gh-task-button
|
||||
task=setup
|
||||
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>{{inline-svg "spinner" class="gh-icon-spinner gh-btn-icon-no-margin"}}</span>
|
||||
{{else}}
|
||||
<span>Last step: Invite your team {{inline-svg "arrow-right-small" class="gh-btn-icon-right"}}</span>
|
||||
{{/if}}
|
||||
{{/gh-task-button}}
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -9,24 +9,55 @@
|
|||
|
||||
<form id="login" method="post" class="gh-signin" novalidate="novalidate" {{action "authenticate" on="submit"}}>
|
||||
{{#if config.ghostOAuth}}
|
||||
{{gh-task-button "Sign in with Ghost" task=authenticateWithGhostOrg class="login gh-btn gh-btn-blue gh-btn-block gh-btn-icon" tabindex="3"}}
|
||||
{{gh-task-button "Sign in with Ghost"
|
||||
task=authenticateWithGhostOrg
|
||||
class="login gh-btn gh-btn-blue gh-btn-block gh-btn-icon"
|
||||
tabindex="3"}}
|
||||
{{else}}
|
||||
{{#gh-form-group errors=model.errors hasValidated=hasValidated property="identification"}}
|
||||
<span class="gh-input-icon gh-icon-mail">
|
||||
{{inline-svg "email"}}
|
||||
{{gh-trim-focus-input model.identification class="email" type="email" placeholder="Email Address" name="identification" autocapitalize="off" autocorrect="off" tabindex="1" focusOut=(action "validate" "identification") update=(action (mut model.identification))}}
|
||||
{{gh-trim-focus-input model.identification
|
||||
class="email"
|
||||
type="email"
|
||||
placeholder="Email Address"
|
||||
name="identification"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
tabindex="1"
|
||||
focusOut=(action "validate" "identification")
|
||||
update=(action (mut model.identification))}}
|
||||
</span>
|
||||
{{/gh-form-group}}
|
||||
{{#gh-form-group errors=model.errors hasValidated=hasValidated property="password"}}
|
||||
<span class="gh-input-icon gh-icon-lock forgotten-wrap">
|
||||
{{inline-svg "lock"}}
|
||||
{{gh-input model.password class="password" type="password" placeholder="Password" name="password" tabindex="2" autocorrect="off" update=(action (mut model.password))}}
|
||||
{{#gh-task-button task=forgotten class="forgotten-link gh-btn gh-btn-link gh-btn-icon" successClass="" failureClass="" tabindex="4" type="button" as |task|}}
|
||||
<span>{{#if task.isRunning}}<span class="spinner"></span>{{else}}Forgot?{{/if}}</span>
|
||||
{{gh-input model.password
|
||||
class="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
tabindex="2"
|
||||
autocorrect="off"
|
||||
update=(action (mut model.password))}}
|
||||
|
||||
{{#gh-task-button
|
||||
task=forgotten
|
||||
class="forgotten-link gh-btn gh-btn-link gh-btn-icon"
|
||||
tabindex="4"
|
||||
type="button"
|
||||
as |task|
|
||||
}}
|
||||
<span>{{#if task.isRunning}}{{inline-svg "spinner"}}{{else}}Forgot?{{/if}}</span>
|
||||
{{/gh-task-button}}
|
||||
</span>
|
||||
{{/gh-form-group}}
|
||||
{{gh-task-button "Sign in" task=validateAndAuthenticate class="login gh-btn gh-btn-blue gh-btn-block gh-btn-icon" type="submit" tabindex="3"}}
|
||||
|
||||
{{gh-task-button "Sign in"
|
||||
task=validateAndAuthenticate
|
||||
class="login gh-btn gh-btn-blue gh-btn-block gh-btn-icon"
|
||||
type="submit"
|
||||
tabindex="3"}}
|
||||
{{/if}}
|
||||
</form>
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
tabindex="3"}}
|
||||
{{/if}}
|
||||
|
||||
<p class="main-error">{{{flowErrors}}}</p>
|
||||
<p class="main-error">{{{if flowErrors flowErrors " "}}}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
stubFailedOAuthConnect
|
||||
} from '../helpers/oauth';
|
||||
import moment from 'moment';
|
||||
import testSelector from 'ember-test-selectors';
|
||||
|
||||
describe('Acceptance: Setup', function () {
|
||||
let application;
|
||||
|
@ -497,7 +498,7 @@ describe('Acceptance: Setup', function () {
|
|||
});
|
||||
|
||||
fillIn('input[name="blog-title"]', 'Ghostbusters');
|
||||
click('.gh-btn-green');
|
||||
click(testSelector('submit-button'));
|
||||
|
||||
andThen(() => {
|
||||
expect(
|
||||
|
|
Loading…
Reference in a new issue