mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-23 02:31:26 +01:00
[PM-8457] [PM-8608] Members page - remove paging logic / fix search (#9515)
* update admin console members page to use Component Library components and tools, including virtual scroll and table filtering * temporarily duplicate the base component to avoid impacting other subclasses
This commit is contained in:
parent
6687ef5978
commit
b35930074c
@ -0,0 +1,415 @@
|
|||||||
|
import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
|
import { FormControl } from "@angular/forms";
|
||||||
|
import { firstValueFrom, lastValueFrom, debounceTime } from "rxjs";
|
||||||
|
|
||||||
|
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||||
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||||
|
import {
|
||||||
|
OrganizationUserStatusType,
|
||||||
|
OrganizationUserType,
|
||||||
|
ProviderUserStatusType,
|
||||||
|
ProviderUserType,
|
||||||
|
} from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||||
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { DialogService, TableDataSource } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { OrganizationUserView } from "../organizations/core/views/organization-user.view";
|
||||||
|
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||||
|
|
||||||
|
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
|
||||||
|
|
||||||
|
const MaxCheckedCount = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A refactored copy of BasePeopleComponent, using the component library table and other modern features.
|
||||||
|
* This will replace BasePeopleComponent once all subclasses have been changed over to use this class.
|
||||||
|
*/
|
||||||
|
@Directive()
|
||||||
|
export abstract class NewBasePeopleComponent<
|
||||||
|
UserView extends ProviderUserUserDetailsResponse | OrganizationUserView,
|
||||||
|
> {
|
||||||
|
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||||
|
confirmModalRef: ViewContainerRef;
|
||||||
|
|
||||||
|
get allCount() {
|
||||||
|
return this.activeUsers != null ? this.activeUsers.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get invitedCount() {
|
||||||
|
return this.statusMap.has(this.userStatusType.Invited)
|
||||||
|
? this.statusMap.get(this.userStatusType.Invited).length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get acceptedCount() {
|
||||||
|
return this.statusMap.has(this.userStatusType.Accepted)
|
||||||
|
? this.statusMap.get(this.userStatusType.Accepted).length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedCount() {
|
||||||
|
return this.statusMap.has(this.userStatusType.Confirmed)
|
||||||
|
? this.statusMap.get(this.userStatusType.Confirmed).length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get revokedCount() {
|
||||||
|
return this.statusMap.has(this.userStatusType.Revoked)
|
||||||
|
? this.statusMap.get(this.userStatusType.Revoked).length
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a banner alerting the admin that users need to be confirmed.
|
||||||
|
*/
|
||||||
|
get showConfirmUsers(): boolean {
|
||||||
|
return (
|
||||||
|
this.activeUsers != null &&
|
||||||
|
this.statusMap != null &&
|
||||||
|
this.activeUsers.length > 1 &&
|
||||||
|
this.confirmedCount > 0 &&
|
||||||
|
this.confirmedCount < 3 &&
|
||||||
|
this.acceptedCount > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get showBulkConfirmUsers(): boolean {
|
||||||
|
return this.acceptedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;
|
||||||
|
abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType;
|
||||||
|
|
||||||
|
protected dataSource = new TableDataSource<UserView>();
|
||||||
|
|
||||||
|
firstLoaded: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hashmap that groups users by their status (invited/accepted/etc). This is used by the toggles to show
|
||||||
|
* user counts and filter data by user status.
|
||||||
|
*/
|
||||||
|
statusMap = new Map<StatusType, UserView[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected status filter, or null to show all active users.
|
||||||
|
*/
|
||||||
|
status: StatusType | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently executing promise - used to avoid multiple user actions executing at once.
|
||||||
|
*/
|
||||||
|
actionPromise: Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All users, loaded from the server, before any filtering has been applied.
|
||||||
|
*/
|
||||||
|
protected allUsers: UserView[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active users only, that is, users that are not in the revoked status.
|
||||||
|
*/
|
||||||
|
protected activeUsers: UserView[] = [];
|
||||||
|
|
||||||
|
protected searchControl = new FormControl("", { nonNullable: true });
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected validationService: ValidationService,
|
||||||
|
protected modalService: ModalService,
|
||||||
|
private logService: LogService,
|
||||||
|
protected userNamePipe: UserNamePipe,
|
||||||
|
protected dialogService: DialogService,
|
||||||
|
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||||
|
) {
|
||||||
|
// Connect the search input to the table dataSource filter input
|
||||||
|
this.searchControl.valueChanges
|
||||||
|
.pipe(debounceTime(200), takeUntilDestroyed())
|
||||||
|
.subscribe((v) => (this.dataSource.filter = v));
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract edit(user: UserView): void;
|
||||||
|
abstract getUsers(): Promise<ListResponse<UserView> | UserView[]>;
|
||||||
|
abstract deleteUser(id: string): Promise<void>;
|
||||||
|
abstract revokeUser(id: string): Promise<void>;
|
||||||
|
abstract restoreUser(id: string): Promise<void>;
|
||||||
|
abstract reinviteUser(id: string): Promise<void>;
|
||||||
|
abstract confirmUser(user: UserView, publicKey: Uint8Array): Promise<void>;
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
// Load new users from the server
|
||||||
|
const response = await this.getUsers();
|
||||||
|
|
||||||
|
// Reset and repopulate the statusMap
|
||||||
|
this.statusMap.clear();
|
||||||
|
this.activeUsers = [];
|
||||||
|
for (const status of Utils.iterateEnum(this.userStatusType)) {
|
||||||
|
this.statusMap.set(status, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response instanceof ListResponse) {
|
||||||
|
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
|
||||||
|
} else if (Array.isArray(response)) {
|
||||||
|
this.allUsers = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.allUsers.forEach((u) => {
|
||||||
|
if (!this.statusMap.has(u.status)) {
|
||||||
|
this.statusMap.set(u.status, [u]);
|
||||||
|
} else {
|
||||||
|
this.statusMap.get(u.status).push(u);
|
||||||
|
}
|
||||||
|
if (u.status !== this.userStatusType.Revoked) {
|
||||||
|
this.activeUsers.push(u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter based on UserStatus - this also populates the table on first load
|
||||||
|
this.filter(this.status);
|
||||||
|
|
||||||
|
this.firstLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the data source by user status.
|
||||||
|
* This overwrites dataSource.data because this filtering needs to apply first, before the search input
|
||||||
|
*/
|
||||||
|
filter(status: StatusType | null) {
|
||||||
|
this.status = status;
|
||||||
|
if (this.status != null) {
|
||||||
|
this.dataSource.data = this.statusMap.get(this.status);
|
||||||
|
} else {
|
||||||
|
this.dataSource.data = this.activeUsers;
|
||||||
|
}
|
||||||
|
// Reset checkbox selection
|
||||||
|
this.selectAll(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkUser(user: UserView, select?: boolean) {
|
||||||
|
(user as any).checked = select == null ? !(user as any).checked : select;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll(select: boolean) {
|
||||||
|
if (select) {
|
||||||
|
// Reset checkbox selection first so we know nothing else is selected
|
||||||
|
this.selectAll(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredUsers = this.dataSource.filteredData;
|
||||||
|
|
||||||
|
const selectCount =
|
||||||
|
select && filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
|
||||||
|
for (let i = 0; i < selectCount; i++) {
|
||||||
|
this.checkUser(filteredUsers[i], select);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invite() {
|
||||||
|
this.edit(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async removeUserConfirmationDialog(user: UserView) {
|
||||||
|
return this.dialogService.openSimpleDialog({
|
||||||
|
title: this.userNamePipe.transform(user),
|
||||||
|
content: { key: "removeUserConfirmation" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(user: UserView) {
|
||||||
|
const confirmed = await this.removeUserConfirmationDialog(user);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionPromise = this.deleteUser(user.id);
|
||||||
|
try {
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("removedUserId", this.userNamePipe.transform(user)),
|
||||||
|
);
|
||||||
|
this.removeUser(user);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async revokeUserConfirmationDialog(user: UserView) {
|
||||||
|
return this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] },
|
||||||
|
content: this.revokeWarningMessage(),
|
||||||
|
acceptButtonText: { key: "revokeAccess" },
|
||||||
|
type: "warning",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async revoke(user: UserView) {
|
||||||
|
const confirmed = await this.revokeUserConfirmationDialog(user);
|
||||||
|
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionPromise = this.revokeUser(user.id);
|
||||||
|
try {
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)),
|
||||||
|
);
|
||||||
|
await this.load();
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(user: UserView) {
|
||||||
|
this.actionPromise = this.restoreUser(user.id);
|
||||||
|
try {
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)),
|
||||||
|
);
|
||||||
|
await this.load();
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reinvite(user: UserView) {
|
||||||
|
if (this.actionPromise != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.actionPromise = this.reinviteUser(user.id);
|
||||||
|
try {
|
||||||
|
await this.actionPromise;
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
}
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirm(user: UserView) {
|
||||||
|
function updateUser(self: NewBasePeopleComponent<UserView>) {
|
||||||
|
user.status = self.userStatusType.Confirmed;
|
||||||
|
const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user);
|
||||||
|
if (mapIndex > -1) {
|
||||||
|
self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1);
|
||||||
|
self.statusMap.get(self.userStatusType.Confirmed).push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmUser = async (publicKey: Uint8Array) => {
|
||||||
|
try {
|
||||||
|
this.actionPromise = this.confirmUser(user, publicKey);
|
||||||
|
await this.actionPromise;
|
||||||
|
updateUser(this);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.validationService.showError(e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
this.actionPromise = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.actionPromise != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||||
|
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||||
|
|
||||||
|
const autoConfirm = await firstValueFrom(
|
||||||
|
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||||
|
);
|
||||||
|
if (autoConfirm == null || !autoConfirm) {
|
||||||
|
const dialogRef = UserConfirmComponent.open(this.dialogService, {
|
||||||
|
data: {
|
||||||
|
name: this.userNamePipe.transform(user),
|
||||||
|
userId: user != null ? user.userId : null,
|
||||||
|
publicKey: publicKey,
|
||||||
|
confirmUser: () => confirmUser(publicKey),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await lastValueFrom(dialogRef.closed);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fingerprint = await this.cryptoService.getFingerprint(user.userId, publicKey);
|
||||||
|
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
await confirmUser(publicKey);
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(`Handled exception: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected revokeWarningMessage(): string {
|
||||||
|
return this.i18nService.t("revokeUserConfirmation");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getCheckedUsers() {
|
||||||
|
return this.dataSource.data.filter((u) => (u as any).checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a user row from the table and all related data sources
|
||||||
|
*/
|
||||||
|
protected removeUser(user: UserView) {
|
||||||
|
let index = this.dataSource.data.indexOf(user);
|
||||||
|
if (index > -1) {
|
||||||
|
// Clone the array so that the setter for dataSource.data is triggered to update the table rendering
|
||||||
|
const updatedData = [...this.dataSource.data];
|
||||||
|
updatedData.splice(index, 1);
|
||||||
|
this.dataSource.data = updatedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = this.allUsers.indexOf(user);
|
||||||
|
if (index > -1) {
|
||||||
|
this.allUsers.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusMap.has(user.status)) {
|
||||||
|
index = this.statusMap.get(user.status).indexOf(user);
|
||||||
|
if (index > -1) {
|
||||||
|
this.statusMap.get(user.status).splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||||
@ -22,6 +23,7 @@ import { PeopleComponent } from "./people.component";
|
|||||||
MembersRoutingModule,
|
MembersRoutingModule,
|
||||||
UserDialogModule,
|
UserDialogModule,
|
||||||
PasswordCalloutComponent,
|
PasswordCalloutComponent,
|
||||||
|
ScrollingModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BulkConfirmComponent,
|
BulkConfirmComponent,
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
</bit-toggle>
|
</bit-toggle>
|
||||||
</bit-toggle-group>
|
</bit-toggle-group>
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="!firstLoaded">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
@ -45,16 +45,9 @@
|
|||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container
|
<ng-container *ngIf="firstLoaded">
|
||||||
*ngIf="
|
<p *ngIf="!dataSource.filteredData.length">{{ "noMembersInList" | i18n }}</p>
|
||||||
!loading &&
|
<ng-container *ngIf="dataSource.filteredData.length">
|
||||||
((isPaging$ | async)
|
|
||||||
? pagedUsers
|
|
||||||
: (users | search: searchControl.value : 'name' : 'email' : 'id')) as searchedUsers
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p *ngIf="!searchedUsers.length">{{ "noMembersInList" | i18n }}</p>
|
|
||||||
<ng-container *ngIf="searchedUsers.length">
|
|
||||||
<app-callout
|
<app-callout
|
||||||
type="info"
|
type="info"
|
||||||
title="{{ 'confirmUsers' | i18n }}"
|
title="{{ 'confirmUsers' | i18n }}"
|
||||||
@ -63,256 +56,262 @@
|
|||||||
>
|
>
|
||||||
{{ "usersNeedConfirmed" | i18n }}
|
{{ "usersNeedConfirmed" | i18n }}
|
||||||
</app-callout>
|
</app-callout>
|
||||||
<bit-table
|
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||||
infinite-scroll
|
from overflowing the <main> element. -->
|
||||||
[infiniteScrollDistance]="1"
|
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||||
[infiniteScrollDisabled]="!(isPaging$ | async)"
|
<bit-table [dataSource]="dataSource">
|
||||||
(scrolled)="loadMore()"
|
<ng-container header>
|
||||||
>
|
<tr>
|
||||||
<ng-container header>
|
<th bitCell class="tw-w-20">
|
||||||
<tr>
|
<input
|
||||||
<th bitCell class="tw-w-20">
|
type="checkbox"
|
||||||
<input
|
bitCheckbox
|
||||||
type="checkbox"
|
class="tw-mr-1"
|
||||||
bitCheckbox
|
(change)="selectAll($any($event.target).checked)"
|
||||||
class="tw-mr-1"
|
id="selectAll"
|
||||||
(change)="selectAll($any($event.target).checked)"
|
/>
|
||||||
id="selectAll"
|
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
|
||||||
/>
|
"all" | i18n
|
||||||
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
|
}}</label>
|
||||||
"all" | i18n
|
</th>
|
||||||
}}</label>
|
<th bitCell bitSortable="email" default>{{ "name" | i18n }}</th>
|
||||||
</th>
|
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
|
||||||
<th bitCell>{{ "name" | i18n }}</th>
|
<th bitCell bitSortable="type">{{ "role" | i18n }}</th>
|
||||||
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
|
<th bitCell>{{ "policies" | i18n }}</th>
|
||||||
<th bitCell>{{ "role" | i18n }}</th>
|
<th bitCell class="tw-w-10">
|
||||||
<th bitCell>{{ "policies" | i18n }}</th>
|
|
||||||
<th bitCell class="tw-w-10">
|
|
||||||
<button
|
|
||||||
[bitMenuTriggerFor]="headerMenu"
|
|
||||||
type="button"
|
|
||||||
bitIconButton="bwi-ellipsis-v"
|
|
||||||
size="small"
|
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
|
||||||
></button>
|
|
||||||
|
|
||||||
<bit-menu #headerMenu>
|
|
||||||
<ng-container *ngIf="canUseSecretsManager$ | async">
|
|
||||||
<button type="button" bitMenuItem (click)="bulkEnableSM()">
|
|
||||||
{{ "activateSecretsManager" | i18n }}
|
|
||||||
</button>
|
|
||||||
<bit-menu-divider></bit-menu-divider>
|
|
||||||
</ng-container>
|
|
||||||
<button type="button" bitMenuItem (click)="bulkReinvite()">
|
|
||||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
|
||||||
{{ "reinviteSelected" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
|
[bitMenuTriggerFor]="headerMenu"
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitIconButton="bwi-ellipsis-v"
|
||||||
(click)="bulkConfirm()"
|
|
||||||
*ngIf="showBulkConfirmUsers"
|
|
||||||
>
|
|
||||||
<span class="tw-text-success">
|
|
||||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
|
||||||
{{ "confirmSelected" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="bulkRestore()">
|
|
||||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
|
||||||
{{ "restoreAccess" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="bulkRevoke()">
|
|
||||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
|
||||||
{{ "revokeAccess" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" bitMenuItem (click)="bulkRemove()">
|
|
||||||
<span class="tw-text-danger">
|
|
||||||
<i aria-hidden="true" class="bwi bwi-close"></i>
|
|
||||||
{{ "remove" | i18n }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</bit-menu>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
<ng-template body>
|
|
||||||
<tr bitRow *ngFor="let u of searchedUsers" alignContent="middle">
|
|
||||||
<td bitCell (click)="checkUser(u)">
|
|
||||||
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
|
||||||
</td>
|
|
||||||
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
|
||||||
<div class="tw-flex tw-items-center">
|
|
||||||
<bit-avatar
|
|
||||||
size="small"
|
size="small"
|
||||||
[text]="u | userName"
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
[id]="u.userId"
|
></button>
|
||||||
[color]="u.avatarColor"
|
|
||||||
class="tw-mr-3"
|
<bit-menu #headerMenu>
|
||||||
></bit-avatar>
|
<ng-container *ngIf="canUseSecretsManager$ | async">
|
||||||
<div class="tw-flex tw-flex-col">
|
<button type="button" bitMenuItem (click)="bulkEnableSM()">
|
||||||
<div>
|
{{ "activateSecretsManager" | i18n }}
|
||||||
<button type="button" bitLink>
|
|
||||||
{{ u.name ?? u.email }}
|
|
||||||
</button>
|
</button>
|
||||||
<span
|
<bit-menu-divider></bit-menu-divider>
|
||||||
bitBadge
|
</ng-container>
|
||||||
class="tw-text-xs"
|
<button type="button" bitMenuItem (click)="bulkReinvite()">
|
||||||
variant="secondary"
|
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||||
*ngIf="u.status === userStatusType.Invited"
|
{{ "reinviteSelected" | i18n }}
|
||||||
>{{ "invited" | i18n }}</span
|
</button>
|
||||||
>
|
<button
|
||||||
<span
|
type="button"
|
||||||
bitBadge
|
bitMenuItem
|
||||||
class="tw-text-xs"
|
(click)="bulkConfirm()"
|
||||||
variant="warning"
|
*ngIf="showBulkConfirmUsers"
|
||||||
*ngIf="u.status === userStatusType.Accepted"
|
>
|
||||||
>{{ "needsConfirmation" | i18n }}</span
|
<span class="tw-text-success">
|
||||||
>
|
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
<span
|
{{ "confirmSelected" | i18n }}
|
||||||
bitBadge
|
</span>
|
||||||
class="tw-text-xs"
|
</button>
|
||||||
variant="secondary"
|
<button type="button" bitMenuItem (click)="bulkRestore()">
|
||||||
*ngIf="u.status === userStatusType.Revoked"
|
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||||
>{{ "revoked" | i18n }}</span
|
{{ "restoreAccess" | i18n }}
|
||||||
>
|
</button>
|
||||||
</div>
|
<button type="button" bitMenuItem (click)="bulkRevoke()">
|
||||||
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||||
{{ u.email }}
|
{{ "revokeAccess" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="bulkRemove()">
|
||||||
|
<span class="tw-text-danger">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-close"></i>
|
||||||
|
{{ "remove" | i18n }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template body let-rows$>
|
||||||
|
<tr
|
||||||
|
bitRow
|
||||||
|
*cdkVirtualFor="let u of rows$"
|
||||||
|
alignContent="middle"
|
||||||
|
[ngClass]="rowHeightClass"
|
||||||
|
>
|
||||||
|
<td bitCell (click)="checkUser(u)">
|
||||||
|
<input type="checkbox" bitCheckbox [(ngModel)]="$any(u).checked" />
|
||||||
|
</td>
|
||||||
|
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
||||||
|
<div class="tw-flex tw-items-center">
|
||||||
|
<bit-avatar
|
||||||
|
size="small"
|
||||||
|
[text]="u | userName"
|
||||||
|
[id]="u.userId"
|
||||||
|
[color]="u.avatarColor"
|
||||||
|
class="tw-mr-3"
|
||||||
|
></bit-avatar>
|
||||||
|
<div class="tw-flex tw-flex-col">
|
||||||
|
<div>
|
||||||
|
<button type="button" bitLink>
|
||||||
|
{{ u.name ?? u.email }}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Invited"
|
||||||
|
>{{ "invited" | i18n }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="warning"
|
||||||
|
*ngIf="u.status === userStatusType.Accepted"
|
||||||
|
>{{ "needsConfirmation" | i18n }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
bitBadge
|
||||||
|
class="tw-text-xs"
|
||||||
|
variant="secondary"
|
||||||
|
*ngIf="u.status === userStatusType.Revoked"
|
||||||
|
>{{ "revoked" | i18n }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="tw-text-sm tw-text-muted" *ngIf="u.name">
|
||||||
|
{{ u.email }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
|
||||||
|
|
||||||
<td
|
<td
|
||||||
bitCell
|
bitCell
|
||||||
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
|
(click)="edit(u, organization.useGroups ? memberTab.Groups : memberTab.Collections)"
|
||||||
class="tw-cursor-pointer"
|
class="tw-cursor-pointer"
|
||||||
>
|
>
|
||||||
<bit-badge-list
|
<bit-badge-list
|
||||||
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
|
||||||
[maxItems]="3"
|
[maxItems]="3"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
></bit-badge-list>
|
></bit-badge-list>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
bitCell
|
bitCell
|
||||||
(click)="edit(u, memberTab.Role)"
|
(click)="edit(u, memberTab.Role)"
|
||||||
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
class="tw-cursor-pointer tw-text-sm tw-text-muted"
|
||||||
>
|
>
|
||||||
{{ u.type | userType }}
|
{{ u.type | userType }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td bitCell class="tw-text-muted">
|
<td bitCell class="tw-text-muted">
|
||||||
<ng-container *ngIf="u.twoFactorEnabled">
|
<ng-container *ngIf="u.twoFactorEnabled">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-lock"
|
class="bwi bwi-lock"
|
||||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="showEnrolledStatus($any(u))">
|
<ng-container *ngIf="showEnrolledStatus($any(u))">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-key"
|
class="bwi bwi-key"
|
||||||
title="{{ 'enrolledAccountRecovery' | i18n }}"
|
title="{{ 'enrolledAccountRecovery' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
|
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
<td bitCell>
|
<td bitCell>
|
||||||
<button
|
<button
|
||||||
[bitMenuTriggerFor]="rowMenu"
|
[bitMenuTriggerFor]="rowMenu"
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton="bwi-ellipsis-v"
|
bitIconButton="bwi-ellipsis-v"
|
||||||
size="small"
|
size="small"
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
></button>
|
></button>
|
||||||
|
|
||||||
<bit-menu #rowMenu>
|
<bit-menu #rowMenu>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="reinvite(u)"
|
(click)="reinvite(u)"
|
||||||
*ngIf="u.status === userStatusType.Invited"
|
*ngIf="u.status === userStatusType.Invited"
|
||||||
>
|
>
|
||||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||||
{{ "resendInvitation" | i18n }}
|
{{ "resendInvitation" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitMenuItem
|
bitMenuItem
|
||||||
(click)="confirm(u)"
|
(click)="confirm(u)"
|
||||||
*ngIf="u.status === userStatusType.Accepted"
|
*ngIf="u.status === userStatusType.Accepted"
|
||||||
>
|
>
|
||||||
<span class="tw-text-success">
|
<span class="tw-text-success">
|
||||||
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<bit-menu-divider
|
<bit-menu-divider
|
||||||
*ngIf="u.status === userStatusType.Accepted || u.status === userStatusType.Invited"
|
*ngIf="
|
||||||
></bit-menu-divider>
|
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
|
"
|
||||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
></bit-menu-divider>
|
||||||
</button>
|
<button type="button" bitMenuItem (click)="edit(u, memberTab.Role)">
|
||||||
<button
|
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
||||||
type="button"
|
</button>
|
||||||
bitMenuItem
|
<button
|
||||||
(click)="edit(u, memberTab.Groups)"
|
type="button"
|
||||||
*ngIf="organization.useGroups"
|
bitMenuItem
|
||||||
>
|
(click)="edit(u, memberTab.Groups)"
|
||||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
*ngIf="organization.useGroups"
|
||||||
</button>
|
>
|
||||||
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||||
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
|
</button>
|
||||||
</button>
|
<button type="button" bitMenuItem (click)="edit(u, memberTab.Collections)">
|
||||||
<bit-menu-divider></bit-menu-divider>
|
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<bit-menu-divider></bit-menu-divider>
|
||||||
bitMenuItem
|
<button
|
||||||
(click)="events(u)"
|
type="button"
|
||||||
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
bitMenuItem
|
||||||
>
|
(click)="openEventsDialog(u)"
|
||||||
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
*ngIf="organization.useEvents && u.status === userStatusType.Confirmed"
|
||||||
</button>
|
>
|
||||||
<button
|
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
||||||
type="button"
|
</button>
|
||||||
bitMenuItem
|
<button
|
||||||
(click)="resetPassword(u)"
|
type="button"
|
||||||
*ngIf="allowResetPassword(u)"
|
bitMenuItem
|
||||||
>
|
(click)="resetPassword(u)"
|
||||||
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
*ngIf="allowResetPassword(u)"
|
||||||
</button>
|
>
|
||||||
<button
|
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
|
||||||
type="button"
|
</button>
|
||||||
bitMenuItem
|
<button
|
||||||
(click)="restore(u)"
|
type="button"
|
||||||
*ngIf="u.status === userStatusType.Revoked"
|
bitMenuItem
|
||||||
>
|
(click)="restore(u)"
|
||||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
*ngIf="u.status === userStatusType.Revoked"
|
||||||
{{ "restoreAccess" | i18n }}
|
>
|
||||||
</button>
|
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||||
<button
|
{{ "restoreAccess" | i18n }}
|
||||||
type="button"
|
</button>
|
||||||
bitMenuItem
|
<button
|
||||||
(click)="revoke(u)"
|
type="button"
|
||||||
*ngIf="u.status !== userStatusType.Revoked"
|
bitMenuItem
|
||||||
>
|
(click)="revoke(u)"
|
||||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
*ngIf="u.status !== userStatusType.Revoked"
|
||||||
{{ "revokeAccess" | i18n }}
|
>
|
||||||
</button>
|
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||||
<button type="button" bitMenuItem (click)="remove(u)">
|
{{ "revokeAccess" | i18n }}
|
||||||
<span class="tw-text-danger">
|
</button>
|
||||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
<button type="button" bitMenuItem (click)="remove(u)">
|
||||||
</span>
|
<span class="tw-text-danger">
|
||||||
</button>
|
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
||||||
</bit-menu>
|
</span>
|
||||||
</td>
|
</button>
|
||||||
</tr>
|
</bit-menu>
|
||||||
</ng-template>
|
</td>
|
||||||
</bit-table>
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #addEdit></ng-template>
|
<ng-template #addEdit></ng-template>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@ -9,16 +10,12 @@ import {
|
|||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
Subject,
|
|
||||||
switchMap,
|
switchMap,
|
||||||
takeUntil,
|
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
|
||||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||||
@ -50,7 +47,7 @@ import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/respon
|
|||||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||||
|
|
||||||
import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component";
|
import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component";
|
||||||
import { BasePeopleComponent } from "../../common/base.people.component";
|
import { NewBasePeopleComponent } from "../../common/new-base.people.component";
|
||||||
import { GroupService } from "../core";
|
import { GroupService } from "../core";
|
||||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||||
|
|
||||||
@ -70,7 +67,7 @@ import { ResetPasswordComponent } from "./components/reset-password.component";
|
|||||||
selector: "app-org-people",
|
selector: "app-org-people",
|
||||||
templateUrl: "people.component.html",
|
templateUrl: "people.component.html",
|
||||||
})
|
})
|
||||||
export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
export class PeopleComponent extends NewBasePeopleComponent<OrganizationUserView> {
|
||||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||||
groupsModalRef: ViewContainerRef;
|
groupsModalRef: ViewContainerRef;
|
||||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||||
@ -95,7 +92,9 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
|
|
||||||
protected canUseSecretsManager$: Observable<boolean>;
|
protected canUseSecretsManager$: Observable<boolean>;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
// Fixed sizes used for cdkVirtualScroll
|
||||||
|
protected rowHeight = 62;
|
||||||
|
protected rowHeightClass = `tw-h-[62px]`;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
@ -104,12 +103,10 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
searchService: SearchService,
|
|
||||||
validationService: ValidationService,
|
validationService: ValidationService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private policyApiService: PolicyApiService,
|
private policyApiService: PolicyApiService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
searchPipe: SearchPipe,
|
|
||||||
userNamePipe: UserNamePipe,
|
userNamePipe: UserNamePipe,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
@ -124,21 +121,17 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
apiService,
|
apiService,
|
||||||
searchService,
|
|
||||||
i18nService,
|
i18nService,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
validationService,
|
validationService,
|
||||||
modalService,
|
modalService,
|
||||||
logService,
|
logService,
|
||||||
searchPipe,
|
|
||||||
userNamePipe,
|
userNamePipe,
|
||||||
dialogService,
|
dialogService,
|
||||||
organizationManagementPreferencesService,
|
organizationManagementPreferencesService,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
const organization$ = this.route.params.pipe(
|
const organization$ = this.route.params.pipe(
|
||||||
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
||||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
@ -198,29 +191,19 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
await this.load();
|
await this.load();
|
||||||
|
|
||||||
this.searchControl.setValue(qParams.search);
|
this.searchControl.setValue(qParams.search);
|
||||||
|
|
||||||
if (qParams.viewEvents != null) {
|
if (qParams.viewEvents != null) {
|
||||||
const user = this.users.filter((u) => u.id === qParams.viewEvents);
|
const user = this.dataSource.data.filter((u) => u.id === qParams.viewEvents);
|
||||||
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
|
if (user.length > 0 && user[0].status === OrganizationUserStatusType.Confirmed) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
this.openEventsDialog(user[0]);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.events(user[0]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
takeUntil(this.destroy$),
|
takeUntilDestroyed(),
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
await super.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUsers(): Promise<OrganizationUserView[]> {
|
async getUsers(): Promise<OrganizationUserView[]> {
|
||||||
let groupsPromise: Promise<Map<string, string>>;
|
let groupsPromise: Promise<Map<string, string>>;
|
||||||
let collectionsPromise: Promise<Map<string, string>>;
|
let collectionsPromise: Promise<Map<string, string>>;
|
||||||
@ -593,8 +576,8 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
|
|||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async events(user: OrganizationUserView) {
|
openEventsDialog(user: OrganizationUserView) {
|
||||||
await openEntityEventsDialog(this.dialogService, {
|
openEntityEventsDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
organizationId: this.organization.id,
|
organizationId: this.organization.id,
|
||||||
|
Loading…
Reference in New Issue
Block a user