mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +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 { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
|
||||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-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";
|
import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||||
|
|
||||||
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
|
type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
|
||||||
@@ -28,7 +29,7 @@ const MaxCheckedCount = 500;
|
|||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class BasePeopleComponent<
|
export abstract class BasePeopleComponent<
|
||||||
UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse
|
UserType extends ProviderUserUserDetailsResponse | OrganizationUserView
|
||||||
> {
|
> {
|
||||||
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("confirmTemplate", { read: ViewContainerRef, static: true })
|
||||||
confirmModalRef: ViewContainerRef;
|
confirmModalRef: ViewContainerRef;
|
||||||
@@ -110,7 +111,7 @@ export abstract class BasePeopleComponent<
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
abstract edit(user: UserType): void;
|
abstract edit(user: UserType): void;
|
||||||
abstract getUsers(): Promise<ListResponse<UserType>>;
|
abstract getUsers(): Promise<ListResponse<UserType> | UserType[]>;
|
||||||
abstract deleteUser(id: string): Promise<void>;
|
abstract deleteUser(id: string): Promise<void>;
|
||||||
abstract revokeUser(id: string): Promise<void>;
|
abstract revokeUser(id: string): Promise<void>;
|
||||||
abstract restoreUser(id: string): Promise<void>;
|
abstract restoreUser(id: string): Promise<void>;
|
||||||
@@ -125,9 +126,14 @@ export abstract class BasePeopleComponent<
|
|||||||
this.statusMap.set(status, []);
|
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(
|
this.allUsers.sort(
|
||||||
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse>(
|
Utils.getSortFunction<ProviderUserUserDetailsResponse | OrganizationUserView>(
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
"email"
|
"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 { SharedModule } from "../../shared";
|
||||||
|
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
import { EntityUsersComponent } from "./entity-users.component";
|
||||||
import { UserDialogModule } from "./member-dialog";
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, ScrollingModule, UserDialogModule],
|
imports: [SharedModule, ScrollingModule],
|
||||||
declarations: [EntityUsersComponent],
|
declarations: [EntityUsersComponent],
|
||||||
exports: [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 { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { SharedModule } from "../../../shared/shared.module";
|
import { SharedModule } from "../../../../shared/shared.module";
|
||||||
|
|
||||||
import { MemberDialogComponent } from "./member-dialog.component";
|
import { MemberDialogComponent } from "./member-dialog.component";
|
||||||
import { NestedCheckboxComponent } from "./nested-checkbox.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 { Utils } from "@bitwarden/common/misc/utils";
|
||||||
import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request";
|
import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request";
|
||||||
|
|
||||||
import { GroupService, GroupView } from "../core";
|
import { GroupService, GroupView } from "../../core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-user-groups",
|
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 { 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 { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.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 { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||||
import { PolicyType } from "@bitwarden/common/enums/policyType";
|
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 { OrganizationKeysRequest } from "@bitwarden/common/models/request/organization-keys.request";
|
||||||
import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request";
|
import { OrganizationUserBulkRequest } from "@bitwarden/common/models/request/organization-user-bulk.request";
|
||||||
import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request/organization-user-confirm.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 { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response";
|
import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response";
|
||||||
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
|
import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BasePeopleComponent } from "../../common/base.people.component";
|
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 { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component";
|
||||||
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
import { BulkRemoveComponent } from "./components/bulk/bulk-remove.component";
|
||||||
import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component";
|
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
||||||
import { BulkStatusComponent } from "./bulk/bulk-status.component";
|
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
|
||||||
import { EntityEventsComponent } from "./entity-events.component";
|
import { MemberDialogResult, openUserAddEditDialog } from "./components/member-dialog";
|
||||||
import { openUserAddEditDialog, MemberDialogResult } from "./member-dialog/member-dialog.component";
|
import { ResetPasswordComponent } from "./components/reset-password.component";
|
||||||
import { ResetPasswordComponent } from "./reset-password.component";
|
import { UserGroupsComponent } from "./components/user-groups.component";
|
||||||
import { UserGroupsComponent } from "./user-groups.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-people",
|
selector: "app-org-people",
|
||||||
templateUrl: "people.component.html",
|
templateUrl: "people.component.html",
|
||||||
})
|
})
|
||||||
export class PeopleComponent
|
export class PeopleComponent
|
||||||
extends BasePeopleComponent<OrganizationUserUserDetailsResponse>
|
extends BasePeopleComponent<OrganizationUserView>
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true })
|
||||||
@@ -94,7 +100,9 @@ export class PeopleComponent
|
|||||||
stateService: StateService,
|
stateService: StateService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private dialogService: DialogService
|
private dialogService: DialogService,
|
||||||
|
private groupService: GroupService,
|
||||||
|
private collectionService: CollectionService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
apiService,
|
apiService,
|
||||||
@@ -167,12 +175,68 @@ export class PeopleComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
super.load();
|
|
||||||
await super.load();
|
await super.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
getUsers(): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
|
async getUsers(): Promise<OrganizationUserView[]> {
|
||||||
return this.apiService.getOrganizationUsers(this.organizationId);
|
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> {
|
deleteUser(id: string): Promise<void> {
|
||||||
@@ -191,10 +255,7 @@ export class PeopleComponent
|
|||||||
return this.apiService.postOrganizationUserReinvite(this.organizationId, id);
|
return this.apiService.postOrganizationUserReinvite(this.organizationId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmUser(
|
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
||||||
user: OrganizationUserUserDetailsResponse,
|
|
||||||
publicKey: Uint8Array
|
|
||||||
): Promise<void> {
|
|
||||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||||
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
const key = await this.cryptoService.rsaEncrypt(orgKey.key, publicKey.buffer);
|
||||||
const request = new OrganizationUserConfirmRequest();
|
const request = new OrganizationUserConfirmRequest();
|
||||||
@@ -202,7 +263,7 @@ export class PeopleComponent
|
|||||||
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request);
|
await this.apiService.postOrganizationUserConfirm(this.organizationId, user.id, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
allowResetPassword(orgUser: OrganizationUserUserDetailsResponse): boolean {
|
allowResetPassword(orgUser: OrganizationUserView): boolean {
|
||||||
// Hierarchy check
|
// Hierarchy check
|
||||||
let callingUserHasPermission = false;
|
let callingUserHasPermission = false;
|
||||||
|
|
||||||
@@ -240,7 +301,7 @@ export class PeopleComponent
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit(user: OrganizationUserUserDetailsResponse) {
|
async edit(user: OrganizationUserView) {
|
||||||
const dialog = openUserAddEditDialog(this.dialogService, {
|
const dialog = openUserAddEditDialog(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(user),
|
name: this.userNamePipe.transform(user),
|
||||||
@@ -274,6 +335,7 @@ export class PeopleComponent
|
|||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
comp.onSavedUser.subscribe(() => {
|
comp.onSavedUser.subscribe(() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
this.load();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -376,7 +438,7 @@ export class PeopleComponent
|
|||||||
await this.load();
|
await this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
async events(user: OrganizationUserUserDetailsResponse) {
|
async events(user: OrganizationUserView) {
|
||||||
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => {
|
||||||
comp.name = this.userNamePipe.transform(user);
|
comp.name = this.userNamePipe.transform(user);
|
||||||
comp.organizationId = this.organizationId;
|
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(
|
const [modal] = await this.modalService.openViewRef(
|
||||||
ResetPasswordComponent,
|
ResetPasswordComponent,
|
||||||
this.resetPasswordModalRef,
|
this.resetPasswordModalRef,
|
||||||
@@ -405,7 +467,7 @@ export class PeopleComponent
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async removeUserConfirmationDialog(user: OrganizationUserUserDetailsResponse) {
|
protected async removeUserConfirmationDialog(user: OrganizationUserView) {
|
||||||
const warningMessage = user.usesKeyConnector
|
const warningMessage = user.usesKeyConnector
|
||||||
? this.i18nService.t("removeUserConfirmationKeyConnector")
|
? this.i18nService.t("removeUserConfirmationKeyConnector")
|
||||||
: this.i18nService.t("removeOrgUserConfirmation");
|
: this.i18nService.t("removeOrgUserConfirmation");
|
||||||
@@ -420,8 +482,8 @@ export class PeopleComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async showBulkStatus(
|
private async showBulkStatus(
|
||||||
users: OrganizationUserUserDetailsResponse[],
|
users: OrganizationUserView[],
|
||||||
filteredUsers: OrganizationUserUserDetailsResponse[],
|
filteredUsers: OrganizationUserView[],
|
||||||
request: Promise<ListResponse<OrganizationUserBulkResponse>>,
|
request: Promise<ListResponse<OrganizationUserBulkResponse>>,
|
||||||
successfullMessage: string
|
successfullMessage: string
|
||||||
) {
|
) {
|
||||||
@@ -3,9 +3,8 @@ import { RouterModule, Routes } from "@angular/router";
|
|||||||
|
|
||||||
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
import { AuthGuard } from "@bitwarden/angular/guards/auth.guard";
|
||||||
import {
|
import {
|
||||||
canAccessOrgAdmin,
|
|
||||||
canAccessGroupsTab,
|
canAccessGroupsTab,
|
||||||
canAccessMembersTab,
|
canAccessOrgAdmin,
|
||||||
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
} from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
|
|
||||||
import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard";
|
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 { CollectionsComponent } from "./manage/collections.component";
|
||||||
import { GroupsComponent } from "./manage/groups.component";
|
import { GroupsComponent } from "./manage/groups.component";
|
||||||
import { ManageComponent } from "./manage/manage.component";
|
import { ManageComponent } from "./manage/manage.component";
|
||||||
import { PeopleComponent } from "./manage/people.component";
|
|
||||||
import { VaultModule } from "./vault/vault.module";
|
import { VaultModule } from "./vault/vault.module";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
@@ -36,12 +34,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "members",
|
path: "members",
|
||||||
component: PeopleComponent,
|
loadChildren: () => import("./members").then((m) => m.MembersModule),
|
||||||
canActivate: [OrganizationPermissionsGuard],
|
|
||||||
data: {
|
|
||||||
titleId: "members",
|
|
||||||
organizationPermissions: canAccessMembersTab,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "groups",
|
path: "groups",
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import { NgModule } from "@angular/core";
|
|||||||
import { CoreOrganizationModule } from "./core";
|
import { CoreOrganizationModule } from "./core";
|
||||||
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
|
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
|
||||||
import { GroupsComponent } from "./manage/groups.component";
|
import { GroupsComponent } from "./manage/groups.component";
|
||||||
import { UserGroupsComponent } from "./manage/user-groups.component";
|
|
||||||
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
||||||
import { SharedOrganizationModule } from "./shared";
|
import { SharedOrganizationModule } from "./shared";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedOrganizationModule, CoreOrganizationModule, OrganizationsRoutingModule],
|
imports: [SharedOrganizationModule, CoreOrganizationModule, OrganizationsRoutingModule],
|
||||||
declarations: [GroupsComponent, GroupAddEditComponent, UserGroupsComponent],
|
declarations: [GroupsComponent, GroupAddEditComponent],
|
||||||
})
|
})
|
||||||
export class OrganizationModule {}
|
export class OrganizationModule {}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ import { SearchInputComponent } from "./components/search-input/search-input.com
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
|
imports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
|
||||||
declarations: [SearchInputComponent],
|
declarations: [SearchInputComponent],
|
||||||
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule],
|
exports: [SharedModule, CollectionDialogModule, AccessSelectorModule, SearchInputComponent],
|
||||||
})
|
})
|
||||||
export class SharedOrganizationModule {}
|
export class SharedOrganizationModule {}
|
||||||
|
|||||||
@@ -27,16 +27,10 @@ import { NavbarComponent } from "../layouts/navbar.component";
|
|||||||
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
import { UserLayoutComponent } from "../layouts/user-layout.component";
|
||||||
import { OrganizationCreateModule } from "../organizations/create/organization-create.module";
|
import { OrganizationCreateModule } from "../organizations/create/organization-create.module";
|
||||||
import { OrganizationLayoutComponent } from "../organizations/layouts/organization-layout.component";
|
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 { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
|
||||||
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
|
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
|
||||||
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
||||||
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.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 { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||||
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||||
@@ -172,10 +166,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
OrganizationLayoutComponent,
|
OrganizationLayoutComponent,
|
||||||
OrganizationPlansComponent,
|
OrganizationPlansComponent,
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgBulkConfirmComponent,
|
|
||||||
OrgBulkRestoreRevokeComponent,
|
|
||||||
OrgBulkRemoveComponent,
|
|
||||||
OrgBulkStatusComponent,
|
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
OrgEventsComponent,
|
OrgEventsComponent,
|
||||||
@@ -183,8 +173,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
OrgInactiveTwoFactorReportComponent,
|
OrgInactiveTwoFactorReportComponent,
|
||||||
OrgManageCollectionsComponent,
|
OrgManageCollectionsComponent,
|
||||||
OrgManageComponent,
|
OrgManageComponent,
|
||||||
OrgPeopleComponent,
|
|
||||||
OrgResetPasswordComponent,
|
|
||||||
OrgReusedPasswordsReportComponent,
|
OrgReusedPasswordsReportComponent,
|
||||||
OrgToolsComponent,
|
OrgToolsComponent,
|
||||||
OrgUnsecuredWebsitesReportComponent,
|
OrgUnsecuredWebsitesReportComponent,
|
||||||
@@ -289,10 +277,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
OrganizationLayoutComponent,
|
OrganizationLayoutComponent,
|
||||||
OrganizationPlansComponent,
|
OrganizationPlansComponent,
|
||||||
OrgAttachmentsComponent,
|
OrgAttachmentsComponent,
|
||||||
OrgBulkConfirmComponent,
|
|
||||||
OrgBulkRestoreRevokeComponent,
|
|
||||||
OrgBulkRemoveComponent,
|
|
||||||
OrgBulkStatusComponent,
|
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
OrgEventsComponent,
|
OrgEventsComponent,
|
||||||
@@ -300,8 +284,6 @@ import { SharedModule } from "./shared.module";
|
|||||||
OrgInactiveTwoFactorReportComponent,
|
OrgInactiveTwoFactorReportComponent,
|
||||||
OrgManageCollectionsComponent,
|
OrgManageCollectionsComponent,
|
||||||
OrgManageComponent,
|
OrgManageComponent,
|
||||||
OrgPeopleComponent,
|
|
||||||
OrgResetPasswordComponent,
|
|
||||||
OrgReusedPasswordsReportComponent,
|
OrgReusedPasswordsReportComponent,
|
||||||
OrgToolsComponent,
|
OrgToolsComponent,
|
||||||
OrgUnsecuredWebsitesReportComponent,
|
OrgUnsecuredWebsitesReportComponent,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
NavigationModule,
|
NavigationModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
|
ToggleGroupModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
// Register the locales for the application
|
// Register the locales for the application
|
||||||
@@ -59,6 +60,7 @@ import "./locales";
|
|||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
|
LinkModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
NavigationModule,
|
NavigationModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
@@ -91,6 +93,7 @@ import "./locales";
|
|||||||
MenuModule,
|
MenuModule,
|
||||||
NavigationModule,
|
NavigationModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
|
ToggleGroupModule,
|
||||||
LinkModule,
|
LinkModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,9 @@
|
|||||||
"searchOrganization": {
|
"searchOrganization": {
|
||||||
"message": "Search organization"
|
"message": "Search organization"
|
||||||
},
|
},
|
||||||
|
"searchMembers": {
|
||||||
|
"message": "Search members"
|
||||||
|
},
|
||||||
"allItems": {
|
"allItems": {
|
||||||
"message": "All items"
|
"message": "All items"
|
||||||
},
|
},
|
||||||
@@ -729,6 +732,9 @@
|
|||||||
"noUsersInList": {
|
"noUsersInList": {
|
||||||
"message": "There are no users to list."
|
"message": "There are no users to list."
|
||||||
},
|
},
|
||||||
|
"noMembersInList": {
|
||||||
|
"message": "There are no members to list."
|
||||||
|
},
|
||||||
"noEventsInList": {
|
"noEventsInList": {
|
||||||
"message": "There are no events to list."
|
"message": "There are no events to list."
|
||||||
},
|
},
|
||||||
@@ -2437,9 +2443,6 @@
|
|||||||
"editMember": {
|
"editMember": {
|
||||||
"message": "Edit member"
|
"message": "Edit member"
|
||||||
},
|
},
|
||||||
"inviteMember": {
|
|
||||||
"message": "Invite member"
|
|
||||||
},
|
|
||||||
"inviteUserDesc": {
|
"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."
|
"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": {
|
"confirmUsers": {
|
||||||
"message": "Confirm users"
|
"message": "Confirm members"
|
||||||
},
|
},
|
||||||
"usersNeedConfirmed": {
|
"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": {
|
"startDate": {
|
||||||
"message": "Start date"
|
"message": "Start date"
|
||||||
@@ -5883,19 +5886,25 @@
|
|||||||
"selectGroupsAndMembers": {
|
"selectGroupsAndMembers": {
|
||||||
"message": "Select groups and members"
|
"message": "Select groups and members"
|
||||||
},
|
},
|
||||||
"selectMembers": {
|
|
||||||
"message": "Select members"
|
|
||||||
},
|
|
||||||
"userPermissionOverrideHelper": {
|
"userPermissionOverrideHelper": {
|
||||||
"message": "Permissions set for a member will replace permissions set by that member's group"
|
"message": "Permissions set for a member will replace permissions set by that member's group"
|
||||||
},
|
},
|
||||||
"noMembersOrGroupsAdded": {
|
"noMembersOrGroupsAdded": {
|
||||||
"message": "No members or groups added"
|
"message": "No members or groups added"
|
||||||
},
|
},
|
||||||
"noMembersAdded": {
|
|
||||||
"message": "No members added"
|
|
||||||
},
|
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"message": "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 { ProviderUserStatusType } from "@bitwarden/common/enums/providerUserStatusType";
|
||||||
import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk-confirm.request";
|
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 { 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 { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-confirm.component";
|
||||||
import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/manage/bulk/bulk-status.component";
|
import { BulkUserDetails } from "@bitwarden/web-vault/app/organizations/members/components/bulk/bulk-status.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl:
|
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 {
|
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
|
||||||
@Input() providerId: string;
|
@Input() providerId: string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Component, Input } from "@angular/core";
|
import { Component, Input } from "@angular/core";
|
||||||
|
|
||||||
import { ProviderUserBulkRequest } from "@bitwarden/common/models/request/provider/provider-user-bulk.request";
|
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({
|
@Component({
|
||||||
templateUrl:
|
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 {
|
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
|
||||||
@Input() providerId: string;
|
@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 { ProviderUserBulkResponse } from "@bitwarden/common/models/response/provider/provider-user-bulk.response";
|
||||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response";
|
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/models/response/provider/provider-user.response";
|
||||||
import { BasePeopleComponent } from "@bitwarden/web-vault/app/common/base.people.component";
|
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 { 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 { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
|
||||||
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
|
||||||
|
|||||||
@@ -354,7 +354,11 @@ export abstract class ApiService {
|
|||||||
) => Promise<OrganizationUserDetailsResponse>;
|
) => Promise<OrganizationUserDetailsResponse>;
|
||||||
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
|
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
|
||||||
getOrganizationUsers: (
|
getOrganizationUsers: (
|
||||||
organizationId: string
|
organizationId: string,
|
||||||
|
options?: {
|
||||||
|
includeCollections?: boolean;
|
||||||
|
includeGroups?: boolean;
|
||||||
|
}
|
||||||
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
||||||
getOrganizationUserResetPasswordDetails: (
|
getOrganizationUserResetPasswordDetails: (
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
|
|||||||
email: string;
|
email: string;
|
||||||
twoFactorEnabled: boolean;
|
twoFactorEnabled: boolean;
|
||||||
usesKeyConnector: boolean;
|
usesKeyConnector: boolean;
|
||||||
|
collections: SelectionReadOnlyResponse[] = [];
|
||||||
|
groups: string[] = [];
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@@ -39,6 +41,14 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
|
|||||||
this.email = this.getResponseProperty("Email");
|
this.email = this.getResponseProperty("Email");
|
||||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||||
this.usesKeyConnector = this.getResponseProperty("UsesKeyConnector") ?? false;
|
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(
|
async getOrganizationUsers(
|
||||||
organizationId: string
|
organizationId: string,
|
||||||
|
options?: {
|
||||||
|
includeCollections?: boolean;
|
||||||
|
includeGroups?: boolean;
|
||||||
|
}
|
||||||
): Promise<ListResponse<OrganizationUserUserDetailsResponse>> {
|
): 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(
|
const r = await this.send(
|
||||||
"GET",
|
"GET",
|
||||||
"/organizations/" + organizationId + "/users",
|
`/organizations/${organizationId}/users?${params.toString()}`,
|
||||||
null,
|
null,
|
||||||
true,
|
true,
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ let nextId = 0;
|
|||||||
templateUrl: "./toggle-group.component.html",
|
templateUrl: "./toggle-group.component.html",
|
||||||
preserveWhitespaces: false,
|
preserveWhitespaces: false,
|
||||||
})
|
})
|
||||||
export class ToggleGroupComponent {
|
export class ToggleGroupComponent<TValue = unknown> {
|
||||||
private id = nextId++;
|
private id = nextId++;
|
||||||
name = `bit-toggle-group-${this.id}`;
|
name = `bit-toggle-group-${this.id}`;
|
||||||
|
|
||||||
@Input() selected?: unknown;
|
@Input() selected?: TValue;
|
||||||
@Output() selectedChange = new EventEmitter<unknown>();
|
@Output() selectedChange = new EventEmitter<TValue>();
|
||||||
|
|
||||||
@HostBinding("attr.role") role = "radiogroup";
|
@HostBinding("attr.role") role = "radiogroup";
|
||||||
@HostBinding("class") classList = ["tw-flex"];
|
@HostBinding("class") classList = ["tw-flex"];
|
||||||
|
|
||||||
onInputInteraction(value: unknown) {
|
onInputInteraction(value: TValue) {
|
||||||
this.selected = value;
|
this.selected = value;
|
||||||
this.selectedChange.emit(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";
|
import { ToggleGroupComponent } from "./toggle-group.component";
|
||||||
|
|
||||||
@@ -9,12 +9,12 @@ let nextId = 0;
|
|||||||
templateUrl: "./toggle.component.html",
|
templateUrl: "./toggle.component.html",
|
||||||
preserveWhitespaces: false,
|
preserveWhitespaces: false,
|
||||||
})
|
})
|
||||||
export class ToggleComponent {
|
export class ToggleComponent<TValue> {
|
||||||
id = nextId++;
|
id = nextId++;
|
||||||
|
|
||||||
@Input() value?: string;
|
@Input() value?: TValue;
|
||||||
|
|
||||||
constructor(private groupComponent: ToggleGroupComponent) {}
|
constructor(private groupComponent: ToggleGroupComponent<TValue>) {}
|
||||||
|
|
||||||
@HostBinding("tabIndex") tabIndex = "-1";
|
@HostBinding("tabIndex") tabIndex = "-1";
|
||||||
@HostBinding("class") classList = ["tw-group"];
|
@HostBinding("class") classList = ["tw-group"];
|
||||||
@@ -67,6 +67,9 @@ export class ToggleComponent {
|
|||||||
"tw-py-1.5",
|
"tw-py-1.5",
|
||||||
"tw-px-3",
|
"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.
|
// Fix for badge being pushed slightly lower when inside a button.
|
||||||
// Insipired by bootstrap, which does the same.
|
// Insipired by bootstrap, which does the same.
|
||||||
"[&>[bitBadge]]:tw-relative",
|
"[&>[bitBadge]]:tw-relative",
|
||||||
|
|||||||
Reference in New Issue
Block a user