From 617d6fb8a2a64abed0b858f2d72cb69179961b2c Mon Sep 17 00:00:00 2001 From: William Martin Date: Wed, 28 May 2025 00:40:50 -0400 Subject: [PATCH] update vault usage to surface bit-item-action to be in same template as bit-item --- .../item-copy-actions.component.html | 218 ------------- .../item-copy-actions.component.ts | 116 ------- .../item-more-options.component.html | 70 +++-- .../item-more-options.component.ts | 10 +- .../vault-list-items-container.component.html | 287 ++++++++++++++++-- .../vault-list-items-container.component.ts | 10 +- .../vault-popup-copy-buttons.service.ts | 103 +++++++ 7 files changed, 414 insertions(+), 400 deletions(-) delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html delete mode 100644 apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.ts 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 @@ - - - - - - - - - - - + + + + + - - - - {{ "clone" | i18n }} - - - {{ "assignToCollections" | 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); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- -
-
+ + +
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 + ); + } }