mirror of
https://github.com/bitwarden/clients.git
synced 2026-07-21 21:17:06 +08:00
[PM-13643] avatars in org member remove/revoke modal do not match members page fix (#21931)
* Implement AvatarIdPipe for consistent avatar identification across components and update usages in member dialogs and lists. Add tests for AvatarIdPipe and resolveAvatarId utility function to ensure correct behavior. * Add AvatarIdPipe to bulk confirm and remove dialog components for enhanced avatar handling * Refactor AvatarIdPipe and resolveAvatarId utility function documentation for clarity and conciseness. Remove redundant comments while retaining essential information about avatar id resolution.
This commit is contained in:
parent
49b38db499
commit
631f4e0099
@ -36,7 +36,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
@ -56,7 +56,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
@ -90,7 +90,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
@ -67,7 +67,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[id]="u | avatarId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[id]="u | avatarId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
@ -81,7 +81,7 @@
|
||||
<td bitCell class="tw-w-5">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
<div class="tw-mr-6">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</div>
|
||||
@ -97,7 +97,7 @@
|
||||
<div class="tw-mr-6">
|
||||
<bit-avatar
|
||||
[text]="user | userName"
|
||||
[id]="user.userId ?? user.id"
|
||||
[id]="user | avatarId"
|
||||
[color]="user.avatarColor"
|
||||
></bit-avatar>
|
||||
</div>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
<td width="30" bitCell>
|
||||
<bit-avatar
|
||||
[text]="item.user | userName"
|
||||
[id]="item.user.userId ?? item.user.id"
|
||||
[id]="item.user | avatarId"
|
||||
[color]="item.user.avatarColor"
|
||||
></bit-avatar>
|
||||
</td>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -222,7 +222,7 @@
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[id]="u | avatarId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
@ -283,7 +283,7 @@
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-avatar
|
||||
[text]="u | userName"
|
||||
[id]="u.userId"
|
||||
[id]="u | avatarId"
|
||||
[color]="u.avatarColor"
|
||||
class="tw-mr-3"
|
||||
></bit-avatar>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./avatar-id.pipe";
|
||||
export * from "./user-status.pipe";
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user