1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-04 09:33:27 +00:00

[PM-12048] Wire up vNextCollectionService (#14871)

* remove derived state, add cache in service. Fix ts strict errors

* cleanup

* promote vNextCollectionService

* wip

* replace callers in web WIP

* refactor tests for web

* update callers to use vNextCollectionServcie methods in CLI

* WIP make decryptMany public again, fix callers, imports

* wip cli

* wip desktop

* update callers in browser, fix tests

* remove in service cache

* cleanup

* fix test

* clean up

* address cr feedback

* remove duplicate userId

* clean up

* remove unused import

* fix vault-settings-import-nudge.service

* fix caching issue

* clean up

* refactor decryption, cleanup, update callers

* clean up

* Use in-memory statedefinition

* Ac/pm 12048 v next collection service pairing (#15239)

* Draft from pairing with Gibson

* Add todos

* Add comment

* wip

* refactor upsert

---------

Co-authored-by: Brandon <btreston@bitwarden.com>

* clean up

* fix state definitions

* fix linter error

* cleanup

* add test, fix shareReplay

* fix item-more-options component

* fix desktop build

* refactor state to account for null as an initial value, remove caching

* add proper cache, add unit test, update callers

* clean up

* fix routing when deleting collections

* cleanup

* use combineLatest

* fix ts-strict errors, fix error handling

* refactor Collection and CollectionView properties for ts-strict

* Revert "refactor Collection and CollectionView properties for ts-strict"

This reverts commit a5c63aab76.

---------

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Brandon Treston
2025-07-23 19:05:15 -04:00
committed by GitHub
parent 7a24a538a4
commit d0d1359ff4
56 changed files with 906 additions and 1112 deletions

View File

@@ -1030,18 +1030,23 @@ export default class NotificationBackground {
private async getCollectionData(
message: NotificationBackgroundExtensionMessage,
): Promise<CollectionView[]> {
const collections = (await this.collectionService.getAllDecrypted()).reduce<CollectionView[]>(
(acc, collection) => {
if (collection.organizationId === message?.orgId) {
acc.push({
id: collection.id,
name: collection.name,
organizationId: collection.organizationId,
});
}
return acc;
},
[],
const collections = await firstValueFrom(
this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
map((collections) =>
collections.reduce<CollectionView[]>((acc, collection) => {
if (collection.organizationId === message?.orgId) {
acc.push({
id: collection.id,
name: collection.name,
organizationId: collection.organizationId,
});
}
return acc;
}, []),
),
),
);
return collections;
}

View File

@@ -1620,7 +1620,6 @@ export default class MainBackground {
this.keyService.clearKeys(userBeingLoggedOut),
this.cipherService.clear(userBeingLoggedOut),
this.folderService.clear(userBeingLoggedOut),
this.collectionService.clear(userBeingLoggedOut),
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
this.vaultFilterService.clear(),
this.biometricStateService.logout(userBeingLoggedOut),

View File

@@ -10,6 +10,7 @@ import { Observable, combineLatest, filter, first, map, switchMap } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -70,7 +71,12 @@ export class AssignCollections {
),
);
combineLatest([cipher$, this.collectionService.decryptedCollections$])
const decryptedCollection$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
);
combineLatest([cipher$, decryptedCollection$])
.pipe(takeUntilDestroyed(), first())
.subscribe(([cipherView, collections]) => {
let availableCollections = collections;

View File

@@ -93,7 +93,7 @@ export class ItemMoreOptionsComponent {
switchMap((userId) => {
return combineLatest([
this.organizationService.hasOrganizations(userId),
this.collectionService.decryptedCollections$,
this.collectionService.decryptedCollections$(userId),
]).pipe(
map(([hasOrgs, collections]) => {
const canEditCollections = collections.some((c) => !c.readOnly);

View File

@@ -139,7 +139,7 @@ describe("VaultPopupItemsService", () => {
];
organizationServiceMock.organizations$.mockReturnValue(new BehaviorSubject([mockOrg]));
collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections);
collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections));
activeUserLastSync$ = new BehaviorSubject(new Date());
syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$);

View File

@@ -72,6 +72,11 @@ export class VaultPopupItemsService {
private organizations$ = this.activeUserId$.pipe(
switchMap((userId) => this.organizationService.organizations$(userId)),
);
private decryptedCollections$ = this.activeUserId$.pipe(
switchMap((userId) => this.collectionService.decryptedCollections$(userId)),
);
/**
* 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.
@@ -130,7 +135,7 @@ export class VaultPopupItemsService {
private _activeCipherList$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
switchMap((ciphers) =>
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
map(([organizations, collections]) => {
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
@@ -291,7 +296,7 @@ export class VaultPopupItemsService {
*/
deletedCiphers$: Observable<PopupCipherViewLike[]> = this._allDecryptedCiphers$.pipe(
switchMap((ciphers) =>
combineLatest([this.organizations$, this.collectionService.decryptedCollections$]).pipe(
combineLatest([this.organizations$, this.decryptedCollections$]).pipe(
map(([organizations, collections]) => {
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));

View File

@@ -20,6 +20,7 @@ import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
@@ -58,7 +59,7 @@ describe("VaultPopupListFiltersService", () => {
};
const collectionService = {
decryptedCollections$,
decryptedCollections$: () => decryptedCollections$,
getAllNested: () => Promise.resolve([]),
} as unknown as CollectionService;
@@ -106,7 +107,7 @@ describe("VaultPopupListFiltersService", () => {
signal: jest.fn(() => mockCachedSignal),
};
collectionService.getAllNested = () => Promise.resolve([]);
collectionService.getAllNested = () => [];
TestBed.configureTestingModule({
providers: [
{
@@ -382,14 +383,7 @@ describe("VaultPopupListFiltersService", () => {
beforeEach(() => {
decryptedCollections$.next(testCollections);
collectionService.getAllNested = () =>
Promise.resolve(
testCollections.map((c) => ({
children: [],
node: c,
parent: null,
})),
);
collectionService.getAllNested = () => testCollections.map((c) => new TreeNode(c, null));
});
it("returns all collections", (done) => {
@@ -755,15 +749,13 @@ function createSeededVaultPopupListFiltersService(
} as any;
const collectionServiceMock = {
decryptedCollections$: seededCollections$,
decryptedCollections$: () => seededCollections$,
getAllNested: () =>
Promise.resolve(
seededCollections$.value.map((c) => ({
children: [],
node: c,
parent: null,
})),
),
seededCollections$.value.map((c) => ({
children: [],
node: c,
parent: null,
})),
} as any;
const folderServiceMock = {

View File

@@ -6,7 +6,6 @@ import {
debounceTime,
distinctUntilChanged,
filter,
from,
map,
Observable,
shareReplay,
@@ -446,7 +445,7 @@ export class VaultPopupListFiltersService {
this.filters$.pipe(
distinctUntilChanged((prev, curr) => prev.organization?.id === curr.organization?.id),
),
this.collectionService.decryptedCollections$,
this.collectionService.decryptedCollections$(userId),
this.organizationService.memberOrganizations$(userId),
this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation),
]),
@@ -463,16 +462,11 @@ export class VaultPopupListFiltersService {
}
return sortDefaultCollections(filtered, orgs, this.i18nService.collator);
}),
switchMap((collections) => {
return from(this.collectionService.getAllNested(collections)).pipe(
map(
(nested) =>
new DynamicTreeNode<CollectionView>({
fullList: collections,
nestedList: nested,
}),
),
);
map((fullList) => {
return new DynamicTreeNode<CollectionView>({
fullList,
nestedList: this.collectionService.getAllNested(fullList),
});
}),
map((tree) =>
tree.nestedList.map((c) => this.convertToChipSelectOption(c, "bwi-collection-shared")),