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: {
|