mirror of
https://github.com/bitwarden/clients.git
synced 2026-07-04 21:05:54 +08:00
[PM-25627] convert policy dialogs to drawers (#20078)
* [PM-34804] Implement policy drawer feature for editing policies * Added `openDrawer` method to `PolicyDialogComponent` and `MultiStepPolicyEditDialogComponent` to support opening dialogs in a drawer format. * Updated `edit` method in `PoliciesComponent` to conditionally use the drawer based on the `PolicyDrawers` feature flag. * Introduced `PolicyDrawers` feature flag in `feature-flag.enum.ts` to control the new drawer functionality. * Enhance policy edit dialogs with drawer support * Updated `policy-edit-dialog.component.html` and `multi-step-policy-edit-dialog.component.html` to conditionally apply full height class based on drawer state. * Modified `PolicyEditDialogComponent` to include a host binding for full height when in drawer mode. * Enhance MultiStepPolicyEditDialogComponent to support full height in drawer mode by adding a host binding for the full height class based on the dialog reference state. * Enhance PoliciesComponent to manage dialog references and ensure proper cleanup on destroy. Added drawerRef to handle drawer state and close it when the component is destroyed. * Refactor PoliciesComponent to change drawerRef from readonly to mutable, allowing for potential updates to the dialog reference during component lifecycle. * Enhance PoliciesComponent tests to support async operations and add new test for drawer functionality. Update policy-edit-dialog components to remove deprecated host bindings. Adjust layout component styles for drawer container. * Refactor layout component: Simplify drawer container markup by removing unnecessary grid styling. * Refactor PoliciesComponent: Replace drawerRef with signal for improved state management and update cleanup logic on component destruction. * Refactor policy edit dialog components: Simplify form markup by integrating dialog properties directly into the form tag and enhancing title template handling for improved readability and maintainability. * Refactor loading indicator in policy edit dialog to use @if syntax for improved readability * Refactor policy edit dialog to dynamically set title and subtitle based on current step content
This commit is contained in:
parent
dc94f964d2
commit
c3c5faf413
@ -31,6 +31,10 @@ export interface PolicyDialogComponent {
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<PolicyEditDialogData>,
|
||||
) => DialogRef<PolicyEditDialogResult>;
|
||||
openDrawer?: (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<PolicyEditDialogData>,
|
||||
) => DialogRef<PolicyEditDialogResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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<DialogRef<any> | undefined>(undefined);
|
||||
|
||||
private readonly userId$: Observable<UserId> = this.accountService.activeAccount$.pipe(getUserId);
|
||||
|
||||
protected readonly organizationId$: Observable<OrganizationId> = 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading()" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
|
||||
<ng-container bitDialogContent>
|
||||
<div *ngIf="loading()">
|
||||
<form
|
||||
[formGroup]="formGroup"
|
||||
[bitSubmit]="submit"
|
||||
bit-dialog
|
||||
[loading]="loading()"
|
||||
[title]="'editPolicy' | i18n"
|
||||
[subtitle]="policy.name | i18n"
|
||||
>
|
||||
<ng-container bitDialogContent>
|
||||
@if (loading()) {
|
||||
<div>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
@ -9,26 +16,20 @@
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div [hidden]="loading()">
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
<ng-template #policyForm></ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="saveDisabled()"
|
||||
bitFormButton
|
||||
type="submit"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
}
|
||||
<div [hidden]="loading()">
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
<ng-template #policyForm></ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" [disabled]="saveDisabled()" bitFormButton type="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</form>
|
||||
|
||||
@ -197,4 +197,11 @@ export class PolicyEditDialogComponent implements AfterViewInit {
|
||||
) => {
|
||||
return dialogService.open<PolicyEditDialogResult>(PolicyEditDialogComponent, config);
|
||||
};
|
||||
|
||||
static readonly openDrawer = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<PolicyEditDialogData>,
|
||||
) => {
|
||||
return dialogService.openDrawer<PolicyEditDialogResult>(PolicyEditDialogComponent, config);
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,47 +1,48 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
@let titleTemplate = policySteps()[currentStep()]?.titleContent?.();
|
||||
<bit-dialog
|
||||
[loading]="loading()"
|
||||
[title]="titleTemplate ? undefined : ('editPolicy' | i18n)"
|
||||
[subtitle]="titleTemplate ? undefined : (policy.name | i18n)"
|
||||
>
|
||||
<ng-container bitDialogTitle>
|
||||
@if (titleTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="titleTemplate"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
<form
|
||||
[formGroup]="formGroup"
|
||||
[bitSubmit]="submit"
|
||||
bit-dialog
|
||||
[loading]="loading()"
|
||||
[title]="policySteps()[currentStep()]?.titleContent?.() ? undefined : ('editPolicy' | i18n)"
|
||||
[subtitle]="policySteps()[currentStep()]?.titleContent?.() ? undefined : (policy.name | i18n)"
|
||||
>
|
||||
<ng-container bitDialogTitle>
|
||||
@let title = policySteps()[currentStep()]?.titleContent?.();
|
||||
@if (title) {
|
||||
<ng-container [ngTemplateOutlet]="title"></ng-container>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogContent>
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
<ng-container bitDialogContent>
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
|
||||
@let bodyTemplate = policySteps()[currentStep()]?.bodyContent?.();
|
||||
@if (bodyTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="bodyTemplate"></ng-container>
|
||||
}
|
||||
<!-- Always rendered so viewChild resolves before ngAfterViewInit runs. -->
|
||||
<ng-template #policyForm></ng-template>
|
||||
</ng-container>
|
||||
@let bodyTemplate = policySteps()[currentStep()]?.bodyContent?.();
|
||||
@if (bodyTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="bodyTemplate"></ng-container>
|
||||
}
|
||||
<!-- Always rendered so viewChild resolves before ngAfterViewInit runs. -->
|
||||
<ng-template #policyForm></ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
@let footerTemplate = policySteps()[currentStep()]?.footerContent?.();
|
||||
@if (footerTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="footerTemplate"></ng-container>
|
||||
} @else {
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="loading() || saveDisabled()"
|
||||
bitFormButton
|
||||
type="submit"
|
||||
>
|
||||
{{ (currentStep() === policySteps().length - 1 ? "save" : "next") | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
<ng-container bitDialogFooter>
|
||||
@let footerTemplate = policySteps()[currentStep()]?.footerContent?.();
|
||||
@if (footerTemplate) {
|
||||
<ng-container [ngTemplateOutlet]="footerTemplate"></ng-container>
|
||||
} @else {
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="loading() || saveDisabled()"
|
||||
bitFormButton
|
||||
type="submit"
|
||||
>
|
||||
{{ (currentStep() === policySteps().length - 1 ? "save" : "next") | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</form>
|
||||
|
||||
@ -163,4 +163,14 @@ export class MultiStepPolicyEditDialogComponent
|
||||
config,
|
||||
);
|
||||
};
|
||||
|
||||
static override readonly openDrawer = (
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<PolicyEditDialogData>,
|
||||
) => {
|
||||
return dialogService.openDrawer<PolicyEditDialogResult, PolicyEditDialogData>(
|
||||
MultiStepPolicyEditDialogComponent,
|
||||
config,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user