0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-13 22:41:32 -05:00

Merge pull request #5359 from josephwegner/onboarding-invites

invite users after signing up during setup
This commit is contained in:
Jason Williams 2015-06-09 21:56:54 -05:00
commit 9e5d9e975f
6 changed files with 174 additions and 35 deletions

View file

@ -1,41 +1,134 @@
import Ember from 'ember'; import Ember from 'ember';
import ValidationEngine from 'ghost/mixins/validation-engine';
var SetupThreeController = Ember.Controller.extend(ValidationEngine, { var SetupThreeController = Ember.Controller.extend({
notifications: Ember.inject.service(),
users: '', users: '',
usersArray: Ember.computed('users', function () { usersArray: Ember.computed('users', function () {
return this.get('users').split('\n').filter(function (user) { var users = this.get('users').split('\n').filter(function (email) {
return email.trim().length > 0;
});
return users.uniq();
}),
validUsersArray: Ember.computed('usersArray', function () {
return this.get('usersArray').filter(function (user) {
return validator.isEmail(user); return validator.isEmail(user);
}); });
}), }),
numUsers: Ember.computed('usersArray', function () { validateUsers: Ember.computed('usersArray', function () {
return this.get('usersArray').length; var errors = [];
this.get('usersArray').forEach(function (user) {
if (!validator.isEmail(user)) {
errors.push({
user: user,
error: 'email'
});
}
});
return errors.length === 0 ? true : errors;
}), }),
buttonText: Ember.computed('numUsers', function () { numUsers: Ember.computed('validUsersArray', function () {
var user = this.get('numUsers') === 1 ? 'user' : 'users'; return this.get('validUsersArray').length;
return this.get('numUsers') > 0 ?
'Invite ' + this.get('numUsers') + ' ' + user : 'I\'ll do this later, take me to my blog!';
}), }),
buttonClass: Ember.computed('numUsers', function () { buttonText: Ember.computed('usersArray', function () {
return this.get('numUsers') > 0 ? 'btn-green' : 'btn-minor'; var num = this.get('usersArray').length,
user;
if (num > 0) {
user = num === 1 ? 'user' : 'users';
user = num + ' ' + user;
} else {
user = 'some users';
}
return 'Invite ' + user;
}),
buttonClass: Ember.computed('validateUsers', 'numUsers', function () {
if (this.get('validateUsers') === true && this.get('numUsers') > 0) {
return 'btn-green';
} else {
return 'btn-minor';
}
}),
authorRole: Ember.computed(function () {
return this.store.find('role').then(function (roles) {
return roles.findBy('name', 'Author');
});
}), }),
actions: { actions: {
invite: function () { invite: function () {
console.log('inviting', this.get('usersArray')); var self = this,
validationErrors = this.get('validateUsers'),
users = this.get('usersArray'),
errorMessages,
notifications = this.get('notifications'),
invitationsString;
if (this.get('numUsers') === 0) { if (validationErrors === true && users.length > 0) {
this.sendAction('signin'); this.get('authorRole').then(function (authorRole) {
Ember.RSVP.Promise.all(
users.map(function (user) {
var newUser = self.store.createRecord('user', {
email: user,
status: 'invited',
role: authorRole
});
return newUser.save().then(function () {
return {
email: user,
success: newUser.get('status') === 'invited'
};
}).catch(function () {
return {
email: user,
success: false
};
});
})
).then(function (invites) {
var successCount = 0,
erroredEmails = [],
message;
invites.forEach(function (invite) {
if (invite.success) {
successCount++;
} else {
erroredEmails.push(invite.email);
}
});
if (erroredEmails.length > 0) {
message = 'Failed to send ' + erroredEmails.length + ' invitations: ';
message += erroredEmails.join(', ');
notifications.showError(message, {delayed: successCount > 0});
} }
// TODO: do invites if (successCount > 0) {
}, // pluralize
signin: function () { invitationsString = successCount > 1 ? 'invitations' : 'invitation';
var self = this;
this.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', { notifications.showSuccess(successCount + ' ' + invitationsString + ' sent!', {delayed: true});
identification: self.get('email'), self.transitionTo('posts.index');
password: self.get('password') }
}); });
});
} else if (users.length === 0) {
notifications.showError('No users to invite.');
} else {
errorMessages = validationErrors.map(function (error) {
// Only one error type here so far, but one day the errors might be more detailed
switch (error.error) {
case 'email':
return {message: error.user + ' is not a valid email.'};
}
});
notifications.showErrors(errorMessages);
}
} }
} }
}); });

View file

@ -14,6 +14,7 @@ export default Ember.Controller.extend(ValidationEngine, {
ghostPaths: Ember.inject.service('ghost-paths'), ghostPaths: Ember.inject.service('ghost-paths'),
notifications: Ember.inject.service(), notifications: Ember.inject.service(),
application: Ember.inject.controller(),
gravatarUrl: Ember.computed('email', function () { gravatarUrl: Ember.computed('email', function () {
var email = this.get('email'), var email = this.get('email'),
@ -55,9 +56,15 @@ export default Ember.Controller.extend(ValidationEngine, {
}] }]
} }
}).then(function () { }).then(function () {
// Don't call the success handler, otherwise we will be redirected to admin
self.get('application').set('skipAuthSuccessHandler', true);
self.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', { self.get('session').authenticate('simple-auth-authenticator:oauth2-password-grant', {
identification: self.get('email'), identification: self.get('email'),
password: self.get('password') password: self.get('password')
}).then(function () {
self.set('password', '');
self.transitionToRoute('setup.three');
}); });
}).catch(function (resp) { }).catch(function (resp) {
self.toggleProperty('submitting'); self.toggleProperty('submitting');

View file

@ -208,6 +208,13 @@
max-width: 400px; max-width: 400px;
} }
.gh-flow-content .gh-flow-skip {
display: inline-block;
margin-top: 5px;
color: #7d878a;
font-size: 1.2rem;
}
.gh-flow-content .gh-flow-create { .gh-flow-content .gh-flow-create {
position: relative; position: relative;
margin: 70px auto 30px; margin: 70px auto 30px;

View file

@ -12,7 +12,10 @@
sally.sanders@example.com" value=users}} sally.sanders@example.com" value=users}}
</form> </form>
<button {{action "signin"}} class="btn btn-default btn-lg btn-block {{buttonClass}}"> <button {{action 'invite'}} class="btn btn-default btn-lg btn-block {{buttonClass}}">
{{buttonText}} {{buttonText}}
</button> </button>
{{#link-to "posts" class="gh-flow-skip"}}
I'll do this later, take me to my blog!
{{/link-to}}
</section> </section>

View file

@ -239,7 +239,7 @@ http = function http(apiMethod) {
return apiMethod(object, options).tap(function onSuccess(response) { return apiMethod(object, options).tap(function onSuccess(response) {
// Add X-Cache-Invalidate, Location, and Content-Disposition headers // Add X-Cache-Invalidate, Location, and Content-Disposition headers
return addHeaders(apiMethod, req, res, response); return addHeaders(apiMethod, req, res, (response || {}));
}).then(function then(response) { }).then(function then(response) {
// Send a properly formatting HTTP response containing the data with correct headers // Send a properly formatting HTTP response containing the data with correct headers
res.json(response || {}); res.json(response || {});

View file

@ -1,9 +1,8 @@
// # Setup Test
// Test that setup works correctly // Test that setup works correctly
/*global CasperTest, casper, email, user, password */ /*global CasperTest, casper, email, user, password */
CasperTest.begin('Ghost setup fails properly', 5, function suite(test) { CasperTest.begin('Ghost setup fails properly', 12, function suite(test) {
casper.thenOpenAndWaitForPageLoad('setup', function then() { casper.thenOpenAndWaitForPageLoad('setup', function then() {
test.assertUrlMatch(/ghost\/setup\/one\/$/, 'Landed on the correct URL'); test.assertUrlMatch(/ghost\/setup\/one\/$/, 'Landed on the correct URL');
}); });
@ -27,12 +26,42 @@ CasperTest.begin('Ghost setup fails properly', 5, function suite(test) {
// This can take quite a long time // This can take quite a long time
casper.wait(5000); casper.wait(5000);
casper.waitForResource(/\d+/, function testForDashboard() { casper.waitForScreenLoad('setup.three', function inviteUsers() {
test.assertUrlMatch(/ghost\/\d+\/$/, 'Landed on the correct URL'); casper.thenClick('.gh-flow-content .btn');
test.assertExists('.gh-nav-main-content.active', 'Now we are on Content'); });
}, function onTimeOut() {
test.fail('Failed to signin'); casper.waitForSelector('.notification-error', function onSuccess() {
}, 20000); test.assert(true, 'Got error notification');
test.assertSelectorHasText('.notification-error', 'No users to invite.');
test.assertExists('.gh-flow-content .btn-minor', 'Submit button is not minor');
test.assertSelectorHasText('.gh-flow-content .btn', 'Invite some users', 'Submit button has wrong text');
}, function onTimeout() {
test.assert(false, 'No error notification for empty invitation list');
});
casper.then(function fillInvitationForm() {
casper.fill('form.gh-flow-invite', {users: 'test@example.com'});
test.assertSelectorHasText('.gh-flow-content .btn', 'Invite 1 user', 'One invitation button text is incorrect');
test.assertExists('.gh-flow-content .btn-green', 'Submit button is not green');
casper.fill('form.gh-flow-invite', {users: 'test@example.com\ntest2@example.com'});
test.assertSelectorHasText('.gh-flow-content .btn', 'Invite 2 users', 'Two invitations button text is incorrect');
});
casper.thenClick('.gh-flow-content .btn');
// This might take awhile
casper.wait(5000);
// These invitations will fail, because Casper can't send emails
casper.waitForSelector('.notification-error', function onSuccess() {
test.assert(true, 'Got error notification');
test.assertSelectorHasText('.notification-error', 'Failed to send 2 invitations: test@example.com, test2@example.com');
}, function onTimeout() {
test.assert(false, 'No error notification after invite.');
});
}, true); }, true);
CasperTest.begin('Authenticated user is redirected', 6, function suite(test) { CasperTest.begin('Authenticated user is redirected', 6, function suite(test) {