1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-21 14:19:43 +02:00

add/remove group membership

This commit is contained in:
sauls8t 2018-03-01 19:14:27 +00:00
parent 0b5ed8fd9e
commit ed11c0ad11
17 changed files with 287 additions and 87 deletions

View file

@ -94,7 +94,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) {
} }
// User list from Documize // User list from Documize
dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx) dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx, "")
if err != nil { if err != nil {
result.Message = "Error: unable to fetch Documize users" result.Message = "Error: unable to fetch Documize users"
result.IsError = true result.IsError = true

View file

@ -141,3 +141,25 @@ func (s Scope) LeaveGroup(ctx domain.RequestContext, groupID, userID string) (er
return return
} }
// GetMembers returns members for every group.
// Useful when you need to bulk fetch membership records
// for subsequent processing.
func (s Scope) GetMembers(ctx domain.RequestContext) (r []group.Record, err error) {
err = s.Runtime.Db.Select(&r,
`SELECT a.id, a.orgid, a.roleid, a.userid, b.role as name, b.purpose
FROM rolemember a, role b
WHERE a.orgid=? AND a.roleid=b.refid
ORDER BY a.userid`,
ctx.OrgID)
if err == sql.ErrNoRows || len(r) == 0 {
err = nil
r = []group.Record{}
}
if err != nil {
err = errors.Wrap(err, "select group members")
}
return
}

View file

@ -110,7 +110,7 @@ type UserStorer interface {
GetByToken(ctx RequestContext, token string) (u user.User, err error) GetByToken(ctx RequestContext, token string) (u user.User, err error)
GetBySerial(ctx RequestContext, serial string) (u user.User, err error) GetBySerial(ctx RequestContext, serial string) (u user.User, err error)
GetActiveUsersForOrganization(ctx RequestContext) (u []user.User, err error) GetActiveUsersForOrganization(ctx RequestContext) (u []user.User, err error)
GetUsersForOrganization(ctx RequestContext) (u []user.User, err error) GetUsersForOrganization(ctx RequestContext, filter string) (u []user.User, err error)
GetSpaceUsers(ctx RequestContext, spaceID string) (u []user.User, err error) GetSpaceUsers(ctx RequestContext, spaceID string) (u []user.User, err error)
GetUsersForSpaces(ctx RequestContext, spaces []string) (u []user.User, err error) GetUsersForSpaces(ctx RequestContext, spaces []string) (u []user.User, err error)
UpdateUser(ctx RequestContext, u user.User) (err error) UpdateUser(ctx RequestContext, u user.User) (err error)
@ -277,6 +277,7 @@ type GroupStorer interface {
Update(ctx RequestContext, g group.Group) (err error) Update(ctx RequestContext, g group.Group) (err error)
Delete(ctx RequestContext, refID string) (rows int64, err error) Delete(ctx RequestContext, refID string) (rows int64, err error)
GetGroupMembers(ctx RequestContext, groupID string) (m []group.Member, err error) GetGroupMembers(ctx RequestContext, groupID string) (m []group.Member, err error)
GetMembers(ctx RequestContext) (r []group.Record, err error)
JoinGroup(ctx RequestContext, groupID, userID string) (err error) JoinGroup(ctx RequestContext, groupID, userID string) (err error)
LeaveGroup(ctx RequestContext, groupID, userID string) (err error) LeaveGroup(ctx RequestContext, groupID, userID string) (err error)
} }

View file

@ -35,6 +35,7 @@ import (
"github.com/documize/community/domain/organization" "github.com/documize/community/domain/organization"
"github.com/documize/community/model/account" "github.com/documize/community/model/account"
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/group"
"github.com/documize/community/model/user" "github.com/documize/community/model/user"
) )
@ -228,6 +229,8 @@ func (h *Handler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
return return
} }
filter := request.Query(r, "filter")
active, err := strconv.ParseBool(request.Query(r, "active")) active, err := strconv.ParseBool(request.Query(r, "active"))
if err != nil { if err != nil {
active = false active = false
@ -243,20 +246,33 @@ func (h *Handler) GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
return return
} }
} else { } else {
u, err = h.Store.User.GetUsersForOrganization(ctx) u, err = h.Store.User.GetUsersForOrganization(ctx, filter)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
} }
} }
// prefetch all group membership records
if len(u) == 0 { groups, err := h.Store.Group.GetMembers(ctx)
u = []user.User{} if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
} }
// for each user...
for i := range u { for i := range u {
// 1. attach user accounts
AttachUserAccounts(ctx, *h.Store, ctx.OrgID, &u[i]) AttachUserAccounts(ctx, *h.Store, ctx.OrgID, &u[i])
// 2. attach user groups
u[i].Groups = []group.Record{}
for j := range groups {
if groups[j].UserID == u[i].RefID {
u[i].Groups = append(u[i].Groups, groups[j])
}
}
} }
response.WriteJSON(w, u) response.WriteJSON(w, u)

View file

@ -118,6 +118,11 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use
ORDER BY u.firstname,u.lastname`, ORDER BY u.firstname,u.lastname`,
ctx.OrgID) ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 {
err = nil
u = []user.User{}
}
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", ctx.OrgID)) err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", ctx.OrgID))
} }
@ -127,13 +132,24 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use
// GetUsersForOrganization returns a slice containing all of the user records for the organizaiton // GetUsersForOrganization returns a slice containing all of the user records for the organizaiton
// identified in the Persister. // identified in the Persister.
func (s Scope) GetUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) { func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string) (u []user.User, err error) {
filter = strings.TrimSpace(strings.ToLower(filter))
likeQuery := ""
if len(filter) > 0 {
likeQuery = " AND (LOWER(u.firstname) LIKE '%" + filter + "%' OR LOWER(u.lastname) LIKE '%" + filter + "%' OR LOWER(u.email) LIKE '%" + filter + "%') "
}
err = s.Runtime.Db.Select(&u, err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised, `SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers u.global, a.active, a.editor, a.admin, a.users as viewusers
FROM user u, account a FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+
ORDER BY u.firstname, u.lastname`, ctx.OrgID) `ORDER BY u.firstname, u.lastname LIMIT 100`, ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 {
err = nil
u = []user.User{}
}
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf(" get users for org %s", ctx.OrgID)) err = errors.Wrap(err, fmt.Sprintf(" get users for org %s", ctx.OrgID))

View file

@ -10,12 +10,14 @@
// https://documize.com // https://documize.com
import $ from 'jquery'; import $ from 'jquery';
import Component from '@ember/component'; import { inject as service } from '@ember/service';
import { schedule, debounce } from '@ember/runloop'; import { schedule, debounce } from '@ember/runloop';
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';
export default Component.extend(AuthProvider, ModalMixin, { export default Component.extend(AuthProvider, ModalMixin, {
groupSvc: service('group'),
editUser: null, editUser: null,
deleteUser: null, deleteUser: null,
filter: '', filter: '',
@ -25,13 +27,16 @@ export default Component.extend(AuthProvider, ModalMixin, {
init() { init() {
this._super(...arguments); this._super(...arguments);
this.password = {}; this.password = {};
this.filteredUsers = [];
this.selectedUsers = []; this.selectedUsers = [];
}, },
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); this._super(...arguments);
this.get('groupSvc').getAll().then((groups) => {
this.set('groups', groups);
});
let users = this.get('users'); let users = this.get('users');
users.forEach(user => { users.forEach(user => {
@ -40,7 +45,6 @@ export default Component.extend(AuthProvider, ModalMixin, {
}); });
this.set('users', users); this.set('users', users);
this.set('filteredUsers', users);
}, },
onKeywordChange: function () { onKeywordChange: function () {
@ -48,17 +52,7 @@ export default Component.extend(AuthProvider, ModalMixin, {
}.observes('filter'), }.observes('filter'),
filterUsers() { filterUsers() {
let users = this.get('users'); this.get('onFilter')(this.get('filter'));
let filteredUsers = [];
let filter = this.get('filter').toLowerCase();
users.forEach(user => {
if (user.get('fullname').toLowerCase().includes(filter) || user.get('email').toLowerCase().includes(filter)) {
filteredUsers.pushObject(user);
}
});
this.set('filteredUsers', filteredUsers);
}, },
actions: { actions: {
@ -184,6 +178,44 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.set('hasSelectedUsers', false); this.set('hasSelectedUsers', false);
this.modalClose('#admin-user-delete-modal'); this.modalClose('#admin-user-delete-modal');
},
onShowGroupsModal(userId) {
let user = this.get('users').findBy('id', userId);
this.set('selectedUser', user);
let userGroups = user.get('groups');
// mark up groups user belongs to...
let groups = this.get('groups');
groups.forEach((g) => {
console.log(userGroups);
let hasGroup = userGroups.findBy('roleId', g.get('id'));
g.set('isMember', is.not.undefined(hasGroup));
})
this.set('groups', groups);
this.modalOpen("#group-member-modal", {"show": true});
},
onLeaveGroup(groupId) {
let userId = this.get('selectedUser.id');
let group = this.get('groups').findBy('id', groupId);
group.set('isMember', false);
this.get('groupSvc').leave(groupId, userId).then(() => {
this.filterUsers();
});
},
onJoinGroup(groupId) {
let userId = this.get('selectedUser.id');
let group = this.get('groups').findBy('id', groupId);
group.set('isMember', true);
this.get('groupSvc').join(groupId, userId).then(() => {
this.filterUsers();
});
} }
} }
}); });

View file

@ -12,7 +12,6 @@
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Model from 'ember-data/model'; import Model from 'ember-data/model';
import attr from 'ember-data/attr'; import attr from 'ember-data/attr';
// import { belongsTo, hasMany } from 'ember-data/relationships';
export default Model.extend({ export default Model.extend({
firstname: attr('string'), firstname: attr('string'),
@ -25,6 +24,7 @@ export default Model.extend({
viewUsers: attr('boolean', { defaultValue: false }), viewUsers: attr('boolean', { defaultValue: false }),
global: attr('boolean', { defaultValue: false }), global: attr('boolean', { defaultValue: false }),
accounts: attr(), accounts: attr(),
groups: attr(),
created: attr(), created: attr(),
revised: attr(), revised: attr(),

View file

@ -15,8 +15,8 @@ import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
userService: service('user'), userService: service('user'),
loadUsers() { loadUsers(filter) {
this.get('userService').getComplete().then((users) => { this.get('userService').getComplete(filter).then((users) => {
this.set('model', users); this.set('model', users);
}); });
}, },
@ -30,26 +30,28 @@ export default Controller.extend({
onAddUsers(list) { onAddUsers(list) {
return this.get('userService').addBulk(list).then(() => { return this.get('userService').addBulk(list).then(() => {
this.loadUsers(); this.loadUsers('');
}); });
}, },
onDelete(userId) { onDelete(userId) {
this.get('userService').remove(userId).then( () => { this.get('userService').remove(userId).then( () => {
this.loadUsers(); this.loadUsers('');
}); });
}, },
onSave(user) { onSave(user) {
this.get('userService').save(user).then(() => { this.get('userService').save(user).then(() => {
this.get('userService').getComplete().then((users) => { this.loadUsers('');
this.set('model', users);
});
}); });
}, },
onPassword(user, password) { onPassword(user, password) {
this.get('userService').updatePassword(user.id, password); this.get('userService').updatePassword(user.id, password);
},
onFilter(filter) {
this.loadUsers(filter);
} }
} }
}); });

View file

@ -30,12 +30,12 @@ export default Route.extend(AuthenticatedRouteMixin, {
return new EmberPromise((resolve) => { return new EmberPromise((resolve) => {
if (this.get('appMeta.authProvider') == constants.AuthProvider.Keycloak) { if (this.get('appMeta.authProvider') == constants.AuthProvider.Keycloak) {
this.get('global').syncExternalUsers().then(() => { this.get('global').syncExternalUsers().then(() => {
this.get('userService').getComplete().then((users) =>{ this.get('userService').getComplete('').then((users) =>{
resolve(users); resolve(users);
}); });
}); });
} else { } else {
this.get('userService').getComplete().then((users) => { this.get('userService').getComplete('').then((users) => {
resolve(users); resolve(users);
}); });
} }

View file

@ -1,8 +1,9 @@
{{customize/user-admin {{customize/user-admin users=model
onAddUser=(action 'onAddUser') onAddUser=(action 'onAddUser')
onAddUsers=(action 'onAddUsers')}} onAddUsers=(action 'onAddUsers')}}
{{customize/user-list users=model {{customize/user-list users=model
onFilter=(action "onFilter")
onDelete=(action "onDelete") onDelete=(action "onDelete")
onSave=(action "onSave") onSave=(action "onSave")
onPassword=(action "onPassword")}} onPassword=(action "onPassword")}}

View file

@ -64,8 +64,13 @@ export default Service.extend({
}, },
// Returns all active and inactive users for organization. // Returns all active and inactive users for organization.
getComplete() { // Only available for admins and limits results to max. 100 users.
return this.get('ajax').request(`users?active=0`).then((response) => { // Takes filter for user search criteria.
getComplete(filter) {
filter = filter.trim();
if (filter.length > 0) filter = encodeURIComponent(filter);
return this.get('ajax').request(`users?active=0&filter=${filter}`).then((response) => {
return response.map((obj) => { return response.map((obj) => {
let data = this.get('store').normalize('user', obj); let data = this.get('store').normalize('user', obj);
return this.get('store').push(data); return this.get('store').push(data);

View file

@ -11,16 +11,37 @@
} }
.user-table { .user-table {
.name { tbody tr td, thead tr th {
font-size: 1rem; border-top: none !important;
color: $color-off-black; border-bottom: none !important;
margin: 0 0 0 30px;
} }
.email { .name {
font-size: 1.2rem;
font-weight: bold;
color: $color-link;
margin: 0 0 0 10px;
display: inline-block;
cursor: pointer;
> .email {
font-size: 0.9rem; font-size: 0.9rem;
color: $color-off-black;
margin: 0;
display: inline-block;
font-weight: normal;
}
}
.groups {
cursor: pointer;
margin: 5px 0 0 10px;
font-size: 1rem;
color: $color-gray; color: $color-gray;
margin: 0 0 0 30px;
&:hover {
color: $color-link;
}
} }
.inactive-user .inactive-user
@ -76,6 +97,7 @@
} }
} }
// used for group admin
> .group-users-members { > .group-users-members {
> .item { > .item {
margin: 10px 0; margin: 10px 0;
@ -86,4 +108,20 @@
} }
} }
} }
// used for user admin
> .group-membership {
> .item {
margin: 10px 0;
> .group-name {
color: $color-primary;
font-size: 1.2rem;
> .group-purpose {
font-size: 0.9rem;
}
}
}
}
} }

View file

@ -2,7 +2,7 @@
<div class="col"> <div class="col">
<div class="view-customize"> <div class="view-customize">
<h1 class="admin-heading">Users</h1> <h1 class="admin-heading">Users</h1>
<h2 class="sub-heading">Set basic information, passwords and permissions for {{model.length}} users</h2> <h2 class="sub-heading">Set basic information, passwords and permissions for {{users.length}} users</h2>
{{#if isAuthProviderDocumize}} {{#if isAuthProviderDocumize}}
<div class="btn btn-success mt-3 mb-3" {{action 'onOpenUserModal'}}>Add user</div> <div class="btn btn-success mt-3 mb-3" {{action 'onOpenUserModal'}}>Add user</div>

View file

@ -115,7 +115,7 @@
<div class="form-group"> <div class="form-group">
<label for="group-members-search">Search for group members, non-members</label> <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')}} {{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">matches firstname, lastname, email</small> <small class="form-text text-muted">search firstname, lastname, email</small>
</div> </div>
<div class="view-customize"> <div class="view-customize">
<div class="group-users-members my-5"> <div class="group-users-members my-5">
@ -124,7 +124,7 @@
<div class="row item"> <div class="row item">
<div class="col-10 fullname">{{member.fullname}}</div> <div class="col-10 fullname">{{member.fullname}}</div>
<div class="col-2 text-right"> <div class="col-2 text-right">
<button class="btn btn-danger" {{action 'onLeaveGroup' member.userId}}>Leave</button> <button class="btn btn-danger" {{action 'onLeaveGroup' member.userId}}>Remove</button>
</div> </div>
</div> </div>
{{/each}} {{/each}}
@ -135,9 +135,9 @@
<div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div> <div class="col-10 fullname">{{user.firstname}} {{user.lastname}}</div>
<div class="col-2 text-right"> <div class="col-2 text-right">
{{#if user.isMember}} {{#if user.isMember}}
<button class="btn btn-danger" {{action 'onLeaveGroup' user.id}}>Leave</button> <button class="btn btn-danger" {{action 'onLeaveGroup' user.id}}>Remove</button>
{{else}} {{else}}
<button class="btn btn-success" {{action 'onJoinGroup' user.id}}>Join</button> <button class="btn btn-success" {{action 'onJoinGroup' user.id}}>Add</button>
{{/if}} {{/if}}
</div> </div>
</div> </div>

View file

@ -1,37 +1,45 @@
<div class="view-customize mb-5"> <div class="view-customize my-5">
<h3>Users</h3>
<table class="table table-hover table-responsive user-table"> <div class="my-2">
<span class="font-weight-bold">Spaces</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can add spaces, both personal and shared with others</span>
</div>
<div class="my-2">
<span class="font-weight-bold">Visible</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can see names of users and groups, can disable for external users like customers/partners</span>
</div>
<div class="my-2">
<span class="font-weight-bold">Admin</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can manage all aspects of Documize, like this screen</span>
</div>
<div class="mt-2 mb-4">
<span class="font-weight-bold">Active</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can login and use Documize</span>
</div>
<div class="form-group mt-5 mb-3">
{{focus-input type="text" class="form-control" placeholder="filter users" value=filter}}
<small class="form-text text-muted">search firstname, lastname, email</small>
</div>
<table class="table table-responsive table-borderless user-table">
<thead> <thead>
<tr> <tr>
<th>{{input type="text" class="form-control" placeholder="filter users" value=filter}}</th> <th class="text-muted">
<th class="no-width">Add Space</th> {{#if hasSelectedUsers}}
<th class="no-width">View Users</th> <button id="bulk-delete-users" type="button" class="btn btn-danger" data-toggle="modal" data-target="#admin-user-delete-modal" data-backdrop="static">Delete selected users</button>
{{/if}}
</th>
<th class="no-width">Spaces</th>
<th class="no-width">Visible</th>
<th class="no-width">Admin</th> <th class="no-width">Admin</th>
<th class="no-width">Active</th> <th class="no-width">Active</th>
<th class="no-width"> <th class="no-width">
{{#if hasSelectedUsers}}
<button id="bulk-delete-users" type="button" class="btn btn-danger btn-sm" data-toggle="modal" data-target="#admin-user-delete-modal" data-backdrop="static">Delete</button>
<div id="admin-user-delete-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">Delete Users</div>
<div class="modal-body">
<p>Are you sure you want to delete selected users?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" onclick={{action 'onBulkDelete'}}>Delete</button>
</div>
</div>
</div>
</div>
{{/if}}
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each filteredUsers key="id" as |user|}} {{#each users key="id" as |user|}}
<tr> <tr>
<td class="{{unless user.active 'inactive-user'}} {{if user.admin 'admin-user'}}"> <td class="{{unless user.active 'inactive-user'}} {{if user.admin 'admin-user'}}">
<div class="d-inline-block align-top"> <div class="d-inline-block align-top">
@ -44,8 +52,16 @@
{{/if}} {{/if}}
</div> </div>
<div class="d-inline-block"> <div class="d-inline-block">
<div class="name d-inline-block">{{ user.fullname }}</div> <div class="name" {{action "onShowEdit" user.id}}>{{user.fullname}}<div class="email">&nbsp;&nbsp;({{user.email}})</div></div>
<div class="email">{{ user.email }}</div> <div class="groups" {{action "onShowGroupsModal" user.id}}>
{{#each user.groups as |group|}}
<span class="group">
{{group.name}}{{#if (not-eq group user.groups.lastObject)}}, {{/if}}
</span>
{{else}}
<span class="group">&lt;no groups&gt;</span>
{{/each}}
</div>
</div> </div>
</td> </td>
<td class="no-width text-center"> <td class="no-width text-center">
@ -81,19 +97,11 @@
{{/if}} {{/if}}
</td> </td>
<td class="no-width text-center"> <td class="no-width text-center">
{{#if user.me}} {{#unless user.me}}
<div class="edit-button-{{user.id}} button-icon-gray" title="Edit" {{action "onShowEdit" user.id}}> <div class="delete-button-{{user.id}} button-icon-red button-icon-small" title="Delete" {{action "onShowDelete" user.id}}>
<i class="material-icons">edit</i>
</div>
{{else}}
<div class="edit-button-{{user.id}} button-icon-gray" title="Edit" {{action "onShowEdit" user.id}}>
<i class="material-icons">edit</i>
</div>
<div class="button-icon-gap"></div>
<div class="delete-button-{{user.id}} button-icon-danger" title="Delete" {{action "onShowDelete" user.id}}>
<i class="material-icons">delete</i> <i class="material-icons">delete</i>
</div> </div>
{{/if}} {{/unless}}
</td> </td>
</tr> </tr>
{{/each}} {{/each}}
@ -151,3 +159,50 @@
<p>Are you sure you want to delete {{deleteUser.fullname}}?</p> <p>Are you sure you want to delete {{deleteUser.fullname}}?</p>
{{/ui/ui-dialog}} {{/ui/ui-dialog}}
<div id="admin-user-delete-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">Delete Users</div>
<div class="modal-body">
<p>Are you sure you want to delete selected users?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" onclick={{action 'onBulkDelete'}}>Delete</button>
</div>
</div>
</div>
</div>
<div id="group-member-modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">{{selectedUser.fullname}}</div>
<div class="modal-body">
<div class="view-customize">
<div class="group-membership my-5">
{{#each groups as |group|}}
<div class="row item">
<div class="col-10 group-name">{{group.name}}
{{#if group.purpose}}
<span class="text-muted group-purpose">&nbsp;&nbsp;&mdash;&nbsp;{{group.purpose}}</span>
{{/if}}
</div>
<div class="col-2 text-right">
{{#if group.isMember}}
<button class="btn btn-danger" {{action 'onLeaveGroup' group.id}}>Leave</button>
{{else}}
<button class="btn btn-success" {{action 'onJoinGroup' group.id}}>Join</button>
{{/if}}
</div>
</div>
{{/each}}
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View file

@ -31,3 +31,13 @@ type Member struct {
Firstname string `json:"firstname"` //read-only info Firstname string `json:"firstname"` //read-only info
Lastname string `json:"lastname"` //read-only info Lastname string `json:"lastname"` //read-only info
} }
// Record details user membership of a user group.
type Record struct {
ID uint64 `json:"id"`
OrgID string `json:"orgId"`
RoleID string `json:"roleId"`
UserID string `json:"userId"`
Name string `json:"name"`
Purpose string `json:"purpose"`
}

View file

@ -16,6 +16,7 @@ import (
"github.com/documize/community/model" "github.com/documize/community/model"
"github.com/documize/community/model/account" "github.com/documize/community/model/account"
"github.com/documize/community/model/group"
) )
// User defines a login. // User defines a login.
@ -34,6 +35,7 @@ type User struct {
Salt string `json:"-"` Salt string `json:"-"`
Reset string `json:"-"` Reset string `json:"-"`
Accounts []account.Account `json:"accounts"` Accounts []account.Account `json:"accounts"`
Groups []group.Record `json:"groups"`
} }
// ProtectSecrets blanks sensitive data. // ProtectSecrets blanks sensitive data.