diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts index 786c5de740e..efc018717d4 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/vault-filter.component.ts @@ -1,12 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { Router } from "@angular/router"; import { firstValueFrom, merge, Subject, switchMap, takeUntil } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -47,7 +47,6 @@ export class VaultFilterComponent implements OnInit, OnDestroy { isLoaded = false; protected destroy$: Subject = new Subject(); - private router = inject(Router); get filtersList() { return this.filters ? Object.values(this.filters) : []; } @@ -59,6 +58,9 @@ export class VaultFilterComponent implements OnInit, OnDestroy { if (this.activeFilter.isDeleted) { return "searchTrash"; } + if (this.activeFilter.isArchived) { + return "searchArchive"; + } if (this.activeFilter.cipherType === CipherType.Login) { return "searchLogin"; } @@ -185,6 +187,14 @@ export class VaultFilterComponent implements OnInit, OnDestroy { builderFilter.typeFilter = await this.addTypeFilter(); builderFilter.folderFilter = await this.addFolderFilter(); builderFilter.collectionFilter = await this.addCollectionFilter(); + // PM19148: Innovation Archive + if ( + await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.PM19148_InnovationArchive), + ) + ) { + builderFilter.archiveFilter = await this.addArchiveFilter(); + } builderFilter.trashFilter = await this.addTrashFilter(); return builderFilter; } @@ -322,4 +332,31 @@ export class VaultFilterComponent implements OnInit, OnDestroy { }; return trashFilterSection; } + + protected async addArchiveFilter(): Promise { + const archiveFilterSection: VaultFilterSection = { + data$: this.vaultFilterService.buildTypeTree( + { + id: "headArchive", + name: "HeadArchive", + type: "archive", + icon: "bwi-archive", + }, + [ + { + id: "archive", + name: this.i18nService.t("archive"), + type: "archive", + icon: "bwi-archive", + }, + ], + ), + header: { + showHeader: false, + isSelectable: true, + }, + action: this.applyTypeFilter, + }; + return archiveFilterSection; + } } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts index 25874d683be..936dfb0e675 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts @@ -174,6 +174,11 @@ function createLegacyFilterForEndUser( { id: "trash", name: "", type: "trash", icon: "" }, null, ); + } else if (filter.type !== undefined && filter.type === "archive") { + legacyFilter.selectedCipherTypeNode = new TreeNode( + { id: "archive", name: "", type: "archive", icon: "" }, + null, + ); } else if (filter.type !== undefined && filter.type !== "trash") { legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject( cipherTypeTree, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index a39918df4a7..2c983d66a10 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -6,7 +6,10 @@ import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; export type FilterFunction = (cipher: CipherView) => boolean; -export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction { +export function createFilterFunction( + filter: RoutedVaultFilterModel, + archiveEnabled?: boolean, +): FilterFunction { return (cipher) => { if (filter.type === "favorites" && !cipher.favorite) { return false; @@ -33,6 +36,15 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc if (filter.type !== "trash" && cipher.isDeleted) { return false; } + // Archive filter logic is only applied if the feature flag is enabled + if (archiveEnabled) { + if (filter.type === "archive" && !cipher.isArchived) { + return false; + } + if (filter.type !== "archive" && cipher.isArchived) { + return false; + } + } // No folder if (filter.folderId === Unassigned && cipher.folderId !== null) { return false; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts index fe236a089e0..a168b8001f9 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -129,6 +129,9 @@ export class RoutedVaultFilterBridge implements VaultFilter { get isDeleted(): boolean { return this.legacyFilter.isDeleted; } + get isArchived(): boolean { + return this.legacyFilter.isArchived; + } get organizationId(): string { return this.legacyFilter.organizationId; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts index 866ba1d9848..c333922864f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -8,6 +8,7 @@ const itemTypes = [ "identity", "note", "sshKey", + "archive", "trash", All, ] as const; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts index 0f949e17146..7f91e7ba133 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter-section.type.ts @@ -20,6 +20,7 @@ export enum VaultFilterLabel { TypeFilter = "typeFilter", FolderFilter = "folderFilter", CollectionFilter = "collectionFilter", + ArchiveFilter = "archiveFilter", TrashFilter = "trashFilter", } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.model.ts index c486ad800ab..dadf482565e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.model.ts @@ -72,6 +72,10 @@ export class VaultFilter { return this.selectedCipherTypeNode?.node.type === "trash" ? true : null; } + get isArchived(): boolean { + return this.selectedCipherTypeNode?.node.type === "archive"; + } + get organizationId(): string { return this.selectedOrganizationNode?.node.id; } @@ -121,6 +125,9 @@ export class VaultFilter { if (this.isDeleted && cipherPassesFilter) { cipherPassesFilter = cipher.isDeleted; } + if (this.isArchived && cipherPassesFilter) { + cipherPassesFilter = cipher.isArchived; + } if (this.cipherType && cipherPassesFilter) { cipherPassesFilter = cipher.type === this.cipherType; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts index 9259dd08114..7a92db6a381 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts @@ -4,7 +4,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -export type CipherStatus = "all" | "favorites" | "trash" | CipherType; +export type CipherStatus = "all" | "favorites" | "archive" | "trash" | CipherType; export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string }; export type CollectionFilter = CollectionAdminView & { diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 489f42649f9..bdfcabb6a4b 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -143,6 +143,10 @@ export class VaultHeaderComponent implements OnInit { return this.i18nService.t("myVault"); } + if (this.filter.type === "archive") { + return this.i18nService.t("archive"); + } + const activeOrganization = this.activeOrganization; if (activeOrganization) { return `${activeOrganization.name} ${this.i18nService.t("vault").toLowerCase()}`; diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 1a2a1fdbca6..00f12a77cd9 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -70,17 +70,20 @@ class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start" > -

{{ "noItemsInList" | i18n }}

- + @if (filter.type === "archive") { +
{{ "noItemsInArchive" | i18n }}
+

+ {{ "archivedItemsDescription" | i18n }} +

+ } @else { +

{{ "noItemsInList" | i18n }}

+ @if (filter.type !== "trash") { + + } + } diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index d4441c73719..5eefd14f9b2 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -1,7 +1,15 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { DialogRef } from "@angular/cdk/dialog"; -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core"; +import { + ChangeDetectorRef, + Component, + NgZone, + OnDestroy, + OnInit, + ViewChild, + inject, +} from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; import { BehaviorSubject, @@ -50,7 +58,9 @@ import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; 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"; @@ -148,11 +158,13 @@ const SearchTextDebounceInterval = 200; ], }) export class VaultComponent implements OnInit, OnDestroy { + private configService: ConfigService = inject(ConfigService); @ViewChild("vaultFilter", { static: true }) filterComponent: VaultFilterComponent; trashCleanupWarning: string = null; kdfIterations: number; activeFilter: VaultFilter = new VaultFilter(); + archiveItemEnabled$: Observable; protected noItemIcon = Icons.Search; protected performingInitialLoad = true; @@ -270,7 +282,11 @@ export class VaultComponent implements OnInit, OnDestroy { private trialFlowService: TrialFlowService, private organizationBillingService: OrganizationBillingServiceAbstraction, private billingNotificationService: BillingNotificationService, - ) {} + ) { + this.archiveItemEnabled$ = this.configService.getFeatureFlag$( + FeatureFlag.PM19148_InnovationArchive, + ); + } async ngOnInit() { this.trashCleanupWarning = this.i18nService.t( @@ -346,13 +362,14 @@ export class VaultComponent implements OnInit, OnDestroy { this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)), filter$, this.currentSearchText$, + this.archiveItemEnabled$, ]).pipe( filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), - concatMap(async ([ciphers, filter, searchText]) => { + concatMap(async ([ciphers, filter, searchText, archiveEnabled]) => { const failedCiphers = await firstValueFrom( this.cipherService.failedToDecryptCiphers$(activeUserId), ); - const filterFunction = createFilterFunction(filter); + const filterFunction = createFilterFunction(filter, archiveEnabled); // Append any failed to decrypt ciphers to the top of the cipher list const allCiphers = [...failedCiphers, ...ciphers]; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 7f9c89484c2..afc3c950ea6 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -10570,5 +10570,17 @@ }, "cannotCreateCollection": { "message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections." + }, + "searchArchive": { + "message": "Search archive" + }, + "archive": { + "message": "Archive" + }, + "noItemsInArchive": { + "message": "No items in archive" + }, + "archivedItemsDescription": { + "message": "Archived items will appear here and will be excluded from general search results and autofill suggestions." } } diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg index c8535bebef8..b59907d0a7c 100644 --- a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg +++ b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg @@ -217,6 +217,9 @@ + + + diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf index 9696152a498..b61ad0f4e4e 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf and b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff index 554375a1ce9..cbca17c53eb 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 index 80544871f3a..04bee37352d 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 differ diff --git a/libs/angular/src/scss/bwicons/styles/style.scss b/libs/angular/src/scss/bwicons/styles/style.scss index 3a0c9610bd4..c0976966684 100644 --- a/libs/angular/src/scss/bwicons/styles/style.scss +++ b/libs/angular/src/scss/bwicons/styles/style.scss @@ -295,6 +295,7 @@ $icons: ( "icon-7": "\e9b7", "icon-8": "\e9b8", "icon-9": "\e9b9", + "archive": "\e9c1", ); @each $name, $glyph in $icons { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index dda27d202de..6622b309400 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -46,6 +46,7 @@ export enum FeatureFlag { PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal", PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features", PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method", + PM19148_InnovationArchive = "pm-19148-innovation-archive", } export type AllowedFeatureFlagTypes = boolean | number | string; @@ -102,6 +103,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE, [FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE, [FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE, + [FeatureFlag.PM19148_InnovationArchive]: FALSE, } satisfies Record; export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; diff --git a/libs/common/src/vault/models/data/cipher.data.ts b/libs/common/src/vault/models/data/cipher.data.ts index ee5e5b3e72b..47cf948b422 100644 --- a/libs/common/src/vault/models/data/cipher.data.ts +++ b/libs/common/src/vault/models/data/cipher.data.ts @@ -40,6 +40,7 @@ export class CipherData { collectionIds?: string[]; creationDate: string; deletedDate: string; + archivedDate: string | null; reprompt: CipherRepromptType; key: string; @@ -63,6 +64,7 @@ export class CipherData { this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds; this.creationDate = response.creationDate; this.deletedDate = response.deletedDate; + this.archivedDate = response.archivedDate; this.reprompt = response.reprompt; this.key = response.key; diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 1b2b093a553..fd058580330 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -56,6 +56,7 @@ describe("Cipher DTO", () => { passwordHistory: null, key: null, permissions: undefined, + archivedDate: null, }); }); @@ -80,6 +81,7 @@ describe("Cipher DTO", () => { permissions: new CipherPermissionsApi(), reprompt: CipherRepromptType.None, key: "EncryptedString", + archivedDate: null, login: { uris: [ { @@ -155,6 +157,7 @@ describe("Cipher DTO", () => { permissions: new CipherPermissionsApi(), reprompt: 0, key: { encryptedString: "EncryptedString", encryptionType: 0 }, + archivedDate: null, login: { passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"), autofillOnPageLoad: false, @@ -233,6 +236,7 @@ describe("Cipher DTO", () => { cipher.reprompt = CipherRepromptType.None; cipher.key = mockEnc("EncKey"); cipher.permissions = new CipherPermissionsApi(); + cipher.archivedDate = null; const loginView = new LoginView(); loginView.username = "username"; @@ -276,6 +280,7 @@ describe("Cipher DTO", () => { reprompt: 0, localData: undefined, permissions: new CipherPermissionsApi(), + archivedDate: null, }); }); }); @@ -304,6 +309,7 @@ describe("Cipher DTO", () => { type: SecureNoteType.Generic, }, permissions: new CipherPermissionsApi(), + archivedDate: null, }; }); @@ -334,6 +340,7 @@ describe("Cipher DTO", () => { passwordHistory: null, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), + archivedDate: null, }); }); @@ -362,6 +369,7 @@ describe("Cipher DTO", () => { cipher.secureNote.type = SecureNoteType.Generic; cipher.key = mockEnc("EncKey"); cipher.permissions = new CipherPermissionsApi(); + cipher.archivedDate = null; const keyService = mock(); const encryptService = mock(); @@ -397,6 +405,7 @@ describe("Cipher DTO", () => { reprompt: 0, localData: undefined, permissions: new CipherPermissionsApi(), + archivedDate: null, }); }); }); @@ -430,6 +439,7 @@ describe("Cipher DTO", () => { code: "EncryptedString", }, key: "EncKey", + archivedDate: null, }; }); @@ -467,6 +477,7 @@ describe("Cipher DTO", () => { passwordHistory: null, key: { encryptedString: "EncKey", encryptionType: 0 }, permissions: new CipherPermissionsApi(), + archivedDate: null, }); }); @@ -493,6 +504,7 @@ describe("Cipher DTO", () => { cipher.reprompt = CipherRepromptType.None; cipher.key = mockEnc("EncKey"); cipher.permissions = new CipherPermissionsApi(); + cipher.archivedDate = null; const cardView = new CardView(); cardView.cardholderName = "cardholderName"; @@ -536,6 +548,7 @@ describe("Cipher DTO", () => { reprompt: 0, localData: undefined, permissions: new CipherPermissionsApi(), + archivedDate: null, }); }); }); @@ -561,6 +574,7 @@ describe("Cipher DTO", () => { permissions: new CipherPermissionsApi(), reprompt: CipherRepromptType.None, key: "EncKey", + archivedDate: null, identity: { title: "EncryptedString", firstName: "EncryptedString", @@ -605,6 +619,7 @@ describe("Cipher DTO", () => { creationDate: new Date("2022-01-01T12:00:00.000Z"), deletedDate: null, reprompt: 0, + archivedDate: null, identity: { title: { encryptedString: "EncryptedString", encryptionType: 0 }, firstName: { encryptedString: "EncryptedString", encryptionType: 0 }, @@ -656,6 +671,7 @@ describe("Cipher DTO", () => { cipher.reprompt = CipherRepromptType.None; cipher.key = mockEnc("EncKey"); cipher.permissions = new CipherPermissionsApi(); + cipher.archivedDate = null; const identityView = new IdentityView(); identityView.firstName = "firstName"; @@ -699,6 +715,7 @@ describe("Cipher DTO", () => { reprompt: 0, localData: undefined, permissions: new CipherPermissionsApi(), + archivedDate: null, }); }); }); @@ -712,6 +729,7 @@ describe("Cipher DTO", () => { const revisionDate = new Date("2022-08-04T01:06:40.441Z"); const deletedDate = new Date("2022-09-04T01:06:40.441Z"); + const archivedDate = new Date("2022-10-04T01:06:40.441Z"); const actual = Cipher.fromJSON({ name: "myName", notes: "myNotes", @@ -720,6 +738,7 @@ describe("Cipher DTO", () => { fields: ["field1", "field2"] as any, passwordHistory: ["ph1", "ph2"] as any, deletedDate: deletedDate.toISOString(), + archivedDate: archivedDate.toISOString(), } as Jsonify); expect(actual).toMatchObject({ @@ -730,6 +749,7 @@ describe("Cipher DTO", () => { fields: ["field1_fromJSON", "field2_fromJSON"], passwordHistory: ["ph1_fromJSON", "ph2_fromJSON"], deletedDate: deletedDate, + archivedDate: archivedDate, }); expect(actual).toBeInstanceOf(Cipher); }); diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index f23e3c0c579..588e5017d52 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -54,6 +54,7 @@ export class Cipher extends Domain implements Decryptable { collectionIds: string[]; creationDate: Date; deletedDate: Date; + archivedDate: Date; reprompt: CipherRepromptType; key: EncString; @@ -92,6 +93,7 @@ export class Cipher extends Domain implements Decryptable { this.localData = localData; this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null; this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : null; + this.archivedDate = obj.archivedDate != null ? new Date(obj.archivedDate) : null; this.reprompt = obj.reprompt; switch (this.type) { @@ -248,6 +250,7 @@ export class Cipher extends Domain implements Decryptable { c.reprompt = this.reprompt; c.key = this.key?.encryptedString; c.permissions = this.permissions; + c.archivedDate = this.archivedDate != null ? this.archivedDate.toISOString() : null; this.buildDataModel(this, c, { name: null, @@ -300,6 +303,7 @@ export class Cipher extends Domain implements Decryptable { const fields = obj.fields?.map((f: any) => Field.fromJSON(f)); const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph)); const key = EncString.fromJSON(obj.key); + const archivedDate = obj.archivedDate == null ? null : new Date(obj.archivedDate); Object.assign(domain, obj, { name, @@ -310,6 +314,7 @@ export class Cipher extends Domain implements Decryptable { fields, passwordHistory, key, + archivedDate, }); switch (obj.type) { diff --git a/libs/common/src/vault/models/response/cipher.response.ts b/libs/common/src/vault/models/response/cipher.response.ts index 944a19e088b..d8d891ab48b 100644 --- a/libs/common/src/vault/models/response/cipher.response.ts +++ b/libs/common/src/vault/models/response/cipher.response.ts @@ -37,6 +37,7 @@ export class CipherResponse extends BaseResponse { collectionIds: string[]; creationDate: string; deletedDate: string; + archivedDate: string; reprompt: CipherRepromptType; key: string; @@ -61,6 +62,7 @@ export class CipherResponse extends BaseResponse { this.collectionIds = this.getResponseProperty("CollectionIds"); this.creationDate = this.getResponseProperty("CreationDate"); this.deletedDate = this.getResponseProperty("DeletedDate"); + this.archivedDate = this.getResponseProperty("ArchivedDate"); const login = this.getResponseProperty("Login"); if (login != null) { diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 7ddba9e2ed5..8f778303cc8 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -46,6 +46,7 @@ export class CipherView implements View, InitializerMetadata { revisionDate: Date = null; creationDate: Date = null; deletedDate: Date = null; + archivedDate: Date = null; reprompt: CipherRepromptType = CipherRepromptType.None; /** @@ -72,6 +73,7 @@ export class CipherView implements View, InitializerMetadata { this.revisionDate = c.revisionDate; this.creationDate = c.creationDate; this.deletedDate = c.deletedDate; + this.archivedDate = c.archivedDate; // Old locally stored ciphers might have reprompt == null. If so set it to None. this.reprompt = c.reprompt ?? CipherRepromptType.None; } @@ -135,6 +137,10 @@ export class CipherView implements View, InitializerMetadata { return this.deletedDate != null; } + get isArchived(): boolean { + return this.archivedDate != null; + } + get linkedFieldOptions() { return this.item?.linkedFieldOptions; } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index def0c04dd16..76e1ce44018 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -60,7 +60,9 @@ const cipherData: CipherData = { deletedDate: null, permissions: new CipherPermissionsApi(), key: "EncKey", + archivedDate: null, reprompt: CipherRepromptType.None, + login: { uris: [ { uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain }, diff --git a/libs/components/src/stories/icons.mdx b/libs/components/src/stories/icons.mdx index f16b5d56b16..b83230b36fb 100644 --- a/libs/components/src/stories/icons.mdx +++ b/libs/components/src/stories/icons.mdx @@ -91,6 +91,7 @@ or an options menu icon. | | bwi-trash | delete action or trash area | | | bwi-undo | restore action | | | bwi-unlock | unlocked | +| | bwi-archive | archive action or archive area | ## Directional and Menu Indicators