mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 14:53:33 +00:00
[PM-24533] Initialize Archive Feature (#16226)
* [PM-19237] Add Archive Filter Type (#13852) * Browser can archive and unarchive items * Create Archive Cipher Service * Add flag and premium permissions to Archive --------- Co-authored-by: SmithThe4th <gsmith@bitwarden.com> Co-authored-by: Shane <smelton@bitwarden.com> Co-authored-by: Patrick Pimentel <ppimentel@bitwarden.com>
This commit is contained in:
@@ -7,13 +7,13 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
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";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CipherArchiveService } from "@bitwarden/vault";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
@@ -51,10 +51,10 @@ export class VaultFilterComponent
|
||||
protected toastService: ToastService,
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
protected accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
protected cipherService: CipherService,
|
||||
protected cipherArchiveService: CipherArchiveService,
|
||||
) {
|
||||
super(
|
||||
vaultFilterService,
|
||||
@@ -64,10 +64,10 @@ export class VaultFilterComponent
|
||||
toastService,
|
||||
billingApiService,
|
||||
dialogService,
|
||||
configService,
|
||||
accountService,
|
||||
restrictedItemTypesService,
|
||||
cipherService,
|
||||
cipherArchiveService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
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";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -25,6 +24,7 @@ import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { CipherArchiveService } from "@bitwarden/vault";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/organizations/warnings/services";
|
||||
|
||||
import { VaultFilterService } from "../services/abstractions/vault-filter.service";
|
||||
@@ -112,6 +112,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";
|
||||
}
|
||||
@@ -153,10 +156,10 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
protected toastService: ToastService,
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
protected accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
protected cipherService: CipherService,
|
||||
protected cipherArchiveService: CipherArchiveService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -248,11 +251,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.organizationFilter = await this.addOrganizationFilter();
|
||||
builderFilter.typeFilter = await this.addTypeFilter();
|
||||
builderFilter.folderFilter = await this.addFolderFilter();
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
if (
|
||||
(await firstValueFrom(this.cipherArchiveService.userCanArchive$(userId))) ||
|
||||
(await firstValueFrom(this.cipherArchiveService.showArchiveVault$(userId)))
|
||||
) {
|
||||
builderFilter.archiveFilter = await this.addArchiveFilter();
|
||||
}
|
||||
builderFilter.trashFilter = await this.addTrashFilter();
|
||||
return builderFilter;
|
||||
}
|
||||
@@ -412,4 +422,31 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
return trashFilterSection;
|
||||
}
|
||||
|
||||
protected async addArchiveFilter(): Promise<VaultFilterSection> {
|
||||
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 as (filterNode: TreeNode<VaultFilterType>) => Promise<void>,
|
||||
};
|
||||
return archiveFilterSection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CipherTypeFilter>(
|
||||
{ id: "archive", name: "", type: "archive", icon: "" },
|
||||
null,
|
||||
);
|
||||
} else if (filter.type !== undefined && filter.type !== "trash") {
|
||||
legacyFilter.selectedCipherTypeNode = ServiceUtils.getTreeNodeObject(
|
||||
cipherTypeTree,
|
||||
|
||||
@@ -9,7 +9,10 @@ import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
|
||||
|
||||
export type FilterFunction = (cipher: CipherViewLike) => boolean;
|
||||
|
||||
export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunction {
|
||||
export function createFilterFunction(
|
||||
filter: RoutedVaultFilterModel,
|
||||
archiveEnabled?: boolean,
|
||||
): FilterFunction {
|
||||
return (cipher) => {
|
||||
const type = CipherViewLikeUtils.getType(cipher);
|
||||
const isDeleted = CipherViewLikeUtils.isDeleted(cipher);
|
||||
@@ -39,6 +42,15 @@ export function createFilterFunction(filter: RoutedVaultFilterModel): FilterFunc
|
||||
if (filter.type !== "trash" && isDeleted) {
|
||||
return false;
|
||||
}
|
||||
// Archive filter logic is only applied if the feature flag is enabled
|
||||
if (archiveEnabled) {
|
||||
if (filter.type === "archive" && !CipherViewLikeUtils.isArchived(cipher)) {
|
||||
return false;
|
||||
}
|
||||
if (filter.type !== "archive" && CipherViewLikeUtils.isArchived(cipher)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No folder
|
||||
if (filter.folderId === Unassigned && cipher.folderId != null) {
|
||||
return false;
|
||||
|
||||
@@ -130,6 +130,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;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const itemTypes = [
|
||||
"identity",
|
||||
"note",
|
||||
"sshKey",
|
||||
"archive",
|
||||
"trash",
|
||||
All,
|
||||
] as const;
|
||||
|
||||
@@ -21,6 +21,7 @@ export const VaultFilterLabel = {
|
||||
TypeFilter: "typeFilter",
|
||||
FolderFilter: "folderFilter",
|
||||
CollectionFilter: "collectionFilter",
|
||||
ArchiveFilter: "archiveFilter",
|
||||
TrashFilter: "trashFilter",
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -139,6 +139,10 @@ export class VaultHeaderComponent {
|
||||
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()}`;
|
||||
|
||||
@@ -69,14 +69,18 @@
|
||||
*ngIf="isEmpty && !performingInitialLoad"
|
||||
>
|
||||
<bit-no-items [icon]="noItemIcon">
|
||||
<div slot="title">{{ "noItemsInList" | i18n }}</div>
|
||||
<div slot="title" *ngIf="filter.type === 'archive'">{{ "noItemsInArchive" | i18n }}</div>
|
||||
<p slot="description" class="tw-text-center tw-max-w-md" *ngIf="filter.type === 'archive'">
|
||||
{{ "archivedItemsDescription" | i18n }}
|
||||
</p>
|
||||
<div slot="title" *ngIf="filter.type !== 'archive'">{{ "noItemsInList" | i18n }}</div>
|
||||
<button
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
*ngIf="filter.type !== 'trash'"
|
||||
slot="button"
|
||||
*ngIf="filter.type !== 'trash' && filter.type !== 'archive'"
|
||||
>
|
||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||
{{ "newItem" | i18n }}
|
||||
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
AttachmentDialogCloseResult,
|
||||
AttachmentDialogResult,
|
||||
AttachmentsV2Component,
|
||||
CipherArchiveService,
|
||||
CipherFormConfig,
|
||||
CollectionAssignmentResult,
|
||||
DecryptionFailureDialogComponent,
|
||||
@@ -183,6 +184,13 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
.pipe(map((a) => a?.id))
|
||||
.pipe(switchMap((id) => this.organizationService.organizations$(id)));
|
||||
|
||||
private userCanArchive$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => {
|
||||
return this.cipherArchiveService.userCanArchive$(userId);
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private syncService: SyncService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -213,6 +221,7 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
private cipherFormConfigService: DefaultCipherFormConfigService,
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
) {}
|
||||
|
||||
@@ -309,12 +318,17 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
),
|
||||
);
|
||||
|
||||
const ciphers$ = combineLatest([allowedCiphers$, filter$, this.currentSearchText$]).pipe(
|
||||
const ciphers$ = combineLatest([
|
||||
allowedCiphers$,
|
||||
filter$,
|
||||
this.currentSearchText$,
|
||||
this.userCanArchive$,
|
||||
]).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];
|
||||
|
||||
|
||||
@@ -11009,6 +11009,18 @@
|
||||
"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."
|
||||
},
|
||||
"businessUnit": {
|
||||
"message": "Business Unit"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user