diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 9f15bfd840f..5026f5e2799 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -6123,6 +6123,12 @@
"whyAmISeeingThis": {
"message": "Why am I seeing this?"
},
+ "items": {
+ "message": "Items"
+ },
+ "searchResults": {
+ "message": "Search results"
+ },
"resizeSideNavigation": {
"message": "Resize side navigation"
},
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
index 20871b4b134..f0a6b0d6000 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
@@ -107,20 +107,32 @@
@if (vaultState === null) {
@if (!(loading$ | async)) {
-
-
-
+
+ @if (hasSearchText$ | async) {
+
+ } @else {
+
+
+
+
+ }
}
}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts
index d7824f3df58..a322fbc53dd 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.spec.ts
@@ -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(false),
favoriteCiphers$: new BehaviorSubject([]),
remainingCiphers$: new BehaviorSubject([]),
+ filteredCiphers$: new BehaviorSubject([]),
cipherCount$: new BehaviorSubject(0),
- loading$: new BehaviorSubject(true),
+ hasSearchText$: new BehaviorSubject(false),
} as Partial;
- const filtersSvc = {
+ const filtersSvc: any = {
allFilters$: new Subject(),
filters$: new BehaviorSubject({}),
filterVisibilityState$: new BehaviorSubject({}),
- } as Partial;
+ numberOfAppliedFilters$: new BehaviorSubject(0),
+ };
+
+ const loadingSvc: any = {
+ loading$: new BehaviorSubject(false),
+ };
const activeAccount$ = new BehaviorSubject({ 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;
+ const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject;
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject;
const readySubject$ = component["readySubject"] as unknown as BehaviorSubject;
const values: boolean[] = [];
getObs(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;
- const itemsLoading$ = itemsSvc.loading$ as unknown as BehaviorSubject;
+ const vaultLoading$ = loadingSvc.loading$ as unknown as BehaviorSubject;
const allFilters$ = filtersSvc.allFilters$ as unknown as Subject;
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;
+
+ // 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;
+
+ // 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;
+
+ // 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;
+
+ // 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;
+
+ // 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();
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
index 51e735fb1ef..fce084542a9 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
@@ -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$;
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
index 7cd73279c3d..093fdbfb66d 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
@@ -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) => {
diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
index 321d7936806..7ccfc834c87 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
@@ -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 = 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.