Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
EnvironmentService,
} from "@bitwarden/common/platform/abstractions/environment.service";

import { PeopleTableDataSource } from "./people-table-data-source";
import { PeopleTableDataSource, peopleFilter } from "./people-table-data-source";

interface MockUser {
id: string;
Expand Down Expand Up @@ -109,14 +109,52 @@ describe("PeopleTableDataSource", () => {
{ ...createMockUser("5"), status: OrganizationUserStatusType.Confirmed },
{ ...createMockUser("6"), status: OrganizationUserStatusType.Confirmed },
{ ...createMockUser("7"), status: OrganizationUserStatusType.Revoked },
{ ...createMockUser("8"), status: OrganizationUserStatusType.Staged },
{ ...createMockUser("9"), status: OrganizationUserStatusType.Staged },
];
dataSource.data = users;

expect(dataSource.invitedUserCount).toBe(2);
expect(dataSource.acceptedUserCount).toBe(1);
expect(dataSource.confirmedUserCount).toBe(3);
expect(dataSource.revokedUserCount).toBe(1);
expect(dataSource.activeUserCount).toBe(6); // All except revoked
expect(dataSource.stagedUserCount).toBe(2);
// All except revoked. Staged users are active and counted here.
expect(dataSource.activeUserCount).toBe(8);
});
});

describe("peopleFilter status filtering", () => {
const userWithStatus = (id: string, status: OrganizationUserStatusType) =>
({ ...createMockUser(id), status }) as any;

const staged = userWithStatus("1", OrganizationUserStatusType.Staged);
const invited = userWithStatus("2", OrganizationUserStatusType.Invited);
const confirmed = userWithStatus("3", OrganizationUserStatusType.Confirmed);
const revoked = userWithStatus("4", OrganizationUserStatusType.Revoked);

it("matches only Staged users when filtering by the Staged status", () => {
const matchesStaged = peopleFilter("", OrganizationUserStatusType.Staged);

expect(matchesStaged(staged)).toBe(true);
expect(matchesStaged(invited)).toBe(false);
expect(matchesStaged(confirmed)).toBe(false);
expect(matchesStaged(revoked)).toBe(false);
});

it("requires both the status and the text search to match", () => {
// createMockUser("1") => email "user1@example.com"
expect(peopleFilter("user1", OrganizationUserStatusType.Staged)(staged)).toBe(true);
expect(peopleFilter("nomatch", OrganizationUserStatusType.Staged)(staged)).toBe(false);
});

it("includes Staged in the default 'All' view and excludes only Revoked", () => {
const matchesAll = peopleFilter("", undefined);

expect(matchesAll(staged)).toBe(true);
expect(matchesAll(invited)).toBe(true);
expect(matchesAll(confirmed)).toBe(true);
expect(matchesAll(revoked)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
acceptedUserCount: number;
confirmedUserCount: number;
revokedUserCount: number;
stagedUserCount: number;

/** True when increased bulk limit feature is enabled (cloud environment) */
readonly isIncreasedBulkLimitEnabled: Signal<boolean>;
Expand All @@ -100,6 +101,8 @@ export abstract class PeopleTableDataSource<T extends UserViewTypes> extends Tab
this.data?.filter((u) => u.status === this.statusType.Confirmed).length ?? 0;
this.revokedUserCount =
this.data?.filter((u) => u.status === this.statusType.Revoked).length ?? 0;
this.stagedUserCount =
this.data?.filter((u) => u.status === OrganizationUserStatusType.Staged).length ?? 0;

this.checkedUsersUpdated$.next();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
<bit-berry [value]="dataSource.activeUserCount"></bit-berry>
</bit-toggle>

@if (dataSource.stagedUserCount > 0) {
<bit-toggle [value]="userStatusType.Staged">
{{ "staged" | i18n }}
<bit-berry [value]="dataSource.stagedUserCount"></bit-berry>
</bit-toggle>
}

<bit-toggle [value]="userStatusType.Invited">
{{ "invited" | i18n }}
<bit-berry [value]="dataSource.invitedUserCount"></bit-berry>
Expand Down Expand Up @@ -224,6 +231,19 @@
<button type="button" bitLink>
{{ u.name ?? u.email }}
</button>
@if (u.status === userStatusType.Staged) {
<span
bitBadge
class="tw-text-xs"
variant="secondary"
startIcon="bwi-info"
title=""
[bitTooltip]="'stagedMemberTooltip' | i18n"
tooltipPosition="below-center"
>
{{ "staged" | i18n }}
</span>
}
@if (u.status === userStatusType.Invited) {
<span bitBadge class="tw-text-xs" variant="secondary">
{{ "invited" | i18n }}
Expand Down Expand Up @@ -270,6 +290,20 @@
<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.Staged) {
<span
bitBadge
class="tw-text-xs"
variant="secondary"
startIcon="bwi-info"
title=""
[bitTooltip]="'stagedMemberTooltip' | i18n"
[addTooltipToDescribedby]="true"
tooltipPosition="below-center"
>
{{ "staged" | i18n }}
</span>
}
@if (u.status === userStatusType.Invited) {
<span bitBadge class="tw-text-xs" variant="secondary">
{{ "invited" | i18n }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ describe("UserStatusPipe", () => {
expect(i18nService.t).toHaveBeenCalledWith("confirmed");
});

it("transforms OrganizationUserStatusType.Staged to 'staged'", () => {
expect(pipe.transform(OrganizationUserStatusType.Staged)).toBe("staged");
expect(i18nService.t).toHaveBeenCalledWith("staged");
});

it("transforms OrganizationUserStatusType.Revoked to 'revoked'", () => {
expect(pipe.transform(OrganizationUserStatusType.Revoked)).toBe("revoked");
expect(i18nService.t).toHaveBeenCalledWith("revoked");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export class UserStatusPipe implements PipeTransform {
return this.i18nService.t("accepted");
case OrganizationUserStatusType.Confirmed:
return this.i18nService.t("confirmed");
case OrganizationUserStatusType.Staged:
return this.i18nService.t("staged");
case OrganizationUserStatusType.Revoked:
return this.i18nService.t("revoked");
default:
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6718,6 +6718,12 @@
"accepted": {
"message": "Accepted"
},
"staged": {
"message": "Staged"
},
"stagedMemberTooltip": {
"message": "Pending account invitation"
},
Comment thread
sven-bitwarden marked this conversation as resolved.
"sendLink": {
"message": "Send link",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum OrganizationUserStatusType {
Invited = 0,
Accepted = 1,
Confirmed = 2,
Revoked = -1,
}
import { OrganizationUserStatusType as SdkOrganizationUserStatusType } from "@bitwarden/sdk-internal";

// Re-export the SdkOrganizationUserStatusType so we have a single source of truth
export {
/** @deprecated Import directly from @bitwarden/sdk-internal instead. **/
SdkOrganizationUserStatusType as OrganizationUserStatusType
};
Loading
Loading