mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00: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:
@@ -26,5 +26,15 @@
|
|||||||
<button bitButton type="submit" form="cipherForm" buttonType="primary" #submitBtn>
|
<button bitButton type="submit" form="cipherForm" buttonType="primary" #submitBtn>
|
||||||
{{ "save" | i18n }}
|
{{ "save" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
slot="end"
|
||||||
|
*ngIf="canDeleteCipher$ | async"
|
||||||
|
[bitAction]="delete"
|
||||||
|
type="button"
|
||||||
|
buttonType="danger"
|
||||||
|
bitIconButton="bwi-trash"
|
||||||
|
[appA11yTitle]="'delete' | i18n"
|
||||||
|
></button>
|
||||||
</popup-footer>
|
</popup-footer>
|
||||||
</popup-page>
|
</popup-page>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { mock, MockProxy } from "jest-mock-extended";
|
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 { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
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 { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info";
|
||||||
import {
|
import {
|
||||||
CipherFormConfig,
|
CipherFormConfig,
|
||||||
@@ -40,7 +42,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
const buildConfigResponse = { originalCipher: {} } as CipherFormConfig;
|
const buildConfigResponse = { originalCipher: {} } as CipherFormConfig;
|
||||||
const buildConfig = jest.fn((mode: CipherFormMode) =>
|
const buildConfig = jest.fn((mode: CipherFormMode) =>
|
||||||
Promise.resolve({ mode, ...buildConfigResponse }),
|
Promise.resolve({ ...buildConfigResponse, mode }),
|
||||||
);
|
);
|
||||||
const queryParams$ = new BehaviorSubject({});
|
const queryParams$ = new BehaviorSubject({});
|
||||||
const disable = jest.fn();
|
const disable = jest.fn();
|
||||||
@@ -55,9 +57,10 @@ describe("AddEditV2Component", () => {
|
|||||||
back.mockClear();
|
back.mockClear();
|
||||||
collect.mockClear();
|
collect.mockClear();
|
||||||
|
|
||||||
addEditCipherInfo$ = new BehaviorSubject(null);
|
addEditCipherInfo$ = new BehaviorSubject<AddEditCipherInfo | null>(null);
|
||||||
cipherServiceMock = mock<CipherService>();
|
cipherServiceMock = mock<CipherService>();
|
||||||
cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable();
|
cipherServiceMock.addEditCipherInfo$ =
|
||||||
|
addEditCipherInfo$.asObservable() as Observable<AddEditCipherInfo>;
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [AddEditV2Component],
|
imports: [AddEditV2Component],
|
||||||
@@ -71,6 +74,13 @@ describe("AddEditV2Component", () => {
|
|||||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
{ provide: CipherService, useValue: cipherServiceMock },
|
{ provide: CipherService, useValue: cipherServiceMock },
|
||||||
{ provide: EventCollectionService, useValue: { collect } },
|
{ provide: EventCollectionService, useValue: { collect } },
|
||||||
|
{ provide: LogService, useValue: mock<LogService>() },
|
||||||
|
{
|
||||||
|
provide: CipherAuthorizationService,
|
||||||
|
useValue: {
|
||||||
|
canDeleteCipher$: jest.fn().mockReturnValue(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideProvider(CipherFormConfigService, {
|
.overrideProvider(CipherFormConfigService, {
|
||||||
@@ -92,7 +102,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
expect(buildConfig.mock.lastCall[0]).toBe("add");
|
expect(buildConfig.mock.lastCall![0]).toBe("add");
|
||||||
expect(component.config.mode).toBe("add");
|
expect(component.config.mode).toBe("add");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -101,7 +111,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
expect(buildConfig.mock.lastCall[0]).toBe("clone");
|
expect(buildConfig.mock.lastCall![0]).toBe("clone");
|
||||||
expect(component.config.mode).toBe("clone");
|
expect(component.config.mode).toBe("clone");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -111,7 +121,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
expect(buildConfig.mock.lastCall[0]).toBe("edit");
|
expect(buildConfig.mock.lastCall![0]).toBe("edit");
|
||||||
expect(component.config.mode).toBe("edit");
|
expect(component.config.mode).toBe("edit");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -121,7 +131,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
expect(buildConfig.mock.lastCall[0]).toBe("edit");
|
expect(buildConfig.mock.lastCall![0]).toBe("edit");
|
||||||
expect(component.config.mode).toBe("partial-edit");
|
expect(component.config.mode).toBe("partial-edit");
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -218,7 +228,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
tick();
|
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(() => {
|
it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => {
|
||||||
@@ -231,7 +241,7 @@ describe("AddEditV2Component", () => {
|
|||||||
|
|
||||||
tick();
|
tick();
|
||||||
|
|
||||||
expect(component.config.initialValues.name).toBe("AddEditCipherName");
|
expect(component.config.initialValues!.name).toBe("AddEditCipherName");
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it("clears `addEditCipherInfo` after initialization", fakeAsync(() => {
|
it("clears `addEditCipherInfo` after initialization", fakeAsync(() => {
|
||||||
@@ -326,4 +336,30 @@ describe("AddEditV2Component", () => {
|
|||||||
expect(back).toHaveBeenCalled();
|
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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,18 +5,27 @@ import { Component, OnInit } from "@angular/core";
|
|||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { FormsModule } from "@angular/forms";
|
import { FormsModule } from "@angular/forms";
|
||||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
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 { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
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 { 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 {
|
import {
|
||||||
CipherFormConfig,
|
CipherFormConfig,
|
||||||
CipherFormConfigService,
|
CipherFormConfigService,
|
||||||
@@ -131,11 +140,13 @@ export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
|||||||
CipherFormModule,
|
CipherFormModule,
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
PopOutComponent,
|
PopOutComponent,
|
||||||
|
IconButtonModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AddEditV2Component implements OnInit {
|
export class AddEditV2Component implements OnInit {
|
||||||
headerText: string;
|
headerText: string;
|
||||||
config: CipherFormConfig;
|
config: CipherFormConfig;
|
||||||
|
canDeleteCipher$: Observable<boolean>;
|
||||||
|
|
||||||
get loading() {
|
get loading() {
|
||||||
return this.config == null;
|
return this.config == null;
|
||||||
@@ -165,6 +176,10 @@ export class AddEditV2Component implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
|
private logService: LogService,
|
||||||
|
private toastService: ToastService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||||
) {
|
) {
|
||||||
this.subscribeToParams();
|
this.subscribeToParams();
|
||||||
}
|
}
|
||||||
@@ -281,6 +296,10 @@ export class AddEditV2Component implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) {
|
if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) {
|
||||||
|
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(
|
||||||
|
config.originalCipher,
|
||||||
|
);
|
||||||
|
|
||||||
await this.eventCollectionService.collect(
|
await this.eventCollectionService.collect(
|
||||||
EventType.Cipher_ClientViewed,
|
EventType.Cipher_ClientViewed,
|
||||||
config.originalCipher.id,
|
config.originalCipher.id,
|
||||||
@@ -337,6 +356,43 @@ export class AddEditV2Component implements OnInit {
|
|||||||
return this.i18nService.t(partOne, this.i18nService.t("typeSshKey"));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user