= combineLatest([
+ this.vaultPopupItemsService.hasFilterApplied$,
+ this.autofillCiphers$,
+ this.vaultPopupItemsService.autofillAllowed$,
+ ]).pipe(
+ map(([hasFilter, ciphers, canAutoFill]) => !hasFilter && canAutoFill && ciphers.length === 0),
+ );
+
+ constructor(private vaultPopupItemsService: VaultPopupItemsService) {
+ // TODO: Migrate logic to show Autofill policy toast PM-8144
+ }
+}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/index.ts b/apps/browser/src/vault/popup/components/vault-v2/index.ts
new file mode 100644
index 0000000000..13618d007d
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/index.ts
@@ -0,0 +1,2 @@
+export * from "./vault-list-items-container/vault-list-items-container.component";
+export * from "./autofill-vault-list-items/autofill-vault-list-items.component";
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
new file mode 100644
index 0000000000..55463a85f8
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html
@@ -0,0 +1,35 @@
+ 0">
+
+ {{ ciphers.length }}
+
+
+
+
+
+ {{ cipher.name }}
+ {{ cipher.subTitle }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts
new file mode 100644
index 0000000000..27ee0a2cc0
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts
@@ -0,0 +1,44 @@
+import { CommonModule } from "@angular/common";
+import { booleanAttribute, Component, Input } from "@angular/core";
+import { RouterLink } from "@angular/router";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import {
+ BadgeModule,
+ ButtonModule,
+ IconButtonModule,
+ ItemModule,
+ SectionComponent,
+ TypographyModule,
+} from "@bitwarden/components";
+
+import { PopupSectionHeaderComponent } from "../../../../../platform/popup/popup-section-header/popup-section-header.component";
+
+@Component({
+ imports: [
+ CommonModule,
+ ItemModule,
+ ButtonModule,
+ BadgeModule,
+ IconButtonModule,
+ SectionComponent,
+ TypographyModule,
+ JslibModule,
+ PopupSectionHeaderComponent,
+ RouterLink,
+ ],
+ selector: "app-vault-list-items-container",
+ templateUrl: "vault-list-items-container.component.html",
+ standalone: true,
+})
+export class VaultListItemsContainerComponent {
+ @Input()
+ ciphers: CipherView[];
+
+ @Input()
+ title: string;
+
+ @Input({ transform: booleanAttribute })
+ showAutoFill: boolean;
+}
diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html
index c36d2d2db9..a9814f892e 100644
--- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html
+++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html
@@ -10,4 +10,40 @@
+
+
+
+ {{ "yourVaultIsEmpty" | i18n }}
+ {{ "autofillSuggestionsTip" | i18n }}
+
+
+
+
+
+
+
+
+
+ {{ "noItemsMatchSearch" | i18n }}
+ {{ "clearFiltersOrTryAnother" | i18n }}
+
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts
index 332e5d1a4e..7e0be4607b 100644
--- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts
@@ -1,13 +1,55 @@
+import { CommonModule } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
+import { Router, RouterLink } from "@angular/router";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
+
+import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
+import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
+import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
+import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
+import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
@Component({
selector: "app-vault",
templateUrl: "vault-v2.component.html",
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ PopOutComponent,
+ CurrentAccountComponent,
+ NoItemsModule,
+ JslibModule,
+ CommonModule,
+ AutofillVaultListItemsComponent,
+ VaultListItemsContainerComponent,
+ ButtonModule,
+ RouterLink,
+ ],
})
export class VaultV2Component implements OnInit, OnDestroy {
- constructor() {}
+ protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
+ protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
+
+ protected showEmptyState$ = this.vaultPopupItemsService.emptyVault$;
+ protected showNoResultsState$ = this.vaultPopupItemsService.noFilteredResults$;
+
+ protected vaultIcon = Icons.Vault;
+
+ constructor(
+ private vaultPopupItemsService: VaultPopupItemsService,
+ private router: Router,
+ ) {}
ngOnInit(): void {}
ngOnDestroy(): void {}
+
+ addCipher() {
+ // TODO: Add currently filtered organization to query params if available
+ void this.router.navigate(["/add-cipher"], {});
+ }
}
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
new file mode 100644
index 0000000000..1830d4be35
--- /dev/null
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts
@@ -0,0 +1,248 @@
+import { mock } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { CipherId } from "@bitwarden/common/types/guid";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
+import { CipherType } from "@bitwarden/common/vault/enums";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+
+import { BrowserApi } from "../../../platform/browser/browser-api";
+import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
+
+import { VaultPopupItemsService } from "./vault-popup-items.service";
+
+describe("VaultPopupItemsService", () => {
+ let service: VaultPopupItemsService;
+ let allCiphers: Record;
+ let autoFillCiphers: CipherView[];
+
+ const cipherServiceMock = mock();
+ const vaultSettingsServiceMock = mock();
+
+ beforeEach(() => {
+ allCiphers = cipherFactory(10);
+ const cipherList = Object.values(allCiphers);
+ // First 2 ciphers are autofill
+ autoFillCiphers = cipherList.slice(0, 2);
+
+ // First autofill cipher is also favorite
+ autoFillCiphers[0].favorite = true;
+
+ // 3rd and 4th ciphers are favorite
+ cipherList[2].favorite = true;
+ cipherList[3].favorite = true;
+
+ cipherServiceMock.cipherViews$ = new BehaviorSubject(allCiphers).asObservable();
+ cipherServiceMock.filterCiphersForUrl.mockImplementation(async () => autoFillCiphers);
+ vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(false).asObservable();
+ vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(false).asObservable();
+ jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false);
+ jest
+ .spyOn(BrowserApi, "getTabFromCurrentWindow")
+ .mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab);
+ service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
+ });
+
+ it("should be created", () => {
+ service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
+ expect(service).toBeTruthy();
+ });
+
+ describe("autoFillCiphers$", () => {
+ it("should return empty array if there is no current tab", (done) => {
+ jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
+ service.autoFillCiphers$.subscribe((ciphers) => {
+ expect(ciphers).toEqual([]);
+ done();
+ });
+ });
+
+ it("should return empty array if in Popout window", (done) => {
+ jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
+ service.autoFillCiphers$.subscribe((ciphers) => {
+ expect(ciphers).toEqual([]);
+ done();
+ });
+ });
+
+ it("should filter ciphers for the current tab and types", (done) => {
+ const currentTab = { url: "https://example.com" } as chrome.tabs.Tab;
+
+ vaultSettingsServiceMock.showCardsCurrentTab$ = new BehaviorSubject(true).asObservable();
+ vaultSettingsServiceMock.showIdentitiesCurrentTab$ = new BehaviorSubject(true).asObservable();
+ jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(currentTab);
+
+ service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
+
+ service.autoFillCiphers$.subscribe((ciphers) => {
+ expect(cipherServiceMock.filterCiphersForUrl.mock.calls.length).toBe(1);
+ expect(cipherServiceMock.filterCiphersForUrl).toHaveBeenCalledWith(
+ expect.anything(),
+ currentTab.url,
+ [CipherType.Card, CipherType.Identity],
+ );
+ done();
+ });
+ });
+
+ it("should return ciphers sorted by type, then by last used date, then by name", (done) => {
+ const expectedTypeOrder: Record = {
+ [CipherType.Login]: 1,
+ [CipherType.Card]: 2,
+ [CipherType.Identity]: 3,
+ [CipherType.SecureNote]: 4,
+ };
+
+ // Assume all ciphers are autofill ciphers to test sorting
+ cipherServiceMock.filterCiphersForUrl.mockImplementation(async () =>
+ Object.values(allCiphers),
+ );
+
+ service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
+
+ service.autoFillCiphers$.subscribe((ciphers) => {
+ expect(ciphers.length).toBe(10);
+
+ for (let i = 0; i < ciphers.length - 1; i++) {
+ const current = ciphers[i];
+ const next = ciphers[i + 1];
+
+ expect(expectedTypeOrder[current.type]).toBeLessThanOrEqual(expectedTypeOrder[next.type]);
+ }
+ expect(cipherServiceMock.sortCiphersByLastUsedThenName).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+
+ describe("favoriteCiphers$", () => {
+ it("should exclude autofill ciphers", (done) => {
+ service.favoriteCiphers$.subscribe((ciphers) => {
+ // 2 autofill ciphers, 3 favorite ciphers, 1 favorite cipher is also autofill = 2 favorite ciphers to show
+ expect(ciphers.length).toBe(2);
+ done();
+ });
+ });
+
+ it("should sort by last used then by name", (done) => {
+ service.favoriteCiphers$.subscribe((ciphers) => {
+ expect(cipherServiceMock.sortCiphersByLastUsedThenName).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+
+ describe("remainingCiphers$", () => {
+ it("should exclude autofill and favorite ciphers", (done) => {
+ service.remainingCiphers$.subscribe((ciphers) => {
+ // 2 autofill ciphers, 2 favorite ciphers = 6 remaining ciphers to show
+ expect(ciphers.length).toBe(6);
+ done();
+ });
+ });
+
+ it("should sort by last used then by name", (done) => {
+ service.remainingCiphers$.subscribe((ciphers) => {
+ expect(cipherServiceMock.getLocaleSortingFunction).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+
+ describe("emptyVault$", () => {
+ it("should return true if there are no ciphers", (done) => {
+ cipherServiceMock.cipherViews$ = new BehaviorSubject({}).asObservable();
+ service = new VaultPopupItemsService(cipherServiceMock, vaultSettingsServiceMock);
+ service.emptyVault$.subscribe((empty) => {
+ expect(empty).toBe(true);
+ done();
+ });
+ });
+
+ it("should return false if there are ciphers", (done) => {
+ service.emptyVault$.subscribe((empty) => {
+ expect(empty).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe("autoFillAllowed$", () => {
+ it("should return true if there is a current tab", (done) => {
+ service.autofillAllowed$.subscribe((allowed) => {
+ expect(allowed).toBe(true);
+ done();
+ });
+ });
+
+ it("should return false if there is no current tab", (done) => {
+ jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null);
+ service.autofillAllowed$.subscribe((allowed) => {
+ expect(allowed).toBe(false);
+ done();
+ });
+ });
+
+ it("should return false if in a Popout", (done) => {
+ jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true);
+ service.autofillAllowed$.subscribe((allowed) => {
+ expect(allowed).toBe(false);
+ done();
+ });
+ });
+ });
+});
+
+// A function to generate a list of ciphers of different types
+function cipherFactory(count: number): Record {
+ const ciphers: CipherView[] = [];
+ for (let i = 0; i < count; i++) {
+ const type = ((i % 4) + 1) as CipherType;
+ switch (type) {
+ case CipherType.Login:
+ ciphers.push({
+ id: `${i}`,
+ type: CipherType.Login,
+ name: `Login ${i}`,
+ login: {
+ username: `username${i}`,
+ password: `password${i}`,
+ },
+ } as CipherView);
+ break;
+ case CipherType.SecureNote:
+ ciphers.push({
+ id: `${i}`,
+ type: CipherType.SecureNote,
+ name: `SecureNote ${i}`,
+ notes: `notes${i}`,
+ } as CipherView);
+ break;
+ case CipherType.Card:
+ ciphers.push({
+ id: `${i}`,
+ type: CipherType.Card,
+ name: `Card ${i}`,
+ card: {
+ cardholderName: `cardholderName${i}`,
+ number: `number${i}`,
+ brand: `brand${i}`,
+ },
+ } as CipherView);
+ break;
+ case CipherType.Identity:
+ ciphers.push({
+ id: `${i}`,
+ type: CipherType.Identity,
+ name: `Identity ${i}`,
+ identity: {
+ firstName: `firstName${i}`,
+ lastName: `lastName${i}`,
+ },
+ } as CipherView);
+ break;
+ }
+ }
+ return Object.fromEntries(ciphers.map((c) => [c.id, c]));
+}
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
new file mode 100644
index 0000000000..52de117e6b
--- /dev/null
+++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts
@@ -0,0 +1,186 @@
+import { Injectable } from "@angular/core";
+import {
+ combineLatest,
+ map,
+ Observable,
+ of,
+ shareReplay,
+ startWith,
+ Subject,
+ switchMap,
+} from "rxjs";
+
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
+import { CipherType } from "@bitwarden/common/vault/enums";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+
+import { BrowserApi } from "../../../platform/browser/browser-api";
+import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
+
+/**
+ * Service for managing the various item lists on the new Vault tab in the browser popup.
+ */
+@Injectable({
+ providedIn: "root",
+})
+export class VaultPopupItemsService {
+ private _refreshCurrentTab$ = new Subject();
+
+ /**
+ * Observable that contains the list of other cipher types that should be shown
+ * in the autofill section of the Vault tab. Depends on vault settings.
+ * @private
+ */
+ private _otherAutoFillTypes$: Observable = combineLatest([
+ this.vaultSettingsService.showCardsCurrentTab$,
+ this.vaultSettingsService.showIdentitiesCurrentTab$,
+ ]).pipe(
+ map(([showCards, showIdentities]) => {
+ return [
+ ...(showCards ? [CipherType.Card] : []),
+ ...(showIdentities ? [CipherType.Identity] : []),
+ ];
+ }),
+ );
+
+ /**
+ * Observable that contains the current tab to be considered for autofill. If there is no current tab
+ * or the popup is in a popout window, this will be null.
+ * @private
+ */
+ private _currentAutofillTab$: Observable = this._refreshCurrentTab$.pipe(
+ startWith(null),
+ switchMap(async () => {
+ if (BrowserPopupUtils.inPopout(window)) {
+ return null;
+ }
+ return await BrowserApi.getTabFromCurrentWindow();
+ }),
+ shareReplay({ refCount: false, bufferSize: 1 }),
+ );
+
+ /**
+ * Observable that contains the list of all decrypted ciphers.
+ * @private
+ */
+ private _cipherList$: Observable = this.cipherService.cipherViews$.pipe(
+ map((ciphers) => Object.values(ciphers)),
+ 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.
+ *
+ * See {@link refreshCurrentTab} to trigger re-evaluation of the current tab.
+ */
+ autoFillCiphers$: Observable = combineLatest([
+ this._cipherList$,
+ this._otherAutoFillTypes$,
+ this._currentAutofillTab$,
+ ]).pipe(
+ switchMap(([ciphers, otherTypes, tab]) => {
+ if (!tab) {
+ return of([]);
+ }
+ return this.cipherService.filterCiphersForUrl(ciphers, tab.url, otherTypes);
+ }),
+ map((ciphers) => ciphers.sort(this.sortCiphersForAutofill.bind(this))),
+ shareReplay({ refCount: false, bufferSize: 1 }),
+ );
+
+ /**
+ * List of favorite ciphers that are not currently suggested for autofill.
+ * Ciphers are sorted by last used date, then by name.
+ */
+ favoriteCiphers$: Observable = combineLatest([
+ this.autoFillCiphers$,
+ this._cipherList$,
+ ]).pipe(
+ map(([autoFillCiphers, ciphers]) =>
+ ciphers.filter((cipher) => cipher.favorite && !autoFillCiphers.includes(cipher)),
+ ),
+ map((ciphers) =>
+ ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)),
+ ),
+ shareReplay({ refCount: false, bufferSize: 1 }),
+ );
+
+ /**
+ * List of all remaining ciphers that are not currently suggested for autofill or marked as favorite.
+ * Ciphers are sorted by name.
+ */
+ remainingCiphers$: Observable = combineLatest([
+ this.autoFillCiphers$,
+ this.favoriteCiphers$,
+ this._cipherList$,
+ ]).pipe(
+ map(([autoFillCiphers, favoriteCiphers, ciphers]) =>
+ ciphers.filter(
+ (cipher) => !autoFillCiphers.includes(cipher) && !favoriteCiphers.includes(cipher),
+ ),
+ ),
+ map((ciphers) => ciphers.sort(this.cipherService.getLocaleSortingFunction())),
+ shareReplay({ refCount: false, bufferSize: 1 }),
+ );
+
+ /**
+ * Observable that indicates whether a filter is currently applied to the ciphers.
+ * @todo Implement filter/search functionality in PM-6824 and PM-6826.
+ */
+ hasFilterApplied$: Observable = of(false);
+
+ /**
+ * Observable that indicates whether autofill is allowed in the current context.
+ * Autofill is allowed when there is a current tab and the popup is not in a popout window.
+ */
+ autofillAllowed$: Observable = this._currentAutofillTab$.pipe(map((tab) => !!tab));
+
+ /**
+ * Observable that indicates whether the user's vault is empty.
+ */
+ emptyVault$: Observable = this._cipherList$.pipe(map((ciphers) => !ciphers.length));
+
+ /**
+ * Observable that indicates whether there are no ciphers to show with the current filter.
+ * @todo Implement filter/search functionality in PM-6824 and PM-6826.
+ */
+ noFilteredResults$: Observable = of(false);
+
+ constructor(
+ private cipherService: CipherService,
+ private vaultSettingsService: VaultSettingsService,
+ ) {}
+
+ /**
+ * Re-fetch the current tab to trigger a re-evaluation of the autofill ciphers.
+ */
+ refreshCurrentTab() {
+ this._refreshCurrentTab$.next(null);
+ }
+
+ /**
+ * Sort function for ciphers to be used in the autofill section of the Vault tab.
+ * Sorts by type, then by last used date, and finally by name.
+ * @private
+ */
+ private sortCiphersForAutofill(a: CipherView, b: CipherView): number {
+ const typeOrder: Record = {
+ [CipherType.Login]: 1,
+ [CipherType.Card]: 2,
+ [CipherType.Identity]: 3,
+ [CipherType.SecureNote]: 4,
+ };
+
+ // Compare types first
+ if (typeOrder[a.type] < typeOrder[b.type]) {
+ return -1;
+ } else if (typeOrder[a.type] > typeOrder[b.type]) {
+ return 1;
+ }
+
+ // If types are the same, then sort by last used then name
+ return this.cipherService.sortCiphersByLastUsedThenName(a, b);
+ }
+}
diff --git a/apps/browser/tailwind.config.js b/apps/browser/tailwind.config.js
index affbddf2b2..be5c9ce4d9 100644
--- a/apps/browser/tailwind.config.js
+++ b/apps/browser/tailwind.config.js
@@ -1,6 +1,10 @@
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
const config = require("../../libs/components/tailwind.config.base");
-config.content = ["./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}"];
+config.content = [
+ "./src/**/*.{html,ts}",
+ "../../libs/components/src/**/*.{html,ts}",
+ "../../libs/angular/src/**/*.{html,ts}",
+];
module.exports = config;
diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss
index 8ed6a9b54b..ccc0af8fa4 100644
--- a/apps/desktop/src/scss/misc.scss
+++ b/apps/desktop/src/scss/misc.scss
@@ -577,6 +577,17 @@ app-vault-view .box-footer {
user-select: auto;
}
+/* override for vault icon in desktop */
+app-vault-icon > div {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ float: left;
+ height: 36px;
+ width: 34px;
+ margin-left: -5px;
+}
+
/* tweak for inconsistent line heights in cipher view */
.box-footer button,
.box-footer a {
diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js
index affbddf2b2..be5c9ce4d9 100644
--- a/apps/desktop/tailwind.config.js
+++ b/apps/desktop/tailwind.config.js
@@ -1,6 +1,10 @@
/* eslint-disable no-undef, @typescript-eslint/no-var-requires */
const config = require("../../libs/components/tailwind.config.base");
-config.content = ["./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}"];
+config.content = [
+ "./src/**/*.{html,ts}",
+ "../../libs/components/src/**/*.{html,ts}",
+ "../../libs/angular/src/**/*.{html,ts}",
+];
module.exports = config;
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
index e80bf6a834..08673c3f9a 100644
--- a/apps/web/tailwind.config.js
+++ b/apps/web/tailwind.config.js
@@ -5,6 +5,7 @@ config.content = [
"./src/**/*.{html,ts}",
"../../libs/components/src/**/*.{html,ts}",
"../../libs/auth/src/**/*.{html,ts}",
+ "../../libs/angular/src/**/*.{html,ts}",
"../../bitwarden_license/bit-web/src/**/*.{html,ts}",
];
diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html
index fd91d4095c..976c6ea421 100644
--- a/libs/angular/src/vault/components/icon.component.html
+++ b/libs/angular/src/vault/components/icon.component.html
@@ -1,9 +1,10 @@
-
+
>;
/**
* An observable monitoring the add/edit cipher info saved to memory.
*/
@@ -34,6 +35,12 @@ export abstract class CipherService {
includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchStrategySetting,
) => Promise;
+ filterCiphersForUrl: (
+ ciphers: CipherView[],
+ url: string,
+ includeOtherTypes?: CipherType[],
+ defaultMatch?: UriMatchStrategySetting,
+ ) => Promise;
getAllFromApiForOrganization: (organizationId: string) => Promise;
/**
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to.
diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts
index c03b440ff5..4648a8cc40 100644
--- a/libs/common/src/vault/services/cipher.service.ts
+++ b/libs/common/src/vault/services/cipher.service.ts
@@ -1,4 +1,4 @@
-import { Observable, firstValueFrom, map, share, skipWhile, switchMap } from "rxjs";
+import { firstValueFrom, map, Observable, share, skipWhile, switchMap } from "rxjs";
import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service";
@@ -29,7 +29,7 @@ import {
StateProvider,
} from "../../platform/state";
import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
-import { UserKey, OrgKey } from "../../types/key";
+import { OrgKey, UserKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType } from "../enums";
@@ -65,10 +65,10 @@ import { PasswordHistoryView } from "../models/view/password-history.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
import {
- ENCRYPTED_CIPHERS,
- LOCAL_DATA_KEY,
ADD_EDIT_CIPHER_INFO_KEY,
DECRYPTED_CIPHERS,
+ ENCRYPTED_CIPHERS,
+ LOCAL_DATA_KEY,
} from "./key-state/ciphers.state";
const CIPHER_KEY_ENC_MIN_SERVER_VER = new SemVer("2024.2.0");
@@ -443,15 +443,24 @@ export class CipherService implements CipherServiceAbstraction {
url: string,
includeOtherTypes?: CipherType[],
defaultMatch: UriMatchStrategySetting = null,
+ ): Promise {
+ const ciphers = await this.getAllDecrypted();
+ return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch);
+ }
+
+ async filterCiphersForUrl(
+ ciphers: CipherView[],
+ url: string,
+ includeOtherTypes?: CipherType[],
+ defaultMatch: UriMatchStrategySetting = null,
): Promise {
if (url == null && includeOtherTypes == null) {
- return Promise.resolve([]);
+ return [];
}
const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(url),
);
- const ciphers = await this.getAllDecrypted();
defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
return ciphers.filter((cipher) => {
diff --git a/libs/components/src/icon/icons/index.ts b/libs/components/src/icon/icons/index.ts
index 02cb975e09..0e0ce4d909 100644
--- a/libs/components/src/icon/icons/index.ts
+++ b/libs/components/src/icon/icons/index.ts
@@ -1,2 +1,3 @@
export * from "./search";
export * from "./no-access";
+export * from "./vault";
diff --git a/libs/components/src/icon/icons/vault.ts b/libs/components/src/icon/icons/vault.ts
new file mode 100644
index 0000000000..21e0eda016
--- /dev/null
+++ b/libs/components/src/icon/icons/vault.ts
@@ -0,0 +1,17 @@
+import { svgIcon } from "../icon";
+
+export const Vault = svgIcon`
+
+`;
diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js
index 12af316b38..88e7549780 100644
--- a/libs/components/tailwind.config.base.js
+++ b/libs/components/tailwind.config.base.js
@@ -60,6 +60,7 @@ module.exports = {
contrast: rgba("--color-text-contrast"),
alt2: rgba("--color-text-alt2"),
code: rgba("--color-text-code"),
+ headers: rgba("--color-text-headers"),
},
background: {
DEFAULT: rgba("--color-background"),