1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 19:04:01 +00:00
Files
browser/apps/web/src/app/admin-console/organizations/members/members.component.html
Jared 7a3bfa5f95 [PM-29771] Make invitation non-plural if only 1 member selected (#18684)
* Make invitation non-plural if only 1 member selected

* Add isSingleInvite as per Jimmy's suggestion
2026-02-17 09:52:01 -08:00

508 lines
20 KiB
HTML

@let organization = this.organization();
@let dataSource = this.dataSource();
@let bulkActions = bulkMenuOptions$ | async;
@let showConfirmBanner = showConfirmBanner$ | async;
@let isProcessing = this.isProcessing();
@let isSingleInvite = isSingleInvite$ | async;
@if (organization && dataSource) {
<app-organization-free-trial-warning
[organization]="organization"
(clicked)="billingConstraint.navigateToPaymentMethod(organization)"
>
</app-organization-free-trial-warning>
<app-header>
<bit-search
class="tw-grow"
[formControl]="searchControl"
[placeholder]="'searchMembers' | i18n"
></bit-search>
@if (showUserManagementControls()) {
<button
type="button"
bitButton
buttonType="primary"
(click)="invite(organization)"
[disabled]="!firstLoaded()"
>
<i class="bwi bwi-plus bwi-fw tw-me-2" aria-hidden="true"></i>
{{ "inviteMember" | i18n }}
</button>
}
</app-header>
<div class="tw-mb-4 tw-flex tw-flex-col tw-space-y-4">
@if (showUserManagementControls()) {
<bit-toggle-group
[selected]="statusToggle | async"
(selectedChange)="statusToggle.next($event)"
[attr.aria-label]="'memberStatusFilter' | i18n"
>
<bit-toggle [value]="undefined">
{{ "all" | i18n }}
@if (dataSource.activeUserCount; as allCount) {
<span bitBadge variant="info">{{ allCount }}</span>
}
</bit-toggle>
<bit-toggle [value]="userStatusType.Invited">
{{ "invited" | i18n }}
@if (dataSource.invitedUserCount; as invitedCount) {
<span bitBadge variant="info">{{ invitedCount }}</span>
}
</bit-toggle>
<bit-toggle [value]="userStatusType.Accepted">
{{ "needsConfirmation" | i18n }}
@if (dataSource.acceptedUserCount; as acceptedUserCount) {
<span bitBadge variant="info">{{ acceptedUserCount }}</span>
}
</bit-toggle>
<bit-toggle [value]="userStatusType.Revoked">
{{ "revoked" | i18n }}
@if (dataSource.revokedUserCount; as revokedCount) {
<span bitBadge variant="info">{{ revokedCount }}</span>
}
</bit-toggle>
</bit-toggle-group>
}
</div>
@if (!firstLoaded() || !organization || !dataSource) {
<i
class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
} @else {
@if (!dataSource.filteredData?.length) {
<p>{{ "noMembersInList" | i18n }}</p>
}
@if (dataSource.filteredData?.length) {
@if (showConfirmBanner) {
<bit-callout type="info" title="{{ 'confirmUsers' | i18n }}" icon="bwi-check-circle">
{{ "usersNeedConfirmed" | i18n }}
</bit-callout>
}
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
from overflowing the <main> element. -->
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
<bit-table [dataSource]="dataSource">
<ng-container header>
<tr>
@if (showUserManagementControls()) {
<th bitCell class="tw-w-20">
<input
type="checkbox"
bitCheckbox
class="tw-mr-1"
(change)="dataSource.checkAllFilteredUsers($any($event.target).checked)"
id="selectAll"
/>
<label class="tw-mb-0 !tw-font-medium !tw-text-muted" for="selectAll">{{
"all" | i18n
}}</label>
</th>
}
<th bitCell bitSortable="email" default>{{ "name" | i18n }}</th>
<th bitCell>{{ (organization.useGroups ? "groups" : "collections") | i18n }}</th>
<th bitCell bitSortable="type">{{ "role" | i18n }}</th>
<th bitCell>{{ "policies" | i18n }}</th>
<th bitCell class="tw-w-10">
@if (showUserManagementControls()) {
<th bitCell>
<div class="tw-flex tw-flex-row tw-items-center tw-justify-end tw-gap-2">
<button
type="button"
bitIconButton="bwi-download"
size="small"
[bitAction]="exportMembers"
[disabled]="!firstLoaded"
label="{{ 'export' | i18n }}"
></button>
<button
[bitMenuTriggerFor]="headerMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
label="{{ 'options' | i18n }}"
></button>
</div>
</th>
}
<bit-menu #headerMenu>
@if (canUseSecretsManager()) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkEnableSM(organization)"
>
{{ "activateSecretsManager" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
}
@if (bulkActions.showBulkReinviteUsers) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkReinvite(organization)"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ (isSingleInvite ? "resendInvitation" : "reinviteSelected") | i18n }}
</button>
}
@if (bulkActions.showBulkConfirmUsers) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkConfirm(organization)"
>
<span class="tw-text-success">
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</span>
</button>
}
@if (bulkActions.showBulkRestoreUsers) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkRevokeOrRestore(false, organization)"
>
<i class="bwi bwi-fw bwi-plus-circle" aria-hidden="true"></i>
{{ "restoreAccess" | i18n }}
</button>
}
@if (bulkActions.showBulkRevokeUsers) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkRevokeOrRestore(true, organization)"
>
<i class="bwi bwi-fw bwi-minus-circle" aria-hidden="true"></i>
{{ "revokeAccess" | i18n }}
</button>
}
@if (bulkActions.showBulkRemoveUsers) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkRemove(organization)"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-fw bwi-close"></i>
{{ "remove" | i18n }}
</span>
</button>
}
@if (bulkActions.showBulkDeleteUsers) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : bulkDelete(organization)"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-fw bwi-trash"></i>
{{ "delete" | i18n }}
</span>
</button>
}
</bit-menu>
</th>
</tr>
</ng-container>
<ng-template body let-rows$>
<tr
bitRow
*cdkVirtualFor="let u of rows$"
alignContent="middle"
[ngClass]="rowHeightClass"
>
@if (showUserManagementControls()) {
<td bitCell (click)="dataSource.checkUser(u)">
<input type="checkbox" bitCheckbox [(ngModel)]="u.checked" />
</td>
<td bitCell (click)="edit(u, organization)" class="tw-cursor-pointer">
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-row tw-gap-2">
<button type="button" bitLink>
{{ u.name ?? u.email }}
</button>
@if (u.status === userStatusType.Invited) {
<span bitBadge class="tw-text-xs" variant="secondary">
{{ "invited" | i18n }}
</span>
}
@if (u.status === userStatusType.Accepted) {
<span bitBadge class="tw-text-xs" variant="warning">
{{ "needsConfirmation" | i18n }}
</span>
}
@if (u.status === userStatusType.Revoked) {
<span bitBadge class="tw-text-xs" variant="secondary">
{{ "revoked" | i18n }}
</span>
}
</div>
@if (u.name) {
<div class="tw-text-sm tw-text-muted">
{{ u.email }}
</div>
}
</div>
</div>
</td>
} @else {
<td bitCell>
<div class="tw-flex tw-items-center">
<bit-avatar
size="small"
[text]="u | userName"
[id]="u.userId"
[color]="u.avatarColor"
class="tw-mr-3"
></bit-avatar>
<div class="tw-flex tw-flex-col">
<div class="tw-flex tw-flex-row tw-gap-2">
<span>{{ u.name ?? u.email }}</span>
@if (u.status === userStatusType.Invited) {
<span bitBadge class="tw-text-xs" variant="secondary">
{{ "invited" | i18n }}
</span>
}
@if (u.status === userStatusType.Accepted) {
<span bitBadge class="tw-text-xs" variant="warning">
{{ "needsConfirmation" | i18n }}
</span>
}
@if (u.status === userStatusType.Revoked) {
<span bitBadge class="tw-text-xs" variant="secondary">
{{ "revoked" | i18n }}
</span>
}
</div>
@if (u.name) {
<div class="tw-text-sm tw-text-muted">
{{ u.email }}
</div>
}
</div>
</div>
</td>
}
@if (showUserManagementControls()) {
<td
bitCell
(click)="
edit(
u,
organization,
organization.useGroups ? memberTab.Groups : memberTab.Collections
)
"
class="tw-cursor-pointer"
>
<bit-badge-list
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
</td>
} @else {
<td bitCell>
<bit-badge-list
[items]="organization.useGroups ? u.groupNames : u.collectionNames"
[maxItems]="3"
variant="secondary"
></bit-badge-list>
</td>
}
@if (showUserManagementControls()) {
<td
bitCell
(click)="edit(u, organization, memberTab.Role)"
class="tw-cursor-pointer tw-text-sm tw-text-muted"
>
{{ u.type | userType }}
</td>
} @else {
<td bitCell class="tw-text-sm tw-text-muted">
{{ u.type | userType }}
</td>
}
<td bitCell class="tw-text-muted">
@if (u.twoFactorEnabled) {
<i
class="bwi bwi-lock"
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "userUsingTwoStep" | i18n }}</span>
}
@let resetPasswordPolicyEnabled = resetPasswordPolicyEnabled$ | async;
@if (showEnrolledStatus(u, organization, resetPasswordPolicyEnabled)) {
<i
class="bwi bwi-key"
title="{{ 'enrolledAccountRecovery' | i18n }}"
aria-hidden="true"
></i>
<span class="tw-sr-only">{{ "enrolledAccountRecovery" | i18n }}</span>
}
</td>
<td bitCell>
<div class="tw-flex tw-flex-row tw-items-center tw-justify-end tw-gap-2">
<div class="tw-w-[32px]"></div>
<button
[bitMenuTriggerFor]="rowMenu"
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
label="{{ 'options' | i18n }}"
></button>
</div>
<bit-menu #rowMenu>
@if (showUserManagementControls()) {
@if (u.status === userStatusType.Invited) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : reinvite(u, organization)"
>
<i aria-hidden="true" class="bwi bwi-envelope"></i>
{{ "resendInvitation" | i18n }}
</button>
}
@if (u.status === userStatusType.Accepted) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : confirm(u, organization)"
>
<span class="tw-text-success">
<i aria-hidden="true" class="bwi bwi-check"></i> {{ "confirm" | i18n }}
</span>
</button>
}
@if (
u.status === userStatusType.Accepted || u.status === userStatusType.Invited
) {
<bit-menu-divider></bit-menu-divider>
}
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : edit(u, organization, memberTab.Role)"
>
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "memberRole" | i18n }}
</button>
@if (organization.useGroups) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : edit(u, organization, memberTab.Groups)"
>
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "groups" | i18n }}
</button>
}
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : edit(u, organization, memberTab.Collections)"
>
<i aria-hidden="true" class="bwi bwi-collection-shared"></i>
{{ "collections" | i18n }}
</button>
<bit-menu-divider></bit-menu-divider>
@if (organization.useEvents && u.status === userStatusType.Confirmed) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : openEventsDialog(u, organization)"
>
<i aria-hidden="true" class="bwi bwi-file-text"></i>
{{ "eventLogs" | i18n }}
</button>
}
}
<!-- Account recovery is available to all users with appropriate permissions -->
@if (allowResetPassword(u, organization, resetPasswordPolicyEnabled)) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : resetPassword(u, organization)"
>
<i aria-hidden="true" class="bwi bwi-key"></i> {{ "recoverAccount" | i18n }}
</button>
}
@if (showUserManagementControls()) {
@if (u.status === userStatusType.Revoked) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : restore(u, organization)"
>
<i aria-hidden="true" class="bwi bwi-plus-circle"></i>
{{ "restoreAccess" | i18n }}
</button>
}
@if (u.status !== userStatusType.Revoked) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : revoke(u, organization)"
>
<i aria-hidden="true" class="bwi bwi-minus-circle"></i>
{{ "revokeAccess" | i18n }}
</button>
}
@if (!u.managedByOrganization) {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : remove(u, organization)"
>
<span class="tw-text-danger">
<i aria-hidden="true" class="bwi bwi-close"></i> {{ "remove" | i18n }}
</span>
</button>
} @else {
<button
type="button"
bitMenuItem
(click)="isProcessing ? null : deleteUser(u, organization)"
>
<span class="tw-text-danger">
<i class="bwi bwi-trash" aria-hidden="true"></i>
{{ "delete" | i18n }}
</span>
</button>
}
}
</bit-menu>
</td>
</tr>
</ng-template>
</bit-table>
</cdk-virtual-scroll-viewport>
}
}
}