diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts
index bcd7400294b..52a52ffd225 100644
--- a/apps/desktop/src/app/app.module.ts
+++ b/apps/desktop/src/app/app.module.ts
@@ -26,6 +26,7 @@ import { UserVerificationComponent } from "./components/user-verification.compon
import { AccountSwitcherComponent } from "./layout/account-switcher.component";
import { HeaderComponent } from "./layout/header.component";
import { NavComponent } from "./layout/nav.component";
+import { SearchComponent } from "./layout/search/search.component";
import { SharedModule } from "./shared/shared.module";
@NgModule({
@@ -50,6 +51,7 @@ import { SharedModule } from "./shared/shared.module";
ColorPasswordCountPipe,
HeaderComponent,
PremiumComponent,
+ SearchComponent,
],
providers: [
SshAgentService,
diff --git a/apps/desktop/src/app/layout/search/search.component.html b/apps/desktop/src/app/layout/search/search.component.html
index 5d546769151..515385c2076 100644
--- a/apps/desktop/src/app/layout/search/search.component.html
+++ b/apps/desktop/src/app/layout/search/search.component.html
@@ -1,4 +1,11 @@
-@if (state.enabled) {
-
-
-}
+
+
+
+
diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts
index dec646f3c84..c0b088a13d9 100644
--- a/apps/desktop/src/app/layout/search/search.component.ts
+++ b/apps/desktop/src/app/layout/search/search.component.ts
@@ -1,12 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
-import { ReactiveFormsModule, UntypedFormControl } from "@angular/forms";
+import { UntypedFormControl } from "@angular/forms";
import { Subscription } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { AutofocusDirective, SearchModule } from "@bitwarden/components";
import { SearchBarService, SearchBarState } from "./search-bar.service";
@@ -15,7 +13,7 @@ import { SearchBarService, SearchBarState } from "./search-bar.service";
@Component({
selector: "app-search",
templateUrl: "search.component.html",
- imports: [CommonModule, ReactiveFormsModule, AutofocusDirective, SearchModule],
+ standalone: false,
})
export class SearchComponent implements OnInit, OnDestroy {
state: SearchBarState;
diff --git a/apps/desktop/src/vault/app/vault-v3/vault-list.component.html b/apps/desktop/src/vault/app/vault-v3/vault-list.component.html
index 9a128a56d9c..4e17684bdb6 100644
--- a/apps/desktop/src/vault/app/vault-v3/vault-list.component.html
+++ b/apps/desktop/src/vault/app/vault-v3/vault-list.component.html
@@ -12,7 +12,13 @@
diff --git a/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts b/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
index 7191fa0667c..5488776d8a8 100644
--- a/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
+++ b/apps/desktop/src/vault/app/vault-v3/vault-list.component.ts
@@ -5,12 +5,14 @@ import { ScrollingModule } from "@angular/cdk/scrolling";
import { AsyncPipe } from "@angular/common";
import { Component, input, output, effect, inject, computed } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { FormsModule } from "@angular/forms";
import { Observable, of, switchMap } from "rxjs";
+import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { CollectionView } from "@bitwarden/common/admin-console/models/collections";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
+import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import {
@@ -28,14 +30,13 @@ import {
MenuModule,
ButtonModule,
IconButtonModule,
+ SearchModule,
} from "@bitwarden/components";
import { OrganizationId } from "@bitwarden/sdk-internal";
import { I18nPipe } from "@bitwarden/ui-common";
import { NewCipherMenuComponent, VaultItem, VaultItemEvent } from "@bitwarden/vault";
import { DesktopHeaderComponent } from "../../../app/layout/header/desktop-header.component";
-import { SearchBarService } from "../../../app/layout/search/search-bar.service";
-import { SearchComponent } from "../../../app/layout/search/search.component";
import { VaultItemsModule } from "./vault-items/vault-items.module";
@@ -57,7 +58,8 @@ export const RowHeightClass = `tw-h-[75px]`;
ButtonModule,
IconButtonModule,
VaultItemsModule,
- SearchComponent,
+ SearchModule,
+ FormsModule,
DesktopHeaderComponent,
NewCipherMenuComponent,
],
@@ -80,6 +82,7 @@ export class VaultListComponent {
protected readonly activeCollection = input();
protected readonly userCanArchive = input();
protected readonly enforceOrgDataOwnershipPolicy = input();
+ protected readonly placeholderText = input("");
protected readonly ciphers = input([]);
@@ -92,20 +95,17 @@ export class VaultListComponent {
protected cipherAuthorizationService = inject(CipherAuthorizationService);
protected restrictedItemTypesService = inject(RestrictedItemTypesService);
protected cipherArchiveService = inject(CipherArchiveService);
- private searchBarService = inject(SearchBarService);
- private i18nService = inject(I18nService);
+ private searchService = inject(SearchService);
+ private searchPipe = inject(SearchPipe);
protected dataSource = new TableDataSource>();
protected selection = new SelectionModel>(true, [], true);
private restrictedTypes: RestrictedCipherType[] = [];
+ protected searchText = "";
protected archiveFeatureEnabled$ = this.cipherArchiveService.hasArchiveFlagEnabled$;
constructor() {
- // Enable the search bar
- this.searchBarService.setEnabled(true);
- this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
-
this.restrictedItemTypesService.restricted$.pipe(takeUntilDestroyed()).subscribe((types) => {
this.restrictedTypes = types;
this.refreshItems();
@@ -133,6 +133,11 @@ export class VaultListComponent {
this.onAddFolder.emit();
}
+ protected onSearchTextChanged(searchText: string) {
+ this.searchText = searchText;
+ this.refreshItems();
+ }
+
protected canClone$(vaultItem: VaultItem): Observable {
return this.restrictedItemTypesService.restricted$.pipe(
switchMap((restrictedTypes) => {
@@ -202,14 +207,25 @@ export class VaultListComponent {
}
private refreshItems() {
- const collections: VaultItem[] =
- this.collections()?.map((collection) => ({ collection })) || [];
- const ciphers: VaultItem[] = this.ciphers()
- .filter(
- (cipher) =>
- !this.restrictedItemTypesService.isCipherRestricted(cipher, this.restrictedTypes),
- )
- .map((cipher) => ({ cipher }));
+ const filteredCollections: CollectionView[] = this.searchText
+ ? this.searchPipe.transform(
+ this.collections() || [],
+ this.searchText,
+ (collection) => collection.name,
+ (collection) => collection.id,
+ )
+ : this.collections() || [];
+
+ const allowedCiphers = this.ciphers().filter(
+ (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, this.restrictedTypes),
+ );
+
+ const filteredCiphers: C[] = this.searchText
+ ? this.searchService.searchCiphersBasic(allowedCiphers, this.searchText)
+ : allowedCiphers;
+
+ const collections: VaultItem[] = filteredCollections.map((collection) => ({ collection }));
+ const ciphers: VaultItem[] = filteredCiphers.map((cipher) => ({ cipher }));
const items: VaultItem[] = [].concat(collections).concat(ciphers);
this.dataSource.data = items;
diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.html b/apps/desktop/src/vault/app/vault-v3/vault.component.html
index 46d8dcf2101..380b126fc13 100644
--- a/apps/desktop/src/vault/app/vault-v3/vault.component.html
+++ b/apps/desktop/src/vault/app/vault-v3/vault.component.html
@@ -14,6 +14,7 @@
[showAdminActions]="false"
[userCanArchive]="userCanArchive$ | async"
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy$ | async"
+ [placeholderText]="searchPlaceholderText"
(onEvent)="onVaultItemsEvent($event)"
(onAddCipher)="addCipher($event)"
(onAddFolder)="addFolder()"
diff --git a/apps/desktop/src/vault/app/vault-v3/vault.component.ts b/apps/desktop/src/vault/app/vault-v3/vault.component.ts
index 08123c34848..0cbc9c152e6 100644
--- a/apps/desktop/src/vault/app/vault-v3/vault.component.ts
+++ b/apps/desktop/src/vault/app/vault-v3/vault.component.ts
@@ -18,8 +18,6 @@ import {
switchMap,
lastValueFrom,
Observable,
- debounceTime,
- distinctUntilChanged,
BehaviorSubject,
combineLatest,
} from "rxjs";
@@ -27,7 +25,6 @@ import { filter, map, take, first, shareReplay, concatMap, tap } from "rxjs/oper
import { CollectionService } from "@bitwarden/admin-console/common";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
-import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@@ -35,10 +32,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { CollectionView, Unassigned } from "@bitwarden/common/admin-console/models/collections";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
-import {
- getNestedCollectionTree,
- getFlatCollectionTree,
-} from "@bitwarden/common/admin-console/utils";
+import { getNestedCollectionTree } from "@bitwarden/common/admin-console/utils";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
@@ -50,14 +44,12 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { uuidAsString } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
import { getByIds } from "@bitwarden/common/platform/misc";
-import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherId, OrganizationId, UserId, CollectionId } from "@bitwarden/common/types/guid";
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
-import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -66,7 +58,6 @@ import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
-import { SearchTextDebounceInterval } from "@bitwarden/common/vault/services/search.service";
import {
CipherViewLike,
CipherViewLikeUtils,
@@ -114,7 +105,6 @@ import {
} from "@bitwarden/vault";
import { DesktopHeaderComponent } from "../../../app/layout/header/desktop-header.component";
-import { SearchBarService } from "../../../app/layout/search/search-bar.service";
import { DesktopCredentialGenerationService } from "../../../services/desktop-cipher-form-generator.service";
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
import { AssignCollectionsDesktopComponent } from "../vault/assign-collections";
@@ -183,7 +173,6 @@ export class VaultComponent
private eventCollectionService = inject(EventCollectionService);
private totpService = inject(TotpService);
private passwordRepromptService = inject(PasswordRepromptService);
- private searchBarService = inject(SearchBarService);
private dialogService = inject(DialogService);
private billingAccountProfileStateService = inject(BillingAccountProfileStateService);
private toastService = inject(ToastService);
@@ -202,8 +191,6 @@ export class VaultComponent
private routedVaultFilterBridgeService = inject(RoutedVaultFilterBridgeService);
private vaultFilterService = inject(VaultFilterService);
private routedVaultFilterService = inject(RoutedVaultFilterService);
- private searchService = inject(SearchService);
- private searchPipe = inject(SearchPipe);
private vaultItemTransferService: VaultItemsTransferService = inject(VaultItemsTransferService);
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
@@ -318,9 +305,7 @@ export class VaultComponent
protected filteredCollections: CollectionView[] = [];
protected collectionsToDisplay: CollectionView[] = [];
protected selectedCollection: TreeNode | undefined;
- protected currentSearchText$: Observable = this.route.queryParams.pipe(
- map((queryParams) => queryParams.search),
- );
+ protected searchPlaceholderText: string;
private userId$ = this.accountService.activeAccount$.pipe(getUserId);
protected ciphers: C[] = [];
protected filteredCiphers: C[] = [];
@@ -390,8 +375,8 @@ export class VaultComponent
.pipe(takeUntil(this.destroy$))
.subscribe((activeFilter) => {
this.activeFilter = activeFilter;
- this.searchBarService.setPlaceholderText(
- this.i18nService.t(this.calculateSearchBarLocalizationString(activeFilter)),
+ this.searchPlaceholderText = this.i18nService.t(
+ this.calculateSearchBarLocalizationString(activeFilter),
);
});
@@ -402,24 +387,6 @@ export class VaultComponent
map((collections) => getNestedCollectionTree(collections)),
);
- // Connect search bar to route query params
- this.searchBarService.searchText$
- .pipe(
- debounceTime(SearchTextDebounceInterval),
- distinctUntilChanged(),
- takeUntil(this.destroy$),
- )
- .subscribe((searchText: string) => {
- void this.router.navigate([], {
- queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
- queryParamsHandling: "merge",
- replaceUrl: true,
- state: {
- focusMainAfterNav: false,
- },
- });
- });
-
const _ciphers = this.cipherService
.cipherListViews$(activeUserId)
.pipe(filter((c) => c !== null));
@@ -441,34 +408,24 @@ export class VaultComponent
const ciphers$ = combineLatest([
allowedCiphers$,
filter$,
- this.currentSearchText$,
this.cipherArchiveService.hasArchiveFlagEnabled$,
]).pipe(
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
- concatMap(async ([ciphers, filter, searchText, showArchiveVault]) => {
+ concatMap(async ([ciphers, filter, showArchiveVault]) => {
const failedCiphers =
(await firstValueFrom(this.cipherService.failedToDecryptCiphers$(activeUserId))) ?? [];
const filterFunction = createFilterFunction(filter, showArchiveVault);
// Append any failed to decrypt ciphers to the top of the cipher list
const allCiphers = [...failedCiphers, ...ciphers];
- if (await this.searchService.isSearchable(activeUserId, searchText)) {
- return await this.searchService.searchCiphers(
- activeUserId,
- searchText,
- [filterFunction],
- allCiphers as C[],
- );
- }
-
- return ciphers.filter(filterFunction) as C[];
+ return allCiphers.filter(filterFunction) as C[];
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
- const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe(
+ const collections$ = combineLatest([nestedCollections$, filter$]).pipe(
filter(([collections, filter]) => collections != undefined && filter != undefined),
- concatMap(async ([collections, filter, searchText]) => {
+ map(([collections, filter]) => {
if (filter.collectionId === undefined || filter.collectionId === Unassigned) {
return [];
}
@@ -487,19 +444,6 @@ export class VaultComponent
searchableCollectionNodes = selectedCollection?.children ?? [];
}
- if (await this.searchService.isSearchable(activeUserId, searchText)) {
- // Flatten the tree for searching through all levels
- const flatCollectionTree: CollectionView[] =
- getFlatCollectionTree(searchableCollectionNodes);
-
- return this.searchPipe.transform(
- flatCollectionTree,
- searchText,
- (collection) => collection.name,
- (collection) => collection.id,
- );
- }
-
return searchableCollectionNodes.map((treeNode: TreeNode) => treeNode.node);
}),
shareReplay({ refCount: true, bufferSize: 1 }),