diff --git a/ghost/admin/app/components/gh-user-invited.js b/ghost/admin/app/components/gh-user-invited.js
index c7918a8dc4..badf662c83 100644
--- a/ghost/admin/app/components/gh-user-invited.js
+++ b/ghost/admin/app/components/gh-user-invited.js
@@ -1,36 +1,48 @@
import Component from 'ember-component';
import computed from 'ember-computed';
import service from 'ember-service/inject';
+import {isNotFoundError} from 'ember-ajax/errors';
export default Component.extend({
tagName: '',
- user: null,
+ invite: null,
isSending: false,
notifications: service(),
+ store: service(),
- createdAtUTC: computed('user.createdAtUTC', function () {
- let createdAtUTC = this.get('user.createdAtUTC');
+ createdAt: computed('invite.createdAtUTC', function () {
+ let createdAtUTC = this.get('invite.createdAtUTC');
return createdAtUTC ? moment(createdAtUTC).fromNow() : '';
}),
+ expiresAt: computed('invite.expires', function () {
+ let expires = this.get('invite.expires');
+
+ return expires ? moment(expires).fromNow() : '';
+ }),
+
actions: {
resend() {
- let user = this.get('user');
+ let invite = this.get('invite');
let notifications = this.get('notifications');
this.set('isSending', true);
- user.resendInvite().then((result) => {
- let notificationText = `Invitation resent! (${user.get('email')})`;
+ invite.resend().then((result) => {
+ let notificationText = `Invitation resent! (${invite.get('email')})`;
+
+ // the server deletes the old record and creates a new one when
+ // resending so we need to update the store accordingly
+ invite.unloadRecord();
+ this.get('store').pushPayload('invite', result);
// If sending the invitation email fails, the API will still return a status of 201
- // but the user's status in the response object will be 'invited-pending'.
- if (result.users[0].status === 'invited-pending') {
+ // but the invite's status in the response object will be 'invited-pending'.
+ if (result.invites[0].status === 'invited-pending') {
notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.resend.not-sent'});
} else {
- user.set('status', result.users[0].status);
notifications.showNotification(notificationText, {key: 'invite.resend.success'});
}
}).catch((error) => {
@@ -41,23 +53,25 @@ export default Component.extend({
},
revoke() {
- let user = this.get('user');
- let email = user.get('email');
+ let invite = this.get('invite');
+ let email = invite.get('email');
let notifications = this.get('notifications');
- // reload the user to get the most up-to-date information
- user.reload().then(() => {
- if (user.get('invited')) {
- user.destroyRecord().then(() => {
- let notificationText = `Invitation revoked. (${email})`;
- notifications.showNotification(notificationText, {key: 'invite.revoke.success'});
- }).catch((error) => {
- notifications.showAPIError(error, {key: 'invite.revoke'});
- });
- } else {
- // if the user is no longer marked as "invited", then show a warning and reload the route
+ // reload the invite to get the most up-to-date information
+ invite.reload().then(() => {
+ invite.destroyRecord().then(() => {
+ let notificationText = `Invitation revoked. (${email})`;
+ notifications.showNotification(notificationText, {key: 'invite.revoke.success'});
+ }).catch((error) => {
+ notifications.showAPIError(error, {key: 'invite.revoke'});
+ });
+ }).catch((error) => {
+ if (isNotFoundError(error)) {
+ // if the invite no longer exists, then show a warning and reload the route
this.sendAction('reload');
- notifications.showAlert('This user has already accepted the invitation.', {type: 'error', delayed: true, key: 'invite.revoke.already-accepted'});
+ notifications.showAlert('This invite has been revoked or a user has already accepted the invitation.', {type: 'error', delayed: true, key: 'invite.revoke.already-accepted'});
+ } else {
+ throw error;
}
});
}
diff --git a/ghost/admin/app/components/modals/invite-new-user.js b/ghost/admin/app/components/modals/invite-new-user.js
index 06984c2c67..9cc85c347a 100644
--- a/ghost/admin/app/components/modals/invite-new-user.js
+++ b/ghost/admin/app/components/modals/invite-new-user.js
@@ -53,15 +53,19 @@ export default ModalComponent.extend(ValidationEngine, {
// the API should return an appropriate error when attempting to save
return new Promise((resolve, reject) => {
return this._super().then(() => {
- this.get('store').findAll('user', {reload: true}).then((result) => {
- let invitedUser = result.findBy('email', email);
+ return RSVP.hash({
+ users: this.get('store').findAll('user', {reload: true}),
+ invites: this.get('store').findAll('invite', {reload: true})
+ }).then((data) => {
+ let existingUser = data.users.findBy('email', email);
+ let existingInvite = data.invites.findBy('email', email);
- if (invitedUser) {
+ if (existingUser || existingInvite) {
this.get('errors').clear('email');
- if (invitedUser.get('status') === 'invited' || invitedUser.get('status') === 'invited-pending') {
- this.get('errors').add('email', 'A user with that email address was already invited.');
- } else {
+ if (existingUser) {
this.get('errors').add('email', 'A user with that email address already exists.');
+ } else {
+ this.get('errors').add('email', 'A user with that email address was already invited.');
}
// TODO: this shouldn't be needed, ValidationEngine doesn't mark
@@ -90,29 +94,28 @@ export default ModalComponent.extend(ValidationEngine, {
let email = this.get('email');
let role = this.get('role');
let notifications = this.get('notifications');
- let newUser;
+ let invite;
this.validate().then(() => {
this.set('submitting', true);
- newUser = this.get('store').createRecord('user', {
+ invite = this.get('store').createRecord('invite', {
email,
- role,
- status: 'invited'
+ role
});
- newUser.save().then(() => {
+ invite.save().then(() => {
let notificationText = `Invitation sent! (${email})`;
// If sending the invitation email fails, the API will still return a status of 201
- // but the user's status in the response object will be 'invited-pending'.
- if (newUser.get('status') === 'invited-pending') {
+ // but the invite's status in the response object will be 'invited-pending'.
+ if (invite.get('status') === 'pending') {
notifications.showAlert('Invitation email was not sent. Please try resending.', {type: 'error', key: 'invite.send.failed'});
} else {
notifications.showNotification(notificationText, {key: 'invite.send.success'});
}
}).catch((error) => {
- newUser.deleteRecord();
+ invite.deleteRecord();
notifications.showAPIError(error, {key: 'invite.send'});
}).finally(() => {
this.send('closeModal');
diff --git a/ghost/admin/app/controllers/setup/three.js b/ghost/admin/app/controllers/setup/three.js
index 9f0d80e53a..cf48cacddf 100644
--- a/ghost/admin/app/controllers/setup/three.js
+++ b/ghost/admin/app/controllers/setup/three.js
@@ -164,16 +164,15 @@ export default Controller.extend({
this.get('authorRole').then((authorRole) => {
RSVP.Promise.all(
users.map((user) => {
- let newUser = this.store.createRecord('user', {
+ let invite = this.store.createRecord('invite', {
email: user,
- status: 'invited',
role: authorRole
});
- return newUser.save().then(() => {
+ return invite.save().then(() => {
return {
email: user,
- success: newUser.get('status') === 'invited'
+ success: invite.get('status') === 'sent'
};
}).catch(() => {
return {
diff --git a/ghost/admin/app/controllers/team/index.js b/ghost/admin/app/controllers/team/index.js
index eb28ca27c3..aea8fc6e67 100644
--- a/ghost/admin/app/controllers/team/index.js
+++ b/ghost/admin/app/controllers/team/index.js
@@ -1,24 +1,18 @@
import Controller from 'ember-controller';
-import {alias, filter} from 'ember-computed';
import injectService from 'ember-service/inject';
+import {sort} from 'ember-computed';
export default Controller.extend({
showInviteUserModal: false,
- users: alias('model'),
+ users: null,
+ invites: null,
session: injectService(),
- activeUsers: filter('users', function (user) {
- return /^active|warn-[1-4]|locked$/.test(user.get('status'));
- }),
-
- invitedUsers: filter('users', function (user) {
- let status = user.get('status');
-
- return status === 'invited' || status === 'invited-pending';
- }),
+ inviteOrder: ['email'],
+ sortedInvites: sort('invites', 'inviteOrder'),
actions: {
toggleInviteUserModal() {
diff --git a/ghost/admin/app/mirage/config.js b/ghost/admin/app/mirage/config.js
index 18c7688d1c..6dc79c5781 100644
--- a/ghost/admin/app/mirage/config.js
+++ b/ghost/admin/app/mirage/config.js
@@ -1,4 +1,5 @@
import mockAuthentication from './config/authentication';
+import mockInvites from './config/invites';
import mockPosts from './config/posts';
import mockRoles from './config/roles';
import mockSettings from './config/settings';
@@ -19,6 +20,7 @@ export default function () {
// this.put('/posts/:id/', versionMismatchResponse);
// mockSubscribers(this);
this.loadFixtures('settings');
+ mockInvites(this);
mockSettings(this);
mockThemes(this);
@@ -38,6 +40,7 @@ export function testConfig() {
// this.logging = true;
mockAuthentication(this);
+ mockInvites(this);
mockPosts(this);
mockRoles(this);
mockSettings(this);
diff --git a/ghost/admin/app/mirage/config/invites.js b/ghost/admin/app/mirage/config/invites.js
new file mode 100644
index 0000000000..7208c6ef9b
--- /dev/null
+++ b/ghost/admin/app/mirage/config/invites.js
@@ -0,0 +1,58 @@
+import Mirage from 'ember-cli-mirage';
+import {paginatedResponse} from '../utils';
+
+export default function mockInvites(server) {
+ server.get('/invites/', function (db, request) {
+ let response = paginatedResponse('invites', db.invites, request);
+ return response;
+ });
+
+ server.get('/invites/:id', function (db, request) {
+ let {id} = request.params;
+ let invite = db.invites.find(id);
+
+ if (!invite) {
+ return new Mirage.Response(404, {}, {
+ errors: [{
+ errorType: 'NotFoundError',
+ message: 'Invite not found.'
+ }]
+ });
+ } else {
+ return {invites: [invite]};
+ }
+ });
+
+ server.post('/invites/', function (db, request) {
+ let [attrs] = JSON.parse(request.requestBody).invites;
+ let [oldInvite] = db.invites.where({email: attrs.email});
+
+ if (oldInvite) {
+ // resend - server deletes old invite and creates a new one with new ID
+ attrs.id = db.invites[db.invites.length - 1].id + 1;
+ db.invites.remove(oldInvite.id);
+ }
+
+ /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+ attrs.token = `${db.invites.length}-token`;
+ attrs.expires = moment.utc().add(1, 'day').unix();
+ attrs.created_at = moment.utc().format();
+ attrs.created_by = 1;
+ attrs.updated_at = moment.utc().format();
+ attrs.updated_by = 1;
+ attrs.status = 'sent';
+ /* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
+
+ let invite = db.invites.insert(attrs);
+
+ return {
+ invites: [invite]
+ };
+ });
+
+ server.del('/invites/:id/', function (db, request) {
+ db.invites.remove(request.params.id);
+
+ return new Mirage.Response(204, {}, {});
+ });
+}
diff --git a/ghost/admin/app/mirage/factories/invite.js b/ghost/admin/app/mirage/factories/invite.js
new file mode 100644
index 0000000000..62b21c804c
--- /dev/null
+++ b/ghost/admin/app/mirage/factories/invite.js
@@ -0,0 +1,14 @@
+/* jscs:disable */
+import Mirage from 'ember-cli-mirage';
+
+export default Mirage.Factory.extend({
+ token(i) { return `${i}-token`; },
+ email(i) { return `invited-user-${i}@example.com`; },
+ expires() { return moment.utc().add(1, 'day').unix(); },
+ created_at() { return moment.utc().format(); },
+ created_by() { return 1; },
+ updated_at() { return moment.utc().format(); },
+ updated_by() { return 1; },
+ status() { return 'sent'; },
+ roles() { return []; }
+});
diff --git a/ghost/admin/app/models/invite.js b/ghost/admin/app/models/invite.js
new file mode 100644
index 0000000000..9fa42052d4
--- /dev/null
+++ b/ghost/admin/app/models/invite.js
@@ -0,0 +1,50 @@
+import Model from 'ember-data/model';
+import attr from 'ember-data/attr';
+import {hasMany} from 'ember-data/relationships';
+import computed from 'ember-computed';
+import injectService from 'ember-service/inject';
+
+export default Model.extend({
+ token: attr('string'),
+ email: attr('string'),
+ expires: attr('number'),
+ createdAtUTC: attr('moment-utc'),
+ createdBy: attr('number'),
+ updatedAtUTC: attr('moment-utc'),
+ updatedBy: attr('number'),
+ status: attr('string'),
+ roles: hasMany('role', {
+ embedded: 'always',
+ async: false
+ }),
+
+ ajax: injectService(),
+ ghostPaths: injectService(),
+
+ role: computed('roles', {
+ get() {
+ return this.get('roles.firstObject');
+ },
+ set(key, value) {
+ // Only one role per user, so remove any old data.
+ this.get('roles').clear();
+ this.get('roles').pushObject(value);
+
+ return value;
+ }
+ }),
+
+ resend() {
+ let fullInviteData = this.toJSON();
+ let inviteData = {
+ email: fullInviteData.email,
+ roles: fullInviteData.roles
+ };
+ let inviteUrl = this.get('ghostPaths.url').api('invites');
+
+ return this.get('ajax').post(inviteUrl, {
+ data: JSON.stringify({invites: [inviteData]}),
+ contentType: 'application/json'
+ });
+ }
+});
diff --git a/ghost/admin/app/models/user.js b/ghost/admin/app/models/user.js
index 2c29cb1ed2..677622163b 100644
--- a/ghost/admin/app/models/user.js
+++ b/ghost/admin/app/models/user.js
@@ -58,12 +58,6 @@ export default Model.extend(ValidationEngine, {
return ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4', 'locked'].indexOf(this.get('status')) > -1;
}),
- invited: computed('status', function () {
- return ['invited', 'invited-pending'].indexOf(this.get('status')) > -1;
- }),
-
- pending: equal('status', 'invited-pending'),
-
role: computed('roles', {
get() {
return this.get('roles.firstObject');
@@ -116,19 +110,5 @@ export default Model.extend(ValidationEngine, {
} catch (error) {
this.get('notifications').showAPIError(error, {key: 'user.change-password'});
}
- }).drop(),
-
- resendInvite() {
- let fullUserData = this.toJSON();
- let userData = {
- email: fullUserData.email,
- roles: fullUserData.roles
- };
- let inviteUrl = this.get('ghostPaths.url').api('users');
-
- return this.get('ajax').post(inviteUrl, {
- data: JSON.stringify({users: [userData]}),
- contentType: 'application/json'
- });
- }
+ }).drop()
});
diff --git a/ghost/admin/app/routes/team/index.js b/ghost/admin/app/routes/team/index.js
index 40c19d7c80..cd9b613256 100644
--- a/ghost/admin/app/routes/team/index.js
+++ b/ghost/admin/app/routes/team/index.js
@@ -2,6 +2,8 @@ import AuthenticatedRoute from 'ghost-admin/routes/authenticated';
import CurrentUserSettings from 'ghost-admin/mixins/current-user-settings';
import PaginationMixin from 'ghost-admin/mixins/pagination';
import styleBody from 'ghost-admin/mixins/style-body';
+import RSVP from 'rsvp';
+import {isBlank} from 'ember-utils';
export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, PaginationMixin, {
titleToken: 'Team',
@@ -10,20 +12,37 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, Paginat
paginationModel: 'user',
paginationSettings: {
- status: 'active',
+ status: 'all',
limit: 20
},
model() {
- this.loadFirstPage();
+ return this.get('session.user').then((user) => {
+ let modelPromises = {
+ users: this.loadFirstPage().then(() => {
+ return this.store.filter('user', (user) => {
+ return !user.get('isNew') && !isBlank(user.get('status'));
+ });
+ })
+ };
- return this.store.query('user', {limit: 'all', status: 'invited'}).then(() => {
- return this.store.filter('user', () => {
- return true;
- });
+ // authors do not have permission to hit the invites endpoint
+ if (!user.get('isAuthor')) {
+ modelPromises.invites = this.store.query('invite', {limit: 'all'}).then(() => {
+ return this.store.filter('invite', (invite) => {
+ return !invite.get('isNew');
+ });
+ });
+ }
+
+ return RSVP.hash(modelPromises);
});
},
+ setupController(controller, models) {
+ controller.setProperties(models);
+ },
+
actions: {
reload() {
this.refresh();
diff --git a/ghost/admin/app/serializers/invite.js b/ghost/admin/app/serializers/invite.js
new file mode 100644
index 0000000000..fc842ee9a9
--- /dev/null
+++ b/ghost/admin/app/serializers/invite.js
@@ -0,0 +1,10 @@
+import ApplicationSerializer from 'ghost-admin/serializers/application';
+import EmbeddedRecordsMixin from 'ember-data/serializers/embedded-records-mixin';
+
+export default ApplicationSerializer.extend(EmbeddedRecordsMixin, {
+ attrs: {
+ roles: {embedded: 'always'},
+ createdAtUTC: {key: 'created_at'},
+ updatedAtUTC: {key: 'updated_at'}
+ }
+});
diff --git a/ghost/admin/app/templates/components/modals/invite-new-user.hbs b/ghost/admin/app/templates/components/modals/invite-new-user.hbs
index 353029213f..d83b078ea3 100644
--- a/ghost/admin/app/templates/components/modals/invite-new-user.hbs
+++ b/ghost/admin/app/templates/components/modals/invite-new-user.hbs
@@ -27,12 +27,14 @@
- {{gh-select-native id="new-user-role"
- content=roles
+ {{one-way-select
+ id="new-user-role"
+ name="role"
+ options=roles
optionValuePath="id"
optionLabelPath="name"
- selection=role
- action="setRole"
+ value=role
+ update=(action "setRole")
}}
diff --git a/ghost/admin/app/templates/team/index.hbs b/ghost/admin/app/templates/team/index.hbs
index dd3725c2c8..6a0839a6e1 100644
--- a/ghost/admin/app/templates/team/index.hbs
+++ b/ghost/admin/app/templates/team/index.hbs
@@ -23,22 +23,23 @@
}}
{{!-- Do not show invited users to authors --}}
{{#unless session.user.isAuthor}}
- {{#if invitedUsers}}
+ {{#if invites}}
Invited users
- {{#each invitedUsers as |user|}}
- {{#gh-user-invited user=user reload="reload" as |component|}}
+ {{#each sortedInvites as |invite|}}
+ {{#gh-user-invited invite=invite reload="reload" as |component|}}
ic
- {{user.email}}
- {{#if user.pending}}
+ {{invite.email}}
+ {{#if invite.pending}}
Invitation not sent - please try again
{{else}}
- Invitation sent: {{component.createdAtUTC}}
+ Invitation sent: {{component.createdAt}},
+ expires {{component.expiresAt}}
{{/if}}
@@ -46,13 +47,13 @@
{{#if component.isSending}}
Sending Invite...
{{else}}
-
+
Revoke
-
+
Resend
- {{#each user.roles as |role|}}
+ {{#each invite.roles as |role|}}
{{role.name}}
{{/each}}
{{/if}}
@@ -66,7 +67,7 @@
Active users
- {{#each activeUsers key="id" as |user|}}
+ {{#each users key="id" as |user|}}
{{!-- For authors only shows users as a list, otherwise show users with links to user page --}}
{{#unless session.user.isAuthor}}
{{#gh-user-active user=user as |component|}}
diff --git a/ghost/admin/tests/acceptance/setup-test.js b/ghost/admin/tests/acceptance/setup-test.js
index 02cb71121f..df0e3e7c54 100644
--- a/ghost/admin/tests/acceptance/setup-test.js
+++ b/ghost/admin/tests/acceptance/setup-test.js
@@ -266,13 +266,13 @@ describe('Acceptance: Setup', function () {
it('handles validation errors in step 3', function () {
let input = '[name="users"]';
let postCount = 0;
- let button, formGroup, user;
+ let button, formGroup, invite;
invalidateSession(application);
server.loadFixtures('roles');
- server.post('/users', function (db, request) {
- let [params] = JSON.parse(request.requestBody).users;
+ server.post('/invites', function (db, request) {
+ let [params] = JSON.parse(request.requestBody).invites;
postCount++;
@@ -288,10 +288,21 @@ describe('Acceptance: Setup', function () {
});
}
+ // TODO: duplicated from mirage/config/invites - extract method?
+ /* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
+ params.token = `${db.invites.length}-token`;
+ params.expires = moment.utc().add(1, 'day').unix();
+ params.created_at = moment.utc().format();
+ params.created_by = 1;
+ params.updated_at = moment.utc().format();
+ params.updated_by = 1;
+ params.status = 'sent';
+ /* jscs:enable requireCamelCaseOrUpperCaseIdentifiers */
+
// valid
- user = db.users.insert(params);
+ invite = db.invites.insert(params);
return {
- users: [user]
+ invites: [invite]
};
});
diff --git a/ghost/admin/tests/acceptance/team-test.js b/ghost/admin/tests/acceptance/team-test.js
index fcd71d26cc..be24512252 100644
--- a/ghost/admin/tests/acceptance/team-test.js
+++ b/ghost/admin/tests/acceptance/team-test.js
@@ -61,12 +61,12 @@ describe('Acceptance: Team', function () {
});
});
- describe('when logged in', function () {
- let admin;
+ describe('when logged in as admin', function () {
+ let admin, adminRole;
beforeEach(function () {
- let role = server.create('role', {name: 'Administrator'});
- admin = server.create('user', {roles: [role]});
+ adminRole = server.create('role', {name: 'Administrator'});
+ admin = server.create('user', {email: 'admin@example.com', roles: [adminRole]});
server.loadFixtures();
@@ -113,127 +113,242 @@ describe('Acceptance: Team', function () {
});
});
- describe('invite new user', function () {
+ it('can manage invites', function () {
let emailInputField = '.fullscreen-modal input[name="email"]';
- // @TODO: Evaluate after the modal PR goes in
- it('modal loads correctly', function () {
- visit('/team');
+ visit('/team');
- andThen(() => {
- // url is correct
- expect(currentURL(), 'currentURL').to.equal('/team');
+ andThen(() => {
+ // invite user button exists
+ expect(
+ find('.view-actions .btn-green').text().trim(),
+ 'invite people button text'
+ ).to.equal('Invite People');
- // invite user button exists
- expect(find('.view-actions .btn-green').html(), 'invite people button text')
- .to.equal('Invite People');
- });
+ // existing users are listed
+ expect(
+ find('.user-list.active-users .user-list-item').length,
+ 'initial number of active users'
+ ).to.equal(1);
- click('.view-actions .btn-green');
+ expect(
+ find('.user-list.active-users .user-list-item:first-of-type .role-label').text().trim(),
+ 'active user\'s role label'
+ ).to.equal('Administrator');
- andThen(() => {
- let roleOptions = find('#new-user-role select option');
-
- function checkOwnerExists() {
- for (let i in roleOptions) {
- if (roleOptions[i].tagName === 'option' && roleOptions[i].text === 'Owner') {
- return true;
- }
- }
- return false;
- }
-
- function checkSelectedIsAuthor() {
- for (let i in roleOptions) {
- if (roleOptions[i].selected) {
- return roleOptions[i].text === 'Author';
- }
- }
- return false;
- }
-
- // should be 3 available roles
- expect(roleOptions.length, 'number of available roles').to.equal(3);
-
- expect(checkOwnerExists(), 'owner role isn\'t available').to.be.false;
- expect(checkSelectedIsAuthor(), 'author role is selected initially').to.be.true;
- });
+ // no invites are shown
+ expect(
+ find('.user-list.invited-users .user-list-item').length,
+ 'initial number of invited users'
+ ).to.equal(0);
});
- it('sends an invite correctly', function () {
- visit('/team');
+ // click the invite people button
+ click('.view-actions .btn-green');
- andThen(() => {
- expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users').to.equal(0);
- });
+ andThen(() => {
+ let roleOptions = find('.fullscreen-modal select[name="role"] option');
- click('.view-actions .btn-green');
- click(emailInputField);
- triggerEvent(emailInputField, 'blur');
+ function checkOwnerExists() {
+ for (let i in roleOptions) {
+ if (roleOptions[i].tagName === 'option' && roleOptions[i].text === 'Owner') {
+ return true;
+ }
+ }
+ return false;
+ }
- andThen(() => {
- expect(find('.modal-body .form-group:first').hasClass('error'), 'email input has error status').to.be.true;
- expect(find('.modal-body .form-group:first .response').text()).to.contain('Please enter an email.');
- });
+ function checkSelectedIsAuthor() {
+ for (let i in roleOptions) {
+ if (roleOptions[i].selected) {
+ return roleOptions[i].text === 'Author';
+ }
+ }
+ return false;
+ }
- fillIn(emailInputField, 'test@example.com');
- click('.fullscreen-modal .btn-green');
+ // modal is displayed
+ expect(
+ find('.fullscreen-modal h1').text().trim(),
+ 'correct modal is displayed'
+ ).to.equal('Invite a New User');
- andThen(() => {
- expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users').to.equal(1);
- expect(find('.user-list.invited-users .user-list-item:first .name').text(), 'name of invited user').to.equal('test@example.com');
- });
+ // number of roles is correct
+ expect(
+ find('.fullscreen-modal select[name="role"] option').length,
+ 'number of selectable roles'
+ ).to.equal(3);
- click('.user-list.invited-users .user-list-item:first .user-list-item-aside .user-list-action:contains("Revoke")');
-
- andThen(() => {
- expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users').to.equal(0);
- });
+ expect(checkOwnerExists(), 'owner role isn\'t available').to.be.false;
+ expect(checkSelectedIsAuthor(), 'author role is selected initially').to.be.true;
});
- it('fails sending an invite correctly', function () {
- server.create('user', {email: 'test1@example.com'});
- server.create('user', {email: 'test2@example.com', status: 'invited'});
+ // submit valid invite form
+ fillIn('.fullscreen-modal input[name="email"]', 'invite1@example.com');
+ click('.fullscreen-modal .btn-green');
- visit('/team');
+ andThen(() => {
+ // modal closes
+ expect(
+ find('.fullscreen-modal').length,
+ 'number of modals after sending invite'
+ ).to.equal(0);
- // check our users lists are what we expect
- andThen(() => {
- expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users')
- .to.equal(1);
- // number of active users is 2 because of the logged-in user
- expect(find('.user-list.active-users .user-list-item').length, 'number of active users')
- .to.equal(2);
- });
+ // invite is displayed, has correct e-mail + role
+ expect(
+ find('.invited-users .user-list-item').length,
+ 'number of invites after first invite'
+ ).to.equal(1);
- // click the "invite new user" button to open the modal
- click('.view-actions .btn-green');
+ expect(
+ find('.invited-users span.name').first().text().trim(),
+ 'displayed email of first invite'
+ ).to.equal('invite1@example.com');
- // fill in and submit the invite user modal with an existing user
- fillIn(emailInputField, 'test1@example.com');
- click('.fullscreen-modal .btn-green');
+ expect(
+ find('.invited-users span.role-label').first().text().trim(),
+ 'displayed role of first invite'
+ ).to.equal('Author');
- andThen(() => {
- // check the inline-validation
- expect(find('.fullscreen-modal .error .response').text().trim(), 'inviting existing user error')
- .to.equal('A user with that email address already exists.');
- });
+ // number of users is unchanged
+ expect(
+ find('.active-users .user-list-item').length,
+ 'number of active users after first invite'
+ ).to.equal(1);
+ });
- // fill in and submit the invite user modal with an invited user
- fillIn(emailInputField, 'test2@example.com');
- click('.fullscreen-modal .btn-green');
+ // submit new invite with different role
+ click('.view-actions .btn-green');
+ fillIn('.fullscreen-modal input[name="email"]', 'invite2@example.com');
+ fillIn('.fullscreen-modal select[name="role"]', '2');
+ click('.fullscreen-modal .btn-green');
- andThen(() => {
- // check the inline-validation
- expect(find('.fullscreen-modal .error .response').text().trim(), 'inviting invited user error')
- .to.equal('A user with that email address was already invited.');
+ andThen(() => {
+ // number of invites increases
+ expect(
+ find('.invited-users .user-list-item').length,
+ 'number of invites after second invite'
+ ).to.equal(2);
- // ensure that there's been no change in our user lists
- expect(find('.user-list.invited-users .user-list-item').length, 'number of invited users after failed invites')
- .to.equal(1);
- expect(find('.user-list.active-users .user-list-item').length, 'number of active users after failed invites')
- .to.equal(2);
- });
+ // invite has correct e-mail + role
+ expect(
+ find('.invited-users span.name').last().text().trim(),
+ 'displayed email of second invite'
+ ).to.equal('invite2@example.com');
+
+ expect(
+ find('.invited-users span.role-label').last().text().trim(),
+ 'displayed role of second invite'
+ ).to.equal('Editor');
+ });
+
+ // submit invite form with existing user
+ click('.view-actions .btn-green');
+ fillIn('.fullscreen-modal input[name="email"]', 'admin@example.com');
+ click('.fullscreen-modal .btn-green');
+
+ andThen(() => {
+ // validation message is displayed
+ expect(
+ find('.fullscreen-modal .error .response').text().trim(),
+ 'inviting existing user error'
+ ).to.equal('A user with that email address already exists.');
+ });
+
+ // submit invite form with existing invite
+ fillIn('.fullscreen-modal input[name="email"]', 'invite1@example.com');
+ click('.fullscreen-modal .btn-green');
+
+ andThen(() => {
+ // validation message is displayed
+ expect(
+ find('.fullscreen-modal .error .response').text().trim(),
+ 'inviting invited user error'
+ ).to.equal('A user with that email address was already invited.');
+ });
+
+ // submit invite form with an invalid email
+ fillIn('.fullscreen-modal input[name="email"]', 'test');
+ click('.fullscreen-modal .btn-green');
+
+ andThen(() => {
+ // validation message is displayed
+ expect(
+ find('.fullscreen-modal .error .response').text().trim(),
+ 'inviting invalid email error'
+ ).to.equal('Invalid Email.');
+ });
+
+ click('.fullscreen-modal a.close');
+ // revoke latest invite
+ click('.invited-users .user-list-item:last-of-type a[href="#revoke"]');
+
+ andThen(() => {
+ // number of invites decreases
+ expect(
+ find('.invited-users .user-list-item').length,
+ 'number of invites after revoke'
+ ).to.equal(1);
+
+ // notification is displayed
+ expect(
+ find('.gh-notification').text().trim(),
+ 'notifications contain revoke'
+ ).to.match(/Invitation revoked\. \(invite2@example\.com\)/);
+
+ // correct invite is removed
+ expect(
+ find('.invited-users span.name').text().trim(),
+ 'displayed email of remaining invite'
+ ).to.equal('invite1@example.com');
+ });
+
+ // add another invite to test ordering on resend
+ click('.view-actions .btn-green');
+ fillIn('.fullscreen-modal input[name="email"]', 'invite3@example.com');
+ click('.fullscreen-modal .btn-green');
+
+ andThen(() => {
+ // new invite should be last in the list
+ expect(
+ find('.invited-users span.name').last().text().trim(),
+ 'last invite email in list'
+ ).to.equal('invite3@example.com');
+ });
+
+ // resend first invite
+ click('.invited-users .user-list-item:first-of-type a[href="#resend"]');
+
+ andThen(() => {
+ // notification is displayed
+ expect(
+ find('.gh-notification').text().trim(),
+ 'notifications contain resend'
+ ).to.match(/Invitation resent! \(invite1@example\.com\)/);
+
+ // first invite is still at the top
+ expect(
+ find('.invited-users span.name').first().text().trim(),
+ 'first invite email in list'
+ ).to.equal('invite1@example.com');
+ });
+
+ // regression test: can revoke a resent invite
+ click('.invited-users .user-list-item:first-of-type a[href="#resend"]');
+ click('.invited-users .user-list-item:first-of-type a[href="#revoke"]');
+
+ andThen(() => {
+ // number of invites decreases
+ expect(
+ find('.invited-users .user-list-item').length,
+ 'number of invites after resend/revoke'
+ ).to.equal(1);
+
+ // notification is displayed
+ expect(
+ find('.gh-notification').text().trim(),
+ 'notifications contain revoke after resend/revoke'
+ ).to.match(/Invitation revoked\. \(invite1@example\.com\)/);
});
});
@@ -665,4 +780,42 @@ describe('Acceptance: Team', function () {
});
});
});
+
+ describe('when logged in as author', function () {
+ let author, authorRole, adminRole;
+
+ beforeEach(function () {
+ adminRole = server.create('role', {name: 'Administrator'});
+ authorRole = server.create('role', {name: 'Author'});
+ author = server.create('user', {roles: [authorRole]});
+
+ server.loadFixtures();
+
+ server.get('/invites/', function () {
+ return new Mirage.Response(403, {}, {
+ errors: [{
+ errorType: 'NoPermissionError',
+ message: 'You do not have permission to perform this action'
+ }]
+ });
+ });
+
+ return authenticateSession(application);
+ });
+
+ it('can access the team page', function () {
+ let user1 = server.create('user', {roles: [adminRole]});
+ let invite1 = server.create('invite', {roles: [authorRole]});
+
+ errorOverride();
+
+ visit('/team');
+
+ andThen(() => {
+ errorReset();
+ expect(currentPath()).to.equal('team.index');
+ expect(find('.gh-alert').length).to.equal(0);
+ });
+ });
+ });
});
diff --git a/ghost/admin/tests/unit/models/invite-test.js b/ghost/admin/tests/unit/models/invite-test.js
new file mode 100644
index 0000000000..a631ad79b3
--- /dev/null
+++ b/ghost/admin/tests/unit/models/invite-test.js
@@ -0,0 +1,87 @@
+import { expect } from 'chai';
+import { describeModel, it } from 'ember-mocha';
+import run from 'ember-runloop';
+import Pretender from 'pretender';
+
+describeModel(
+ 'invite',
+ 'Unit: Model: invite',
+ {
+ needs: [
+ 'model:role',
+ 'serializer:application',
+ 'serializer:invite',
+ 'transform:moment-utc',
+ 'service:ghost-paths',
+ 'service:ajax',
+ 'service:session',
+ 'service:feature'
+ ]
+ },
+ function() {
+ it('role property returns first role in array', function () {
+ let model = this.subject();
+
+ run(() => {
+ let role = this.store().push({data: {id: 1, type: 'role', attributes: {name: 'Author'}}});
+ model.get('roles').pushObject(role);
+ });
+ expect(model.get('role.name')).to.equal('Author');
+
+ run(() => {
+ let role = this.store().push({data: {id: 1, type: 'role', attributes: {name: 'Editor'}}});
+ model.set('role', role);
+ });
+ expect(model.get('role.name')).to.equal('Editor');
+ });
+
+ describe('with network', function () {
+ let server;
+
+ beforeEach(function () {
+ server = new Pretender();
+ });
+
+ afterEach(function () {
+ server.shutdown();
+ });
+
+ it('resend hits correct endpoint', function () {
+ let model = this.subject();
+ let role;
+
+ server.post('/ghost/api/v0.1/invites/', function () {
+ return [200, {}, '{}'];
+ });
+
+ run(() => {
+ role = this.store().push({data: {id: 1, type: 'role', attributes: {name: 'Editor'}}});
+ model.set('email', 'resend-test@example.com');
+ model.set('role', role);
+ model.resend();
+ });
+
+ expect(
+ server.handledRequests.length,
+ 'number of requests'
+ ).to.equal(1);
+
+ let [lastRequest] = server.handledRequests;
+ let requestBody = JSON.parse(lastRequest.requestBody);
+ let [invite] = requestBody.invites;
+
+ expect(
+ requestBody.invites.length,
+ 'number of invites in request body'
+ ).to.equal(1);
+
+ expect(invite.email).to.equal('resend-test@example.com');
+ expect(
+ invite.roles.length,
+ 'number of roles in request body'
+ ).to.equal(1);
+ expect(invite.roles[0], 'role ID').to.equal('1');
+ });
+ });
+ }
+);
diff --git a/ghost/admin/tests/unit/models/user-test.js b/ghost/admin/tests/unit/models/user-test.js
index 5110403979..5c1a509493 100644
--- a/ghost/admin/tests/unit/models/user-test.js
+++ b/ghost/admin/tests/unit/models/user-test.js
@@ -37,37 +37,6 @@ describeModel(
expect(model.get('active')).to.not.be.ok;
});
- it('invited property is correct', function () {
- let model = this.subject({
- status: 'invited'
- });
-
- expect(model.get('invited')).to.be.ok;
-
- run(() => { model.set('status', 'invited-pending'); });
- expect(model.get('invited')).to.be.ok;
-
- run(() => { model.set('status', 'active'); });
- expect(model.get('invited')).to.not.be.ok;
-
- run(() => { model.set('status', 'inactive'); });
- expect(model.get('invited')).to.not.be.ok;
- });
-
- it('pending property is correct', function () {
- let model = this.subject({
- status: 'invited-pending'
- });
-
- expect(model.get('pending')).to.be.ok;
-
- run(() => { model.set('status', 'invited'); });
- expect(model.get('pending')).to.not.be.ok;
-
- run(() => { model.set('status', 'inactive'); });
- expect(model.get('pending')).to.not.be.ok;
- });
-
it('role property is correct', function () {
let model = this.subject();