mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[EC-15] Members Grid (#4097)
* [EC-623] Introduce shared organization module and search input component * [EC-623] Add search input story * [EC-15] Introduce Members module - Add members module and members routing module - Move members only components into the members module and folder - Remove members only components from LooseComponents module - Update organization routing module to lazy load members module * [EC-15] Enable ToggleGroup component to support generic values Using a generic type for the ToggleGroup allows using both Strings and Enums as values without causing Typescript compiler warning/errors. * [EC-15] Force no bottom margin for Toggle button label * [EC-15] Update Members page header - Use bit-toggle for member status filter - Update bit-toggle Accepted button to say Needs Confirmation - Use bit-search-input - Update search placeholder text - Update invite member button style and text - Import ToggleGroupModule into ShareModule * [EC-15] Update members table - Use the CL bit-table component - Add new table headings - Replace cog options menu with bit-menu component - Add placeholder for groups/collection badges * [EC-15] Specify default generic type for ToggleGroup * [EC-15] Modify getOrganizationUsers() in Api service - Optionally allow the Api service to fetch org user groups and/or collections - Will eventually be moved to an organization user service, but kept here for now * [EC-15] Update member view to fetch groups/collections for users - Use the new Api service functionality - Fetch the organization's list of groups and decrypted collection for rendering their names in the table * [EC-15] Refresh table after editing user groups * [EC-15] Move new members dialog into members module * [EC-15] Show "All" in collections column for users with AccessAll flag * [EC-15] Update copy after talking with design/product
This commit is contained in:
@@ -20,6 +20,7 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response";
|
||||
|
||||
import { OrganizationUserView } from "../organizations/core/views/organization-user.view";
|
||||
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||
|
||||
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
|
||||
@@ -28,7 +29,7 @@ const MaxCheckedCount = 500;
|
||||
|
||||
@Directive()
|
||||
export abstract class BasePeopleComponent<
|
||||
UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse
|
||||
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView
|
||||
> {
|
||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||
confirmModalRef: ViewContainerRef;
|
||||
@@ -110,7 +111,7 @@ export abstract class BasePeopleComponent<
|
||||
) {}
|
||||
|
||||
abstract edit(user: UserType): void;
|
||||
abstract getUsers(): Promise<ListResponse<UserType>>;
|
||||
abstract getUsers(): Promise<ListResponse<UserType> | UserType[]>;
|
||||
abstract deleteUser(id: string): Promise<void>;
|
||||
abstract revokeUser(id: string): Promise<void>;
|
||||
abstract restoreUser(id: string): Promise<void>;
|
||||
@@ -125,9 +126,14 @@ export abstract class BasePeopleComponent<
|
||||
this.statusMap.set(status, []);
|
||||
}
|
||||
|
||||
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
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.sort(
|
||||
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse>(
|
||||
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserView>(
|
||||
this.i18nService,
|
||||
"email"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||
import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api";
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
|
||||
|
||||
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||
|
||||
export class OrganizationUserView {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: OrganizationUserType;
|
||||
status: OrganizationUserStatusType;
|
||||
accessAll: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
name: string;
|
||||
email: string;
|
||||
twoFactorEnabled: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
|
||||
collections: CollectionAccessSelectionView[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
groupNames: string[] = [];
|
||||
collectionNames: string[] = [];
|
||||
|
||||
static fromResponse(response: OrganizationUserUserDetailsResponse): OrganizationUserView {
|
||||
const view = Object.assign(new OrganizationUserView(), response) as OrganizationUserView;
|
||||
|
||||
if (response.collections != undefined) {
|
||||
view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c));
|
||||
}
|
||||
|
||||
if (response.groups != undefined) {
|
||||
view.groups = response.groups;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,9 @@ import { NgModule } from "@angular/core";
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
import { EntityUsersComponent } from "./entity-users.component";
|
||||
import { UserDialogModule } from "./member-dialog";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, ScrollingModule, UserDialogModule],
|
||||
imports: [SharedModule, ScrollingModule],
|
||||
declarations: [EntityUsersComponent],
|
||||
exports: [EntityUsersComponent],
|
||||
})
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
<div class="container page-content">
|
||||
<div
|
||||
class="-tw-mt-2 tw-mb-2 tw-flex tw-flex-wrap tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300 tw-pb-2.5"
|
||||
>
|
||||
<h1 class="tw-mt-2 tw-mb-0 tw-grow tw-pr-3">{{ "members" | i18n }}</h1>
|
||||
<div class="tw-mt-2 tw-flex tw-justify-start tw-space-x-3">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == null }"
|
||||
(click)="filter(null)"
|
||||
>
|
||||
{{ "all" | i18n }}
|
||||
<span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == userStatusType.Invited }"
|
||||
(click)="filter(userStatusType.Invited)"
|
||||
>
|
||||
{{ "invited" | i18n }}
|
||||
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == userStatusType.Accepted }"
|
||||
(click)="filter(userStatusType.Accepted)"
|
||||
>
|
||||
{{ "accepted" | i18n }}
|
||||
<span bitBadge badgeType="warning" *ngIf="acceptedCount">{{ acceptedCount }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
[ngClass]="{ active: status == userStatusType.Revoked }"
|
||||
(click)="filter(userStatusType.Revoked)"
|
||||
>
|
||||
{{ "revoked" | i18n }}
|
||||
<span bitBadge badgeType="info" *ngIf="revokedCount">{{ revokedCount }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tw-w-44">
|
||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
||||
<input
|
||||
type="search"
|
||||
class="form-control form-control-sm"
|
||||
id="search"
|
||||
placeholder="{{ 'search' | i18n }}"
|
||||
[(ngModel)]="searchText"
|
||||
/>
|
||||
</div>
|
||||
<div class="dropdown" 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="bwi bwi-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="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "reinviteSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
class="dropdown-item text-success"
|
||||
appStopClick
|
||||
(click)="bulkConfirm()"
|
||||
*ngIf="showBulkConfirmUsers"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirmSelected" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="bulkRestore()">
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="bulkRevoke()">
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
|
||||
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
|
||||
{{ "selectAll" | i18n }}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
|
||||
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
|
||||
{{ "unselectAll" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="invite()">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteUser" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!loading &&
|
||||
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
|
||||
"
|
||||
>
|
||||
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<app-callout
|
||||
type="info"
|
||||
title="{{ 'confirmUsers' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
*ngIf="showConfirmUsers"
|
||||
>
|
||||
{{ "usersNeedConfirmed" | i18n }}
|
||||
</app-callout>
|
||||
<table
|
||||
class="table table-hover table-list"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()"
|
||||
(scrolled)="loadMore()"
|
||||
>
|
||||
<tbody>
|
||||
<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">
|
||||
<bit-avatar [text]="u | userName" [id]="u.userId" size="small"></bit-avatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a>
|
||||
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Invited">{{
|
||||
"invited" | i18n
|
||||
}}</span>
|
||||
<span bitBadge badgeType="warning" *ngIf="u.status === userStatusType.Accepted">{{
|
||||
"accepted" | i18n
|
||||
}}</span>
|
||||
<span bitBadge badgeType="secondary" *ngIf="u.status === userStatusType.Revoked">{{
|
||||
"revoked" | i18n
|
||||
}}</span>
|
||||
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
<i
|
||||
class="bwi bwi-lock"
|
||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showEnrolledStatus(u)">
|
||||
<i
|
||||
class="bwi bwi-key"
|
||||
title="{{ 'enrolledPasswordReset' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td>
|
||||
<span *ngIf="u.type === userType.Owner">{{ "owner" | i18n }}</span>
|
||||
<span *ngIf="u.type === userType.Admin">{{ "admin" | i18n }}</span>
|
||||
<span *ngIf="u.type === userType.Manager">{{ "manager" | i18n }}</span>
|
||||
<span *ngIf="u.type === userType.User">{{ "user" | i18n }}</span>
|
||||
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="reinvite(u)"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item text-success"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="confirm(u)"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
{{ "confirm" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="groups(u)"
|
||||
*ngIf="accessGroups"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
|
||||
{{ "groups" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="events(u)"
|
||||
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
|
||||
{{ "eventLogs" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="resetPassword(u)"
|
||||
*ngIf="allowResetPassword(u)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>
|
||||
{{ "resetPassword" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="restore(u)"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</a>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="revoke(u)"
|
||||
*ngIf="u.status !== userStatusType.Revoked"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
|
||||
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i>
|
||||
{{ "remove" | i18n }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #groupsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
<ng-template #resetPasswordTemplate></ng-template>
|
||||
<ng-template #bulkStatusTemplate></ng-template>
|
||||
<ng-template #bulkConfirmTemplate></ng-template>
|
||||
<ng-template #bulkRemoveTemplate></ng-template>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
|
||||
import { MemberDialogComponent } from "./member-dialog.component";
|
||||
import { NestedCheckboxComponent } from "./nested-checkbox.component";
|
||||
@@ -7,7 +7,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request";
|
||||
|
||||
import { GroupService, GroupView } from "../core";
|
||||
import { GroupService, GroupView } from "../../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-user-groups",
|
||||
1
apps/web/src/app/organizations/members/index.ts
Normal file
1
apps/web/src/app/organizations/members/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./members.module";
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessMembersTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { PeopleComponent } from "./people.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: PeopleComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "members",
|
||||
organizationPermissions: canAccessMembersTab,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class MembersRoutingModule {}
|
||||
39
apps/web/src/app/organizations/members/members.module.ts
Normal file
39
apps/web/src/app/organizations/members/members.module.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ComponentFactoryResolver, NgModule } from "@angular/core";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
import { SharedOrganizationModule } from "../shared";
|
||||
|
||||
import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
|
||||
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
||||
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
|
||||
import { UserDialogModule } from "./components/member-dialog";
|
||||
import { ResetPasswordComponent } from "./components/reset-password.component";
|
||||
import { UserGroupsComponent } from "./components/user-groups.component";
|
||||
import { MembersRoutingModule } from "./members-routing.module";
|
||||
import { PeopleComponent } from "./people.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
SharedOrganizationModule,
|
||||
LooseComponentsModule,
|
||||
MembersRoutingModule,
|
||||
UserDialogModule,
|
||||
],
|
||||
declarations: [
|
||||
BulkConfirmComponent,
|
||||
BulkRemoveComponent,
|
||||
BulkRestoreRevokeComponent,
|
||||
BulkStatusComponent,
|
||||
PeopleComponent,
|
||||
ResetPasswordComponent,
|
||||
UserGroupsComponent,
|
||||
],
|
||||
})
|
||||
export class MembersModule {
|
||||
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
|
||||
modalService.registerComponentFactoryResolver(UserGroupsComponent, componentFactoryResolver);
|
||||
}
|
||||
}
|
||||
305
apps/web/src/app/organizations/members/people.component.html
Normal file
305
apps/web/src/app/organizations/members/people.component.html
Normal file
@@ -0,0 +1,305 @@
|
||||
<div class="container page-content">
|
||||
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
|
||||
<h1>{{ "members" | i18n }}</h1>
|
||||
<div class="tw-flex tw-items-center tw-justify-end tw-space-x-3">
|
||||
<bit-toggle-group
|
||||
[selected]="status"
|
||||
(selectedChange)="filter($event)"
|
||||
[attr.aria-label]="'memberStatusFilter' | i18n"
|
||||
>
|
||||
<bit-toggle [value]="null">
|
||||
{{ "all" | i18n }} <span bitBadge badgeType="info" *ngIf="allCount">{{ allCount }}</span>
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle [value]="userStatusType.Invited">
|
||||
{{ "invited" | i18n }}
|
||||
<span bitBadge badgeType="info" *ngIf="invitedCount">{{ invitedCount }}</span>
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle [value]="userStatusType.Accepted">
|
||||
{{ "needsConfirmation" | i18n }}
|
||||
<span bitBadge badgeType="info" *ngIf="acceptedCount">{{ acceptedCount }}</span>
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle [value]="userStatusType.Revoked">
|
||||
{{ "revoked" | i18n }}
|
||||
<span bitBadge badgeType="info" *ngIf="revokedCount">{{ revokedCount }}</span>
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
|
||||
<app-search-input
|
||||
class="tw-grow"
|
||||
[(ngModel)]="searchText"
|
||||
[placeholder]="'searchMembers' | i18n"
|
||||
>
|
||||
</app-search-input>
|
||||
|
||||
<button type="button" bitButton buttonType="primary" (click)="invite()">
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "inviteMember" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!loading &&
|
||||
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
|
||||
"
|
||||
>
|
||||
<p *ngIf="!searchedUsers.length">{{ "noMembersInList" | i18n }}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<app-callout
|
||||
type="info"
|
||||
title="{{ 'confirmUsers' | i18n }}"
|
||||
icon="bwi bwi-check-circle"
|
||||
*ngIf="showConfirmUsers"
|
||||
>
|
||||
{{ "usersNeedConfirmed" | i18n }}
|
||||
</app-callout>
|
||||
<bit-table
|
||||
infinite-scroll
|
||||
[infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()"
|
||||
(scrolled)="loadMore()"
|
||||
>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell class="tw-w-20">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="tw-mr-1"
|
||||
(change)="selectAll($any($event.target).checked)"
|
||||
id="selectAll"
|
||||
/>
|
||||
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</th>
|
||||
<th bitCell>{{ "name" | i18n }}</th>
|
||||
<th bitCell>{{ (accessGroups ? "groups" : "collections") | i18n }}</th>
|
||||
<th bitCell>{{ "role" | i18n }}</th>
|
||||
<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>
|
||||
<button type="button" bitMenuItem (click)="bulkReinvite()">
|
||||
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
|
||||
{{ "reinviteSelected" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(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-container body>
|
||||
<tr bitRow *ngFor="let u of searchedUsers" alignContent="middle">
|
||||
<td bitCell (click)="checkUser(u)">
|
||||
<input type="checkbox" [(ngModel)]="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"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
<div class="tw-flex tw-flex-col">
|
||||
<div class="tw-text-sm tw-font-bold tw-text-primary-500">
|
||||
{{ u.name ?? u.email }}
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="secondary"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>{{ "invited" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="warning"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>{{ "needsConfirmation" | i18n }}</span
|
||||
>
|
||||
<span
|
||||
bitBadge
|
||||
badgeType="secondary"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>{{ "revoked" | i18n }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="tw-text-xs tw-text-muted" *ngIf="u.name">
|
||||
{{ u.email }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
||||
<bit-badge-list
|
||||
*ngIf="accessGroups || !u.accessAll"
|
||||
[items]="accessGroups ? u.groupNames : u.collectionNames"
|
||||
[maxItems]="2"
|
||||
badgeType="secondary"
|
||||
></bit-badge-list>
|
||||
<span *ngIf="!accessGroups && u.accessAll">{{ "all" | i18n }}</span>
|
||||
</td>
|
||||
|
||||
<td bitCell (click)="edit(u)" class="tw-cursor-pointer">
|
||||
{{ u.type | userType }}
|
||||
</td>
|
||||
|
||||
<td bitCell>
|
||||
<ng-container *ngIf="u.twoFactorEnabled">
|
||||
<i
|
||||
class="bwi bwi-lock"
|
||||
title="{{ 'userUsingTwoStep' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showEnrolledStatus(u)">
|
||||
<i
|
||||
class="bwi bwi-key"
|
||||
title="{{ 'enrolledPasswordReset' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "enrolledPasswordReset" | i18n }}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td bitCell>
|
||||
<button
|
||||
[bitMenuTriggerFor]="rowMenu"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
size="small"
|
||||
appA11yTitle="{{ 'options' | i18n }}"
|
||||
></button>
|
||||
|
||||
<bit-menu #rowMenu>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="reinvite(u)"
|
||||
*ngIf="u.status === userStatusType.Invited"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-envelope"></i>
|
||||
{{ "resendInvitation" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="confirm(u)"
|
||||
*ngIf="u.status === userStatusType.Accepted"
|
||||
>
|
||||
<span class="tw-text-success">
|
||||
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<bit-menu-divider
|
||||
*ngIf="
|
||||
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
|
||||
"
|
||||
></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="edit(u)">
|
||||
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="groups(u)" *ngIf="accessGroups">
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="edit(u)">
|
||||
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
|
||||
</button>
|
||||
<bit-menu-divider></bit-menu-divider>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="events(u)"
|
||||
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-file-text"></i> {{ "eventLogs" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="resetPassword(u)"
|
||||
*ngIf="allowResetPassword(u)"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "resetPassword" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="restore(u)"
|
||||
*ngIf="u.status === userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
|
||||
{{ "restoreAccess" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="revoke(u)"
|
||||
*ngIf="u.status !== userStatusType.Revoked"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
|
||||
{{ "revokeAccess" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="remove(u)">
|
||||
<span class="tw-text-danger">
|
||||
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</bit-table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-template #addEdit></ng-template>
|
||||
<ng-template #groupsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #confirmTemplate></ng-template>
|
||||
<ng-template #resetPasswordTemplate></ng-template>
|
||||
<ng-template #bulkStatusTemplate></ng-template>
|
||||
<ng-template #bulkConfirmTemplate></ng-template>
|
||||
<ng-template #bulkRemoveTemplate></ng-template>
|
||||
</div>
|
||||
@@ -6,6 +6,7 @@ import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
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 { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
@@ -20,31 +21,36 @@ import { ValidationService } from "@bitwarden/common/abstractions/validation.ser
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
||||
import { CollectionData } from "@bitwarden/common/models/data/collection.data";
|
||||
import { Collection } from "@bitwarden/common/models/domain/collection";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request";
|
||||
import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request";
|
||||
import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.request";
|
||||
import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response";
|
||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { BasePeopleComponent } from "../../common/base.people.component";
|
||||
import { GroupService } from "../core";
|
||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||
import { EntityEventsComponent } from "../manage/entity-events.component";
|
||||
|
||||
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
||||
import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component";
|
||||
import { BulkStatusComponent } from "./bulk/bulk-status.component";
|
||||
import { EntityEventsComponent } from "./entity-events.component";
|
||||
import { openUserAddEditDialog, MemberDialogResult } from "./member-dialog/member-dialog.component";
|
||||
import { ResetPasswordComponent } from "./reset-password.component";
|
||||
import { UserGroupsComponent } from "./user-groups.component";
|
||||
import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
|
||||
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
||||
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
|
||||
import { MemberDialogResult, openUserAddEditDialog } from "./components/member-dialog";
|
||||
import { ResetPasswordComponent } from "./components/reset-password.component";
|
||||
import { UserGroupsComponent } from "./components/user-groups.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-org-people",
|
||||
templateUrl: "people.component.html",
|
||||
})
|
||||
export class PeopleComponent
|
||||
extends BasePeopleComponent<OrganizationUserUserDetailsResponse>
|
||||
extends BasePeopleComponent<OrganizationUserView>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||
@@ -94,7 +100,9 @@ export class PeopleComponent
|
||||
stateService: StateService,
|
||||
private organizationService: OrganizationService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private dialogService: DialogService
|
||||
private dialogService: DialogService,
|
||||
private groupService: GroupService,
|
||||
private collectionService: CollectionService
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -167,12 +175,68 @@ export class PeopleComponent
|
||||
}
|
||||
|
||||
async load() {
|
||||
super.load();
|
||||
await super.load();
|
||||
}
|
||||
|
||||
getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
|
||||
return this.apiService.getOrganizationUsers(this.organizationId);
|
||||
async getUsers(): Promise<OrganizationUserView[]> {
|
||||
let groupsPromise: Promise<Map<string, string>>;
|
||||
let collectionsPromise: Promise<Map<string, string>>;
|
||||
|
||||
// We don't need both groups and collections for the table, so only load one
|
||||
const userPromise = this.apiService.getOrganizationUsers(this.organizationId, {
|
||||
includeGroups: this.accessGroups,
|
||||
includeCollections: !this.accessGroups,
|
||||
});
|
||||
|
||||
// Depending on which column is displayed, we need to load the group/collection names
|
||||
if (this.accessGroups) {
|
||||
groupsPromise = this.getGroupNameMap();
|
||||
} else {
|
||||
collectionsPromise = this.getCollectionNameMap();
|
||||
}
|
||||
|
||||
const [usersResponse, groupNamesMap, collectionNamesMap] = await Promise.all([
|
||||
userPromise,
|
||||
groupsPromise,
|
||||
collectionsPromise,
|
||||
]);
|
||||
|
||||
return usersResponse.data?.map<OrganizationUserView>((r) => {
|
||||
const userView = OrganizationUserView.fromResponse(r);
|
||||
|
||||
userView.groupNames = userView.groups
|
||||
.map((g) => groupNamesMap.get(g))
|
||||
.sort(this.i18nService.collator?.compare);
|
||||
userView.collectionNames = userView.collections
|
||||
.map((c) => collectionNamesMap.get(c.id))
|
||||
.sort(this.i18nService.collator?.compare);
|
||||
|
||||
return userView;
|
||||
});
|
||||
}
|
||||
|
||||
async getGroupNameMap(): Promise<Map<string, string>> {
|
||||
const groups = await this.groupService.getAll(this.organizationId);
|
||||
const groupNameMap = new Map<string, string>();
|
||||
groups.forEach((g) => groupNameMap.set(g.id, g.name));
|
||||
return groupNameMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a map of all collection IDs <-> names for the organization.
|
||||
*/
|
||||
async getCollectionNameMap() {
|
||||
const collectionMap = new Map<string, string>();
|
||||
const response = await this.apiService.getCollections(this.organizationId);
|
||||
|
||||
const collections = response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse))
|
||||
);
|
||||
const decryptedCollections = await this.collectionService.decryptMany(collections);
|
||||
|
||||
decryptedCollections.forEach((c) => collectionMap.set(c.id, c.name));
|
||||
|
||||
return collectionMap;
|
||||
}
|
||||
|
||||
deleteUser(id: string): Promise<void> {
|
||||
@@ -191,10 +255,7 @@ export class PeopleComponent
|
||||
return this.apiService.postOrganizationUserReinvite(this.organizationId, id);
|
||||
}
|
||||
|
||||
async confirmUser(
|
||||
user: OrganizationUserUserDetailsResponse,
|
||||
publicKey: Uint8Array
|
||||
): Promise<void> {
|
||||
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
@@ -202,7 +263,7 @@ export class PeopleComponent
|
||||
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request);
|
||||
}
|
||||
|
||||
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean {
|
||||
allowResetPassword(orgUser: OrganizationUserView): boolean {
|
||||
// Hierarchy check
|
||||
let callingUserHasPermission = false;
|
||||
|
||||
@@ -240,7 +301,7 @@ export class PeopleComponent
|
||||
);
|
||||
}
|
||||
|
||||
async edit(user: OrganizationUserUserDetailsResponse) {
|
||||
async edit(user: OrganizationUserView) {
|
||||
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
@@ -274,6 +335,7 @@ export class PeopleComponent
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
comp.onSavedUser.subscribe(() => {
|
||||
modal.close();
|
||||
this.load();
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -376,7 +438,7 @@ export class PeopleComponent
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async events(user: OrganizationUserUserDetailsResponse) {
|
||||
async events(user: OrganizationUserView) {
|
||||
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
||||
comp.name = this.userNamePipe.transform(user);
|
||||
comp.organizationId = this.organizationId;
|
||||
@@ -386,7 +448,7 @@ export class PeopleComponent
|
||||
});
|
||||
}
|
||||
|
||||
async resetPassword(user: OrganizationUserUserDetailsResponse) {
|
||||
async resetPassword(user: OrganizationUserView) {
|
||||
const [modal] = await this.modalService.openViewRef(
|
||||
ResetPasswordComponent,
|
||||
this.resetPasswordModalRef,
|
||||
@@ -405,7 +467,7 @@ export class PeopleComponent
|
||||
);
|
||||
}
|
||||
|
||||
protected async removeUserConfirmationDialog(user: OrganizationUserUserDetailsResponse) {
|
||||
protected async removeUserConfirmationDialog(user: OrganizationUserView) {
|
||||
const warningMessage = user.usesKeyConnector
|
||||
? this.i18nService.t("removeUserConfirmationKeyConnector")
|
||||
: this.i18nService.t("removeOrgUserConfirmation");
|
||||
@@ -420,8 +482,8 @@ export class PeopleComponent
|
||||
}
|
||||
|
||||
private async showBulkStatus(
|
||||
users: OrganizationUserUserDetailsResponse[],
|
||||
filteredUsers: OrganizationUserUserDetailsResponse[],
|
||||
users: OrganizationUserView[],
|
||||
filteredUsers: OrganizationUserView[],
|
||||
request: Promise<ListResponse<OrganizationUserBulkResponse>>,
|
||||
successfullMessage: string
|
||||
) {
|
||||
@@ -3,9 +3,8 @@ import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
||||
import {
|
||||
canAccessOrgAdmin,
|
||||
canAccessGroupsTab,
|
||||
canAccessMembersTab,
|
||||
canAccessOrgAdmin,
|
||||
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
@@ -13,7 +12,6 @@ import { OrganizationLayoutComponent } from "./layouts/organization-layout.compo
|
||||
import { CollectionsComponent } from "./manage/collections.component";
|
||||
import { GroupsComponent } from "./manage/groups.component";
|
||||
import { ManageComponent } from "./manage/manage.component";
|
||||
import { PeopleComponent } from "./manage/people.component";
|
||||
import { VaultModule } from "./vault/vault.module";
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -36,12 +34,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "members",
|
||||
component: PeopleComponent,
|
||||
canActivate: [OrganizationPermissionsGuard],
|
||||
data: {
|
||||
titleId: "members",
|
||||
organizationPermissions: canAccessMembersTab,
|
||||
},
|
||||
loadChildren: () => import("./members").then((m) => m.MembersModule),
|
||||
},
|
||||
{
|
||||
path: "groups",
|
||||
|
||||
@@ -3,12 +3,11 @@ import { NgModule } from "@angular/core";
|
||||
import { CoreOrganizationModule } from "./core";
|
||||
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
|
||||
import { GroupsComponent } from "./manage/groups.component";
|
||||
import { UserGroupsComponent } from "./manage/user-groups.component";
|
||||
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
||||
import { SharedOrganizationModule } from "./shared";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedOrganizationModule, CoreOrganizationModule, OrganizationsRoutingModule],
|
||||
declarations: [GroupsComponent, GroupAddEditComponent, UserGroupsComponent],
|
||||
declarations: [GroupsComponent, GroupAddEditComponent],
|
||||
})
|
||||
export class OrganizationModule {}
|
||||
|
||||
@@ -9,6 +9,6 @@ import { SearchInputComponent } from "./components/search-input/search-input.com
|
||||
@NgModule({
|
||||
imports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
|
||||
declarations: [SearchInputComponent],
|
||||
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
|
||||
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchInputComponent],
|
||||
})
|
||||
export class SharedOrganizationModule {}
|
||||
|
||||
@@ -27,16 +27,10 @@ import { NavbarComponent } from "../layouts/navbar.component";
|
||||
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||
import { OrganizationCreateModule } from "../organizations/create/organization-create.module";
|
||||
import { OrganizationLayoutComponent } from "../organizations/layouts/organization-layout.component";
|
||||
import { BulkConfirmComponent as OrgBulkConfirmComponent } from "../organizations/manage/bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent as OrgBulkRemoveComponent } from "../organizations/manage/bulk/bulk-remove.component";
|
||||
import { BulkRestoreRevokeComponent as OrgBulkRestoreRevokeComponent } from "../organizations/manage/bulk/bulk-restore-revoke.component";
|
||||
import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/manage/bulk/bulk-status.component";
|
||||
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
|
||||
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
|
||||
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
||||
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
|
||||
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
|
||||
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
|
||||
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||
@@ -172,10 +166,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrganizationLayoutComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgBulkConfirmComponent,
|
||||
OrgBulkRestoreRevokeComponent,
|
||||
OrgBulkRemoveComponent,
|
||||
OrgBulkStatusComponent,
|
||||
OrgCollectionsComponent,
|
||||
OrgEntityEventsComponent,
|
||||
OrgEventsComponent,
|
||||
@@ -183,8 +173,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
OrgManageCollectionsComponent,
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgResetPasswordComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgToolsComponent,
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
@@ -289,10 +277,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrganizationLayoutComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgBulkConfirmComponent,
|
||||
OrgBulkRestoreRevokeComponent,
|
||||
OrgBulkRemoveComponent,
|
||||
OrgBulkStatusComponent,
|
||||
OrgCollectionsComponent,
|
||||
OrgEntityEventsComponent,
|
||||
OrgEventsComponent,
|
||||
@@ -300,8 +284,6 @@ import { SharedModule } from "./shared.module";
|
||||
OrgInactiveTwoFactorReportComponent,
|
||||
OrgManageCollectionsComponent,
|
||||
OrgManageComponent,
|
||||
OrgPeopleComponent,
|
||||
OrgResetPasswordComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgToolsComponent,
|
||||
OrgUnsecuredWebsitesReportComponent,
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
NavigationModule,
|
||||
TableModule,
|
||||
TabsModule,
|
||||
ToggleGroupModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
// Register the locales for the application
|
||||
@@ -59,6 +60,7 @@ import "./locales";
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
IconModule,
|
||||
LinkModule,
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
TableModule,
|
||||
@@ -91,6 +93,7 @@ import "./locales";
|
||||
MenuModule,
|
||||
NavigationModule,
|
||||
TableModule,
|
||||
ToggleGroupModule,
|
||||
LinkModule,
|
||||
TabsModule,
|
||||
|
||||
|
||||
@@ -302,6 +302,9 @@
|
||||
"searchOrganization": {
|
||||
"message": "Search organization"
|
||||
},
|
||||
"searchMembers": {
|
||||
"message": "Search members"
|
||||
},
|
||||
"allItems": {
|
||||
"message": "All items"
|
||||
},
|
||||
@@ -729,6 +732,9 @@
|
||||
"noUsersInList": {
|
||||
"message": "There are no users to list."
|
||||
},
|
||||
"noMembersInList": {
|
||||
"message": "There are no members to list."
|
||||
},
|
||||
"noEventsInList": {
|
||||
"message": "There are no events to list."
|
||||
},
|
||||
@@ -2437,9 +2443,6 @@
|
||||
"editMember": {
|
||||
"message": "Edit member"
|
||||
},
|
||||
"inviteMember": {
|
||||
"message": "Invite member"
|
||||
},
|
||||
"inviteUserDesc": {
|
||||
"message": "Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account."
|
||||
},
|
||||
@@ -2969,10 +2972,10 @@
|
||||
}
|
||||
},
|
||||
"confirmUsers": {
|
||||
"message": "Confirm users"
|
||||
"message": "Confirm members"
|
||||
},
|
||||
"usersNeedConfirmed": {
|
||||
"message": "You have users that have accepted their invitation, but still need to be confirmed. Users will not have access to the organization until they are confirmed."
|
||||
"message": "You have members that have accepted their invitation, but still need to be confirmed. Members will not have access to the organization until they are confirmed."
|
||||
},
|
||||
"startDate": {
|
||||
"message": "Start date"
|
||||
@@ -5883,19 +5886,25 @@
|
||||
"selectGroupsAndMembers": {
|
||||
"message": "Select groups and members"
|
||||
},
|
||||
"selectMembers": {
|
||||
"message": "Select members"
|
||||
},
|
||||
"userPermissionOverrideHelper": {
|
||||
"message": "Permissions set for a member will replace permissions set by that member's group"
|
||||
},
|
||||
"noMembersOrGroupsAdded": {
|
||||
"message": "No members or groups added"
|
||||
},
|
||||
"noMembersAdded": {
|
||||
"message": "No members added"
|
||||
},
|
||||
"deleted": {
|
||||
"message": "Deleted"
|
||||
},
|
||||
"memberStatusFilter": {
|
||||
"message": "Member status filter"
|
||||
},
|
||||
"inviteMember": {
|
||||
"message": "Invite member"
|
||||
},
|
||||
"needsConfirmation": {
|
||||
"message": "Needs confirmation"
|
||||
},
|
||||
"memberRole": {
|
||||
"message": "Member role"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Component, Input } from "@angular/core";
|
||||
import { ProviderUserStatusType } from "@bitwarden/common/enums/providerUserStatusType";
|
||||
import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk-confirm.request";
|
||||
import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request";
|
||||
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-confirm.component";
|
||||
import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component";
|
||||
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-confirm.component";
|
||||
import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
|
||||
|
||||
@Component({
|
||||
templateUrl:
|
||||
"../../../../../../../apps/web/src/app/organizations/manage/bulk/bulk-confirm.component.html",
|
||||
"../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-confirm.component.html",
|
||||
})
|
||||
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
|
||||
@Input() providerId: string;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request";
|
||||
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-remove.component";
|
||||
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-remove.component";
|
||||
|
||||
@Component({
|
||||
templateUrl:
|
||||
"../../../../../../../apps/web/src/app/organizations/manage/bulk/bulk-remove.component.html",
|
||||
"../../../../../../../apps/web/src/app/organizations/members/components/bulk/bulk-remove.component.html",
|
||||
})
|
||||
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
|
||||
@Input() providerId: string;
|
||||
|
||||
@@ -22,8 +22,8 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ProviderUserBulkResponse } from "@bitwarden/common/models/response/provider/provider-user-bulk.response";
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response";
|
||||
import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component";
|
||||
import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component";
|
||||
import { EntityEventsComponent } from "@bitwarden/web-vault/app/organizations/manage/entity-events.component";
|
||||
import { BulkStatusComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
|
||||
|
||||
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
|
||||
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
||||
|
||||
@@ -354,7 +354,11 @@ export abstract class ApiService {
|
||||
) => Promise<OrganizationUserDetailsResponse>;
|
||||
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
|
||||
getOrganizationUsers: (
|
||||
organizationId: string
|
||||
organizationId: string,
|
||||
options?: {
|
||||
includeCollections?: boolean;
|
||||
includeGroups?: boolean;
|
||||
}
|
||||
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
||||
getOrganizationUserResetPasswordDetails: (
|
||||
organizationId: string,
|
||||
|
||||
@@ -32,6 +32,8 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
|
||||
email: string;
|
||||
twoFactorEnabled: boolean;
|
||||
usesKeyConnector: boolean;
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -39,6 +41,14 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
|
||||
this.email = this.getResponseProperty("Email");
|
||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
|
||||
}
|
||||
const groups = this.getResponseProperty("Groups");
|
||||
if (groups != null) {
|
||||
this.groups = groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -984,11 +984,24 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async getOrganizationUsers(
|
||||
organizationId: string
|
||||
organizationId: string,
|
||||
options?: {
|
||||
includeCollections?: boolean;
|
||||
includeGroups?: boolean;
|
||||
}
|
||||
): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options?.includeCollections) {
|
||||
params.set("includeCollections", "true");
|
||||
}
|
||||
if (options?.includeGroups) {
|
||||
params.set("includeGroups", "true");
|
||||
}
|
||||
|
||||
const r = await this.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/users",
|
||||
`/organizations/${organizationId}/users?${params.toString()}`,
|
||||
null,
|
||||
true,
|
||||
true
|
||||
|
||||
@@ -7,17 +7,17 @@ let nextId = 0;
|
||||
templateUrl: "./toggle-group.component.html",
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class ToggleGroupComponent {
|
||||
export class ToggleGroupComponent<TValue = unknown> {
|
||||
private id = nextId++;
|
||||
name = `bit-toggle-group-${this.id}`;
|
||||
|
||||
@Input() selected?: unknown;
|
||||
@Output() selectedChange = new EventEmitter<unknown>();
|
||||
@Input() selected?: TValue;
|
||||
@Output() selectedChange = new EventEmitter<TValue>();
|
||||
|
||||
@HostBinding("attr.role") role = "radiogroup";
|
||||
@HostBinding("class") classList = ["tw-flex"];
|
||||
|
||||
onInputInteraction(value: unknown) {
|
||||
onInputInteraction(value: TValue) {
|
||||
this.selected = value;
|
||||
this.selectedChange.emit(value);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HostBinding, Component, Input } from "@angular/core";
|
||||
import { Component, HostBinding, Input } from "@angular/core";
|
||||
|
||||
import { ToggleGroupComponent } from "./toggle-group.component";
|
||||
|
||||
@@ -9,12 +9,12 @@ let nextId = 0;
|
||||
templateUrl: "./toggle.component.html",
|
||||
preserveWhitespaces: false,
|
||||
})
|
||||
export class ToggleComponent {
|
||||
export class ToggleComponent<TValue> {
|
||||
id = nextId++;
|
||||
|
||||
@Input() value?: string;
|
||||
@Input() value?: TValue;
|
||||
|
||||
constructor(private groupComponent: ToggleGroupComponent) {}
|
||||
constructor(private groupComponent: ToggleGroupComponent<TValue>) {}
|
||||
|
||||
@HostBinding("tabIndex") tabIndex = "-1";
|
||||
@HostBinding("class") classList = ["tw-group"];
|
||||
@@ -67,6 +67,9 @@ export class ToggleComponent {
|
||||
"tw-py-1.5",
|
||||
"tw-px-3",
|
||||
|
||||
// Fix for bootstrap styles that add bottom margin
|
||||
"!tw-mb-0",
|
||||
|
||||
// Fix for badge being pushed slightly lower when inside a button.
|
||||
// Insipired by bootstrap, which does the same.
|
||||
"[&>[bitBadge]]:tw-relative",
|
||||
|
||||
Reference in New Issue
Block a user