mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 03:33:30 +00:00
[PM-26706] Update search results header for extension (#18676)
* dynamically changes the allItems title from 'All items' to 'Search results' based on search text length * updates logic and copy for changing the allItems header text * changes how ciphers are displayed when a user has a search term and/or filters applied * Update apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> * refactors tests --------- Co-authored-by: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
This commit is contained in:
@@ -6123,6 +6123,12 @@
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
},
|
||||
"items": {
|
||||
"message": "Items"
|
||||
},
|
||||
"searchResults": {
|
||||
"message": "Search results"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
|
||||
@@ -107,20 +107,32 @@
|
||||
@if (vaultState === null) {
|
||||
<vault-fade-in-out>
|
||||
@if (!(loading$ | async)) {
|
||||
<app-autofill-vault-list-items></app-autofill-vault-list-items>
|
||||
<app-vault-list-items-container
|
||||
[title]="'favorites' | i18n"
|
||||
[ciphers]="(favoriteCiphers$ | async) || []"
|
||||
id="favorites"
|
||||
collapsibleKey="favorites"
|
||||
></app-vault-list-items-container>
|
||||
<app-vault-list-items-container
|
||||
[title]="'allItems' | i18n"
|
||||
[ciphers]="(remainingCiphers$ | async) || []"
|
||||
id="allItems"
|
||||
disableSectionMargin
|
||||
collapsibleKey="allItems"
|
||||
></app-vault-list-items-container>
|
||||
<!--If there is search text fold all the filtered ciphers into one container-->
|
||||
@if (hasSearchText$ | async) {
|
||||
<app-vault-list-items-container
|
||||
[title]="'searchResults' | i18n"
|
||||
[ciphers]="(filteredCiphers$ | async) || []"
|
||||
id="allItems"
|
||||
disableSectionMargin
|
||||
collapsibleKey="allItems"
|
||||
></app-vault-list-items-container>
|
||||
} @else {
|
||||
<app-autofill-vault-list-items></app-autofill-vault-list-items>
|
||||
<app-vault-list-items-container
|
||||
[title]="'favorites' | i18n"
|
||||
[ciphers]="(favoriteCiphers$ | async) || []"
|
||||
id="favorites"
|
||||
collapsibleKey="favorites"
|
||||
></app-vault-list-items-container>
|
||||
<!--Change the title header when a filter is applied-->
|
||||
<app-vault-list-items-container
|
||||
[title]="((numberOfAppliedFilters$ | async) === 0 ? 'allItems' : 'items') | i18n"
|
||||
[ciphers]="(remainingCiphers$ | async) || []"
|
||||
id="allItems"
|
||||
disableSectionMargin
|
||||
collapsibleKey="allItems"
|
||||
></app-vault-list-items-container>
|
||||
}
|
||||
}
|
||||
</vault-fade-in-out>
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { VaultPopupAutofillService } from "../../services/vault-popup-autofill.s
|
||||
import { VaultPopupCopyButtonsService } from "../../services/vault-popup-copy-buttons.service";
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
|
||||
import { VaultPopupLoadingService } from "../../services/vault-popup-loading.service";
|
||||
import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service";
|
||||
import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component";
|
||||
|
||||
@@ -174,15 +175,21 @@ describe("VaultV2Component", () => {
|
||||
showDeactivatedOrg$: new BehaviorSubject<boolean>(false),
|
||||
favoriteCiphers$: new BehaviorSubject<any[]>([]),
|
||||
remainingCiphers$: new BehaviorSubject<any[]>([]),
|
||||
filteredCiphers$: new BehaviorSubject<any[]>([]),
|
||||
cipherCount$: new BehaviorSubject<number>(0),
|
||||
loading$: new BehaviorSubject<boolean>(true),
|
||||
hasSearchText$: new BehaviorSubject<boolean>(false),
|
||||
} as Partial<VaultPopupItemsService>;
|
||||
|
||||
const filtersSvc = {
|
||||
const filtersSvc: any = {
|
||||
allFilters$: new Subject<any>(),
|
||||
filters$: new BehaviorSubject<any>({}),
|
||||
filterVisibilityState$: new BehaviorSubject<any>({}),
|
||||
} as Partial<VaultPopupListFiltersService>;
|
||||
numberOfAppliedFilters$: new BehaviorSubject<number>(0),
|
||||
};
|
||||
|
||||
const loadingSvc: any = {
|
||||
loading$: new BehaviorSubject<boolean>(false),
|
||||
};
|
||||
|
||||
const activeAccount$ = new BehaviorSubject<FakeAccount | null>({ id: "user-1" });
|
||||
|
||||
@@ -240,6 +247,7 @@ describe("VaultV2Component", () => {
|
||||
provideNoopAnimations(),
|
||||
{ provide: VaultPopupItemsService, useValue: itemsSvc },
|
||||
{ provide: VaultPopupListFiltersService, useValue: filtersSvc },
|
||||
{ provide: VaultPopupLoadingService, useValue: loadingSvc },
|
||||
{ provide: VaultPopupScrollPositionService, useValue: scrollSvc },
|
||||
{
|
||||
provide: AccountService,
|
||||
@@ -366,18 +374,18 @@ describe("VaultV2Component", () => {
|
||||
});
|
||||
|
||||
it("loading$ is true when items loading or filters missing; false when both ready", () => {
|
||||
const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject<boolean>;
|
||||
const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject<boolean>;
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
const readySubject$ = component["readySubject"] as unknown as BehaviorSubject<boolean>;
|
||||
|
||||
const values: boolean[] = [];
|
||||
getObs<boolean>(component, "loading$").subscribe((v) => values.push(!!v));
|
||||
|
||||
itemsLoading$.next(true);
|
||||
vaultLoading$.next(true);
|
||||
|
||||
allFilters$.next({});
|
||||
|
||||
itemsLoading$.next(false);
|
||||
vaultLoading$.next(false);
|
||||
|
||||
readySubject$.next(true);
|
||||
|
||||
@@ -389,7 +397,7 @@ describe("VaultV2Component", () => {
|
||||
const component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"] as unknown as BehaviorSubject<boolean>;
|
||||
const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject<boolean>;
|
||||
const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject<boolean>;
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
fixture.detectChanges();
|
||||
@@ -400,7 +408,7 @@ describe("VaultV2Component", () => {
|
||||
) as HTMLElement;
|
||||
|
||||
// Unblock loading
|
||||
itemsLoading$.next(false);
|
||||
vaultLoading$.next(false);
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
tick();
|
||||
@@ -607,6 +615,127 @@ describe("VaultV2Component", () => {
|
||||
expect(spotlights.length).toBe(0);
|
||||
}));
|
||||
|
||||
it("does not render app-autofill-vault-list-items or favorites item container when hasSearchText$ is true", () => {
|
||||
itemsSvc.hasSearchText$.next(true);
|
||||
|
||||
const fixture = TestBed.createComponent(VaultV2Component);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"];
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
// Unblock loading
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
fixture.detectChanges();
|
||||
|
||||
const autofillElement = fixture.debugElement.query(By.css("app-autofill-vault-list-items"));
|
||||
expect(autofillElement).toBeFalsy();
|
||||
|
||||
const favoritesElement = fixture.debugElement.query(By.css("#favorites"));
|
||||
expect(favoritesElement).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does render app-autofill-vault-list-items and favorites item container when hasSearchText$ is false", () => {
|
||||
// Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg)
|
||||
itemsSvc.emptyVault$.next(false);
|
||||
itemsSvc.noFilteredResults$.next(false);
|
||||
itemsSvc.showDeactivatedOrg$.next(false);
|
||||
itemsSvc.hasSearchText$.next(false);
|
||||
loadingSvc.loading$.next(false);
|
||||
|
||||
const fixture = TestBed.createComponent(VaultV2Component);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"];
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
// Unblock loading
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
fixture.detectChanges();
|
||||
|
||||
const autofillElement = fixture.debugElement.query(By.css("app-autofill-vault-list-items"));
|
||||
expect(autofillElement).toBeTruthy();
|
||||
|
||||
const favoritesElement = fixture.debugElement.query(By.css("#favorites"));
|
||||
expect(favoritesElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does set the title for allItems container to allItems when hasSearchText$ and numberOfAppliedFilters$ are false and 0 respectively", () => {
|
||||
// Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg)
|
||||
itemsSvc.emptyVault$.next(false);
|
||||
itemsSvc.noFilteredResults$.next(false);
|
||||
itemsSvc.showDeactivatedOrg$.next(false);
|
||||
itemsSvc.hasSearchText$.next(false);
|
||||
filtersSvc.numberOfAppliedFilters$.next(0);
|
||||
loadingSvc.loading$.next(false);
|
||||
|
||||
const fixture = TestBed.createComponent(VaultV2Component);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"];
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
// Unblock loading
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
fixture.detectChanges();
|
||||
|
||||
const allItemsElement = fixture.debugElement.query(By.css("#allItems"));
|
||||
const allItemsTitle = allItemsElement.componentInstance.title();
|
||||
expect(allItemsTitle).toBe("allItems");
|
||||
});
|
||||
|
||||
it("does set the title for allItems container to searchResults when hasSearchText$ is true", () => {
|
||||
// Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg)
|
||||
itemsSvc.emptyVault$.next(false);
|
||||
itemsSvc.noFilteredResults$.next(false);
|
||||
itemsSvc.showDeactivatedOrg$.next(false);
|
||||
itemsSvc.hasSearchText$.next(true);
|
||||
loadingSvc.loading$.next(false);
|
||||
|
||||
const fixture = TestBed.createComponent(VaultV2Component);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"];
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
// Unblock loading
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
fixture.detectChanges();
|
||||
|
||||
const allItemsElement = fixture.debugElement.query(By.css("#allItems"));
|
||||
const allItemsTitle = allItemsElement.componentInstance.title();
|
||||
expect(allItemsTitle).toBe("searchResults");
|
||||
});
|
||||
|
||||
it("does set the title for allItems container to items when numberOfAppliedFilters$ is > 0", fakeAsync(() => {
|
||||
// Ensure vaultState is null (not Empty, NoResults, or DeactivatedOrg)
|
||||
itemsSvc.emptyVault$.next(false);
|
||||
itemsSvc.noFilteredResults$.next(false);
|
||||
itemsSvc.showDeactivatedOrg$.next(false);
|
||||
itemsSvc.hasSearchText$.next(false);
|
||||
filtersSvc.numberOfAppliedFilters$.next(1);
|
||||
loadingSvc.loading$.next(false);
|
||||
|
||||
const fixture = TestBed.createComponent(VaultV2Component);
|
||||
component = fixture.componentInstance;
|
||||
|
||||
const readySubject$ = component["readySubject"];
|
||||
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject<any>;
|
||||
|
||||
// Unblock loading
|
||||
readySubject$.next(true);
|
||||
allFilters$.next({});
|
||||
fixture.detectChanges();
|
||||
|
||||
const allItemsElement = fixture.debugElement.query(By.css("#allItems"));
|
||||
const allItemsTitle = allItemsElement.componentInstance.title();
|
||||
expect(allItemsTitle).toBe("items");
|
||||
}));
|
||||
|
||||
describe("AutoConfirmExtensionSetupDialog", () => {
|
||||
beforeEach(() => {
|
||||
autoConfirmDialogSpy.mockClear();
|
||||
|
||||
@@ -160,6 +160,11 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
FeatureFlag.BrowserPremiumSpotlight,
|
||||
);
|
||||
|
||||
protected readonly hasSearchText$ = this.vaultPopupItemsService.hasSearchText$;
|
||||
protected readonly numberOfAppliedFilters$ =
|
||||
this.vaultPopupListFiltersService.numberOfAppliedFilters$;
|
||||
|
||||
protected filteredCiphers$ = this.vaultPopupItemsService.filteredCiphers$;
|
||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||
protected allFilters$ = this.vaultPopupListFiltersService.allFilters$;
|
||||
|
||||
@@ -323,6 +323,25 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("filteredCiphers$", () => {
|
||||
it("should filter filteredCipher$ down to search term", (done) => {
|
||||
const cipherList = Object.values(allCiphers);
|
||||
const searchText = "Login";
|
||||
|
||||
searchService.searchCiphers.mockImplementation(async () => {
|
||||
return cipherList.filter((cipher) => {
|
||||
return cipher.name.includes(searchText);
|
||||
});
|
||||
});
|
||||
|
||||
service.filteredCiphers$.subscribe((ciphers) => {
|
||||
// There are 10 ciphers but only 3 with "Login" in the name
|
||||
expect(ciphers.length).toBe(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("favoriteCiphers$", () => {
|
||||
it("should exclude autofill ciphers", (done) => {
|
||||
service.favoriteCiphers$.subscribe((ciphers) => {
|
||||
|
||||
@@ -201,6 +201,15 @@ export class VaultPopupItemsService {
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
/**
|
||||
* List of ciphers that are filtered using filters and search.
|
||||
* Includes favorite ciphers and ciphers currently suggested for autofill.
|
||||
* Ciphers are sorted by name.
|
||||
*/
|
||||
filteredCiphers$: Observable<PopupCipherViewLike[]> = this._filteredCipherList$.pipe(
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
/**
|
||||
* List of ciphers that can be used for autofill on the current tab. Includes cards and/or identities
|
||||
* if enabled in the vault settings. Ciphers are sorted by type, then by last used date, then by name.
|
||||
|
||||
Reference in New Issue
Block a user