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:
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user