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 fb1e5030c55..a9427f072e3 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
@@ -2,7 +2,16 @@
// @ts-strict-ignore
import { Component, EventEmitter, inject, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Router } from "@angular/router";
-import { debounceTime, firstValueFrom, merge, mergeMap, Subject, switchMap, takeUntil } from "rxjs";
+import {
+ debounceTime,
+ firstValueFrom,
+ merge,
+ mergeMap,
+ Observable,
+ 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";
@@ -14,6 +23,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
+import {
+ FilterName,
+ FilterString,
+ SavedFiltersService,
+} from "@bitwarden/common/vault/search/saved-filters.service";
import {
SearchHistory,
SearchHistoryService,
@@ -100,6 +114,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
private trialFlowService = inject(TrialFlowService);
protected searchHistory: SearchHistory;
+ protected savedFilters$: Observable
>;
constructor(
protected vaultFilterService: VaultFilterService,
@@ -111,6 +126,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
protected dialogService: DialogService,
protected configService: ConfigService,
protected searchHistoryService: SearchHistoryService,
+ protected savedFiltersService: SavedFiltersService,
) {}
async ngOnInit(): Promise {
@@ -133,6 +149,7 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
});
this.searchHistory = this.searchHistoryService.searchHistory(this.userId, this.organizationId);
+ this.savedFilters$ = this.savedFiltersService.filtersFor$(this.userId);
this.searchTextChanged
.asObservable()
.pipe(
@@ -200,6 +217,18 @@ export class VaultFilterComponent implements OnInit, OnDestroy {
this.onEditFolder.emit(folder);
};
+ async onFilterSaved({ name, filter }: { name: string; filter: string }) {
+ await this.savedFiltersService.saveFilter(
+ this.userId,
+ name as FilterName,
+ filter as FilterString,
+ );
+ }
+
+ async onFilterDeleted({ name }: { name: string }) {
+ await this.savedFiltersService.deleteFilter(this.userId, name as FilterName);
+ }
+
async getDefaultFilter(): Promise> {
return await firstValueFrom(this.filters?.typeFilter.data$);
}
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 c66509b1f41..2a1e2c50348 100644
--- a/apps/web/src/app/vault/individual-vault/vault.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault.component.html
@@ -28,7 +28,7 @@
{{ trashCleanupWarning }}
-
+
>;
- abstract SaveFilter(userId: UserId, name: FilterName, filter: FilterString): Promise;
+ abstract saveFilter(userId: UserId, name: FilterName, filter: FilterString): Promise;
+ abstract deleteFilter(userId: UserId, name: FilterName): Promise;
}
export type FilterName = string & Tagged<"FilterName">;
@@ -66,7 +67,7 @@ export class DefaultSavedFiltersService implements SavedFiltersService {
return decryptedState;
}
- async SaveFilter(userId: UserId, name: FilterName, filter: FilterString): Promise {
+ async saveFilter(userId: UserId, name: FilterName, filter: FilterString): Promise {
const state = this.stateProvider.get(userId, SavedFiltersStateDefinition);
await state.update((_, newState) => newState, {
combineLatestWith: state.state$.pipe(
@@ -80,7 +81,28 @@ export class DefaultSavedFiltersService implements SavedFiltersService {
return [oldState, userKey] as const;
}),
mergeMap(async ([newState, userKey]) => {
- return await this.encryptHistory(newState, userKey);
+ return await this.encryptFilters(newState, userKey);
+ }),
+ ),
+ shouldUpdate: (oldEncrypted, newEncrypted) => !recordsEqual(oldEncrypted, newEncrypted),
+ });
+ }
+
+ async deleteFilter(userId: UserId, name: FilterName): Promise {
+ const state = this.stateProvider.get(userId, SavedFiltersStateDefinition);
+ await state.update((_, newState) => newState, {
+ combineLatestWith: state.state$.pipe(
+ combineLatestWith(this.keyService.userKey$(userId)),
+ mergeMap(async ([encrypted, userKey]) => {
+ return [await this.decryptFilters(encrypted, userKey), userKey] as const;
+ }),
+ map(([oldState, userKey]) => {
+ oldState ??= {};
+ delete oldState[name];
+ return [oldState, userKey] as const;
+ }),
+ mergeMap(async ([newState, userKey]) => {
+ return await this.encryptFilters(newState, userKey);
}),
),
shouldUpdate: (oldEncrypted, newEncrypted) => !recordsEqual(oldEncrypted, newEncrypted),
@@ -88,15 +110,15 @@ export class DefaultSavedFiltersService implements SavedFiltersService {
}
private async decryptFilters(
- history: UserSearchFilters | null,
+ filters: UserSearchFilters | null,
userKey: SymmetricCryptoKey | null,
): Promise {
const decrypted: DecryptedSearchFilters = {};
- if (history == null || userKey == null) {
+ if (filters == null || userKey == null) {
return decrypted;
}
- for (const [k, v] of Object.entries(history)) {
+ for (const [k, v] of Object.entries(filters)) {
const encryptedKey = new EncString(k as EncryptedString);
const key = (await encryptedKey.decryptWithKey(userKey, this.encryptService)) as FilterName;
decrypted[key] = (await v.decryptWithKey(userKey, this.encryptService)) as FilterString;
@@ -104,16 +126,16 @@ export class DefaultSavedFiltersService implements SavedFiltersService {
return decrypted;
}
- private async encryptHistory(
- history: DecryptedSearchFilters | null,
+ private async encryptFilters(
+ filters: DecryptedSearchFilters | null,
userKey: SymmetricCryptoKey | null,
) {
- if (history == null || userKey == null) {
+ if (filters == null || userKey == null) {
return null;
}
const encrypted: UserSearchFilters = {};
- for (const [k, v] of Object.entries(history)) {
+ for (const [k, v] of Object.entries(filters)) {
const DecryptedKey = k as FilterName;
const key = (await this.encryptService.encrypt(DecryptedKey, userKey)).encryptedString!;
encrypted[key] = await this.encryptService.encrypt(v, userKey);
diff --git a/libs/components/src/search/search.component.html b/libs/components/src/search/search.component.html
index 1af0e4b5678..68b1e5bdf42 100644
--- a/libs/components/src/search/search.component.html
+++ b/libs/components/src/search/search.component.html
@@ -1,42 +1,53 @@
-
-
-
-
-
-
+
+
+
+
+ 0 ? 'tw-px-9' : 'tw-pl-9'"
+ [ngModel]="searchText"
+ (ngModelChange)="onChange($event)"
+ (focus)="onFocus()"
+ (blur)="onTouch()"
+ [disabled]="disabled"
+ [attr.autocomplete]="autocomplete"
+ />
+
+
+
+
0">
+
+
@@ -50,7 +61,14 @@
(click)="writeValue(data.filter); onChange(data.filter); onTouch()"
class="tw-pl-9 tw-pr-2 tw-block tw-text-ellipsis tw-text-nowrap tw-overflow-hidden hover:tw-bg-background-alt"
>
- {{ data.name }}
+
+
+ {{ data.name }}
+
+
+
+
+
diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts
index c44e5d7155e..db1d8782f7a 100644
--- a/libs/components/src/search/search.component.ts
+++ b/libs/components/src/search/search.component.ts
@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
-import { Component, ElementRef, Input, ViewChild } from "@angular/core";
+import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
@@ -15,6 +15,7 @@ import { I18nPipe } from "@bitwarden/ui-common";
import { IconButtonModule } from "../icon-button";
import { InputModule } from "../input/input.module";
+import { LinkModule } from "../link";
import { FocusableElement } from "../shared/focusable-element";
let nextId = 0;
@@ -41,6 +42,7 @@ let nextId = 0;
I18nPipe,
CommonModule,
IconButtonModule,
+ LinkModule,
],
})
export class SearchComponent implements ControlValueAccessor, FocusableElement {
@@ -54,7 +56,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
// Use `type="text"` for Safari to improve rendering performance
protected inputType = isBrowserSafariApi() ? ("text" as const) : ("search" as const);
private focused = false;
- private textUpdated$ = new BehaviorSubject("");
+ protected textUpdated$ = new BehaviorSubject("");
protected showSavedFilters = false;
@Input() disabled: boolean;
@@ -62,6 +64,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
@Input() autocomplete: string;
@Input() history: string[] | null;
@Input() savedFilters: Record | null;
+ @Output() filterSaved = new EventEmitter<{ name: string; filter: string }>();
+ @Output() filterDeleted = new EventEmitter<{ name: string; filter: string }>();
get savedFilterData() {
if (this.savedFilters == null) {
@@ -104,6 +108,7 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
if (this.notifyOnChange != undefined) {
this.notifyOnChange(searchText);
}
+ this.searchText = searchText;
this.textUpdated$.next(searchText);
}
@@ -142,4 +147,15 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement {
filterShown() {
return this._selectedContent.value !== "filter";
}
+
+ saveFilter() {
+ this.filterSaved.emit({
+ name: this.searchText,
+ filter: this.searchText,
+ });
+ }
+
+ deleteFilter(toDelete: { name: string; filter: string }) {
+ this.filterDeleted.emit(toDelete);
+ }
}
diff --git a/libs/components/src/search/search.module.ts b/libs/components/src/search/search.module.ts
index cb9761eae6b..fa7b59be762 100644
--- a/libs/components/src/search/search.module.ts
+++ b/libs/components/src/search/search.module.ts
@@ -1,9 +1,11 @@
import { NgModule } from "@angular/core";
+import { ButtonModule } from "../button";
+
import { SearchComponent } from "./search.component";
@NgModule({
- imports: [SearchComponent],
+ imports: [SearchComponent, ButtonModule],
exports: [SearchComponent],
})
export class SearchModule {}