diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html index 43edf805a4c..55ab9632f4d 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.html @@ -36,7 +36,7 @@ @@ -56,7 +56,7 @@ @@ -90,7 +90,7 @@ diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts index 3ec920d2e4e..a2e16193e11 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm-dialog.component.ts @@ -31,6 +31,8 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { AvatarIdPipe } from "../../pipes/avatar-id.pipe"; + import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component"; import { BulkUserDetails } from "./bulk-status.component"; @@ -46,6 +48,7 @@ type BulkConfirmDialogParams = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncActionsModule, + AvatarIdPipe, AvatarModule, ButtonModule, CalloutModule, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html index 05eacbb5224..60c3fc87341 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html @@ -29,7 +29,7 @@ @@ -67,7 +67,7 @@ diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts index 90b344ed6d3..b01c68233dd 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.ts @@ -17,6 +17,7 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { AvatarIdPipe } from "../../pipes/avatar-id.pipe"; import { DeleteManagedMemberWarningService } from "../../services/delete-managed-member/delete-managed-member-warning.service"; import { BulkUserDetails } from "./bulk-status.component"; @@ -32,6 +33,7 @@ type BulkDeleteDialogParams = { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ + AvatarIdPipe, AvatarModule, BadgeModule, ButtonModule, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.html index 72cb78e08dd..4328e8c7130 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.html @@ -16,7 +16,7 @@
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts index 46ebc331f41..7fb3729e789 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts @@ -18,6 +18,7 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; import { OrganizationUserView } from "../../../core"; +import { AvatarIdPipe } from "../../pipes/avatar-id.pipe"; export type BulkEnableSecretsManagerDialogData = { orgId: string; @@ -31,6 +32,7 @@ export type BulkEnableSecretsManagerDialogData = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncActionsModule, + AvatarIdPipe, AvatarModule, ButtonModule, DialogModule, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html index 477bb95617e..b0bb3d0eb19 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-reinvite-failure-dialog.component.html @@ -32,7 +32,7 @@
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html index c08293889fc..65dde643d5a 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.html @@ -37,7 +37,7 @@ @@ -81,7 +81,7 @@ diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts index b7d2c58d147..fd3b801de99 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove-dialog.component.ts @@ -20,6 +20,8 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { AvatarIdPipe } from "../../pipes/avatar-id.pipe"; + import { BaseBulkRemoveComponent } from "./base-bulk-remove.component"; import { BulkUserDetails } from "./bulk-status.component"; @@ -35,6 +37,7 @@ type BulkRemoveDialogParams = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncActionsModule, + AvatarIdPipe, AvatarModule, ButtonModule, CalloutModule, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html index 4a7d8556548..cc27e760695 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html @@ -44,7 +44,7 @@
@@ -97,7 +97,7 @@
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts index 68a0c0b657f..f180ab71626 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts @@ -26,6 +26,8 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { AvatarIdPipe } from "../../pipes/avatar-id.pipe"; + import { BulkUserDetails } from "./bulk-status.component"; type BulkRestoreDialogParams = { @@ -41,6 +43,7 @@ type BulkRestoreDialogParams = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncActionsModule, + AvatarIdPipe, AvatarModule, ButtonModule, CalloutModule, diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.html index 145f829ba7a..5efbd35e9a5 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.html @@ -20,7 +20,7 @@ diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index 67a3250e3ab..f31f9201df9 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -23,6 +23,7 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; import { OrganizationUserView } from "../../../core/views/organization-user.view"; +import { AvatarIdPipe } from "../../pipes/avatar-id.pipe"; export interface BulkUserDetails { id: string; @@ -59,6 +60,7 @@ type BulkStatusDialogData = { standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ + AvatarIdPipe, AvatarModule, ButtonModule, CalloutModule, diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index f96943e9abb..87e705e4018 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -222,7 +222,7 @@
@@ -283,7 +283,7 @@
diff --git a/apps/web/src/app/admin-console/organizations/members/members.module.ts b/apps/web/src/app/admin-console/organizations/members/members.module.ts index 371c7fc716e..e0502aeeb2d 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.module.ts @@ -27,7 +27,7 @@ import { InviteMembersDialogComponent } from "./components/invite-members-dialog import { UserDialogModule } from "./components/member-dialog"; import { MembersRoutingModule } from "./members-routing.module"; import { MembersComponent } from "./members.component"; -import { UserStatusPipe } from "./pipes"; +import { AvatarIdPipe, UserStatusPipe } from "./pipes"; import { OrganizationMembersService, MemberActionsService, @@ -50,6 +50,7 @@ import { IconModule, BerryComponent, TooltipDirective, + AvatarIdPipe, BulkConfirmDialogComponent, BulkDeleteDialogComponent, BulkEnableSecretsManagerDialogComponent, diff --git a/apps/web/src/app/admin-console/organizations/members/pipes/avatar-id.pipe.spec.ts b/apps/web/src/app/admin-console/organizations/members/pipes/avatar-id.pipe.spec.ts new file mode 100644 index 00000000000..a278c993382 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/pipes/avatar-id.pipe.spec.ts @@ -0,0 +1,17 @@ +import { AvatarIdPipe } from "./avatar-id.pipe"; + +describe("AvatarIdPipe", () => { + let pipe: AvatarIdPipe; + + beforeEach(() => { + pipe = new AvatarIdPipe(); + }); + + it("prefers the account id (userId) when present", () => { + expect(pipe.transform({ id: "org-user-id", userId: "account-id" })).toBe("account-id"); + }); + + it("falls back to the org/provider user id when userId is not provided", () => { + expect(pipe.transform({ id: "org-user-id" })).toBe("org-user-id"); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/pipes/avatar-id.pipe.ts b/apps/web/src/app/admin-console/organizations/members/pipes/avatar-id.pipe.ts new file mode 100644 index 00000000000..1009b0ea47d --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/pipes/avatar-id.pipe.ts @@ -0,0 +1,13 @@ +import { Pipe, PipeTransform } from "@angular/core"; + +import { AvatarIdentifiable, resolveAvatarId } from "../utils/resolve-avatar-id"; + +/** Resolves a member's `bit-avatar` id. See {@link resolveAvatarId}. */ +@Pipe({ + name: "avatarId", +}) +export class AvatarIdPipe implements PipeTransform { + transform(user: AvatarIdentifiable): string { + return resolveAvatarId(user); + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/pipes/index.ts b/apps/web/src/app/admin-console/organizations/members/pipes/index.ts index 67c485ed361..98909f7ee5a 100644 --- a/apps/web/src/app/admin-console/organizations/members/pipes/index.ts +++ b/apps/web/src/app/admin-console/organizations/members/pipes/index.ts @@ -1 +1,2 @@ +export * from "./avatar-id.pipe"; export * from "./user-status.pipe"; diff --git a/apps/web/src/app/admin-console/organizations/members/utils/resolve-avatar-id.spec.ts b/apps/web/src/app/admin-console/organizations/members/utils/resolve-avatar-id.spec.ts new file mode 100644 index 00000000000..daa769afb76 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/utils/resolve-avatar-id.spec.ts @@ -0,0 +1,33 @@ +import { resolveAvatarId } from "./resolve-avatar-id"; + +describe("resolveAvatarId", () => { + it("prefers the account id (userId) when present", () => { + expect(resolveAvatarId({ id: "org-user-id", userId: "account-id" })).toBe("account-id"); + }); + + it("falls back to the org/provider user id when userId is undefined", () => { + expect(resolveAvatarId({ id: "org-user-id", userId: undefined })).toBe("org-user-id"); + }); + + it("falls back to the org/provider user id when userId is not provided", () => { + expect(resolveAvatarId({ id: "org-user-id" })).toBe("org-user-id"); + }); + + it("falls back to the org/provider user id when userId is null", () => { + const user: { id: string; userId?: string } = { id: "org-user-id", userId: null as any }; + expect(resolveAvatarId(user)).toBe("org-user-id"); + }); + + it("returns the same id for the same member regardless of which caller resolves it", () => { + // Regression test: the members list and every bulk action dialog must resolve the same + // avatar identifier for the same member, otherwise the same person shows a different + // avatar color in different parts of the UI. + const invitedMember: { id: string; userId?: string } = { id: "org-user-id", userId: undefined }; + + const idAsRenderedByMembersList = resolveAvatarId(invitedMember); + const idAsRenderedByBulkDialog = resolveAvatarId(invitedMember); + + expect(idAsRenderedByMembersList).toBe(idAsRenderedByBulkDialog); + expect(idAsRenderedByMembersList).toBe("org-user-id"); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/members/utils/resolve-avatar-id.ts b/apps/web/src/app/admin-console/organizations/members/utils/resolve-avatar-id.ts new file mode 100644 index 00000000000..54d90f74035 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/utils/resolve-avatar-id.ts @@ -0,0 +1,12 @@ +export interface AvatarIdentifiable { + id: string; + userId?: string; +} + +/** + * Resolves a consistent avatar id for a member, preferring their account id (`userId`) and + * falling back to the org/provider user id (e.g. for invited members with no linked account). + */ +export function resolveAvatarId(user: AvatarIdentifiable): string { + return user.userId ?? user.id; +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts index 181089fd487..52f5d4882c0 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-confirm-dialog.component.ts @@ -33,6 +33,7 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component"; import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; +import { AvatarIdPipe } from "@bitwarden/web-vault/app/admin-console/organizations/members/pipes/avatar-id.pipe"; type BulkConfirmDialogParams = { providerId: string; @@ -47,6 +48,7 @@ type BulkConfirmDialogParams = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncActionsModule, + AvatarIdPipe, AvatarModule, ButtonModule, CalloutModule, diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts index 9e21f14579d..9cb0be4442e 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/dialogs/bulk-remove-dialog.component.ts @@ -19,6 +19,7 @@ import { import { I18nPipe } from "@bitwarden/ui-common"; import { BaseBulkRemoveComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component"; import { BulkUserDetails } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/bulk-status.component"; +import { AvatarIdPipe } from "@bitwarden/web-vault/app/admin-console/organizations/members/pipes/avatar-id.pipe"; type BulkRemoveDialogParams = { providerId: string; @@ -33,6 +34,7 @@ type BulkRemoveDialogParams = { changeDetection: ChangeDetectionStrategy.OnPush, imports: [ AsyncActionsModule, + AvatarIdPipe, AvatarModule, ButtonModule, CalloutModule,