1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-29 15:53:45 +00:00

reverted search component, added search directly to vault-list

This commit is contained in:
Leslie Xiong
2026-01-26 14:59:12 -05:00
parent 4c05b98ea5
commit 17fb03c97e
7 changed files with 65 additions and 91 deletions

View File

@@ -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,

View File

@@ -1,4 +1,11 @@
@if (state.enabled) {
<bit-search [placeholder]="state.placeholderText" [formControl]="searchText" appAutofocus>
</bit-search>
}
<div class="search" *ngIf="state.enabled">
<input
type="search"
[placeholder]="state.placeholderText"
id="search"
autocomplete="off"
[formControl]="searchText"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>

View File

@@ -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;

View File

@@ -12,7 +12,13 @@
</app-header>
</div>
<div class="tw-mb-[16px]">
<app-search />
<bit-search
[ngModel]="searchText"
(ngModelChange)="onSearchTextChanged($event)"
[placeholder]="placeholderText()"
appAutofocus
>
</bit-search>
</div>
<bit-table [dataSource]="dataSource" layout="fixed">
<ng-container header>

View File

@@ -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<C extends CipherViewLike> {
protected readonly activeCollection = input<CollectionView | undefined>();
protected readonly userCanArchive = input<boolean>();
protected readonly enforceOrgDataOwnershipPolicy = input<boolean>();
protected readonly placeholderText = input<string>("");
protected readonly ciphers = input<C[]>([]);
@@ -92,20 +95,17 @@ export class VaultListComponent<C extends CipherViewLike> {
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<VaultItem<C>>();
protected selection = new SelectionModel<VaultItem<C>>(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<C extends CipherViewLike> {
this.onAddFolder.emit();
}
protected onSearchTextChanged(searchText: string) {
this.searchText = searchText;
this.refreshItems();
}
protected canClone$(vaultItem: VaultItem<C>): Observable<boolean> {
return this.restrictedItemTypesService.restricted$.pipe(
switchMap((restrictedTypes) => {
@@ -202,14 +207,25 @@ export class VaultListComponent<C extends CipherViewLike> {
}
private refreshItems() {
const collections: VaultItem<C>[] =
this.collections()?.map((collection) => ({ collection })) || [];
const ciphers: VaultItem<C>[] = 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<C>[] = filteredCollections.map((collection) => ({ collection }));
const ciphers: VaultItem<C>[] = filteredCiphers.map((cipher) => ({ cipher }));
const items: VaultItem<C>[] = [].concat(collections).concat(ciphers);
this.dataSource.data = items;

View File

@@ -14,6 +14,7 @@
[showAdminActions]="false"
[userCanArchive]="userCanArchive$ | async"
[enforceOrgDataOwnershipPolicy]="enforceOrgDataOwnershipPolicy$ | async"
[placeholderText]="searchPlaceholderText"
(onEvent)="onVaultItemsEvent($event)"
(onAddCipher)="addCipher($event)"
(onAddFolder)="addFolder()"

View File

@@ -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<C extends CipherViewLike>
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<C extends CipherViewLike>
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<C extends CipherViewLike>
protected filteredCollections: CollectionView[] = [];
protected collectionsToDisplay: CollectionView[] = [];
protected selectedCollection: TreeNode<CollectionView> | undefined;
protected currentSearchText$: Observable<string> = 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<C extends CipherViewLike>
.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<C extends CipherViewLike>
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<C extends CipherViewLike>
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<C>(
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<C extends CipherViewLike>
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<CollectionView>) => treeNode.node);
}),
shareReplay({ refCount: true, bufferSize: 1 }),