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);
+ }
}
/**