diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html
deleted file mode 100644
index 788b7d4ab98..00000000000
--- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- {{ "linkSso" | i18n }}
-
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts
similarity index 86%
rename from apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts
rename to apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts
index 026e1f883a9..e7341c5c333 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.directive.ts
@@ -1,4 +1,4 @@
-import { AfterContentInit, Component, Input } from "@angular/core";
+import { AfterContentInit, Directive, HostListener, Input } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component";
@@ -14,14 +14,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
-@Component({
- selector: "app-link-sso",
- templateUrl: "link-sso.component.html",
+@Directive({
+ selector: "[app-link-sso]",
})
-export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
+export class LinkSsoDirective extends SsoComponent implements AfterContentInit {
@Input() organization: Organization;
returnUri = "/settings/organizations";
+ @HostListener("click", ["$event"])
+ async onClick($event: MouseEvent) {
+ $event.preventDefault();
+ await this.submit(this.returnUri, true);
+ }
+
constructor(
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html
index 15f3f80b3f5..f4fb2cc040a 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html
@@ -1,54 +1,60 @@
-
-
- {{ "loading" | i18n }}
-
-
-
+
+
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts
index 35c51643889..f8c60b26232 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts
@@ -1,7 +1,6 @@
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
-import { map, Subject, takeUntil } from "rxjs";
+import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs";
-import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service";
@@ -13,6 +12,7 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { DialogService } from "@bitwarden/components";
@@ -25,34 +25,58 @@ import { OrganizationFilter } from "../shared/models/vault-filter.type";
templateUrl: "organization-options.component.html",
})
export class OrganizationOptionsComponent implements OnInit, OnDestroy {
- actionPromise: Promise;
- policies: Policy[];
- loaded = false;
+ protected actionPromise: Promise;
+ protected resetPasswordPolicy?: Policy | undefined;
+ protected loaded = false;
+ protected hideMenu = false;
+ protected showLeaveOrgOption = false;
+ protected organization: OrganizationFilter;
private destroy$ = new Subject();
constructor(
- @Inject(OptionsInput) protected organization: OrganizationFilter,
+ @Inject(OptionsInput) protected organization$: Observable,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private apiService: ApiService,
private syncService: SyncService,
private policyService: PolicyService,
- private modalService: ModalService,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction,
private organizationUserService: OrganizationUserService,
- private dialogService: DialogService
+ private dialogService: DialogService,
+ private stateService: StateService
) {}
async ngOnInit() {
- this.policyService.policies$
- .pipe(
- map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword)),
- takeUntil(this.destroy$)
- )
- .subscribe((policies) => {
- this.policies = policies;
+ const resetPasswordPolicies$ = this.policyService.policies$.pipe(
+ map((policies) => policies.filter((policy) => policy.type === PolicyType.ResetPassword))
+ );
+
+ combineLatest([
+ this.organization$,
+ resetPasswordPolicies$,
+ this.stateService.getAccountDecryptionOptions(),
+ ])
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => {
+ this.organization = organization;
+ this.resetPasswordPolicy = resetPasswordPolicies.find(
+ (p) => p.organizationId === organization.id
+ );
+
+ // A user can leave an organization if they are NOT using TDE and Key Connector, or they have a master password.
+ this.showLeaveOrgOption =
+ (decryptionOptions.trustedDeviceOption == undefined &&
+ decryptionOptions.keyConnectorOption == undefined) ||
+ decryptionOptions.hasMasterPassword;
+
+ // Hide the 3 dot menu if the user has no available actions
+ this.hideMenu =
+ !this.showLeaveOrgOption &&
+ !this.showSsoOptions(this.organization) &&
+ !this.allowEnrollmentChanges(this.organization);
+
this.loaded = true;
});
}
@@ -64,21 +88,16 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
allowEnrollmentChanges(org: OrganizationFilter): boolean {
if (org.usePolicies && org.useResetPassword && org.hasPublicAndPrivateKeys) {
- const policy = this.policies.find((p) => p.organizationId === org.id);
- if (policy != null && policy.enabled) {
- return org.resetPasswordEnrolled && policy.data.autoEnrollEnabled ? false : true;
+ if (this.resetPasswordPolicy != undefined && this.resetPasswordPolicy.enabled) {
+ return !(org.resetPasswordEnrolled && this.resetPasswordPolicy.data.autoEnrollEnabled);
}
}
return false;
}
- showEnrolledStatus(org: Organization): boolean {
- return (
- org.useResetPassword &&
- org.resetPasswordEnrolled &&
- this.policies.some((p) => p.organizationId === org.id && p.enabled)
- );
+ showSsoOptions(org: OrganizationFilter) {
+ return org.useSso && org.identifier;
}
async unlinkSso(org: Organization) {
@@ -143,7 +162,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
null,
this.i18nService.t("withdrawPasswordResetSuccess")
);
- this.syncService.fullSync(true);
+ await this.syncService.fullSync(true);
} catch (e) {
this.logService.error(e);
}
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts
index 88f004262e5..23ad5e1a4ae 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts
@@ -39,7 +39,7 @@ describe("vault filter service", () => {
organizations = new ReplaySubject(1);
folderViews = new ReplaySubject(1);
- organizationService.organizations$ = organizations;
+ organizationService.memberOrganizations$ = organizations;
folderService.folderViews$ = folderViews;
vaultFilterService = new VaultFilterService(
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts
index f7663ebc42c..db0dffbdd71 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts
@@ -11,10 +11,7 @@ import {
switchMap,
} from "rxjs";
-import {
- isMember,
- OrganizationService,
-} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+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";
@@ -48,7 +45,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
);
organizationTree$: Observable> =
- this.organizationService.organizations$.pipe(
+ this.organizationService.memberOrganizations$.pipe(
switchMap((orgs) => this.buildOrganizationTree(orgs))
);
@@ -139,7 +136,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
}
if (orgs) {
const orgNodes: TreeNode[] = [];
- orgs.filter(isMember).forEach((org) => {
+ orgs.forEach((org) => {
const orgCopy = org as OrganizationFilter;
orgCopy.icon = "bwi-business";
const node = new TreeNode(orgCopy, headNode, orgCopy.name);
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html
index bcd151193a5..aac08395205 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.html
@@ -98,16 +98,11 @@
class="org-options bwi bwi-fw bwi-exclamation-triangle text-danger"
[attr.aria-label]="'organizationIsDisabled' | i18n"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
- >
-
-
-
-
-
+ >
+
+
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts
index a15521dc228..21e6ef727da 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/components/vault-filter-section.component.ts
@@ -1,5 +1,6 @@
import { Component, InjectionToken, Injector, Input, OnDestroy, OnInit } from "@angular/core";
-import { Subject, takeUntil } from "rxjs";
+import { Observable, Subject, takeUntil } from "rxjs";
+import { map } from "rxjs/operators";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { ITreeNodeObject, TreeNode } from "@bitwarden/common/models/domain/tree-node";
@@ -120,9 +121,15 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
// here we are creating a new injector for each filter that has options
createInjector(data: VaultFilterType) {
let inject = this.injectors.get(data.id);
+
if (!inject) {
+ // Pass an observable to the component in order to update the component when the data changes
+ // as data binding does not work with dynamic components in Angular 15 (inputs are supported starting Angular 16)
+ const data$ = this.section.data$.pipe(
+ map((sectionNode) => sectionNode?.children?.find((node) => node.node.id === data.id)?.node)
+ );
inject = Injector.create({
- providers: [{ provide: OptionsInput, useValue: data }],
+ providers: [{ provide: OptionsInput, useValue: data$ }],
parent: this.injector,
});
this.injectors.set(data.id, inject);
@@ -130,4 +137,4 @@ export class VaultFilterSectionComponent implements OnInit, OnDestroy {
return inject;
}
}
-export const OptionsInput = new InjectionToken("OptionsInput");
+export const OptionsInput = new InjectionToken>("OptionsInput");
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts
index b6edd3e9838..2acab17e383 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/vault-filter.module.ts
@@ -2,7 +2,7 @@ import { NgModule } from "@angular/core";
import { VaultFilterSharedModule } from "../../individual-vault/vault-filter/shared/vault-filter-shared.module";
-import { LinkSsoComponent } from "./components/link-sso.component";
+import { LinkSsoDirective } from "./components/link-sso.directive";
import { OrganizationOptionsComponent } from "./components/organization-options.component";
import { VaultFilterComponent } from "./components/vault-filter.component";
import { VaultFilterService as VaultFilterServiceAbstraction } from "./services/abstractions/vault-filter.service";
@@ -10,7 +10,7 @@ import { VaultFilterService } from "./services/vault-filter.service";
@NgModule({
imports: [VaultFilterSharedModule],
- declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoComponent],
+ declarations: [VaultFilterComponent, OrganizationOptionsComponent, LinkSsoDirective],
exports: [VaultFilterComponent],
providers: [
{