diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index c632d78cde08..16dc0bd4d3f8 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -67,12 +67,12 @@ } @if (!batchBarService?.enabled()) { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts index 3bcc858c1e5e..bd18f91ed80e 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.spec.ts @@ -11,7 +11,12 @@ import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/res import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils"; import { MenuModule, TableModule } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; -import { RoutedVaultFilterService, RoutedVaultFilterModel, VaultItem } from "@bitwarden/vault"; +import { + RoutedVaultFilterService, + RoutedVaultFilterModel, + VaultCopyButtonsService, + VaultItem, +} from "@bitwarden/vault"; import { VaultItemsComponent } from "./vault-items.component"; @@ -70,6 +75,12 @@ describe("VaultItemsComponent", () => { getFeatureFlag$: jest.fn().mockReturnValue(of(false)), }, }, + { + provide: VaultCopyButtonsService, + useValue: { + showQuickCopyActions$: of(false), + }, + }, ], }); @@ -77,6 +88,20 @@ describe("VaultItemsComponent", () => { component = fixture.componentInstance; }); + describe("optionsColumnWidthClass", () => { + it("reserves room for the quick copy icons when they are shown", () => { + expect(component["optionsColumnWidthClass"](true, true)).toBe("tw-w-48"); + }); + + it("reserves room for the combined copy and launch actions", () => { + expect(component["optionsColumnWidthClass"](true, false)).toBe("tw-w-32"); + }); + + it("only fits the options menu when there are no copy or launch actions", () => { + expect(component["optionsColumnWidthClass"](false, false)).toBe("tw-w-12"); + }); + }); + describe("bulkArchiveAllowed", () => { it("returns false when no items are selected", () => { component.userCanArchive = true; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 5db829c1d3c1..0a9fba7f4c60 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -36,6 +36,7 @@ import { compareVaultItems, RoutedVaultFilterService, VaultBatchBarService, + VaultCopyButtonsService, VaultItem, } from "@bitwarden/vault"; @@ -175,8 +176,11 @@ export class VaultItemsComponent { protected canRestoreSelected$: Observable; protected disableMenu$: Observable; protected showCopyAndLaunchActions$: Observable; + protected showQuickCopyActions$: Observable; private restrictedTypes: RestrictedCipherType[] = []; + private readonly vaultCopyButtonsService = inject(VaultCopyButtonsService); + constructor( protected cipherAuthorizationService: CipherAuthorizationService, protected restrictedItemTypesService: RestrictedItemTypesService, @@ -186,6 +190,11 @@ export class VaultItemsComponent { this.showCopyAndLaunchActions$ = this.configService.getFeatureFlag$( FeatureFlag.PM28091_AddCopyAndQuickLaunchActions, ); + + this.showQuickCopyActions$ = combineLatest([ + this.configService.getFeatureFlag$(FeatureFlag.PM40435_QuickCopyIconSetting), + this.vaultCopyButtonsService.showQuickCopyActions$, + ]).pipe(map(([flagEnabled, settingEnabled]) => flagEnabled && settingEnabled)); this.canDeleteSelected$ = this.selection.changed.pipe( startWith(null), switchMap(() => { @@ -280,6 +289,23 @@ export class VaultItemsComponent { return this.showCollections || this.showGroups || this.showOwner; } + /** + * Width of the options column. A row's copy and launch actions are absolutely positioned to the + * left of its options menu, so the column has to be wide enough to hold them all. Otherwise they + * render on top of the preceding columns, e.g. the owner badge. + */ + protected optionsColumnWidthClass( + showCopyAndLaunchActions: boolean, + showQuickCopyActions: boolean, + ): string { + if (showCopyAndLaunchActions) { + // Quick copy shows an icon per copyable field rather than a single combined copy menu + return showQuickCopyActions ? "tw-w-48" : "tw-w-32"; + } + + return this.batchBarService?.enabled() ? "tw-w-24" : "tw-w-12"; + } + get isAllSelected() { // Check selection against sorted items to match toggleAll() behavior const sortedItems = this.getSortedEditableItems(); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.spec.ts b/apps/web/src/app/vault/individual-vault/vault.component.spec.ts index db0dd66eeb2a..81a0a25ad4e4 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.spec.ts @@ -63,6 +63,7 @@ import { RoutedVaultFilterBridgeService, RoutedVaultFilterService, VaultBatchBarService, + VaultCopyButtonsService, VaultFilter, VaultFilterServiceAbstraction, VaultItem, @@ -302,6 +303,10 @@ describe("VaultComponent", () => { provide: VaultItemsTransferService, useValue: mock(), }, + { + provide: VaultCopyButtonsService, + useValue: { showQuickCopyActions$: of(false) }, + }, { provide: VaultBatchBarService, useValue: {