1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 16:43:27 +00:00

[PM-24560] - Add Archive UI Element to View and Edit Item Cards (#16954)

* finalize new UI elements for archive/unarchive

* add tests

* add missing service

* add tests

* updates to edit and view pages

* use structureClone

* fix lint

* fix typo

* clean up return types

* fixes to archive UI

* fix tests

* use @if and userId$
This commit is contained in:
Jordan Aasen
2026-01-09 16:39:22 -08:00
committed by jaasen-livefront
parent 246a549f58
commit 348ed4b616
17 changed files with 824 additions and 122 deletions

View File

@@ -3,12 +3,14 @@ import { Observable } from "rxjs";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { CipherData } from "../models/data/cipher.data";
export abstract class CipherArchiveService {
abstract hasArchiveFlagEnabled$: Observable<boolean>;
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
abstract userCanArchive$(userId: UserId): Observable<boolean>;
abstract userHasPremium$(userId: UserId): Observable<boolean>;
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void>;
abstract archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData>;
abstract unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData>;
abstract showSubscriptionEndedMessaging$(userId: UserId): Observable<boolean>;
}

View File

@@ -109,6 +109,10 @@ export class CipherView implements View, InitializerMetadata {
return this.item?.subTitle;
}
get canBeArchived(): boolean {
return !this.isDeleted && !this.isArchived;
}
get hasPasswordHistory(): boolean {
return this.passwordHistory && this.passwordHistory.length > 0;
}

View File

@@ -1,3 +1,7 @@
/**
* include structuredClone in test environment.
* @jest-environment ../../../../shared/test.environment.ts
*/
import { mock } from "jest-mock-extended";
import { of, firstValueFrom, BehaviorSubject } from "rxjs";

View File

@@ -18,6 +18,7 @@ import {
} from "@bitwarden/common/vault/utils/cipher-view-like-utils";
import { CipherArchiveService } from "../abstractions/cipher-archive.service";
import { CipherData } from "../models/data/cipher.data";
export class DefaultCipherArchiveService implements CipherArchiveService {
constructor(
@@ -84,15 +85,17 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
);
}
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
async archiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData> {
const request = new CipherBulkArchiveRequest(Array.isArray(ids) ? ids : [ids]);
const r = await this.apiService.send("PUT", "/ciphers/archive", request, true, true);
const response = new ListResponse(r, CipherResponse);
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
// prevent mutating ciphers$ state
const localCiphers = structuredClone(currentCiphers);
for (const cipher of response.data) {
const localCipher = currentCiphers[cipher.id as CipherId];
const localCipher = localCiphers[cipher.id as CipherId];
if (localCipher == null) {
continue;
@@ -102,18 +105,21 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
localCipher.revisionDate = cipher.revisionDate;
}
await this.cipherService.upsert(Object.values(currentCiphers), userId);
await this.cipherService.upsert(Object.values(localCiphers), userId);
return response.data[0];
}
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<CipherData> {
const request = new CipherBulkUnarchiveRequest(Array.isArray(ids) ? ids : [ids]);
const r = await this.apiService.send("PUT", "/ciphers/unarchive", request, true, true);
const response = new ListResponse(r, CipherResponse);
const currentCiphers = await firstValueFrom(this.cipherService.ciphers$(userId));
// prevent mutating ciphers$ state
const localCiphers = structuredClone(currentCiphers);
for (const cipher of response.data) {
const localCipher = currentCiphers[cipher.id as CipherId];
const localCipher = localCiphers[cipher.id as CipherId];
if (localCipher == null) {
continue;
@@ -123,6 +129,7 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
localCipher.revisionDate = cipher.revisionDate;
}
await this.cipherService.upsert(Object.values(currentCiphers), userId);
await this.cipherService.upsert(Object.values(localCiphers), userId);
return response.data[0];
}
}