mirror of
https://github.com/documize/community.git
synced 2025-07-22 14:49:42 +02:00
Make bulk group member management easier
Add and Remove members separated. Introduced user list paging size.
This commit is contained in:
parent
bd2e8ac165
commit
1b16be2505
9 changed files with 130 additions and 93 deletions
|
@ -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)
|
||||||
|
|
|
@ -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,9 +33,7 @@ export default Component.extend(AuthProvider, ModalMixin, {
|
||||||
},
|
},
|
||||||
|
|
||||||
loadGroups() {
|
loadGroups() {
|
||||||
this.get('groupSvc')
|
this.get('groupSvc').getAll().then(groups => {
|
||||||
.getAll()
|
|
||||||
.then(groups => {
|
|
||||||
this.set('groups', groups);
|
this.set('groups', groups);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -12,6 +12,4 @@
|
||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
actions: {
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
|
|
9
gui/app/styles/bootstrap.scss
vendored
9
gui/app/styles/bootstrap.scss
vendored
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 — assign users to groups</h2>
|
<h2 class="sub-heading">Create groups for easier user management — 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"> — {{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 — {{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,8 +127,61 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if showUsers}}
|
</div>
|
||||||
<hr />
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<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 — {{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|}}
|
{{#each users as |user|}}
|
||||||
<div class="row item">
|
<div class="row item">
|
||||||
<div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div>
|
<div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div>
|
||||||
|
@ -143,12 +194,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/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>
|
</div>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue