diff --git a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts index 5bd628607d0..a50db4d398f 100644 --- a/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/base-policy-edit.component.ts @@ -31,6 +31,10 @@ export interface PolicyDialogComponent { dialogService: DialogService, config: DialogConfig, ) => DialogRef; + openDrawer?: ( + dialogService: DialogService, + config: DialogConfig, + ) => DialogRef; } /** diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts index 85e7d2c635f..c865e9f50f3 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.spec.ts @@ -496,7 +496,9 @@ describe("PoliciesComponent", () => { }); describe("edit", () => { - it("should call dialogService.open with correct parameters when no custom dialog is specified", () => { + it("should call dialogService.open with correct parameters when no custom dialog is specified", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + const mockPolicy: BasePolicyEditDefinition = { name: "Test Policy", description: "Test Description", @@ -510,7 +512,7 @@ describe("PoliciesComponent", () => { const openSpy = jest.spyOn(PolicyEditDialogComponent, "open"); - component.edit(mockPolicy, mockOrg); + await component.edit(mockPolicy, mockOrg); expect(openSpy).toHaveBeenCalled(); const callArgs = openSpy.mock.calls[0]; @@ -522,7 +524,9 @@ describe("PoliciesComponent", () => { }); }); - it("should call custom dialog open method when specified", () => { + it("should call custom dialog open method when specified", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + const mockDialogRef = { close: jest.fn() }; const mockCustomDialog = { open: jest.fn().mockReturnValue(mockDialogRef), @@ -540,7 +544,7 @@ describe("PoliciesComponent", () => { display$: () => of(true), }; - component.edit(mockPolicy, mockOrg); + await component.edit(mockPolicy, mockOrg); expect(mockCustomDialog.open).toHaveBeenCalled(); const callArgs = mockCustomDialog.open.mock.calls[0]; @@ -553,7 +557,9 @@ describe("PoliciesComponent", () => { expect(PolicyEditDialogComponent.open).not.toHaveBeenCalled(); }); - it("should pass organization to dialog", () => { + it("should pass organization to dialog", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(false); + const customOrg = { id: newGuid() as OrganizationId, name: "Custom Org" } as Organization; const mockPolicy: BasePolicyEditDefinition = { name: "Test Policy", @@ -568,7 +574,7 @@ describe("PoliciesComponent", () => { const openSpy = jest.spyOn(PolicyEditDialogComponent, "open"); - component.edit(mockPolicy, customOrg); + await component.edit(mockPolicy, customOrg); expect(openSpy).toHaveBeenCalled(); const callArgs = openSpy.mock.calls[0]; @@ -579,5 +585,39 @@ describe("PoliciesComponent", () => { }, }); }); + + it("should open drawer when PolicyDrawers flag is enabled and openDrawer is present", async () => { + mockConfigService.getFeatureFlag.mockResolvedValue(true); + + const mockDrawerRef = { close: jest.fn() }; + const mockDrawerDialog = { + open: jest.fn(), + openDrawer: jest.fn().mockReturnValue(mockDrawerRef), + }; + + const mockPolicy: BasePolicyEditDefinition = { + name: "Drawer Policy", + description: "Drawer Description", + type: PolicyType.TwoFactorAuthentication, + category: PolicyCategory.Authentication, + priority: 10, + component: {} as any, + editDialogComponent: mockDrawerDialog as any, + showDescription: true, + display$: () => of(true), + }; + + await component.edit(mockPolicy, mockOrg); + + expect(mockDrawerDialog.openDrawer).toHaveBeenCalled(); + const callArgs = mockDrawerDialog.openDrawer.mock.calls[0]; + expect(callArgs[1]).toEqual({ + data: { + policy: mockPolicy, + organization: mockOrg, + }, + }); + expect(mockDrawerDialog.open).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts index 648e35f3e49..d64b1e1d4ba 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policies.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policies.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, DestroyRef } from "@angular/core"; +import { ChangeDetectionStrategy, Component, DestroyRef, signal } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ActivatedRoute } from "@angular/router"; import { combineLatest, Observable, of, switchMap, first, map, shareReplay } from "rxjs"; @@ -11,10 +11,16 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { getById } from "@bitwarden/common/platform/misc"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { DialogService, ItemModule, SectionHeaderComponent } from "@bitwarden/components"; +import { + DialogRef, + DialogService, + ItemModule, + SectionHeaderComponent, +} from "@bitwarden/components"; import { safeProvider } from "@bitwarden/ui-common"; import { HeaderModule } from "../../../layouts/header/header.module"; @@ -37,6 +43,8 @@ import { POLICY_EDIT_REGISTER } from "./policy-register-token"; changeDetection: ChangeDetectionStrategy.OnPush, }) export class PoliciesComponent { + private readonly drawerRef = signal | undefined>(undefined); + private readonly userId$: Observable = this.accountService.activeAccount$.pipe(getUserId); protected readonly organizationId$: Observable = this.route.params.pipe( @@ -128,6 +136,7 @@ export class PoliciesComponent { private readonly destroyRef: DestroyRef, ) { this.handleLaunchEvent(); + this.destroyRef.onDestroy(() => this.drawerRef()?.close()); } // Handle policies component launch from Event message @@ -142,7 +151,7 @@ export class PoliciesComponent { if (orgPolicy.id === policyIdFromEvents) { for (const policy of policies) { if (policy.type === orgPolicy.type) { - this.edit(policy, organization); + void this.edit(policy, organization); break; } } @@ -156,15 +165,27 @@ export class PoliciesComponent { .subscribe(); } - edit(policy: BasePolicyEditDefinition, organization: Organization) { + async edit(policy: BasePolicyEditDefinition, organization: Organization) { + const useDrawer = await this.configService.getFeatureFlag(FeatureFlag.PolicyDrawers); const dialogComponent: PolicyDialogComponent = policy.editDialogComponent ?? PolicyEditDialogComponent; - dialogComponent.open(this.dialogService, { - data: { - policy: policy, - organization: organization, - }, - }); + if (useDrawer && dialogComponent.openDrawer) { + this.drawerRef.set( + dialogComponent.openDrawer(this.dialogService, { + data: { + policy: policy, + organization: organization, + }, + }), + ); + } else { + dialogComponent.open(this.dialogService, { + data: { + policy: policy, + organization: organization, + }, + }); + } } } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html index 8574a70c215..e17add85fe5 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.html @@ -1,7 +1,14 @@ -
- - -
+ + + @if (loading()) { +
{{ "loading" | i18n }}
-
- @if (policy.showDescription) { -

{{ policy.description | i18n }}

- } - -
-
- - - - - + } +
+ @if (policy.showDescription) { +

{{ policy.description | i18n }}

+ } + +
+ + + + + diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts index c3d1d9afd68..0410e701650 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialog.component.ts @@ -197,4 +197,11 @@ export class PolicyEditDialogComponent implements AfterViewInit { ) => { return dialogService.open(PolicyEditDialogComponent, config); }; + + static readonly openDrawer = ( + dialogService: DialogService, + config: DialogConfig, + ) => { + return dialogService.openDrawer(PolicyEditDialogComponent, config); + }; } diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.html b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.html index fb93a828972..dd55438bfa2 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.html @@ -1,47 +1,48 @@ -
- @let titleTemplate = policySteps()[currentStep()]?.titleContent?.(); - - - @if (titleTemplate) { - - } - + + + @let title = policySteps()[currentStep()]?.titleContent?.(); + @if (title) { + + } + - - @if (policy.showDescription) { -

{{ policy.description | i18n }}

- } + + @if (policy.showDescription) { +

{{ policy.description | i18n }}

+ } - @let bodyTemplate = policySteps()[currentStep()]?.bodyContent?.(); - @if (bodyTemplate) { - - } - - -
+ @let bodyTemplate = policySteps()[currentStep()]?.bodyContent?.(); + @if (bodyTemplate) { + + } + + +
- - @let footerTemplate = policySteps()[currentStep()]?.footerContent?.(); - @if (footerTemplate) { - - } @else { - - - } - -
+ + @let footerTemplate = policySteps()[currentStep()]?.footerContent?.(); + @if (footerTemplate) { + + } @else { + + + } +
diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.ts index 4e7d29f3efa..173f9d731e2 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit-dialogs/multi-step-policy-edit-dialog.component.ts @@ -163,4 +163,14 @@ export class MultiStepPolicyEditDialogComponent config, ); }; + + static override readonly openDrawer = ( + dialogService: DialogService, + config: DialogConfig, + ) => { + return dialogService.openDrawer( + MultiStepPolicyEditDialogComponent, + config, + ); + }; } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 7335263083f..541656aee3e 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -13,6 +13,7 @@ export enum FeatureFlag { /* Admin Console Team */ AdminResetTwoFactor = "pm-15489-reset-two-factor-account-recovery", GenerateInviteLink = "pm-32497-generate-invite-link", + PolicyDrawers = "pm-34804-policy-drawers", /* Auth */ PM27086_UpdateAuthenticationApisForInputPassword = "pm-27086-update-authentication-apis-for-input-password", @@ -117,6 +118,7 @@ export const DefaultFeatureFlagValue = { /* Admin Console Team */ [FeatureFlag.AdminResetTwoFactor]: FALSE, [FeatureFlag.GenerateInviteLink]: FALSE, + [FeatureFlag.PolicyDrawers]: FALSE, /* Autofill */ [FeatureFlag.FillAssistTargetingRules]: FALSE,