mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-19152] Archive in Web (#16686)
* archive and unarchive an individual item * bulk archive and unachive * updates to text strings for archive empty state and tooltips * update translation keys to have an archive verb and noun differentiation * if premium member loses premium and has archive items. apply filter changes, and item more option changes * updating unArchive text * unarchive an archived item on edit if user loses premium * updates for unarchive btn, refactor archive flag for less churn * add services to cipher form stories * add refresh to archive calls in vault, update bulk archive copy * Do not show archive ability for deleted items * add archive check for login menu actions * remove assign to collections for archive filter * update bulk success message * add error handling for archive methods * fix null reference check * add unarchive icon --------- Co-authored-by: Nick Krantz <nick@livefront.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
|
||||
export abstract class CipherArchiveService {
|
||||
abstract hasArchiveFlagEnabled$(): Observable<boolean>;
|
||||
abstract archivedCiphers$(userId: UserId): Observable<CipherViewLike[]>;
|
||||
abstract userCanArchive$(userId: UserId): Observable<boolean>;
|
||||
abstract showArchiveVault$(userId: UserId): Observable<boolean>;
|
||||
|
||||
@@ -27,6 +27,10 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
hasArchiveFlagEnabled$(): Observable<boolean> {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable that contains the list of ciphers that have been archived.
|
||||
*/
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
moduleMetadata,
|
||||
StoryObj,
|
||||
} from "@storybook/angular";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -155,6 +155,20 @@ export default {
|
||||
} as NudgeStatus),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CipherArchiveService,
|
||||
useValue: {
|
||||
userCanArchive$: of(false),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
name: "User 1",
|
||||
}),
|
||||
} as Partial<AccountService>,
|
||||
},
|
||||
{
|
||||
provide: CipherFormService,
|
||||
useClass: TestAddEditFormService,
|
||||
|
||||
@@ -2,14 +2,18 @@ import { ChangeDetectorRef } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ReactiveFormsModule } from "@angular/forms";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/view-cache";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { CipherFormConfig } from "../abstractions/cipher-form-config.service";
|
||||
import { CipherFormService } from "../abstractions/cipher-form.service";
|
||||
@@ -23,6 +27,10 @@ describe("CipherFormComponent", () => {
|
||||
|
||||
const decryptCipher = jest.fn().mockResolvedValue(new CipherView());
|
||||
|
||||
const mockAccountService = mock<AccountService>();
|
||||
const mockCipherArchiveService = mock<CipherArchiveService>();
|
||||
const mockAddEditFormService = { saveCipher: jest.fn(), decryptCipher };
|
||||
|
||||
beforeEach(async () => {
|
||||
decryptCipher.mockClear();
|
||||
|
||||
@@ -32,13 +40,15 @@ describe("CipherFormComponent", () => {
|
||||
{ provide: ChangeDetectorRef, useValue: {} },
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: ToastService, useValue: { showToast: jest.fn() } },
|
||||
{ provide: CipherFormService, useValue: { saveCipher: jest.fn(), decryptCipher } },
|
||||
{ provide: CipherFormService, useValue: mockAddEditFormService },
|
||||
{
|
||||
provide: CipherFormCacheService,
|
||||
useValue: { init: jest.fn(), getCachedCipherView: jest.fn() },
|
||||
},
|
||||
{ provide: ViewCacheService, useValue: { signal: jest.fn(() => (): any => null) } },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: CipherArchiveService, useValue: mockCipherArchiveService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -53,6 +63,29 @@ describe("CipherFormComponent", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("submit", () => {
|
||||
beforeEach(() => {
|
||||
component.config = { mode: "edit" } as CipherFormConfig;
|
||||
|
||||
component["updatedCipherView"] = new CipherView();
|
||||
component["updatedCipherView"].archivedDate = new Date();
|
||||
});
|
||||
|
||||
it("should remove archivedDate when user cannot archive and cipher is archived", async () => {
|
||||
mockAccountService.activeAccount$ = of({ id: "user-id" as UserId } as Account);
|
||||
mockCipherArchiveService.userCanArchive$.mockReturnValue(of(false));
|
||||
mockAddEditFormService.saveCipher = jest.fn().mockResolvedValue(new CipherView());
|
||||
|
||||
const originalArchivedDate = component["updatedCipherView"]?.archivedDate;
|
||||
expect(originalArchivedDate).not.toBeNull();
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(component["updatedCipherView"]?.archivedDate).toBeNull();
|
||||
expect(mockCipherArchiveService.userCanArchive$).toHaveBeenCalledWith("user-id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("website", () => {
|
||||
it("should return null if updatedCipherView is null", () => {
|
||||
component["updatedCipherView"] = null as any;
|
||||
|
||||
@@ -17,9 +17,12 @@ import {
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, Subject, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
@@ -301,6 +304,8 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
||||
private i18nService: I18nService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private cipherFormCacheService: CipherFormCacheService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -342,6 +347,18 @@ export class CipherFormComponent implements AfterViewInit, OnInit, OnChanges, Ci
|
||||
}
|
||||
}
|
||||
|
||||
const userCanArchive = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.cipherArchiveService.userCanArchive$(userId)),
|
||||
),
|
||||
);
|
||||
|
||||
// If the item is archived but user has lost archive permissions, unarchive the item.
|
||||
if (!userCanArchive && this.updatedCipherView.archivedDate) {
|
||||
this.updatedCipherView.archivedDate = null;
|
||||
}
|
||||
|
||||
const savedCipher = await this.addEditFormService.saveCipher(
|
||||
this.updatedCipherView,
|
||||
this.config,
|
||||
|
||||
Reference in New Issue
Block a user