From c03ea5b29f39e06bb7ea4064c4c7ac70197f0968 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Tue, 24 Feb 2026 10:36:29 -0600
Subject: [PATCH] only show delete button when the user has delete permissions
(#19200)
---
.../app/vault/item-footer.component.html | 23 ++-
.../app/vault/item-footer.component.spec.ts | 172 ++++++++++++++++++
.../vault/app/vault/item-footer.component.ts | 10 +-
3 files changed, 192 insertions(+), 13 deletions(-)
create mode 100644 apps/desktop/src/vault/app/vault/item-footer.component.spec.ts
diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.html b/apps/desktop/src/vault/app/vault/item-footer.component.html
index 5e3de1e6a14..50c00313642 100644
--- a/apps/desktop/src/vault/app/vault/item-footer.component.html
+++ b/apps/desktop/src/vault/app/vault/item-footer.component.html
@@ -63,14 +63,21 @@
}
-
+ @if (canDelete) {
+
+ }
}
diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.spec.ts b/apps/desktop/src/vault/app/vault/item-footer.component.spec.ts
new file mode 100644
index 00000000000..4db94d5d717
--- /dev/null
+++ b/apps/desktop/src/vault/app/vault/item-footer.component.spec.ts
@@ -0,0 +1,172 @@
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { By } from "@angular/platform-browser";
+import { mock } from "jest-mock-extended";
+import { of } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { Utils } from "@bitwarden/common/platform/misc/utils";
+import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
+import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
+import { ButtonModule, DialogService, ToastService } from "@bitwarden/components";
+import { ArchiveCipherUtilitiesService, PasswordRepromptService } from "@bitwarden/vault";
+
+import { ItemFooterComponent } from "./item-footer.component";
+
+describe("ItemFooterComponent", () => {
+ let component: ItemFooterComponent;
+ let fixture: ComponentFixture;
+ let accountService: FakeAccountService;
+
+ const mockUserId = Utils.newGuid() as UserId;
+
+ beforeEach(async () => {
+ accountService = mockAccountServiceWith(mockUserId);
+
+ const cipherArchiveService = {
+ userCanArchive$: jest.fn().mockReturnValue(of(false)),
+ hasArchiveFlagEnabled$: of(false),
+ };
+
+ await TestBed.configureTestingModule({
+ imports: [ItemFooterComponent, ButtonModule, JslibModule],
+ providers: [
+ { provide: AccountService, useValue: accountService },
+ { provide: CipherService, useValue: mock() },
+ { provide: DialogService, useValue: mock() },
+ { provide: PasswordRepromptService, useValue: mock() },
+ { provide: CipherAuthorizationService, useValue: mock() },
+ { provide: ToastService, useValue: mock() },
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ { provide: LogService, useValue: mock() },
+ { provide: CipherArchiveService, useValue: cipherArchiveService },
+ { provide: ArchiveCipherUtilitiesService, useValue: mock() },
+ ],
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(ItemFooterComponent);
+ component = fixture.componentInstance;
+ });
+
+ const createCipherView = (overrides: Partial = {}): CipherView => {
+ const cipher = new CipherView();
+ cipher.id = "test-cipher-id";
+ cipher.permissions = {
+ delete: false,
+ restore: false,
+ manage: false,
+ edit: false,
+ view: false,
+ viewPassword: false,
+ };
+ return Object.assign(cipher, overrides);
+ };
+
+ describe("delete button visibility", () => {
+ it("shows the delete button when cipher.permissions.delete is true and action is 'view'", async () => {
+ const cipher = createCipherView({
+ permissions: {
+ delete: true,
+ restore: false,
+ manage: false,
+ edit: false,
+ view: false,
+ viewPassword: false,
+ },
+ });
+
+ component.cipher = cipher;
+ component.action = "view";
+
+ await component.ngOnInit();
+ fixture.detectChanges();
+
+ const deleteButton = fixture.debugElement.query(
+ By.css('[data-test-id="footer-delete-button"]'),
+ );
+
+ expect(deleteButton).toBeTruthy();
+ });
+
+ it("shows the delete button when cipher.permissions.delete is true and action is 'edit'", async () => {
+ const cipher = createCipherView({
+ permissions: {
+ delete: true,
+ restore: false,
+ manage: false,
+ edit: false,
+ view: false,
+ viewPassword: false,
+ },
+ });
+
+ component.cipher = cipher;
+ component.action = "edit";
+
+ await component.ngOnInit();
+ fixture.detectChanges();
+
+ const deleteButton = fixture.debugElement.query(
+ By.css('[data-test-id="footer-delete-button"]'),
+ );
+
+ expect(deleteButton).toBeTruthy();
+ });
+
+ it("does not show the delete button when cipher.permissions.delete is false", async () => {
+ const cipher = createCipherView({
+ permissions: {
+ delete: false,
+ restore: false,
+ manage: false,
+ edit: false,
+ view: false,
+ viewPassword: false,
+ },
+ });
+
+ component.cipher = cipher;
+ component.action = "view";
+
+ await component.ngOnInit();
+ fixture.detectChanges();
+
+ const deleteButton = fixture.debugElement.query(
+ By.css('[data-test-id="footer-delete-button"]'),
+ );
+
+ expect(deleteButton).toBeFalsy();
+ });
+
+ it("does not show the delete button when action is not 'view' or 'edit'", async () => {
+ const cipher = createCipherView({
+ permissions: {
+ delete: true,
+ restore: false,
+ manage: false,
+ edit: false,
+ view: false,
+ viewPassword: false,
+ },
+ });
+
+ component.cipher = cipher;
+ component.action = "add";
+
+ await component.ngOnInit();
+ fixture.detectChanges();
+
+ const deleteButton = fixture.debugElement.query(
+ By.css('[data-test-id="footer-delete-button"]'),
+ );
+
+ expect(deleteButton).toBeFalsy();
+ });
+ });
+});
diff --git a/apps/desktop/src/vault/app/vault/item-footer.component.ts b/apps/desktop/src/vault/app/vault/item-footer.component.ts
index 133a9777fab..891baf716c1 100644
--- a/apps/desktop/src/vault/app/vault/item-footer.component.ts
+++ b/apps/desktop/src/vault/app/vault/item-footer.component.ts
@@ -128,11 +128,7 @@ export class ItemFooterComponent implements OnInit, OnChanges {
}
protected get hasFooterAction() {
- return (
- this.showArchiveButton ||
- this.showUnarchiveButton ||
- (this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view"))
- );
+ return this.showArchiveButton || this.showUnarchiveButton || this.canDelete;
}
protected get showCloneOption() {
@@ -145,6 +141,10 @@ export class ItemFooterComponent implements OnInit, OnChanges {
);
}
+ protected get canDelete() {
+ return this.cipher.permissions?.delete && (this.action === "edit" || this.action === "view");
+ }
+
cancel() {
this.onCancel.emit(this.cipher);
}