diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index a3ac0cda882..99c5960ab2d 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -4175,6 +4175,20 @@
}
}
},
+ "copyFieldValue": {
+ "message": "Copy $FIELD$, $VALUE$",
+ "description": "Title for a button that copies a field value to the clipboard.",
+ "placeholders": {
+ "field": {
+ "content": "$1",
+ "example": "Username"
+ },
+ "value": {
+ "content": "$2",
+ "example": "Foo"
+ }
+ }
+ },
"noValuesToCopy": {
"message": "No values to copy"
},
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
index fbfebe8efff..bb3a7b12096 100644
--- 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
@@ -36,32 +36,46 @@
-
-
+
-
-
+ bitIconButton="bwi-clone"
+ size="small"
+ [appA11yTitle]="
+ hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
+ "
+ [disabled]="!hasLoginValues"
+ [bitMenuTriggerFor]="loginOptions"
+ >
+
+
+
+
+
+
@@ -92,52 +106,78 @@
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
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
index 53439dc4abd..a51e5f5406a 100644
--- 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
@@ -4,6 +4,7 @@ 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";
@@ -11,6 +12,11 @@ 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",
@@ -37,6 +43,50 @@ export class ItemCopyActionsComponent {
);
}
+ 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;
}
@@ -62,5 +112,5 @@ export class ItemCopyActionsComponent {
);
}
- constructor() {}
+ constructor(private i18nService: I18nService) {}
}