Skip to content

Commit

Permalink
Add User Role Dropdown
Browse files Browse the repository at this point in the history
Closes TryGhost#3402, Closes TryGhost#3428

-------------------

 ### Components
- Added GhostSelectComponent to handle async select creation (h/t @rwjblue)
- Added GhostRolesSelector (extends GhostSelect) for displaying user role options
- Created StoreInjector for surgically inserting the store into things that normally wouldn't have them.

 ### Users Settings
- InviteNewUserModal now uses GhostRolesSelector & defaults to Author
- The role dropdown for user settings has permissions set per 3402

 ### User Model
- Added `role` property as an interface to getting and setting `roles`
- Refactored anything that set `roles` to set `role`
- isAdmin, isAuthor, isOwner and isEditor are all keyed off of `role` now

 ### Tests
- Added functional tests for Settings.Users
- updated settings.users and settings.users.user screens
- fix spacing on screens

 ### Server Fixtures
- Fixed owner fixture's roles
  • Loading branch information
novaugust committed Jul 30, 2014
1 parent 80cbef8 commit 21abed7
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 131 deletions.
4 changes: 2 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -748,9 +748,9 @@ var path = require('path'),
//
// You can use the `--target` argument to run any individual test file, or the admin or frontend tests:
//
// `grunt test-functional --target=admin/editor_test.js` - run just the editor tests
// `grunt test-functional --target=client/editor_test.js` - run just the editor tests
//
// `grunt test-functional --target=admin/` - run all of the tests in the admin directory
// `grunt test-functional --target=client/` - run all of the tests in the client directory
//
// Functional tests are run with [phantom.js](http://phantomjs.org/) and defined using the testing api from
// [casper.js](http://docs.casperjs.org/en/latest/testing.html).
Expand Down
13 changes: 13 additions & 0 deletions core/client/components/gh-role-selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import GhostSelect from 'ghost/components/gh-select';

var RolesSelector = GhostSelect.extend({
roles: Ember.computed.alias('options'),
options: Ember.computed(function () {
var rolesPromise = this.store.find('role', { permissions: 'assign' });

return Ember.ArrayProxy.extend(Ember.PromiseProxyMixin)
.create({promise: rolesPromise});
})
});

export default RolesSelector;
69 changes: 69 additions & 0 deletions core/client/components/gh-select.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//GhostSelect is a solution to Ember.Select being evil and worthless.
// (Namely, this solves problems with async data in Ember.Select)
//Inspired by (that is, totally ripped off from) this JSBin
//http://emberjs.jsbin.com/rwjblue/40/edit

//Usage:
//Extend this component and create a template for your component.
//Your component must define the `options` property.
//Optionally use `initialValue` to set the object
// you want to have selected to start with.
//Both options and initalValue are promise safe.
//Set onChange in your template to be the name
// of the action you want called in your
//For an example, see gh-roles-selector

var GhostSelect = Ember.Component.extend({
tagName: 'span',
classNames: ['gh-select'],

options: null,
initialValue: null,

resolvedOptions: null,
resolvedInitialValue: null,

//Convert promises to their values
init: function () {
var self = this;
this._super.apply(this, arguments);

Ember.RSVP.hash({
resolvedOptions: this.get('options'),
resolvedInitialValue: this.get('initialValue')
}).then(function (resolvedHash) {
self.setProperties(resolvedHash);

//Run after render to ensure the <option>s have rendered
Ember.run.schedule('afterRender', function () {
self.setInitialValue();
});
});
},

setInitialValue: function () {
var initialValue = this.get('resolvedInitialValue'),
options = this.get('resolvedOptions'),
initialValueIndex = options.indexOf(initialValue);
if (initialValueIndex > -1) {
this.$('option:eq(' + initialValueIndex + ')').prop('selected', true);
}
},
//Called by DOM events, weee!
change: function () {
this._changeSelection();
},
//Send value to specified action
_changeSelection: function () {
var value = this._selectedValue();
Ember.set(this, 'value', value);
this.sendAction('onChange', value);
},
_selectedValue: function () {
var selectedIndex = this.$('select')[0].selectedIndex;

return this.get('options').objectAt(selectedIndex);
}
});

export default GhostSelect;
50 changes: 19 additions & 31 deletions core/client/controllers/modals/invite-new-user.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
var InviteNewUserController = Ember.Controller.extend({

//Used to set the initial value for the dropdown
authorRole: Ember.computed(function () {
var self = this;
return this.store.find('role').then(function (roles) {
var authorRole = roles.findBy('name', 'Author');
//Initialize role as well.
self.set('role', authorRole);
return authorRole;
});
}),

confirm: {
accept: {
text: 'send invitation now'
Expand All @@ -8,45 +18,23 @@ var InviteNewUserController = Ember.Controller.extend({
buttonClass: 'hidden'
}
},

roles: Ember.computed(function () {
var roles = {},
self = this;

roles.promise = this.store.find('role', { permissions: 'assign' }).then(function (roles) {
return roles.rejectBy('name', 'Owner').sortBy('id');
}).then(function (roles) {
// After the promise containing the roles has been resolved and the array
// has been sorted, explicitly set the selectedRole for the Ember.Select.
// The explicit set is needed because the data-select-text attribute is
// not being set until a change is made in the dropdown list.
// This is only required with Ember.Select when it is bound to async data.
self.set('selectedRole', roles.get('firstObject'));

return roles;
});

return Ember.ArrayProxy.extend(Ember.PromiseProxyMixin).create(roles);
}),


actions: {
setRole: function (role) {
this.set('role', role);
},
confirmAccept: function () {
var email = this.get('email'),
role_id = this.get('role'),
role = this.get('role'),
self = this,
newUser,
role;
newUser;

newUser = self.store.createRecord('user', {
email: email,
status: 'invited'
status: 'invited',
role: role
});

// no need to make an API request, the store will already have this role
role = self.store.getById('role', role_id);

newUser.get('roles').pushObject(role);

newUser.save().then(function () {
var notificationText = 'Invitation sent! (' + email + ')';

Expand Down
10 changes: 4 additions & 6 deletions core/client/controllers/settings/users/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,18 @@ var SettingsUserController = Ember.ObjectController.extend({
var lastLogin = this.get('user.last_login');

return lastLogin ? lastLogin.fromNow() : '';

}.property('user.last_login'),

created_at: function () {
var createdAt = this.get('user.created_at');

return createdAt ? createdAt.fromNow() : '';
}.property('user.created_at'),

isAuthor: function () {
return this.get('user.isAuthor');
}.property('user.isAuthor'),


actions: {
changeRole: function (newRole) {
this.set('model.role', newRole);
},
revoke: function () {
var self = this,
email = this.get('email');
Expand Down
10 changes: 10 additions & 0 deletions core/client/initializers/store-injector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//Used to surgically insert the store into things that wouldn't normally have them.
var StoreInjector = {
name: 'store-injector',
after: 'store',
initialize: function (container, application) {
application.inject('component:gh-role-selector', 'store', 'store:main');
}
};

export default StoreInjector;
20 changes: 12 additions & 8 deletions core/client/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@ var User = DS.Model.extend(NProgressSaveMixin, SelectiveSaveMixin, ValidationEng
updated_by: DS.attr('number'),
roles: DS.hasMany('role', { embedded: 'always' }),


// TODO: Once client-side permissions are in place,
// remove the hard role check.
isAuthor: Ember.computed('roles', function () {
return this.get('roles').objectAt(0).get('name').toLowerCase() === 'author';
role: Ember.computed('roles', function (name, value) {
if (arguments.length > 1) {
//Only one role per user, so remove any old data.
this.get('roles').clear();
this.get('roles').pushObject(value);
return value;
}
return this.get('roles.firstObject');
}),

// TODO: Once client-side permissions are in place,
// remove the hard role check.
isEditor: Ember.computed('roles', function () {
return this.get('roles').objectAt(0).get('name').toLowerCase() === 'editor';
}),
isAuthor: Ember.computed.equal('role.name', 'Author'),
isEditor: Ember.computed.equal('role.name', 'Editor'),
isAdmin: Ember.computed.equal('role.name', 'Administrator'),
isOwner: Ember.computed.equal('role.name', 'Owner'),

saveNewPassword: function () {
var url = this.get('ghostPaths.url').api('users', 'password');
Expand Down
5 changes: 5 additions & 0 deletions core/client/templates/components/gh-role-selector.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<select {{bind-attr id=selectId name=selectName}}>
{{#each roles}}
<option {{bind-attr value=id}}>{{name}}</option>
{{/each}}
</select>
14 changes: 4 additions & 10 deletions core/client/templates/modals/invite-new-user.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@

<div class="form-group for-select">
<label for="new-user-role">Role</label>
<span class="gh-select" {{bind-attr data-select-text=selectedRole.name}}>
{{view Ember.Select
content=roles
id="new-user-role"
optionValuePath="content.id"
optionLabelPath="content.name"
name="role"
value=role
selection=selectedRole}}
</span>
{{gh-role-selector
initialValue=authorRole
onChange="setRole"
selectId="new-user-role"}}
</div>

</fieldset>
Expand Down
23 changes: 8 additions & 15 deletions core/client/templates/settings/users/user.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,16 @@
{{input type="email" value=user.email id="user-email" placeholder="Email Address" autocapitalize="off" autocorrect="off" autocomplete="off"}}
<p>Used for notifications</p>
</div>

{{!-- The correct markup for select boxes. Needs changing to the correct data --}}
{{!-- <div class="form-group">
{{#if view.rolesDropdownIsVisible}}
<div class="form-group">
<label for="user-role">Role</label>
<span class="gh-select">
{{view Ember.Select
id="activeTheme"
name="general[activeTheme]"
content=themes
optionValuePath="content.name"
optionLabelPath="content.label"
value=activeTheme
selection=selectedTheme}}
</span>
{{gh-role-selector
initialValue=role
onChange="changeRole"
selectId="user-role"}}
<p>What permissions should this user have?</p>
</div> --}}

</div>
{{/if}}
<div class="form-group">
<label for="user-location">Location</label>
{{input type="text" value=user.location id="user-location"}}
Expand Down
13 changes: 13 additions & 0 deletions core/client/views/settings/users/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
var SettingsUserView = Ember.View.extend({
currentUser: Ember.computed.alias('controller.session.user'),

isNotOwnProfile: Ember.computed('controller.user.id', 'currentUser.id', function () {
return this.get('controller.user.id') !== this.get('currentUser.id');
}),

canAssignRoles: Ember.computed.or('currentUser.isAdmin', 'currentUser.isOwner'),

rolesDropdownIsVisible: Ember.computed.and('isNotOwnProfile', 'canAssignRoles')
});

export default SettingsUserView;
Loading

0 comments on commit 21abed7

Please sign in to comment.