diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 907ff9af8d..b89de79a20 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -3,6 +3,8 @@ import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, skipWhile } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,6 +25,7 @@ describe("VaultPopupListFiltersService", () => { const folderViews$ = new BehaviorSubject([]); const cipherViews$ = new BehaviorSubject({}); const decryptedCollections$ = new BehaviorSubject([]); + const policyAppliesToActiveUser$ = new BehaviorSubject(false); const collectionService = { decryptedCollections$, @@ -45,9 +48,15 @@ describe("VaultPopupListFiltersService", () => { t: (key: string) => key, } as I18nService; + const policyService = { + policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$), + }; + beforeEach(() => { memberOrganizations$.next([]); decryptedCollections$.next([]); + policyAppliesToActiveUser$.next(false); + policyService.policyAppliesToActiveUser$.mockClear(); collectionService.getAllNested = () => Promise.resolve([]); TestBed.configureTestingModule({ @@ -72,6 +81,10 @@ describe("VaultPopupListFiltersService", () => { provide: CollectionService, useValue: collectionService, }, + { + provide: PolicyService, + useValue: policyService, + }, { provide: FormBuilder, useClass: FormBuilder }, ], }); @@ -127,6 +140,65 @@ describe("VaultPopupListFiltersService", () => { }); }); + describe("PersonalOwnership policy", () => { + it('calls policyAppliesToActiveUser$ with "PersonalOwnership"', () => { + expect(policyService.policyAppliesToActiveUser$).toHaveBeenCalledWith( + PolicyType.PersonalOwnership, + ); + }); + + it("returns an empty array when the policy applies and there is a single organization", (done) => { + policyAppliesToActiveUser$.next(true); + memberOrganizations$.next([ + { name: "bobby's org", id: "1234-3323-23223" }, + ] as Organization[]); + + service.organizations$.subscribe((organizations) => { + expect(organizations).toEqual([]); + done(); + }); + }); + + it('adds "myVault" when the policy does not apply and there are multiple organizations', (done) => { + policyAppliesToActiveUser$.next(false); + const orgs = [ + { name: "bobby's org", id: "1234-3323-23223" }, + { name: "alice's org", id: "2223-4343-99888" }, + ] as Organization[]; + + memberOrganizations$.next(orgs); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([ + "myVault", + "alice's org", + "bobby's org", + ]); + done(); + }); + }); + + it('does not add "myVault" the policy applies and there are multiple organizations', (done) => { + policyAppliesToActiveUser$.next(true); + const orgs = [ + { name: "bobby's org", id: "1234-3323-23223" }, + { name: "alice's org", id: "2223-3242-99888" }, + { name: "catherine's org", id: "77733-4343-99888" }, + ] as Organization[]; + + memberOrganizations$.next(orgs); + + service.organizations$.subscribe((organizations) => { + expect(organizations.map((o) => o.label)).toEqual([ + "alice's org", + "bobby's org", + "catherine's org", + ]); + done(); + }); + }); + }); + describe("icons", () => { it("sets family icon for family organizations", (done) => { const orgs = [ diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 6406e43446..66e264dd6d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -13,6 +13,8 @@ import { import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -88,6 +90,7 @@ export class VaultPopupListFiltersService { private i18nService: I18nService, private collectionService: CollectionService, private formBuilder: FormBuilder, + private policyService: PolicyService, ) { this.filterForm.controls.organization.valueChanges .pipe(takeUntilDestroyed()) @@ -167,44 +170,63 @@ export class VaultPopupListFiltersService { /** * Organization array structured to be directly passed to `ChipSelectComponent` */ - organizations$: Observable[]> = - this.organizationService.memberOrganizations$.pipe( - map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))), - map((orgs) => { - if (!orgs.length) { - return []; - } + organizations$: Observable[]> = combineLatest([ + this.organizationService.memberOrganizations$, + this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership), + ]).pipe( + map(([orgs, personalOwnershipApplies]): [Organization[], boolean] => [ + orgs.sort(Utils.getSortFunction(this.i18nService, "name")), + personalOwnershipApplies, + ]), + map(([orgs, personalOwnershipApplies]) => { + // When there are no organizations return an empty array, + // resulting in the org filter being hidden + if (!orgs.length) { + return []; + } - return [ - // When the user is a member of an organization, make the "My Vault" option available - { - value: { id: MY_VAULT_ID } as Organization, - label: this.i18nService.t("myVault"), - icon: "bwi-user", - }, - ...orgs.map((org) => { - let icon = "bwi-business"; + // When there is only one organization and personal ownership policy applies, + // return an empty array, resulting in the org filter being hidden + if (orgs.length === 1 && personalOwnershipApplies) { + return []; + } - if (!org.enabled) { - // Show a warning icon if the organization is deactivated - icon = "bwi-exclamation-triangle tw-text-danger"; - } else if ( - org.planProductType === ProductType.Families || - org.planProductType === ProductType.Free - ) { - // Show a family icon if the organization is a family or free org - icon = "bwi-family"; - } + const myVaultOrg: ChipSelectOption[] = []; - return { - value: org, - label: org.name, - icon, - }; - }), - ]; - }), - ); + // Only add "My vault" if personal ownership policy does not apply + if (!personalOwnershipApplies) { + myVaultOrg.push({ + value: { id: MY_VAULT_ID } as Organization, + label: this.i18nService.t("myVault"), + icon: "bwi-user", + }); + } + + return [ + ...myVaultOrg, + ...orgs.map((org) => { + let icon = "bwi-business"; + + if (!org.enabled) { + // Show a warning icon if the organization is deactivated + icon = "bwi-exclamation-triangle tw-text-danger"; + } else if ( + org.planProductType === ProductType.Families || + org.planProductType === ProductType.Free + ) { + // Show a family icon if the organization is a family or free org + icon = "bwi-family"; + } + + return { + value: org, + label: org.name, + icon, + }; + }), + ]; + }), + ); /** * Folder array structured to be directly passed to `ChipSelectComponent`