1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-06 23:51:28 +01:00

[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>
This commit is contained in:
Jason Ng 2025-01-23 10:54:52 -05:00 committed by GitHub
parent caf3e77d1c
commit 2d488a8e68
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 12 deletions

View File

@ -26,5 +26,15 @@
<button bitButton type="submit" form="cipherForm" buttonType="primary" #submitBtn>
{{ "save" | i18n }}
</button>
<button
slot="end"
*ngIf="canDeleteCipher$ | async"
[bitAction]="delete"
type="button"
buttonType="danger"
bitIconButton="bwi-trash"
[appA11yTitle]="'delete' | i18n"
></button>
</popup-footer>
</popup-page>

View File

@ -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<AddEditCipherInfo | null>(null);
cipherServiceMock = mock<CipherService>();
cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable();
cipherServiceMock.addEditCipherInfo$ =
addEditCipherInfo$.asObservable() as Observable<AddEditCipherInfo>;
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<LogService>() },
{
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"]);
});
});
});

View File

@ -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<Record<keyof QueryParams, string>>;
CipherFormModule,
AsyncActionsModule,
PopOutComponent,
IconButtonModule,
],
})
export class AddEditV2Component implements OnInit {
headerText: string;
config: CipherFormConfig;
canDeleteCipher$: Observable<boolean>;
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);
}
}
/**