1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23: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:
Shane Melton
2022-12-14 13:23:33 -08:00
committed by GitHub
parent 443b3e0367
commit 913af652c5
41 changed files with 584 additions and 380 deletions

View File

@@ -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, []);
}
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"
)

View File

@@ -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;
}
}

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -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";

View File

@@ -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",

View File

@@ -0,0 +1 @@
export * from "./members.module";

View File

@@ -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 {}

View 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);
}
}

View 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>

View File

@@ -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
) {

View File

@@ -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",

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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";

View File

@@ -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,

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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",