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 }),