From 6acd671f355ef3b00555ec96cc398bc463d40d50 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Thu, 20 Feb 2025 17:02:19 -0800 Subject: [PATCH 1/2] wip - copy button overhaul --- apps/browser/src/_locales/en/messages.json | 14 +++++ .../item-copy-actions.component.html | 49 ++++++++++------ .../item-copy-actions.component.ts | 58 +++++++++++++++++++ 3 files changed, 103 insertions(+), 18 deletions(-) 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..fbdc0b4b4b7 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 @@ -40,28 +40,41 @@ bitIconButton="bwi-clone" size="small" [appA11yTitle]=" - hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) + 'copyFieldValue' | i18n: singleCopiableLogin.field : singleCopiableLogin.value " - [disabled]="!hasLoginValues" - [bitMenuTriggerFor]="loginOptions" + [appCopyClick]="singleCopiableLogin.value" + [valueLabel]="singleCopiableLogin.field" + *ngIf="singleCopiableLogin" > - - + - - + bitIconButton="bwi-clone" + size="small" + [appA11yTitle]=" + hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n) + " + [disabled]="!hasLoginValues" + [bitMenuTriggerFor]="loginOptions" + > + + + + + + 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..d14675cc11c 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 @@ -37,6 +37,64 @@ export class ItemCopyActionsComponent { ); } + get singleCopiableLogin() { + const { username, password, hasTotp, totp } = this.cipher.login; + // If there is both a username and password but the password is not viewable, then the username is the only copiable value + if (username && password && !this.cipher.viewPassword) { + return { + value: username, + field: "username", + }; + } + if (username && !password && !hasTotp) { + return { + value: username, + field: "username", + }; + } + if (!username && password && !hasTotp) { + return { + value: password, + field: "password", + }; + } + if (!username && !password && hasTotp) { + return { + value: totp, + field: "totp", + }; + } + return null; + } + + get singleCopiableCardValue() { + const { code, number } = this.cipher.card; + if (code && !number) { + return code; + } + if (!code && number) { + return number; + } + return null; + } + + get singleCopiableIdentityValue() { + const { fullAddressForCopy, email, username, phone } = this.cipher.identity; + if (fullAddressForCopy && !email && !username && !phone) { + return fullAddressForCopy; + } + if (!fullAddressForCopy && email && !username && !phone) { + return email; + } + if (!fullAddressForCopy && !email && username && !phone) { + return username; + } + if (!fullAddressForCopy && !email && !username && phone) { + return phone; + } + return null; + } + get hasCardValues() { return !!this.cipher.card.code || !!this.cipher.card.number; } From 2b11013bbd5511a45ae6aa2bda5f2f4a9d2c2478 Mon Sep 17 00:00:00 2001 From: jaasen-livefront Date: Fri, 21 Feb 2025 16:56:37 -0800 Subject: [PATCH 2/2] finalize item copy actions single item copy --- .../item-copy-actions.component.html | 93 +++++++++++------- .../item-copy-actions.component.ts | 94 +++++++++---------- 2 files changed, 103 insertions(+), 84 deletions(-) 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 fbdc0b4b4b7..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,15 +36,16 @@ - - - - + + + + + + + - - - - - - + + + + + + + + + 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 d14675cc11c..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", @@ -38,61 +44,47 @@ export class ItemCopyActionsComponent { } get singleCopiableLogin() { - const { username, password, hasTotp, totp } = this.cipher.login; - // If there is both a username and password but the password is not viewable, then the username is the only copiable value - if (username && password && !this.cipher.viewPassword) { - return { - value: username, - field: "username", - }; - } - if (username && !password && !hasTotp) { - return { - value: username, - field: "username", - }; - } - if (!username && password && !hasTotp) { - return { - value: password, - field: "password", - }; - } - if (!username && !password && hasTotp) { - return { - value: totp, - field: "totp", - }; + 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 null; + return this.findSingleCopiableItem(loginItems); } - get singleCopiableCardValue() { - const { code, number } = this.cipher.card; - if (code && !number) { - return code; - } - if (!code && number) { - return number; - } - return null; + get singleCopiableCard() { + const cardItems: CipherItem[] = [ + { value: this.cipher.card.code, key: "code" }, + { value: this.cipher.card.number, key: "number" }, + ]; + return this.findSingleCopiableItem(cardItems); } - get singleCopiableIdentityValue() { - const { fullAddressForCopy, email, username, phone } = this.cipher.identity; - if (fullAddressForCopy && !email && !username && !phone) { - return fullAddressForCopy; - } - if (!fullAddressForCopy && email && !username && !phone) { - return email; - } - if (!fullAddressForCopy && !email && username && !phone) { - return username; - } - if (!fullAddressForCopy && !email && !username && phone) { - return phone; - } - return null; + 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() { @@ -120,5 +112,5 @@ export class ItemCopyActionsComponent { ); } - constructor() {} + constructor(private i18nService: I18nService) {} }