1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-30 23:23:52 +00:00

[PM-27675] Browser item transfer integration (#17918)

* [PM-27675] Integrate dialogs into VaultItemTransferService

* [PM-27675] Update tests for new dialogs

* [PM-27675] Center dialogs and prevent closing with escape or pointer events

* [PM-27675] Add transferInProgress$ observable to VaultItemsTransferService

* [PM-27675] Hook vault item transfer service into browser vault component

* [PM-27675] Move defaultUserCollection$ to collection service

* [PM-27675] Cleanup dialog styles

* [PM-27675] Introduce readySubject to popup vault component to keep prevent flashing content while item transfer is in progress

* [PM-27675] Fix vault-v2 tests
This commit is contained in:
Shane Melton
2025-12-16 15:03:48 -08:00
committed by GitHub
parent 3049cfad7d
commit 06d15e9681
13 changed files with 464 additions and 137 deletions

View File

@@ -9,6 +9,14 @@ import { CollectionData, Collection, CollectionView } from "../models";
export abstract class CollectionService {
abstract encryptedCollections$(userId: UserId): Observable<Collection[] | null>;
abstract decryptedCollections$(userId: UserId): Observable<CollectionView[]>;
/**
* Gets the default collection for a user in a given organization, if it exists.
*/
abstract defaultUserCollection$(
userId: UserId,
orgId: OrganizationId,
): Observable<CollectionView | undefined>;
abstract upsert(collection: CollectionData, userId: UserId): Promise<any>;
abstract replace(collections: { [id: string]: CollectionData }, userId: UserId): Promise<any>;
/**

View File

@@ -15,9 +15,10 @@ import {
} from "@bitwarden/common/spec";
import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { newGuid } from "@bitwarden/guid";
import { KeyService } from "@bitwarden/key-management";
import { CollectionData, CollectionView } from "../models";
import { CollectionData, CollectionTypes, CollectionView } from "../models";
import { DECRYPTED_COLLECTION_DATA_KEY, ENCRYPTED_COLLECTION_DATA_KEY } from "./collection.state";
import { DefaultCollectionService } from "./default-collection.service";
@@ -389,6 +390,83 @@ describe("DefaultCollectionService", () => {
});
});
describe("defaultUserCollection$", () => {
it("returns the default collection when one exists matching the org", async () => {
const orgId = newGuid() as OrganizationId;
const defaultCollection = collectionViewDataFactory(orgId);
defaultCollection.type = CollectionTypes.DefaultUserCollection;
const regularCollection = collectionViewDataFactory(orgId);
regularCollection.type = CollectionTypes.SharedCollection;
await setDecryptedState([defaultCollection, regularCollection]);
const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgId));
expect(result).toBeDefined();
expect(result?.id).toBe(defaultCollection.id);
expect(result?.isDefaultCollection).toBe(true);
});
it("returns undefined when no default collection exists", async () => {
const orgId = newGuid() as OrganizationId;
const collection1 = collectionViewDataFactory(orgId);
collection1.type = CollectionTypes.SharedCollection;
const collection2 = collectionViewDataFactory(orgId);
collection2.type = CollectionTypes.SharedCollection;
await setDecryptedState([collection1, collection2]);
const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgId));
expect(result).toBeUndefined();
});
it("returns undefined when default collection exists but for different org", async () => {
const orgA = newGuid() as OrganizationId;
const orgB = newGuid() as OrganizationId;
const defaultCollectionForOrgA = collectionViewDataFactory(orgA);
defaultCollectionForOrgA.type = CollectionTypes.DefaultUserCollection;
await setDecryptedState([defaultCollectionForOrgA]);
const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgB));
expect(result).toBeUndefined();
});
it("returns undefined when collections array is empty", async () => {
const orgId = newGuid() as OrganizationId;
await setDecryptedState([]);
const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgId));
expect(result).toBeUndefined();
});
it("returns correct collection when multiple orgs have default collections", async () => {
const orgA = newGuid() as OrganizationId;
const orgB = newGuid() as OrganizationId;
const defaultCollectionForOrgA = collectionViewDataFactory(orgA);
defaultCollectionForOrgA.type = CollectionTypes.DefaultUserCollection;
const defaultCollectionForOrgB = collectionViewDataFactory(orgB);
defaultCollectionForOrgB.type = CollectionTypes.DefaultUserCollection;
await setDecryptedState([defaultCollectionForOrgA, defaultCollectionForOrgB]);
const result = await firstValueFrom(collectionService.defaultUserCollection$(userId, orgB));
expect(result).toBeDefined();
expect(result?.id).toBe(defaultCollectionForOrgB.id);
expect(result?.organizationId).toBe(orgB);
});
});
const setEncryptedState = (collectionData: CollectionData[] | null) =>
stateProvider.setUserState(
ENCRYPTED_COLLECTION_DATA_KEY,

View File

@@ -87,6 +87,17 @@ export class DefaultCollectionService implements CollectionService {
return result$;
}
defaultUserCollection$(
userId: UserId,
orgId: OrganizationId,
): Observable<CollectionView | undefined> {
return this.decryptedCollections$(userId).pipe(
map((collections) => {
return collections.find((c) => c.isDefaultCollection && c.organizationId === orgId);
}),
);
}
private initializeDecryptedState(userId: UserId): Observable<CollectionView[]> {
return combineLatest([
this.encryptedCollections$(userId),