1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-22 06:39:43 +02:00

Make bulk group member management easier

Add and Remove members separated.

Introduced user list paging size.
This commit is contained in:
McMatts 2018-08-21 15:33:54 +01:00
parent bd2e8ac165
commit 1b16be2505
9 changed files with 130 additions and 93 deletions

View file

@ -694,7 +694,12 @@ func (h *Handler) MatchUsers(w http.ResponseWriter, r *http.Request) {
} }
searchText := string(body) searchText := string(body)
u, err := h.Store.User.MatchUsers(ctx, searchText, 100) limit, _ := strconv.Atoi(request.Query(r, "limit"))
if limit == 0 {
limit = 100
}
u, err := h.Store.User.MatchUsers(ctx, searchText, limit)
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)

View file

@ -13,19 +13,18 @@ import $ from 'jquery';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { debounce } from '@ember/runloop'; import { debounce } from '@ember/runloop';
import { A } from '@ember/array'; import { A } from '@ember/array';
import Component from '@ember/component';
import AuthProvider from '../../mixins/auth'; import AuthProvider from '../../mixins/auth';
import ModalMixin from '../../mixins/modal'; import ModalMixin from '../../mixins/modal';
import Component from '@ember/component';
export default Component.extend(AuthProvider, ModalMixin, { export default Component.extend(AuthProvider, ModalMixin, {
groupSvc: service('group'), groupSvc: service('group'),
userSvc: service('user'), userSvc: service('user'),
newGroup: null, newGroup: null,
searchText: '', searchText: '',
showUsers: false,
showMembers: true,
users: null, users: null,
members: null, members: null,
userLimit: 100,
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
@ -34,11 +33,9 @@ export default Component.extend(AuthProvider, ModalMixin, {
}, },
loadGroups() { loadGroups() {
this.get('groupSvc') this.get('groupSvc').getAll().then(groups => {
.getAll() this.set('groups', groups);
.then(groups => { });
this.set('groups', groups);
});
}, },
setDefaults() { setDefaults() {
@ -55,32 +52,22 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.set('members', members); this.set('members', members);
this.get('userSvc') this.get('userSvc')
.matchUsers(searchText) .matchUsers(searchText, this.get('userLimit'))
.then(users => { .then(users => {
let filteredUsers = A([]); let filteredUsers = A([]);
users.forEach(user => { users.forEach(user => {
let m = members.findBy('userId', user.get('id')); let m = members.findBy('userId', user.get('id'));
if (is.undefined(m)) filteredUsers.pushObject(user); if (is.undefined(m)) filteredUsers.pushObject(user);
// user.set('isMember', is.not.undefined(m));
}); });
if (this.get('showMembers') && members.length === 0) {
this.set('showMembers', false);
this.set('showUsers', true);
}
this.set('users', filteredUsers); this.set('users', filteredUsers);
}); });
}); });
}, },
actions: { actions: {
onOpenGroupModal() { onShowAddGroupModal() {
this.modalOpen( this.modalOpen('#add-group-modal', { show: true }, '#new-group-name');
'#add-group-modal',
{ show: true },
'#new-group-name'
);
}, },
onAddGroup(e) { onAddGroup(e) {
@ -171,38 +158,23 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.set('editGroup', null); this.set('editGroup', null);
}, },
onShowMembersModal(groupId) { onShowRemoveMemberModal(groupId) {
this.set('membersGroup', this.get('groups').findBy('id', groupId)); this.set('membersGroup', this.get('groups').findBy('id', groupId));
this.modalOpen( this.modalOpen('#group-remove-member-modal', { show: true });
'#group-members-modal',
{ show: true },
'#group-members-search'
);
this.set('members', null); this.set('members', null);
this.loadGroupInfo();
},
onShowAddMemberModal(groupId) {
this.set('membersGroup', this.get('groups').findBy('id', groupId));
this.modalOpen('#group-add-member-modal', { show: true }, '#group-add-members-search');
this.set('users', null); this.set('users', null);
this.set('showMembers', true);
this.set('showUsers', false);
this.set('searchText', ''); this.set('searchText', '');
this.loadGroupInfo(); this.loadGroupInfo();
}, },
onSearch() { onSearch() {
debounce( debounce(this, function() { this.loadGroupInfo(); }, 450);
this,
function() {
let searchText = this.get('searchText');
this.loadGroupInfo();
if (is.not.empty(searchText)) {
this.set('showMembers', false);
this.set('showUsers', true);
} else {
this.set('showMembers', true);
this.set('showUsers', false);
}
},
450
);
}, },
onLeaveGroup(userId) { onLeaveGroup(userId) {
@ -225,6 +197,11 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.loadGroupInfo(); this.loadGroupInfo();
this.loadGroups(); this.loadGroups();
}); });
},
onLimit(limit) {
this.set('userLimit', limit);
this.loadGroupInfo();
} }
} }
}); });

View file

@ -12,11 +12,12 @@
import $ from 'jquery'; import $ from 'jquery';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { schedule, debounce } from '@ember/runloop'; import { schedule, debounce } from '@ember/runloop';
import TooltipMixin from '../../mixins/tooltip';
import AuthProvider from '../../mixins/auth'; import AuthProvider from '../../mixins/auth';
import ModalMixin from '../../mixins/modal'; import ModalMixin from '../../mixins/modal';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend(AuthProvider, ModalMixin, { export default Component.extend(AuthProvider, ModalMixin, TooltipMixin, {
groupSvc: service('group'), groupSvc: service('group'),
editUser: null, editUser: null,
deleteUser: null, deleteUser: null,
@ -45,6 +46,13 @@ export default Component.extend(AuthProvider, ModalMixin, {
}); });
this.set('users', users); this.set('users', users);
this.renderTooltips();
},
willDestroyElement() {
this._super(...arguments);
this.removeTooltips();
}, },
onKeywordChange: function () { onKeywordChange: function () {

View file

@ -12,6 +12,4 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
actions: {
}
}); });

View file

@ -169,8 +169,10 @@ export default Service.extend({
}, },
// matchUsers on firstname, lastname, email // matchUsers on firstname, lastname, email
matchUsers(text) { matchUsers(text, limit) {
return this.get('ajax').request('users/match', { if (is.null(limit) || is.undefined(limit)) limit = 100;
return this.get('ajax').request(`users/match?limit=${limit}`, {
method: 'POST', method: 'POST',
dataType: 'json', dataType: 'json',
contentType: 'text', contentType: 'text',

View file

@ -119,13 +119,14 @@ $link-hover-decoration: none;
.modal-80 { .modal-80 {
max-width: 80% !important; max-width: 80% !important;
} }
body.modal-open { body.modal-open {
padding-right: 0 !important; padding-right: 0 !important;
// Do not scroll body to top. // Do not scroll body to top.
// See: https://stackoverflow.com/questions/21604674/bootstrap-modal-background-jumps-to-top-on-toggle/21881894 // See: https://stackoverflow.com/questions/21604674/bootstrap-modal-background-jumps-to-top-on-toggle/21881894
overflow: visible; overflow: visible;
} }
.modal-header-white { .modal-header-white {
background-color: $color-white !important; background-color: $color-white !important;
border: none !important; border: none !important;
@ -144,6 +145,7 @@ body.modal-open {
.popover-header { .popover-header {
font-size: 1.2rem; font-size: 1.2rem;
} }
.popover-body { .popover-body {
font-size: 1rem; font-size: 1rem;
@ -160,6 +162,11 @@ body.modal-open {
} }
} }
.btn {
text-transform: uppercase;
font-weight: 600;
}
// Bootstrap override that removes gutter space on smaller screens // Bootstrap override that removes gutter space on smaller screens
// @media (max-width: 1200px) { // @media (max-width: 1200px) {
// .container { // .container {

View file

@ -85,13 +85,13 @@
margin: 15px 0; margin: 15px 0;
.name { .name {
font-size: 1.2rem;
color: $color-off-black; color: $color-off-black;
font-size: 1.3rem;
font-weight: 600;
> .purpose { > .purpose {
font-size: 1rem; font-size: 1rem;
color: $color-gray; color: $color-gray;
display: inline-block;
} }
} }
} }
@ -107,15 +107,6 @@
font-size: 1.2rem; font-size: 1.2rem;
} }
} }
> hr {
height: 1px;
color: $color-border;
background: $color-border;
font-size: 0;
border: 0;
margin: 5px 10px;
}
} }
// used for user admin // used for user admin

View file

@ -4,7 +4,7 @@
<h1 class="admin-heading">Groups</h1> <h1 class="admin-heading">Groups</h1>
<h2 class="sub-heading">Create groups for easier user management &mdash; assign users to groups</h2> <h2 class="sub-heading">Create groups for easier user management &mdash; assign users to groups</h2>
<div class="btn btn-success mt-3 mb-3" {{action 'onOpenGroupModal'}}>Add group</div> <div class="btn btn-success mt-3 mb-3" {{action 'onShowAddGroupModal'}}>Add Group</div>
<div id="add-group-modal" class="modal" tabindex="-1" role="dialog"> <div id="add-group-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
@ -36,15 +36,18 @@
<div class="row group"> <div class="row group">
<div class="col-8"> <div class="col-8">
<div class="name"> <div class="name">
{{group.name}} {{group.name}} ({{group.members}})
{{#if group.purpose}} {{#if group.purpose}}
<div class="purpose">&nbsp;&nbsp;&mdash;&nbsp;{{group.purpose}}</div> <div class="purpose">{{group.purpose}}</div>
{{/if}} {{/if}}
</div> </div>
</div> </div>
<div class="col-4 buttons text-right"> <div class="col-4 buttons text-right">
<button class="btn btn-sm btn-secondary" {{action 'onShowMembersModal' group.id}}>{{group.members}} members</button> <button class="btn btn-primary" {{action 'onShowAddMemberModal' group.id}}>Add</button>
{{#if (gt group.members 0)}}
<div class="button-icon-gap" />
<button class="btn btn-danger" {{action 'onShowRemoveMemberModal' group.id}}>Remove</button>
{{/if}}
<div class="button-icon-gap" /> <div class="button-icon-gap" />
<div class="button-icon-gray align-middle" data-toggle="tooltip" data-placement="top" title="Rename" {{action 'onShowEditModal' group.id}}> <div class="button-icon-gray align-middle" data-toggle="tooltip" data-placement="top" title="Rename" {{action 'onShowEditModal' group.id}}>
<i class="material-icons">edit</i> <i class="material-icons">edit</i>
@ -109,16 +112,11 @@
</div> </div>
</div> </div>
<div id="group-members-modal" class="modal" tabindex="-1" role="dialog"> <div id="group-remove-member-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header">{{membersGroup.name}} ({{members.length}})</div> <div class="modal-header">Remove Member &mdash; {{membersGroup.name}} ({{members.length}})</div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group">
<label for="group-members-search">Search for group members, non-members</label>
{{input id="group-members-search" type="text" class="form-control mousetrap" placeholder="Search members and users..." value=searchText key-up=(action 'onSearch')}}
<small class="form-text text-muted">search firstname, lastname, email</small>
</div>
<div class="view-customize"> <div class="view-customize">
<div class="group-users-members my-5"> <div class="group-users-members my-5">
{{#each members as |member|}} {{#each members as |member|}}
@ -129,26 +127,78 @@
</div> </div>
</div> </div>
{{/each}} {{/each}}
{{#if showUsers}}
<hr />
{{#each users as |user|}}
<div class="row item">
<div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div>
<div class="col-2 text-right">
{{#if user.isMember}}
<button class="btn btn-danger" {{action 'onLeaveGroup' user.id}}>Remove</button>
{{else}}
<button class="btn btn-success" {{action 'onJoinGroup' user.id}}>Add</button>
{{/if}}
</div>
</div>
{{/each}}
{{/if}}
</div> </div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div id="group-add-member-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">Add Member &mdash; {{membersGroup.name}} ({{members.length}})</div>
<div class="modal-body">
<div class="form-group">
<label for="group-members-search">Search users to join this group</label>
{{input id="group-members-search" type="text" class="form-control mousetrap" placeholder="Search members and users..." value=searchText key-up=(action 'onSearch')}}
<small class="form-text text-muted">search firstname, lastname, email</small>
</div>
<div class="view-customize">
<div class="text-center">
<div class="btn-group btn-group-toggle" data-toggle="tooltip" data-placement="top" title="Show how many users">
<label class="btn btn-outline-secondary {{if (eq userLimit 1) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 1}}>1
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 10) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 10}}>10
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 25) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 25}}>25
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 50) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 50}}>50
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 100) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 100}}>100
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 250) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 250}}>250
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 500) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 500}}>500
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 1000) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 1000}}>1,000
</label>
<label class="btn btn-outline-secondary {{if (eq userLimit 99999) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 99999}}>all
</label>
</div>
</div>
<div class="clearfix" />
<div class="group-users-members my-5">
{{#each users as |user|}}
<div class="row item">
<div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div>
<div class="col-2 text-right">
{{#if user.isMember}}
<button class="btn btn-danger" {{action 'onLeaveGroup' user.id}}>Remove</button>
{{else}}
<button class="btn btn-success" {{action 'onJoinGroup' user.id}}>Add</button>
{{/if}}
</div>
</div>
{{/each}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -33,9 +33,8 @@
<small class="form-text text-muted">search firstname, lastname, email</small> <small class="form-text text-muted">search firstname, lastname, email</small>
</div> </div>
<div class="max-results py-3"> <div class="max-results py-3">
<div class="btn-group btn-group-toggle"> <div class="btn-group btn-group-toggle" data-toggle="tooltip" data-placement="top" title="Show how many users">
<label class="btn btn-outline-secondary {{if (eq userLimit 1) 'active'}}"> <label class="btn btn-outline-secondary {{if (eq userLimit 1) 'active'}}">
<input type="radio" name="options" autocomplete="off" {{action 'onLimit' 1}}>1 <input type="radio" name="options" autocomplete="off" {{action 'onLimit' 1}}>1
</label> </label>