Bulk re-invite of org users (#961)

* Add support for bulk re-invite of org users

* Add selectAll, resolve review comments
This commit is contained in:
Oscar Hinton 2021-05-12 16:38:17 +02:00 committed by GitHub
parent 3ac2ce079a
commit 51f3fee75d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 83 additions and 1 deletions

2
jslib

@ -1 +1 @@
Subproject commit 8244971026ffefb962e235a79c5cb219163bead9 Subproject commit 1e2c56cacf975eab4527cb3c1a63cf8136b58bd4

View File

@ -25,6 +25,27 @@
<input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}" <input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
[(ngModel)]="searchText"> [(ngModel)]="searchText">
</div> </div>
<div class="dropdown ml-3" appListDropdown>
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
<i class="fa fa-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{'reinviteSelected' | i18n}}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
{{'selectAll' | i18n}}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
{{'unselectAll' | i18n}}
</button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()"> <button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i> <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'inviteUser' | i18n}} {{'inviteUser' | i18n}}
@ -46,6 +67,9 @@
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()"> [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody> <tbody>
<tr *ngFor="let u of searchedUsers"> <tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="u.checked" appStopProp>
</td>
<td width="30"> <td width="30">
<app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true" <app-avatar [data]="u.name || u.email" [email]="u.email" size="25" [circle]="true"
[fontSize]="14"></app-avatar> [fontSize]="14"></app-avatar>

View File

@ -25,6 +25,7 @@ import { UserService } from 'jslib/abstractions/user.service';
import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest'; import { OrganizationUserConfirmRequest } from 'jslib/models/request/organizationUserConfirmRequest';
import { UserBulkReinviteRequest } from 'jslib/models/request/userBulkReinviteRequest';
import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse'; import { OrganizationUserUserDetailsResponse } from 'jslib/models/response/organizationUserResponse';
import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType'; import { OrganizationUserStatusType } from 'jslib/enums/organizationUserStatusType';
@ -38,6 +39,8 @@ import { UserAddEditComponent } from './user-add-edit.component';
import { UserConfirmComponent } from './user-confirm.component'; import { UserConfirmComponent } from './user-confirm.component';
import { UserGroupsComponent } from './user-groups.component'; import { UserGroupsComponent } from './user-groups.component';
const MaxCheckedCount = 500;
@Component({ @Component({
selector: 'app-org-people', selector: 'app-org-people',
templateUrl: 'people.component.html', templateUrl: 'people.component.html',
@ -125,6 +128,8 @@ export class PeopleComponent implements OnInit {
} else { } else {
this.users = this.allUsers; this.users = this.allUsers;
} }
// Reset checkbox selecton
this.selectAll(false);
this.resetPaging(); this.resetPaging();
} }
@ -246,6 +251,30 @@ export class PeopleComponent implements OnInit {
this.actionPromise = null; this.actionPromise = null;
} }
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers().filter(u => u.status === OrganizationUserStatusType.Invited);
if (users.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
}
const request = new UserBulkReinviteRequest(users.map(user => user.id));
this.actionPromise = this.apiService.postManyOrganizationUserReinvite(this.organizationId, request);
try {
await this.actionPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('usersHasBeenReinvited'));
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async confirm(user: OrganizationUserUserDetailsResponse) { async confirm(user: OrganizationUserUserDetailsResponse) {
function updateUser(self: PeopleComponent) { function updateUser(self: PeopleComponent) {
user.status = OrganizationUserStatusType.Confirmed; user.status = OrganizationUserStatusType.Confirmed;
@ -358,6 +387,22 @@ export class PeopleComponent implements OnInit {
return !searching && this.users && this.users.length > this.pageSize; return !searching && this.users && this.users.length > this.pageSize;
} }
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
(user as any).checked = select == null ? !(user as any).checked : select;
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
const selectCount = select && this.users.length > MaxCheckedCount
? MaxCheckedCount
: this.users.length;
for (let i = 0; i < selectCount; i++) {
this.checkUser(this.users[i], select);
}
}
private async doConfirmation(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array) { private async doConfirmation(user: OrganizationUserUserDetailsResponse, publicKey: Uint8Array) {
const orgKey = await this.cryptoService.getOrgKey(this.organizationId); const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer); const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
@ -391,4 +436,8 @@ export class PeopleComponent implements OnInit {
} }
} }
} }
private getCheckedUsers() {
return this.users.filter(u => (u as any).checked);
}
} }

View File

@ -3890,5 +3890,14 @@
}, },
"passwordConfirmationDesc": { "passwordConfirmationDesc": {
"message": "This action is protected. To continue, please re-enter your master password to verify your identity." "message": "This action is protected. To continue, please re-enter your master password to verify your identity."
},
"reinviteSelected": {
"message": "Resend Invitations"
},
"noSelectedUsersApplicable": {
"message": "This action is not applicable to any of the selected users."
},
"usersHasBeenReinvited": {
"message": "The selected users have been reinvited."
} }
} }