1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 10:54:00 +00:00

[PM-31679] remove archive from browser edit (#18854)

* removing archive btns from browser edit form footer, remove archive items from showing in expired premium users vault
This commit is contained in:
Jason Ng
2026-02-11 09:46:21 -05:00
committed by jaasen-livefront
parent c410f04b2f
commit 6fe2aecbf9
5 changed files with 9 additions and 179 deletions

View File

@@ -41,24 +41,6 @@
</button>
<ng-container slot="end">
@if (isEditMode) {
@if ((archiveFlagEnabled$ | async) && isCipherArchived) {
<button
type="button"
[bitAction]="unarchive"
bitIconButton="bwi-unarchive"
[label]="'unarchive' | i18n"
></button>
}
@if ((userCanArchive$ | async) && canCipherBeArchived) {
<button
type="button"
[bitAction]="archive"
bitIconButton="bwi-archive"
[label]="'archiveVerb' | i18n"
></button>
}
}
@if (canDeleteCipher$ | async) {
<button
[bitAction]="delete"

View File

@@ -443,111 +443,6 @@ describe("AddEditComponent", () => {
}));
});
describe("archive", () => {
it("calls archiveCipherUtilsService service to archive the cipher", async () => {
buildConfigResponse.originalCipher = { id: "222-333-444-5555", edit: true } as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
await fixture.whenStable();
await component.archive();
expect(component["archiveCipherUtilsService"].archiveCipher).toHaveBeenCalledWith(
expect.objectContaining({ id: "222-333-444-5555" }),
true,
);
});
});
describe("unarchive", () => {
it("calls archiveCipherUtilsService service to unarchive the cipher", async () => {
buildConfigResponse.originalCipher = {
id: "222-333-444-5555",
archivedDate: new Date(),
edit: true,
} as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
await component.unarchive();
expect(component["archiveCipherUtilsService"].unarchiveCipher).toHaveBeenCalledWith(
expect.objectContaining({ id: "222-333-444-5555" }),
);
});
});
describe("archive button", () => {
beforeEach(() => {
// prevent form from rendering
jest.spyOn(component as any, "loading", "get").mockReturnValue(true);
buildConfigResponse.originalCipher = { archivedDate: undefined, edit: true } as Cipher;
});
it("shows the archive button when the user can archive and the cipher can be archived", fakeAsync(() => {
cipherArchiveService.userCanArchive$.mockReturnValue(of(true));
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']"));
expect(archiveBtn).toBeTruthy();
}));
it("does not show the archive button when the user cannot archive", fakeAsync(() => {
cipherArchiveService.userCanArchive$.mockReturnValue(of(false));
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']"));
expect(archiveBtn).toBeFalsy();
}));
it("does not show the archive button when the cipher cannot be archived", fakeAsync(() => {
cipherArchiveService.userCanArchive$.mockReturnValue(of(true));
buildConfigResponse.originalCipher = { archivedDate: new Date(), edit: true } as Cipher;
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const archiveBtn = fixture.debugElement.query(By.css("button[biticonbutton='bwi-archive']"));
expect(archiveBtn).toBeFalsy();
}));
});
describe("unarchive button", () => {
beforeEach(() => {
// prevent form from rendering
jest.spyOn(component as any, "loading", "get").mockReturnValue(true);
buildConfigResponse.originalCipher = { edit: true } as Cipher;
});
it("shows the unarchive button when the cipher is archived", fakeAsync(() => {
buildConfigResponse.originalCipher = { archivedDate: new Date(), edit: true } as Cipher;
tick();
fixture.detectChanges();
const unarchiveBtn = fixture.debugElement.query(
By.css("button[biticonbutton='bwi-unarchive']"),
);
expect(unarchiveBtn).toBeTruthy();
}));
it("does not show the unarchive button when the cipher is not archived", fakeAsync(() => {
queryParams$.next({ cipherId: "222-333-444-5555" });
tick();
fixture.detectChanges();
const unarchiveBtn = fixture.debugElement.query(
By.css("button[biticonbutton='bwi-unarchive']"),
);
expect(unarchiveBtn).toBeFalsy();
}));
});
describe("delete", () => {
it("dialogService openSimpleDialog called when deleteBtn is hit", async () => {
const dialogSpy = jest

View File

@@ -201,14 +201,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
return new CipherView(this.config?.originalCipher);
}
get canCipherBeArchived(): boolean {
return this.cipher?.canBeArchived;
}
get isCipherArchived(): boolean {
return this.cipher?.isArchived;
}
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
private fido2PopoutSessionData: Fido2SessionData;
@@ -370,10 +362,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
await BrowserApi.sendMessage("addEditCipherSubmitted");
}
get isEditMode(): boolean {
return ["edit", "partial-edit"].includes(this.config?.mode);
}
subscribeToParams(): void {
this.route.queryParams
.pipe(
@@ -487,40 +475,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
return this.i18nService.t(translation[type]);
}
/**
* Update the cipher in the form after archiving/unarchiving.
* @param revisionDate The new revision date.
* @param archivedDate The new archived date (null if unarchived).
**/
updateCipherFromArchive = (revisionDate: Date, archivedDate: Date | null) => {
this.cipherFormComponent().patchCipher((current) => {
current.revisionDate = revisionDate;
current.archivedDate = archivedDate;
return current;
});
};
archive = async () => {
const cipherResponse = await this.archiveCipherUtilsService.archiveCipher(this.cipher, true);
if (!cipherResponse) {
return;
}
this.updateCipherFromArchive(
new Date(cipherResponse.revisionDate),
new Date(cipherResponse.archivedDate),
);
};
unarchive = async () => {
const cipherResponse = await this.archiveCipherUtilsService.unarchiveCipher(this.cipher);
if (!cipherResponse) {
return;
}
this.updateCipherFromArchive(new Date(cipherResponse.revisionDate), null);
};
delete = async () => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteItem" },

View File

@@ -69,7 +69,7 @@ describe("VaultPopupItemsService", () => {
const accountServiceMock = mockAccountServiceWith(userId);
const configServiceMock = mock<ConfigService>();
const cipherArchiveServiceMock = mock<CipherArchiveService>();
cipherArchiveServiceMock.userCanArchive$.mockReturnValue(of(true));
cipherArchiveServiceMock.hasArchiveFlagEnabled$ = of(true);
const restrictedItemTypesService = {
restricted$: new BehaviorSubject<RestrictedCipherType[]>([]),

View File

@@ -135,24 +135,23 @@ export class VaultPopupItemsService {
shareReplay({ refCount: true, bufferSize: 1 }),
);
private userCanArchive$ = this.activeUserId$.pipe(
switchMap((userId) => {
return this.cipherArchiveService.userCanArchive$(userId);
}),
);
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
switchMap((ciphers) =>
combineLatest([this.organizations$, this.decryptedCollections$, this.userCanArchive$]).pipe(
map(([organizations, collections, canArchive]) => {
combineLatest([
this.organizations$,
this.decryptedCollections$,
this.cipherArchiveService.hasArchiveFlagEnabled$,
]).pipe(
map(([organizations, collections, archiveFlag]) => {
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
return ciphers
.filter(
(c) =>
!CipherViewLikeUtils.isDeleted(c) &&
(!canArchive || !CipherViewLikeUtils.isArchived(c)),
(!archiveFlag || !CipherViewLikeUtils.isArchived(c)),
)
.map((cipher) => {
(cipher as PopupCipherViewLike).collections = cipher.collectionIds?.map(
(colId) => collectionMap[colId as CollectionId],