1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 21:50:15 +00:00

[PM-31675] remove archive from web edit (#18764)

* refactor default cipher archive service, update archive/unarchive in vault-item-dialog, remove archive/unarchive items in edit form
This commit is contained in:
Jason Ng
2026-02-09 16:17:46 -05:00
committed by GitHub
parent e92817011b
commit 322ff6b70b
6 changed files with 66 additions and 60 deletions

View File

@@ -7,7 +7,7 @@
[backAction]="handleBackButton"
showBackButton
>
@if (config?.originalCipher?.archivedDate) {
@if (config?.originalCipher?.archivedDate && (archiveFlagEnabled$ | async)) {
<ng-container slot="end">
<span bitBadge variant="secondary" [appA11yTitle]="'archived' | i18n">
{{ "archived" | i18n }}

View File

@@ -1,7 +1,7 @@
<popup-page>
<popup-header slot="header" [pageTitle]="headerText" showBackButton>
<ng-container slot="end">
@if (cipher?.isArchived) {
@if (cipher?.isArchived && (archiveFlagEnabled$ | async)) {
<span bitBadge variant="secondary" [appA11yTitle]="'archived' | i18n">
{{ "archived" | i18n }}
</span>

View File

@@ -3,7 +3,7 @@
{{ title }}
</span>
@if (isCipherArchived && !params.isAdminConsoleAction) {
@if (isCipherArchived && !params.isAdminConsoleAction && (archiveFlagEnabled$ | async)) {
<span bitBadge bitDialogHeaderEnd> {{ "archived" | i18n }} </span>
}
@@ -86,8 +86,8 @@
@if (showActionButtons) {
<div class="tw-ml-auto">
@if ((userCanArchive$ | async) && !params.isAdminConsoleAction) {
@if (isCipherArchived && !cipher?.isDeleted) {
@if (showArchiveOptions) {
@if (showUnarchiveBtn) {
<button
type="button"
class="tw-mr-1"
@@ -96,7 +96,7 @@
[label]="'unArchive' | i18n"
></button>
}
@if (cipher?.canBeArchived) {
@if (showArchiveBtn) {
<button
type="button"
class="tw-mr-1"

View File

@@ -119,7 +119,7 @@ describe("VaultItemDialogComponent", () => {
provide: CipherArchiveService,
useValue: {
userCanArchive$: jest.fn().mockReturnValue(of(true)),
hasArchiveFlagEnabled$: jest.fn().mockReturnValue(of(true)),
hasArchiveFlagEnabled$: of(true),
archiveWithServer: jest.fn().mockResolvedValue({}),
unarchiveWithServer: jest.fn().mockResolvedValue({}),
},
@@ -258,19 +258,19 @@ describe("VaultItemDialogComponent", () => {
expect(archiveButton).toBeFalsy();
});
it("should show archive button when the user can archive the item and the item can be archived", () => {
it("should show archive button when the user can archive the item, item can be archived, and dialog is in view mode", () => {
component.setTestCipher({ canBeArchived: true });
(component as any).userCanArchive$ = of(true);
component.setTestParams({ mode: "form" });
component.setTestParams({ mode: "view" });
fixture.detectChanges();
const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']"));
expect(archiveButton).toBeTruthy();
});
it("should not show archive button when the user cannot archive the item", () => {
it("should not show archive button when the user does not have premium", () => {
(component as any).userCanArchive$ = of(false);
component.setTestCipher({});
component.setTestParams({ mode: "form" });
component.setTestParams({ mode: "view" });
fixture.detectChanges();
const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']"));
expect(archiveButton).toBeFalsy();
@@ -283,18 +283,35 @@ describe("VaultItemDialogComponent", () => {
const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']"));
expect(archiveButton).toBeFalsy();
});
it("should not show archive button when dialog is not in view mode", () => {
component.setTestCipher({ canBeArchived: true });
(component as any).userCanArchive$ = of(true);
component.setTestParams({ mode: "form" });
fixture.detectChanges();
const archiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-archive']"));
expect(archiveButton).toBeFalsy();
});
});
describe("unarchive button", () => {
it("should show the unarchive button when the item is archived", () => {
it("should show the unarchive button when the item is archived, and dialog in view mode", () => {
component.setTestCipher({ isArchived: true });
component.setTestParams({ mode: "form" });
component.setTestParams({ mode: "view" });
fixture.detectChanges();
const unarchiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-unarchive']"));
expect(unarchiveButton).toBeTruthy();
});
it("should not show the unarchive button when the item is not archived", () => {
component.setTestCipher({ isArchived: false });
component.setTestParams({ mode: "view" });
fixture.detectChanges();
const unarchiveButton = fixture.debugElement.query(By.css("[biticonbutton='bwi-unarchive']"));
expect(unarchiveButton).toBeFalsy();
});
it("should not show the unarchive button when dialog is not in view mode", () => {
component.setTestCipher({ isArchived: false });
component.setTestParams({ mode: "form" });
fixture.detectChanges();

View File

@@ -28,7 +28,7 @@ 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, OrganizationId, 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 { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
@@ -293,6 +293,20 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
return this.cipher?.isArchived;
}
private _userCanArchive = false;
protected get showArchiveOptions(): boolean {
return this._userCanArchive && !this.params.isAdminConsoleAction && this.params.mode === "view";
}
protected get showArchiveBtn(): boolean {
return this.cipher?.canBeArchived;
}
protected get showUnarchiveBtn(): boolean {
return this.isCipherArchived && !this.cipher?.isDeleted;
}
/**
* Flag to initialize/attach the form component.
*/
@@ -341,6 +355,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
takeUntilDestroyed(),
)
.subscribe();
this.userCanArchive$.pipe(takeUntilDestroyed()).subscribe((v) => (this._userCanArchive = v));
}
async ngOnInit() {
@@ -574,20 +590,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
await this.changeMode("view");
};
updateCipherFromArchive = (revisionDate: Date, archivedDate: Date | null) => {
this.cipher.archivedDate = archivedDate;
this.cipher.revisionDate = revisionDate;
updateCipherFromResponse = async (cipherResponse: CipherData, userId: UserId) => {
const cipher: Cipher = new Cipher(cipherResponse);
// If we're in View mode, we don't need to update the form.
if (this.params.mode === "view") {
return;
}
cipher.collectionIds = [...this.cipher.collectionIds];
this.cipherFormComponent().patchCipher((current) => {
current.revisionDate = revisionDate;
current.archivedDate = archivedDate;
return current;
});
const cipherView = await this.cipherService.decrypt(cipher, userId);
await this.onCipherSaved(cipherView);
};
archive = async () => {
@@ -597,10 +607,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
this.cipher.id as CipherId,
activeUserId,
);
this.updateCipherFromArchive(
new Date(cipherResponse.revisionDate),
cipherResponse.archivedDate ? new Date(cipherResponse.archivedDate) : null,
);
await this.updateCipherFromResponse(cipherResponse, activeUserId);
this.toastService.showToast({
variant: "success",
@@ -621,7 +629,9 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
this.cipher.id as CipherId,
activeUserId,
);
this.updateCipherFromArchive(new Date(cipherResponse.revisionDate), null);
await this.updateCipherFromResponse(cipherResponse, activeUserId);
this.toastService.showToast({
variant: "success",
message: this.i18nService.t("itemWasUnarchived"),
@@ -631,7 +641,6 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
variant: "error",
message: this.i18nService.t("errorOccurred"),
});
return;
}
};

View File

@@ -91,21 +91,11 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
const response = new ListResponse(r, CipherResponse);
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
// prevent mutating ciphers$ state
const localCiphers = structuredClone(currentCiphers);
const responseDataArray = response.data.map(
(cipher) => new CipherData(cipher, currentCiphers[cipher.id as CipherId]?.collectionIds),
);
for (const cipher of response.data) {
const localCipher = localCiphers[cipher.id as CipherId];
if (localCipher == null) {
continue;
}
localCipher.archivedDate = cipher.archivedDate;
localCipher.revisionDate = cipher.revisionDate;
}
await this.cipherService.upsert(Object.values(localCiphers), userId);
await this.cipherService.upsert(responseDataArray, userId);
return response.data[0];
}
@@ -115,21 +105,11 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
const response = new ListResponse(r, CipherResponse);
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
// prevent mutating ciphers$ state
const localCiphers = structuredClone(currentCiphers);
const responseDataArray = response.data.map(
(cipher) => new CipherData(cipher, currentCiphers[cipher.id as CipherId]?.collectionIds),
);
for (const cipher of response.data) {
const localCipher = localCiphers[cipher.id as CipherId];
if (localCipher == null) {
continue;
}
localCipher.archivedDate = cipher.archivedDate;
localCipher.revisionDate = cipher.revisionDate;
}
await this.cipherService.upsert(Object.values(localCiphers), userId);
await this.cipherService.upsert(responseDataArray, userId);
return response.data[0];
}
}