1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00

[PM-13989] - Extension Vault screen - allow copy icon to copy data directly if only 1 piece of data is available (#13520)

* wip - copy button overhaul

* finalize item copy actions single item copy
This commit is contained in:
Jordan Aasen
2025-02-24 14:56:15 -08:00
committed by GitHub
parent fb20aa556b
commit bc415d807c
3 changed files with 153 additions and 49 deletions

View File

@@ -4236,6 +4236,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"
},

View File

@@ -36,32 +36,46 @@
<ng-template #loginCopyMenu>
<bit-item-action>
<button
*ngIf="singleCopiableLogin"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
'copyFieldValue' | i18n: singleCopiableLogin.key : singleCopiableLogin.value
"
[disabled]="!hasLoginValues"
[bitMenuTriggerFor]="loginOptions"
[appCopyClick]="singleCopiableLogin.value"
[valueLabel]="singleCopiableLogin.key"
showToast
></button>
<bit-menu #loginOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<ng-container *ngIf="!singleCopiableLogin">
<button
*ngIf="cipher.viewPassword"
type="button"
bitMenuItem
appCopyField="password"
[cipher]="cipher"
>
{{ "copyPassword" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
{{ "copyVerificationCode" | i18n }}
</button>
</bit-menu>
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasLoginValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasLoginValues"
[bitMenuTriggerFor]="loginOptions"
></button>
<bit-menu #loginOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<button
*ngIf="cipher.viewPassword"
type="button"
bitMenuItem
appCopyField="password"
[cipher]="cipher"
>
{{ "copyPassword" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="totp" [cipher]="cipher">
{{ "copyVerificationCode" | i18n }}
</button>
</bit-menu>
</ng-container>
</bit-item-action>
</ng-template>
</ng-container>
@@ -92,52 +106,78 @@
<ng-template #cardCopyMenu>
<bit-item-action>
<button
*ngIf="singleCopiableCard"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasCardValues"
[bitMenuTriggerFor]="cardOptions"
[appA11yTitle]="'copyFieldValue' | i18n: singleCopiableCard.key : singleCopiableCard.value"
[appCopyClick]="singleCopiableCard.value"
[valueLabel]="singleCopiableCard.key"
showToast
></button>
<bit-menu #cardOptions>
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher">
{{ "copyNumber" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher">
{{ "copySecurityCode" | i18n }}
</button>
</bit-menu>
<ng-container *ngIf="!singleCopiableCard">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasCardValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasCardValues"
[bitMenuTriggerFor]="cardOptions"
></button>
<bit-menu #cardOptions>
<button type="button" bitMenuItem appCopyField="cardNumber" [cipher]="cipher">
{{ "copyNumber" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="securityCode" [cipher]="cipher">
{{ "copySecurityCode" | i18n }}
</button>
</bit-menu>
</ng-container>
</bit-item-action>
</ng-template>
</ng-container>
<bit-item-action *ngIf="cipher.type === CipherType.Identity">
<button
*ngIf="singleCopiableIdentity"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
'copyFieldValue' | i18n: singleCopiableIdentity.key : singleCopiableIdentity.value
"
[disabled]="!hasIdentityValues"
[bitMenuTriggerFor]="identityOptions"
[appCopyClick]="singleCopiableIdentity.value"
[valueLabel]="singleCopiableIdentity.key"
showToast
></button>
<bit-menu #identityOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher">
{{ "copyEmail" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher">
{{ "copyPhone" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher">
{{ "copyAddress" | i18n }}
</button>
</bit-menu>
<ng-container *ngIf="!singleCopiableIdentity">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasIdentityValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasIdentityValues"
[bitMenuTriggerFor]="identityOptions"
></button>
<bit-menu #identityOptions>
<button type="button" bitMenuItem appCopyField="username" [cipher]="cipher">
{{ "copyUsername" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="email" [cipher]="cipher">
{{ "copyEmail" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="phone" [cipher]="cipher">
{{ "copyPhone" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="address" [cipher]="cipher">
{{ "copyAddress" | i18n }}
</button>
</bit-menu>
</ng-container>
</bit-item-action>
<bit-item-action *ngIf="cipher.type === CipherType.SecureNote">

View File

@@ -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) {}
}