1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-22756] Send minimizeOnCopy message during copy on Desktop platform (#15232)

* [PM-22756] Send minimizeOnCopy message during copy on Desktop platform

* [PM-22756] Introduce optional CopyClickListener pattern

* [PM-22756] Introduce CopyService that wraps PlatformUtilsService.copyToClipboard to allow scoped implementations

* [PM-22756] Introduce DesktopVaultCopyService that sends the minimizeOnCopy message

* [PM-22756] Remove leftover onCopy method

* [PM-22756] Fix failing tests

* [PM-22756] Revert CopyService solution

* [PM-22756] Cleanup

* [PM-22756] Update test

* [PM-22756] Cleanup leftover test changes

(cherry picked from commit e8f53fe9b7)
This commit is contained in:
Shane Melton
2025-06-18 14:44:21 -07:00
committed by Shane
parent bd734b962d
commit 381f528c53
3 changed files with 43 additions and 14 deletions

View File

@@ -13,8 +13,6 @@ import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom } from "rx
import { filter, map, take } from "rxjs/operators"; import { filter, map, take } from "rxjs/operators";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service"; import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -43,6 +41,8 @@ import {
DialogService, DialogService,
ItemModule, ItemModule,
ToastService, ToastService,
CopyClickListener,
COPY_CLICK_LISTENER,
} from "@bitwarden/components"; } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common"; import { I18nPipe } from "@bitwarden/ui-common";
import { import {
@@ -111,9 +111,13 @@ const BroadcasterSubscriptionId = "VaultComponent";
useClass: DesktopPremiumUpgradePromptService, useClass: DesktopPremiumUpgradePromptService,
}, },
{ provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService }, { provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService },
{
provide: COPY_CLICK_LISTENER,
useExisting: VaultV2Component,
},
], ],
}) })
export class VaultV2Component implements OnInit, OnDestroy { export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
@ViewChild(VaultItemsV2Component, { static: true }) @ViewChild(VaultItemsV2Component, { static: true })
vaultItemsComponent: VaultItemsV2Component | null = null; vaultItemsComponent: VaultItemsV2Component | null = null;
@ViewChild(VaultFilterComponent, { static: true }) @ViewChild(VaultFilterComponent, { static: true })
@@ -152,14 +156,12 @@ export class VaultV2Component implements OnInit, OnDestroy {
), ),
); );
private modal: ModalRef | null = null;
private componentIsDestroyed$ = new Subject<boolean>(); private componentIsDestroyed$ = new Subject<boolean>();
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private i18nService: I18nService, private i18nService: I18nService,
private modalService: ModalService,
private broadcasterService: BroadcasterService, private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone, private ngZone: NgZone,
@@ -356,6 +358,13 @@ export class VaultV2Component implements OnInit, OnDestroy {
} }
} }
/**
* Handler for Vault level CopyClickDirectives to send the minimizeOnCopy message
*/
onCopy() {
this.messagingService.send("minimizeOnCopy");
}
async viewCipher(cipher: CipherView) { async viewCipher(cipher: CipherView) {
if (await this.shouldReprompt(cipher, "view")) { if (await this.shouldReprompt(cipher, "view")) {
return; return;

View File

@@ -1,10 +1,11 @@
import { Component, ElementRef, ViewChild } from "@angular/core"; import { Component, ElementRef, ViewChild } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "../"; import { ToastService, CopyClickListener, COPY_CLICK_LISTENER } from "../";
import { CopyClickDirective } from "./copy-click.directive"; import { CopyClickDirective } from "./copy-click.directive";
@@ -34,10 +35,12 @@ describe("CopyClickDirective", () => {
let fixture: ComponentFixture<TestCopyClickComponent>; let fixture: ComponentFixture<TestCopyClickComponent>;
const copyToClipboard = jest.fn(); const copyToClipboard = jest.fn();
const showToast = jest.fn(); const showToast = jest.fn();
const copyClickListener = mock<CopyClickListener>();
beforeEach(async () => { beforeEach(async () => {
copyToClipboard.mockClear(); copyToClipboard.mockClear();
showToast.mockClear(); showToast.mockClear();
copyClickListener.onCopy.mockClear();
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [TestCopyClickComponent], imports: [TestCopyClickComponent],
@@ -55,6 +58,7 @@ describe("CopyClickDirective", () => {
}, },
{ provide: PlatformUtilsService, useValue: { copyToClipboard } }, { provide: PlatformUtilsService, useValue: { copyToClipboard } },
{ provide: ToastService, useValue: { showToast } }, { provide: ToastService, useValue: { showToast } },
{ provide: COPY_CLICK_LISTENER, useValue: copyClickListener },
], ],
}).compileComponents(); }).compileComponents();
@@ -92,7 +96,6 @@ describe("CopyClickDirective", () => {
successToastButton.click(); successToastButton.click();
expect(showToast).toHaveBeenCalledWith({ expect(showToast).toHaveBeenCalledWith({
message: "copySuccessful", message: "copySuccessful",
title: null,
variant: "success", variant: "success",
}); });
}); });
@@ -103,7 +106,6 @@ describe("CopyClickDirective", () => {
infoToastButton.click(); infoToastButton.click();
expect(showToast).toHaveBeenCalledWith({ expect(showToast).toHaveBeenCalledWith({
message: "copySuccessful", message: "copySuccessful",
title: null,
variant: "info", variant: "info",
}); });
}); });
@@ -115,8 +117,15 @@ describe("CopyClickDirective", () => {
expect(showToast).toHaveBeenCalledWith({ expect(showToast).toHaveBeenCalledWith({
message: "valueCopied Content", message: "valueCopied Content",
title: null,
variant: "success", variant: "success",
}); });
}); });
it("should call copyClickListener.onCopy when value is copied", () => {
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
successToastButton.click();
expect(copyClickListener.onCopy).toHaveBeenCalledWith("success toast shown");
});
}); });

View File

@@ -1,12 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line import { Directive, HostListener, Input, InjectionToken, Inject, Optional } from "@angular/core";
// @ts-strict-ignore
import { Directive, HostListener, Input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService, ToastVariant } from "../"; import { ToastService, ToastVariant } from "../";
/**
* Listener that can be provided to receive copy events to allow for customized behavior.
*/
export interface CopyClickListener {
onCopy(value: string): void;
}
export const COPY_CLICK_LISTENER = new InjectionToken<CopyClickListener>("CopyClickListener");
@Directive({ @Directive({
selector: "[appCopyClick]", selector: "[appCopyClick]",
}) })
@@ -18,6 +25,7 @@ export class CopyClickDirective {
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private toastService: ToastService, private toastService: ToastService,
private i18nService: I18nService, private i18nService: I18nService,
@Optional() @Inject(COPY_CLICK_LISTENER) private copyListener?: CopyClickListener,
) {} ) {}
@Input("appCopyClick") valueToCopy = ""; @Input("appCopyClick") valueToCopy = "";
@@ -26,7 +34,7 @@ export class CopyClickDirective {
* When set, the toast displayed will show `<valueLabel> copied` * When set, the toast displayed will show `<valueLabel> copied`
* instead of the default messaging. * instead of the default messaging.
*/ */
@Input() valueLabel: string; @Input() valueLabel?: string;
/** /**
* When set without a value, a success toast will be shown when the value is copied * When set without a value, a success toast will be shown when the value is copied
@@ -54,6 +62,10 @@ export class CopyClickDirective {
@HostListener("click") onClick() { @HostListener("click") onClick() {
this.platformUtilsService.copyToClipboard(this.valueToCopy); this.platformUtilsService.copyToClipboard(this.valueToCopy);
if (this.copyListener) {
this.copyListener.onCopy(this.valueToCopy);
}
if (this._showToast) { if (this._showToast) {
const message = this.valueLabel const message = this.valueLabel
? this.i18nService.t("valueCopied", this.valueLabel) ? this.i18nService.t("valueCopied", this.valueLabel)
@@ -61,7 +73,6 @@ export class CopyClickDirective {
this.toastService.showToast({ this.toastService.showToast({
variant: this.toastVariant, variant: this.toastVariant,
title: null,
message, message,
}); });
} }