1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 04:33:38 +00:00

update vault usage to surface bit-item-action to be in same template as bit-item

This commit is contained in:
William Martin
2025-05-28 00:40:50 -04:00
parent 65cee7f98b
commit 617d6fb8a2
7 changed files with 414 additions and 400 deletions

View File

@@ -1,218 +0,0 @@
<ng-container *ngIf="cipher.type === CipherType.Login">
<ng-container *ngIf="showQuickCopyActions$ | async; else loginCopyMenu">
<bit-item-action>
<button
type="button"
bitIconButton="bwi-user"
size="small"
appCopyField="username"
[cipher]="cipher"
[appA11yTitle]="'copyUsername' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
*ngIf="cipher.viewPassword"
type="button"
bitIconButton="bwi-key"
size="small"
appCopyField="password"
[cipher]="cipher"
[appA11yTitle]="'copyPassword' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-clock"
size="small"
appCopyField="totp"
[cipher]="cipher"
[appA11yTitle]="'copyVerificationCode' | i18n"
></button>
</bit-item-action>
</ng-container>
<ng-template #loginCopyMenu>
<bit-item-action>
<button
*ngIf="singleCopiableLogin"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
'copyFieldValue' | i18n: singleCopiableLogin.key : singleCopiableLogin.value
"
[appCopyClick]="singleCopiableLogin.value"
[valueLabel]="singleCopiableLogin.key"
showToast
></button>
<ng-container *ngIf="!singleCopiableLogin">
<button
type="button"
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>
<ng-container *ngIf="cipher.type === CipherType.Card">
<ng-container *ngIf="showQuickCopyActions$ | async; else cardCopyMenu">
<bit-item-action>
<button
type="button"
bitIconButton="bwi-hashtag"
size="small"
appCopyField="cardNumber"
[cipher]="cipher"
[appA11yTitle]="'copyNumber' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-key"
size="small"
appCopyField="securityCode"
[cipher]="cipher"
[appA11yTitle]="'copySecurityCode' | i18n"
></button>
</bit-item-action>
</ng-container>
<ng-template #cardCopyMenu>
<bit-item-action>
<button
*ngIf="singleCopiableCard"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="'copyFieldValue' | i18n: singleCopiableCard.key : singleCopiableCard.value"
[appCopyClick]="singleCopiableCard.value"
[valueLabel]="singleCopiableCard.key"
showToast
></button>
<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]="
'copyFieldValue' | i18n: singleCopiableIdentity.key : singleCopiableIdentity.value
"
[appCopyClick]="singleCopiableIdentity.value"
[valueLabel]="singleCopiableIdentity.key"
showToast
></button>
<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">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasSecureNoteValue ? ('copyNoteTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
appCopyField="secureNote"
[cipher]="cipher"
></button>
</bit-item-action>
<bit-item-action *ngIf="cipher.type === CipherType.SshKey">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
hasSshKeyValues ? ('copyInfoTitle' | i18n: cipher.name) : ('noValuesToCopy' | i18n)
"
[disabled]="!hasSshKeyValues"
[bitMenuTriggerFor]="sshKeyOptions"
></button>
<bit-menu #sshKeyOptions>
<button type="button" bitMenuItem appCopyField="privateKey" [cipher]="cipher">
{{ "copyPrivateKey" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="publicKey" [cipher]="cipher">
{{ "copyPublicKey" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="keyFingerprint" [cipher]="cipher">
{{ "copyFingerprint" | i18n }}
</button>
</bit-menu>
</bit-item-action>

View File

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

View File

@@ -1,39 +1,37 @@
<bit-item-action>
<button
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
[title]="'moreOptionsTitle' | i18n: cipher.name"
[disabled]="cipher.decryptionFailure"
[bitMenuTriggerFor]="moreOptions"
></button>
<bit-menu #moreOptions>
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
<ng-container *ngIf="autofillAllowed$ | async">
<button type="button" bitMenuItem (click)="doAutofill()">
{{ "autofill" | i18n }}
</button>
<button type="button" bitMenuItem *ngIf="canEdit && isLogin" (click)="doAutofillAndSave()">
{{ "fillAndSave" | i18n }}
</button>
</ng-container>
</ng-container>
<ng-container *ngIf="showViewOption">
<button type="button" bitMenuItem (click)="onView()">
{{ "view" | i18n }}
<button
type="button"
bitIconButton="bwi-ellipsis-v"
size="small"
[attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name"
[title]="'moreOptionsTitle' | i18n: cipher.name"
[disabled]="cipher.decryptionFailure"
[bitMenuTriggerFor]="moreOptions"
></button>
<bit-menu #moreOptions>
<ng-container *ngIf="canAutofill && !hideAutofillOptions">
<ng-container *ngIf="autofillAllowed$ | async">
<button type="button" bitMenuItem (click)="doAutofill()">
{{ "autofill" | i18n }}
</button>
<button type="button" bitMenuItem *ngIf="canEdit && isLogin" (click)="doAutofillAndSave()">
{{ "fillAndSave" | i18n }}
</button>
</ng-container>
<button type="button" bitMenuItem (click)="toggleFavorite()">
{{ favoriteText | i18n }}
</ng-container>
<ng-container *ngIf="showViewOption">
<button type="button" bitMenuItem (click)="onView()">
{{ "view" | i18n }}
</button>
<ng-container *ngIf="canEdit && canViewPassword">
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
{{ "clone" | i18n }}
</a>
<a bitMenuItem *ngIf="hasOrganizations" (click)="conditionallyNavigateToAssignCollections()">
{{ "assignToCollections" | i18n }}
</a>
</ng-container>
</bit-menu>
</bit-item-action>
</ng-container>
<button type="button" bitMenuItem (click)="toggleFavorite()">
{{ favoriteText | i18n }}
</button>
<ng-container *ngIf="canEdit && canViewPassword">
<a bitMenuItem (click)="clone()" *ngIf="canClone$ | async">
{{ "clone" | i18n }}
</a>
<a bitMenuItem *ngIf="hasOrganizations" (click)="conditionallyNavigateToAssignCollections()">
{{ "assignToCollections" | i18n }}
</a>
</ng-container>
</bit-menu>

View File

@@ -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<CipherView>(undefined);

View File

@@ -81,18 +81,18 @@
</ng-template>
<ng-template #itemGroup>
<bit-item-group>
<ng-container *ngFor="let group of cipherGroups$()">
<ng-container *ngIf="group.subHeaderKey">
<h3 class="tw-text-muted tw-text-xs tw-font-semibold tw-pl-1 tw-mb-1 bit-compact:tw-m-0">
{{ group.subHeaderKey | i18n }}
</h3>
</ng-container>
<ng-container *ngFor="let group of cipherGroups$()">
<ng-container *ngIf="group.subHeaderKey">
<h3 class="tw-text-muted tw-text-xs tw-font-semibold tw-pl-1 tw-mb-1 bit-compact:tw-m-0">
{{ group.subHeaderKey | i18n }}
</h3>
</ng-container>
<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
>
<cdk-virtual-scroll-viewport
[itemSize]="itemHeight$ | async"
class="tw-overflow-visible [&>.cdk-virtual-scroll-content-wrapper]:[contain:layout_style]"
>
<bit-item-group>
<bit-item *cdkVirtualFor="let cipher of group.ciphers">
<button
bit-item-content
@@ -147,15 +147,262 @@
[title]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
@let cbv = copyButtonsService.toCopyButtonsView(cipher);
<ng-container *ngIf="cipher.type === CipherType.Login">
<ng-container
*ngIf="copyButtonsService.showQuickCopyActions$ | async; else loginCopyMenu"
>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-user"
size="small"
appCopyField="username"
[cipher]="cipher"
[appA11yTitle]="'copyUsername' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
*ngIf="cipher.viewPassword"
type="button"
bitIconButton="bwi-key"
size="small"
appCopyField="password"
[cipher]="cipher"
[appA11yTitle]="'copyPassword' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-clock"
size="small"
appCopyField="totp"
[cipher]="cipher"
[appA11yTitle]="'copyVerificationCode' | i18n"
></button>
</bit-item-action>
</ng-container>
<ng-template #loginCopyMenu>
<bit-item-action>
<button
*ngIf="cbv.singleCopiableLogin"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
'copyFieldValue'
| i18n: cbv.singleCopiableLogin.key : cbv.singleCopiableLogin.value
"
[appCopyClick]="cbv.singleCopiableLogin.value"
[valueLabel]="cbv.singleCopiableLogin.key"
showToast
></button>
<ng-container *ngIf="!cbv.singleCopiableLogin">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
cbv.hasLoginValues
? ('copyInfoTitle' | i18n: cipher.name)
: ('noValuesToCopy' | i18n)
"
[disabled]="!cbv.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>
<ng-container *ngIf="cipher.type === CipherType.Card">
<ng-container
*ngIf="copyButtonsService.showQuickCopyActions$ | async; else cardCopyMenu"
>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-hashtag"
size="small"
appCopyField="cardNumber"
[cipher]="cipher"
[appA11yTitle]="'copyNumber' | i18n"
></button>
</bit-item-action>
<bit-item-action>
<button
type="button"
bitIconButton="bwi-key"
size="small"
appCopyField="securityCode"
[cipher]="cipher"
[appA11yTitle]="'copySecurityCode' | i18n"
></button>
</bit-item-action>
</ng-container>
<ng-template #cardCopyMenu>
<bit-item-action>
<button
*ngIf="cbv.singleCopiableCard"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
'copyFieldValue'
| i18n: cbv.singleCopiableCard.key : cbv.singleCopiableCard.value
"
[appCopyClick]="cbv.singleCopiableCard.value"
[valueLabel]="cbv.singleCopiableCard.key"
showToast
></button>
<ng-container *ngIf="!cbv.singleCopiableCard">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
cbv.hasCardValues
? ('copyInfoTitle' | i18n: cipher.name)
: ('noValuesToCopy' | i18n)
"
[disabled]="!cbv.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="cbv.singleCopiableIdentity"
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
'copyFieldValue'
| i18n: cbv.singleCopiableIdentity.key : cbv.singleCopiableIdentity.value
"
[appCopyClick]="cbv.singleCopiableIdentity.value"
[valueLabel]="cbv.singleCopiableIdentity.key"
showToast
></button>
<ng-container *ngIf="!cbv.singleCopiableIdentity">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
cbv.hasIdentityValues
? ('copyInfoTitle' | i18n: cipher.name)
: ('noValuesToCopy' | i18n)
"
[disabled]="!cbv.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">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
cbv.hasSecureNoteValue
? ('copyNoteTitle' | i18n: cipher.name)
: ('noValuesToCopy' | i18n)
"
appCopyField="secureNote"
[cipher]="cipher"
></button>
</bit-item-action>
<bit-item-action *ngIf="cipher.type === CipherType.SshKey">
<button
type="button"
bitIconButton="bwi-clone"
size="small"
[appA11yTitle]="
cbv.hasSshKeyValues
? ('copyInfoTitle' | i18n: cipher.name)
: ('noValuesToCopy' | i18n)
"
[disabled]="!cbv.hasSshKeyValues"
[bitMenuTriggerFor]="sshKeyOptions"
></button>
<bit-menu #sshKeyOptions>
<button type="button" bitMenuItem appCopyField="privateKey" [cipher]="cipher">
{{ "copyPrivateKey" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="publicKey" [cipher]="cipher">
{{ "copyPublicKey" | i18n }}
</button>
<button type="button" bitMenuItem appCopyField="keyFingerprint" [cipher]="cipher">
{{ "copyFingerprint" | i18n }}
</button>
</bit-menu>
</bit-item-action>
<!-- <app-item-copy-actions [cipher]="cipher"></app-item-copy-actions> -->
<bit-item-action>
<app-item-more-options
[cipher]="cipher"
[hideAutofillOptions]="hideAutofillOptions$ | async"
[showViewOption]="primaryActionAutofill"
></app-item-more-options>
</bit-item-action>
</ng-container>
</bit-item>
</cdk-virtual-scroll-viewport>
</ng-container>
</bit-item-group>
</bit-item-group>
</cdk-virtual-scroll-viewport>
</ng-container>
</ng-template>

View File

@@ -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;

View File

@@ -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<CopyButtonDisplayMode>(VAULT_APPEARANCE, "copyButtons", {
deserializer: (s) => s,
});
@@ -20,6 +38,7 @@ const COPY_BUTTON = new KeyDefinition<CopyButtonDisplayMode>(VAULT_APPEARANCE, "
export class VaultPopupCopyButtonsService {
private readonly DEFAULT_DISPLAY_MODE = "combined";
private state = inject(GlobalStateProvider).get(COPY_BUTTON);
private i18nService = inject(I18nService);
displayMode$: Observable<CopyButtonDisplayMode> = 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
);
}
}