diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
deleted file mode 100644
index bb3a7b12096..00000000000
--- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html
+++ /dev/null
@@ -1,218 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
deleted file mode 100644
index a51e5f5406a..00000000000
--- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { CommonModule } from "@angular/common";
-import { Component, Input, inject } from "@angular/core";
-
-import { JslibModule } from "@bitwarden/angular/jslib.module";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { CipherType } from "@bitwarden/common/vault/enums";
-import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
-import { IconButtonModule, ItemModule, MenuModule } from "@bitwarden/components";
-import { CopyCipherFieldDirective } from "@bitwarden/vault";
-
-import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
-
-type CipherItem = {
- value: string;
- key: string;
-};
-
-@Component({
- standalone: true,
- selector: "app-item-copy-actions",
- templateUrl: "item-copy-actions.component.html",
- imports: [
- ItemModule,
- IconButtonModule,
- JslibModule,
- MenuModule,
- CommonModule,
- CopyCipherFieldDirective,
- ],
-})
-export class ItemCopyActionsComponent {
- protected showQuickCopyActions$ = inject(VaultPopupCopyButtonsService).showQuickCopyActions$;
-
- @Input() cipher: CipherView;
-
- protected CipherType = CipherType;
-
- get hasLoginValues() {
- return (
- !!this.cipher.login.hasTotp || !!this.cipher.login.password || !!this.cipher.login.username
- );
- }
-
- get singleCopiableLogin() {
- const loginItems: CipherItem[] = [
- { value: this.cipher.login.username, key: "username" },
- { value: this.cipher.login.password, key: "password" },
- { value: this.cipher.login.totp, key: "totp" },
- ];
- // If both the password and username are visible but the password is hidden, return the username
- if (!this.cipher.viewPassword && this.cipher.login.username && this.cipher.login.password) {
- return { value: this.cipher.login.username, key: this.i18nService.t("username") };
- }
- return this.findSingleCopiableItem(loginItems);
- }
-
- get singleCopiableCard() {
- const cardItems: CipherItem[] = [
- { value: this.cipher.card.code, key: "code" },
- { value: this.cipher.card.number, key: "number" },
- ];
- return this.findSingleCopiableItem(cardItems);
- }
-
- get singleCopiableIdentity() {
- const identityItems: CipherItem[] = [
- { value: this.cipher.identity.fullAddressForCopy, key: "address" },
- { value: this.cipher.identity.email, key: "email" },
- { value: this.cipher.identity.username, key: "username" },
- { value: this.cipher.identity.phone, key: "phone" },
- ];
- return this.findSingleCopiableItem(identityItems);
- }
-
- /*
- * Given a list of CipherItems, if there is only one item with a value,
- * return it with the translated key. Otherwise return null
- */
- findSingleCopiableItem(items: { value: string; key: string }[]): CipherItem | null {
- const singleItemWithValue = items.find(
- (key) => key.value && items.every((f) => f === key || !f.value),
- );
- return singleItemWithValue
- ? { value: singleItemWithValue.value, key: this.i18nService.t(singleItemWithValue.key) }
- : null;
- }
-
- get hasCardValues() {
- return !!this.cipher.card.code || !!this.cipher.card.number;
- }
-
- get hasIdentityValues() {
- return (
- !!this.cipher.identity.fullAddressForCopy ||
- !!this.cipher.identity.email ||
- !!this.cipher.identity.username ||
- !!this.cipher.identity.phone
- );
- }
-
- get hasSecureNoteValue() {
- return !!this.cipher.notes;
- }
-
- get hasSshKeyValues() {
- return (
- !!this.cipher.sshKey.privateKey ||
- !!this.cipher.sshKey.publicKey ||
- !!this.cipher.sshKey.keyFingerprint
- );
- }
-
- constructor(private i18nService: I18nService) {}
-}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html
index 6e6e30b359b..86b5cd08fd0 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html
@@ -1,39 +1,37 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+ {{ favoriteText | i18n }}
+
+
+
+ {{ "clone" | i18n }}
+
+
+ {{ "assignToCollections" | i18n }}
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts
index 94b4c2b855b..8817aa2ab6b 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts
@@ -15,13 +15,7 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
-import {
- DialogService,
- IconButtonModule,
- ItemModule,
- MenuModule,
- ToastService,
-} from "@bitwarden/components";
+import { DialogService, IconButtonModule, MenuModule, ToastService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
@@ -31,7 +25,7 @@ import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
standalone: true,
selector: "app-item-more-options",
templateUrl: "./item-more-options.component.html",
- imports: [ItemModule, IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
+ imports: [IconButtonModule, MenuModule, CommonModule, JslibModule, RouterModule],
})
export class ItemMoreOptionsComponent implements OnInit {
private _cipher$ = new BehaviorSubject(undefined);
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
index a55bba622e4..07dd770ff05 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
@@ -81,18 +81,18 @@
-
-
-
-
- {{ group.subHeaderKey | i18n }}
-
-
+
+
+
+ {{ group.subHeaderKey | i18n }}
+
+
-
+
+
-
-
+
+ @let cbv = copyButtonsService.toCopyButtonsView(cipher);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ "copyUsername" | i18n }}
+
+
+ {{ "copyPassword" | i18n }}
+
+
+ {{ "copyVerificationCode" | i18n }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ "copyNumber" | i18n }}
+
+
+ {{ "copySecurityCode" | i18n }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ "copyUsername" | i18n }}
+
+
+ {{ "copyEmail" | i18n }}
+
+
+ {{ "copyPhone" | i18n }}
+
+
+ {{ "copyAddress" | i18n }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ "copyPrivateKey" | i18n }}
+
+
+ {{ "copyPublicKey" | i18n }}
+
+
+ {{ "copyFingerprint" | i18n }}
+
+
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts
index 6df1bdf8ae5..182d2a3d1d7 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts
@@ -42,8 +42,10 @@ import {
SectionComponent,
SectionHeaderComponent,
TypographyModule,
+ MenuModule,
} from "@bitwarden/components";
import {
+ CopyCipherFieldDirective,
DecryptionFailureDialogComponent,
OrgIconDirective,
PasswordRepromptService,
@@ -52,9 +54,9 @@ import {
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
+import { VaultPopupCopyButtonsService } from "../../../services/vault-popup-copy-buttons.service";
import { VaultPopupSectionService } from "../../../services/vault-popup-section.service";
import { PopupCipherView } from "../../../views/popup-cipher.view";
-import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component";
@Component({
@@ -68,13 +70,14 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
TypographyModule,
JslibModule,
SectionHeaderComponent,
- ItemCopyActionsComponent,
ItemMoreOptionsComponent,
OrgIconDirective,
ScrollingModule,
DisclosureComponent,
DisclosureTriggerForDirective,
DecryptionFailureDialogComponent,
+ MenuModule,
+ CopyCipherFieldDirective,
],
selector: "app-vault-list-items-container",
templateUrl: "vault-list-items-container.component.html",
@@ -84,6 +87,9 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options
export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
private compactModeService = inject(CompactModeService);
private vaultPopupSectionService = inject(VaultPopupSectionService);
+ protected copyButtonsService = inject(VaultPopupCopyButtonsService);
+
+ protected CipherType = CipherType;
@ViewChild(CdkVirtualScrollViewport, { static: false }) viewPort: CdkVirtualScrollViewport;
@ViewChild(DisclosureComponent) disclosure: DisclosureComponent;
diff --git a/apps/browser/src/vault/popup/services/vault-popup-copy-buttons.service.ts b/apps/browser/src/vault/popup/services/vault-popup-copy-buttons.service.ts
index 6ea01f9b109..d32e24dfca5 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-copy-buttons.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-copy-buttons.service.ts
@@ -1,14 +1,32 @@
import { inject, Injectable } from "@angular/core";
import { map, Observable, shareReplay } from "rxjs";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
GlobalStateProvider,
KeyDefinition,
VAULT_APPEARANCE,
} from "@bitwarden/common/platform/state";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
export type CopyButtonDisplayMode = "combined" | "quick";
+type CipherItem = {
+ value: string;
+ key: string;
+};
+
+type CopyButtonsView = {
+ hasLoginValues: boolean;
+ hasCardValues: boolean;
+ hasIdentityValues: boolean;
+ hasSecureNoteValue: boolean;
+ hasSshKeyValues: boolean;
+ singleCopiableLogin: CipherItem | null;
+ singleCopiableCard: CipherItem | null;
+ singleCopiableIdentity: CipherItem | null;
+};
+
const COPY_BUTTON = new KeyDefinition(VAULT_APPEARANCE, "copyButtons", {
deserializer: (s) => s,
});
@@ -20,6 +38,7 @@ const COPY_BUTTON = new KeyDefinition(VAULT_APPEARANCE, "
export class VaultPopupCopyButtonsService {
private readonly DEFAULT_DISPLAY_MODE = "combined";
private state = inject(GlobalStateProvider).get(COPY_BUTTON);
+ private i18nService = inject(I18nService);
displayMode$: Observable = this.state.state$.pipe(
map((state) => state ?? this.DEFAULT_DISPLAY_MODE),
@@ -37,4 +56,88 @@ export class VaultPopupCopyButtonsService {
async setShowQuickCopyActions(value: boolean) {
await this.setDisplayMode(value ? "quick" : "combined");
}
+
+ toCopyButtonsView(cipher: CipherView): CopyButtonsView {
+ return {
+ hasLoginValues: this.hasLoginValues(cipher),
+ hasCardValues: this.hasCardValues(cipher),
+ hasIdentityValues: this.hasIdentityValues(cipher),
+ hasSecureNoteValue: this.hasSecureNoteValue(cipher),
+ hasSshKeyValues: this.hasSshKeyValues(cipher),
+ singleCopiableLogin: this.singleCopiableLogin(cipher),
+ singleCopiableCard: this.singleCopiableCard(cipher),
+ singleCopiableIdentity: this.singleCopiableIdentity(cipher),
+ };
+ }
+
+ private hasLoginValues(cipher: CipherView) {
+ return !!cipher.login.hasTotp || !!cipher.login.password || !!cipher.login.username;
+ }
+
+ private singleCopiableLogin(cipher: CipherView) {
+ const loginItems: CipherItem[] = [
+ { value: cipher.login.username, key: "username" },
+ { value: cipher.login.password, key: "password" },
+ { value: cipher.login.totp, key: "totp" },
+ ];
+ // If both the password and username are visible but the password is hidden, return the username
+ if (!cipher.viewPassword && cipher.login.username && cipher.login.password) {
+ return { value: cipher.login.username, key: this.i18nService.t("username") };
+ }
+ return this.findSingleCopiableItem(loginItems);
+ }
+
+ private singleCopiableCard(cipher: CipherView) {
+ const cardItems: CipherItem[] = [
+ { value: cipher.card.code, key: "code" },
+ { value: cipher.card.number, key: "number" },
+ ];
+ return this.findSingleCopiableItem(cardItems);
+ }
+
+ private singleCopiableIdentity(cipher: CipherView) {
+ const identityItems: CipherItem[] = [
+ { value: cipher.identity.fullAddressForCopy, key: "address" },
+ { value: cipher.identity.email, key: "email" },
+ { value: cipher.identity.username, key: "username" },
+ { value: cipher.identity.phone, key: "phone" },
+ ];
+ return this.findSingleCopiableItem(identityItems);
+ }
+
+ /*
+ * Given a list of CipherItems, if there is only one item with a value,
+ * return it with the translated key. Otherwise return null
+ */
+ private findSingleCopiableItem(items: { value: string; key: string }[]): CipherItem | null {
+ const singleItemWithValue = items.find(
+ (key) => key.value && items.every((f) => f === key || !f.value),
+ );
+ return singleItemWithValue
+ ? { value: singleItemWithValue.value, key: this.i18nService.t(singleItemWithValue.key) }
+ : null;
+ }
+
+ private hasCardValues(cipher: CipherView) {
+ return !!cipher.card.code || !!cipher.card.number;
+ }
+
+ private hasIdentityValues(cipher: CipherView) {
+ return (
+ !!cipher.identity.fullAddressForCopy ||
+ !!cipher.identity.email ||
+ !!cipher.identity.username ||
+ !!cipher.identity.phone
+ );
+ }
+
+ private hasSecureNoteValue(cipher: CipherView) {
+ return !!cipher.notes;
+ }
+
+ private hasSshKeyValues(cipher: CipherView) {
+ return (
+ !!cipher.sshKey.privateKey || !!cipher.sshKey.publicKey || !!cipher.sshKey.keyFingerprint
+ );
+ }
}