From 2d488a8e68c38122923ab214226e5657bc494cad Mon Sep 17 00:00:00 2001 From: Jason Ng Date: Thu, 23 Jan 2025 10:54:52 -0500 Subject: [PATCH] [PM-16245] delete btn in browser edit item (#12876) * send the user back to vault after deleting from edit view * [PM-17443] Navigation After Deletion (#13023) * navigate to vault tab after cipher deletion --------- Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> --- .../add-edit/add-edit-v2.component.html | 10 ++++ .../add-edit/add-edit-v2.component.spec.ts | 56 +++++++++++++---- .../add-edit/add-edit-v2.component.ts | 60 ++++++++++++++++++- 3 files changed, 114 insertions(+), 12 deletions(-) diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index a46f5a6955b..152c500d6ca 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -26,5 +26,15 @@ + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index ebfb1ff765f..3252f030fc3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, Observable } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { CipherFormConfig, @@ -40,7 +42,7 @@ describe("AddEditV2Component", () => { const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; const buildConfig = jest.fn((mode: CipherFormMode) => - Promise.resolve({ mode, ...buildConfigResponse }), + Promise.resolve({ ...buildConfigResponse, mode }), ); const queryParams$ = new BehaviorSubject({}); const disable = jest.fn(); @@ -55,9 +57,10 @@ describe("AddEditV2Component", () => { back.mockClear(); collect.mockClear(); - addEditCipherInfo$ = new BehaviorSubject(null); + addEditCipherInfo$ = new BehaviorSubject(null); cipherServiceMock = mock(); - cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + cipherServiceMock.addEditCipherInfo$ = + addEditCipherInfo$.asObservable() as Observable; await TestBed.configureTestingModule({ imports: [AddEditV2Component], @@ -71,6 +74,13 @@ describe("AddEditV2Component", () => { { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: CipherService, useValue: cipherServiceMock }, { provide: EventCollectionService, useValue: { collect } }, + { provide: LogService, useValue: mock() }, + { + provide: CipherAuthorizationService, + useValue: { + canDeleteCipher$: jest.fn().mockReturnValue(true), + }, + }, ], }) .overrideProvider(CipherFormConfigService, { @@ -92,7 +102,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(buildConfig.mock.lastCall![0]).toBe("add"); expect(component.config.mode).toBe("add"); })); @@ -101,7 +111,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(buildConfig.mock.lastCall![0]).toBe("clone"); expect(component.config.mode).toBe("clone"); })); @@ -111,7 +121,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("edit"); })); @@ -121,7 +131,7 @@ describe("AddEditV2Component", () => { tick(); - expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(buildConfig.mock.lastCall![0]).toBe("edit"); expect(component.config.mode).toBe("partial-edit"); })); }); @@ -218,7 +228,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.username).toBe("identity-username"); + expect(component.config.initialValues!.username).toBe("identity-username"); })); it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { @@ -231,7 +241,7 @@ describe("AddEditV2Component", () => { tick(); - expect(component.config.initialValues.name).toBe("AddEditCipherName"); + expect(component.config.initialValues!.name).toBe("AddEditCipherName"); })); it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { @@ -326,4 +336,30 @@ describe("AddEditV2Component", () => { expect(back).toHaveBeenCalled(); }); }); + + describe("delete", () => { + it("dialogService openSimpleDialog called when deleteBtn is hit", async () => { + const dialogSpy = jest + .spyOn(component["dialogService"], "openSimpleDialog") + .mockResolvedValue(true); + + await component.delete(); + expect(dialogSpy).toHaveBeenCalled(); + }); + + it("should call deleteCipher when user confirms deletion", async () => { + const deleteCipherSpy = jest.spyOn(component as any, "deleteCipher"); + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + + await component.delete(); + expect(deleteCipherSpy).toHaveBeenCalled(); + }); + + it("navigates to vault tab after deletion", async () => { + jest.spyOn(component["dialogService"], "openSimpleDialog").mockResolvedValue(true); + await component.delete(); + + expect(navigate).toHaveBeenCalledWith(["/tabs/vault"]); + }); + }); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 2d8c4857c1c..b46b1d61509 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -5,18 +5,27 @@ import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Params, Router } from "@angular/router"; -import { firstValueFrom, map, switchMap } from "rxjs"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { 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 { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; -import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + AsyncActionsModule, + ButtonModule, + SearchModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; import { CipherFormConfig, CipherFormConfigService, @@ -131,11 +140,13 @@ export type AddEditQueryParams = Partial>; CipherFormModule, AsyncActionsModule, PopOutComponent, + IconButtonModule, ], }) export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; + canDeleteCipher$: Observable; get loading() { return this.config == null; @@ -165,6 +176,10 @@ export class AddEditV2Component implements OnInit { private router: Router, private cipherService: CipherService, private eventCollectionService: EventCollectionService, + private logService: LogService, + private toastService: ToastService, + private dialogService: DialogService, + protected cipherAuthorizationService: CipherAuthorizationService, ) { this.subscribeToParams(); } @@ -281,6 +296,10 @@ export class AddEditV2Component implements OnInit { } if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) { + this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$( + config.originalCipher, + ); + await this.eventCollectionService.collect( EventType.Cipher_ClientViewed, config.originalCipher.id, @@ -337,6 +356,43 @@ export class AddEditV2Component implements OnInit { return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); } } + + delete = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/tabs/vault"]); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.config.originalCipher.deletedDate + ? this.cipherService.deleteWithServer(this.config.originalCipher.id) + : this.cipherService.softDeleteWithServer(this.config.originalCipher.id); + } } /**