1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-26031] Drawer Service State Refactoring (#16580)

* refactor(risk-insights-data.service): unify drawer state management with BehaviorSubject

- Replace individual drawer properties with unified drawerDetailsSubject
- Add reactive Observable getters for drawer state checking
- Update all drawer methods to use centralized state management

* risk-insights.component: add special case drawer state sync in component

- Add private _isDrawerOpen property for internal state tracking
- Subscribe to drawerDetails$ changes with takeUntilDestroyed cleanup
- Implement getter/setter for isDrawerOpen to sync component <-> service
- Enable two-way binding while maintaining reactive patterns

* risk-insights.component.html: replace drawer template with unified observable patterns

- Replace dataService.openDrawer with isDrawerOpen special case getter
- Wrap drawer in @if block with drawerDetails$ | async for single subscription
- Update isActiveDrawerType() calls to reactive isActiveDrawerType$() | async
- Replace direct property access with unified drawerDetails object
- Use modern @if control flow syntax for better performance

* all-applications.component.html: replace drawer state with reactive observable patterns

- Replace dataService.drawerInvokerId with drawerDetails$ | async in card highlighting
- Update app-table-row-scrollable input from isDrawerIsOpenForThisRecord function to openApplication string

* critical-applications.component.html: replace drawer state with reactive observable patterns

- Replace dataService.drawerInvokerId with drawerDetails$ | async in card highlighting
- Update table component binding from isDrawerIsOpenForThisRecord to openApplication
- Use reactive drawer state checking for consistent behavior with all-applications

* all-applications.component.ts: remove deprecated drawer state functions

- Remove unused trackByFunction that's no longer needed in template
- Remove getSelectedUrls function that's not used anywhere
- Remove isDrawerOpenForTableRow replaced by reactive openApplication binding
- Clean up unused ApplicationHealthReportDetail import
- Simplifies component interface following reactive pattern migration

* critical-applications.component.ts: remove deprecated drawer state functions

- Remove unused trackByFunction that's no longer needed in template
- Remove isDrawerOpenForTableRow replaced by reactive openApplication binding

* app-table-row-scrollable.component.html: replace drawer function calls with string comparison

- Replace isDrawerIsOpenForThisRecord(row.applicationName) with row.applicationName === openApplication
- Use direct string comparison instead of function calls for better performance
- Matches updated component input from function to string property
- Simplifies template logic following reactive pattern migration

* fix(risk-insights-data.service.ts): restore drawer toggle behavior in setter methods

- Add toggle logic to check if same drawer type and invoker are already open
- Close drawer when clicking same button twice (preserves original UX)
- Switch drawer content when clicking different button
- Maintains reactive patterns while restoring expected behavior

* revert to drawer state functions to maintain scope of task
- the logic replacing these functions will be in pr16523

* fix(risk-insights-data.service.ts): restore boolean isActiveDrawerType function per review feedback

- Keep original isActiveDrawerType() as boolean function using drawerDetailsSubject.value
- Maintain isActiveDrawerType$() as Observable version for reactive templates
- Apply same pattern to isDrawerOpenForInvoker() for consistency
- Addresses review feedback to preserve existing function signatures

* refactor(risk-insights-data.service.ts): use destructuring in drawer setter methods per review feedback

* refactor(all-applications.component.html): optimize single subscription for drawer state per review feedback

* refactor(critical-applications.component.html): optimize single subscription for drawer state per review feedback

* refactor(risk-insights.component.html): use boolean drawer type functions per review feedback

* fix(browser-system-notification.service.ts): restore eslint disable comment removed by prettier

---------

Co-authored-by: Tom <144813356+ttalty@users.noreply.github.com>
This commit is contained in:
Alex
2025-09-26 16:25:16 -04:00
committed by GitHub
parent 8ba22f3080
commit 979e370235
7 changed files with 280 additions and 208 deletions

View File

@@ -1,5 +1,5 @@
import { BehaviorSubject, firstValueFrom, Observable, of } from "rxjs";
import { finalize, switchMap, withLatestFrom } from "rxjs/operators";
import { finalize, switchMap, withLatestFrom, map } from "rxjs/operators";
import {
getOrganizationById,
@@ -14,6 +14,7 @@ import {
AtRiskApplicationDetail,
AtRiskMemberDetail,
DrawerType,
DrawerDetails,
ApplicationHealthReportDetail,
ApplicationHealthReportDetailEnriched,
} from "../models/report-models";
@@ -53,12 +54,17 @@ export class RiskInsightsDataService {
private errorSubject = new BehaviorSubject<string | null>(null);
error$ = this.errorSubject.asObservable();
openDrawer = false;
drawerInvokerId: string = "";
activeDrawerType: DrawerType = DrawerType.None;
atRiskMemberDetails: AtRiskMemberDetail[] = [];
appAtRiskMembers: AppAtRiskMembersDialogParams | null = null;
atRiskAppDetails: AtRiskApplicationDetail[] | null = null;
// ------------------------- Drawer Variables ----------------
// Drawer variables unified into a single BehaviorSubject
private drawerDetailsSubject = new BehaviorSubject<DrawerDetails>({
open: false,
invokerId: "",
activeDrawerType: DrawerType.None,
atRiskMemberDetails: [],
appAtRiskMembers: null,
atRiskAppDetails: null,
});
drawerDetails$ = this.drawerDetailsSubject.asObservable();
constructor(
private accountService: AccountService,
@@ -178,56 +184,96 @@ export class RiskInsightsDataService {
}
// ------------------------------- Drawer management methods -------------------------------
// ------------------------- Drawer functions -----------------------------
isActiveDrawerType$ = (drawerType: DrawerType): Observable<boolean> => {
return this.drawerDetails$.pipe(map((details) => details.activeDrawerType === drawerType));
};
isActiveDrawerType = (drawerType: DrawerType): boolean => {
return this.activeDrawerType === drawerType;
return this.drawerDetailsSubject.value.activeDrawerType === drawerType;
};
isDrawerOpenForInvoker$ = (applicationName: string) => {
return this.drawerDetails$.pipe(map((details) => details.invokerId === applicationName));
};
isDrawerOpenForInvoker = (applicationName: string): boolean => {
return this.drawerDetailsSubject.value.invokerId === applicationName;
};
closeDrawer = (): void => {
this.drawerDetailsSubject.next({
open: false,
invokerId: "",
activeDrawerType: DrawerType.None,
atRiskMemberDetails: [],
appAtRiskMembers: null,
atRiskAppDetails: null,
});
};
setDrawerForOrgAtRiskMembers = (
atRiskMemberDetails: AtRiskMemberDetail[],
invokerId: string = "",
): void => {
this.resetDrawer(DrawerType.OrgAtRiskMembers);
this.activeDrawerType = DrawerType.OrgAtRiskMembers;
this.drawerInvokerId = invokerId;
this.atRiskMemberDetails = atRiskMemberDetails;
this.openDrawer = !this.openDrawer;
const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value;
const shouldClose =
open && activeDrawerType === DrawerType.OrgAtRiskMembers && currentInvokerId === invokerId;
if (shouldClose) {
this.closeDrawer();
} else {
this.drawerDetailsSubject.next({
open: true,
invokerId,
activeDrawerType: DrawerType.OrgAtRiskMembers,
atRiskMemberDetails,
appAtRiskMembers: null,
atRiskAppDetails: null,
});
}
};
setDrawerForAppAtRiskMembers = (
atRiskMembersDialogParams: AppAtRiskMembersDialogParams,
invokerId: string = "",
): void => {
this.resetDrawer(DrawerType.None);
this.activeDrawerType = DrawerType.AppAtRiskMembers;
this.drawerInvokerId = invokerId;
this.appAtRiskMembers = atRiskMembersDialogParams;
this.openDrawer = !this.openDrawer;
const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value;
const shouldClose =
open && activeDrawerType === DrawerType.AppAtRiskMembers && currentInvokerId === invokerId;
if (shouldClose) {
this.closeDrawer();
} else {
this.drawerDetailsSubject.next({
open: true,
invokerId,
activeDrawerType: DrawerType.AppAtRiskMembers,
atRiskMemberDetails: [],
appAtRiskMembers: atRiskMembersDialogParams,
atRiskAppDetails: null,
});
}
};
setDrawerForOrgAtRiskApps = (
atRiskApps: AtRiskApplicationDetail[],
invokerId: string = "",
): void => {
this.resetDrawer(DrawerType.OrgAtRiskApps);
this.activeDrawerType = DrawerType.OrgAtRiskApps;
this.drawerInvokerId = invokerId;
this.atRiskAppDetails = atRiskApps;
this.openDrawer = !this.openDrawer;
};
const { open, activeDrawerType, invokerId: currentInvokerId } = this.drawerDetailsSubject.value;
const shouldClose =
open && activeDrawerType === DrawerType.OrgAtRiskApps && currentInvokerId === invokerId;
closeDrawer = (): void => {
this.resetDrawer(DrawerType.None);
};
private resetDrawer = (drawerType: DrawerType): void => {
if (this.activeDrawerType !== drawerType) {
this.openDrawer = false;
if (shouldClose) {
this.closeDrawer();
} else {
this.drawerDetailsSubject.next({
open: true,
invokerId,
activeDrawerType: DrawerType.OrgAtRiskApps,
atRiskMemberDetails: [],
appAtRiskMembers: null,
atRiskAppDetails: atRiskApps,
});
}
this.activeDrawerType = DrawerType.None;
this.atRiskMemberDetails = [];
this.appAtRiskMembers = null;
this.atRiskAppDetails = null;
this.drawerInvokerId = "";
};
}

View File

@@ -25,44 +25,46 @@
</div>
<div class="tw-mt-4 tw-flex tw-flex-col" *ngIf="!(isLoading$ | async) && dataSource.data.length">
<h2 class="tw-mb-6" bitTypography="h2">{{ "allApplications" | i18n }}</h2>
<div class="tw-flex tw-gap-6">
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
>
<dirt-card
#allAppsOrgAtRiskMembers
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': dataService.drawerInvokerId === 'allAppsOrgAtRiskMembers',
}"
[title]="'atRiskMembers' | i18n"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
@if (dataService.drawerDetails$ | async; as drawerDetails) {
<div class="tw-flex tw-gap-6">
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskMembers('allAppsOrgAtRiskMembers')"
>
</dirt-card>
</button>
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskApps('allAppsOrgAtRiskApplications')"
>
<dirt-card
#allAppsOrgAtRiskApplications
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': dataService.drawerInvokerId === 'allAppsOrgAtRiskApplications',
}"
[title]="'atRiskApplications' | i18n"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
<dirt-card
#allAppsOrgAtRiskMembers
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskMembers',
}"
[title]="'atRiskMembers' | i18n"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
>
</dirt-card>
</button>
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskApps('allAppsOrgAtRiskApplications')"
>
</dirt-card>
</button>
</div>
<dirt-card
#allAppsOrgAtRiskApplications
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'allAppsOrgAtRiskApplications',
}"
[title]="'atRiskApplications' | i18n"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
>
</dirt-card>
</button>
</div>
}
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"

View File

@@ -14,10 +14,7 @@ import {
LEGACY_ApplicationHealthReportDetailWithCriticalFlag,
LEGACY_ApplicationHealthReportDetailWithCriticalFlagAndCipher,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/password-health";
import {
ApplicationHealthReportDetail,
OrganizationReportSummary,
} from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import { OrganizationReportSummary } from "@bitwarden/bit-common/dirt/reports/risk-insights/models/report-models";
import { RiskInsightsEncryptionService } from "@bitwarden/bit-common/dirt/reports/risk-insights/services/risk-insights-encryption.service";
import {
getOrganizationById,
@@ -193,10 +190,6 @@ export class AllApplicationsComponent implements OnInit {
}
};
trackByFunction(_: number, item: ApplicationHealthReportDetail) {
return item.applicationName;
}
showAppAtRiskMembers = async (applicationName: string) => {
const info = {
members:
@@ -226,9 +219,9 @@ export class AllApplicationsComponent implements OnInit {
}
};
getSelectedUrls = () => Array.from(this.selectedUrls);
isDrawerOpenForTableRow = (applicationName: string): boolean => {
return this.dataService.drawerInvokerId === applicationName;
// Note: This function will be replaced by PR #16523 with openApplication binding
// Using private access to BehaviorSubject value for backward compatibility
return (this.dataService as any).drawerDetailsSubject?.value?.invokerId === applicationName;
};
}

View File

@@ -43,44 +43,46 @@
}}
</button>
</div>
<div class="tw-flex tw-gap-6">
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskMembers('criticalAppsAtRiskMembers')"
>
<dirt-card
#criticalAppsAtRiskMembers
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': dataService.drawerInvokerId === 'criticalAppsAtRiskMembers',
}"
[title]="'atRiskMembers' | i18n"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
@if (dataService.drawerDetails$ | async; as drawerDetails) {
<div class="tw-flex tw-gap-6">
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskMembers('criticalAppsAtRiskMembers')"
>
</dirt-card>
</button>
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskApps('criticalAppsAtRiskApplications')"
>
<dirt-card
#criticalAppsAtRiskApplications
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': dataService.drawerInvokerId === 'criticalAppsAtRiskApplications',
}"
[title]="'atRiskApplications' | i18n"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
<dirt-card
#criticalAppsAtRiskMembers
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'criticalAppsAtRiskMembers',
}"
[title]="'atRiskMembers' | i18n"
[value]="applicationSummary.totalAtRiskMemberCount"
[maxValue]="applicationSummary.totalMemberCount"
>
</dirt-card>
</button>
<button
type="button"
class="tw-flex-1"
tabindex="0"
(click)="showOrgAtRiskApps('criticalAppsAtRiskApplications')"
>
</dirt-card>
</button>
</div>
<dirt-card
#criticalAppsAtRiskApplications
class="tw-w-full"
[ngClass]="{
'tw-bg-primary-100': drawerDetails.invokerId === 'criticalAppsAtRiskApplications',
}"
[title]="'atRiskApplications' | i18n"
[value]="applicationSummary.totalAtRiskApplicationCount"
[maxValue]="applicationSummary.totalApplicationCount"
>
</dirt-card>
</button>
</div>
}
<div class="tw-flex tw-mt-8 tw-mb-4 tw-gap-4">
<bit-search
[placeholder]="'searchApps' | i18n"

View File

@@ -202,10 +202,9 @@ export class CriticalApplicationsComponent implements OnInit {
this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
};
trackByFunction(_: number, item: LEGACY_ApplicationHealthReportDetailWithCriticalFlag) {
return item.applicationName;
}
isDrawerOpenForTableRow = (applicationName: string) => {
return this.dataService.drawerInvokerId === applicationName;
// Note: This function will be replaced by PR #16523 with openApplication binding
// Using private access to BehaviorSubject value for backward compatibility
return (this.dataService as any).drawerDetailsSubject?.value?.invokerId === applicationName;
};
}

View File

@@ -53,93 +53,91 @@
</bit-tab>
</bit-tab-group>
<bit-drawer
style="width: 30%"
[(open)]="dataService.openDrawer"
(openChange)="dataService.closeDrawer()"
>
<ng-container *ngIf="dataService.isActiveDrawerType(drawerTypes.OrgAtRiskMembers)">
<bit-drawer-header
title="{{ 'atRiskMembersWithCount' | i18n: dataService.atRiskMemberDetails.length }}"
>
</bit-drawer-header>
<bit-drawer-body>
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
(dataService.atRiskMemberDetails.length > 0
? "atRiskMembersDescription"
: "atRiskMembersDescriptionNone"
) | i18n
}}</span>
<ng-container *ngIf="dataService.atRiskMemberDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">{{ "email" | i18n }}</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
@if (dataService.drawerDetails$ | async; as drawerDetails) {
<bit-drawer style="width: 30%" [(open)]="isDrawerOpen" (openChange)="dataService.closeDrawer()">
<ng-container *ngIf="dataService.isActiveDrawerType(drawerTypes.OrgAtRiskMembers)">
<bit-drawer-header
title="{{ 'atRiskMembersWithCount' | i18n: drawerDetails.atRiskMemberDetails.length }}"
>
</bit-drawer-header>
<bit-drawer-body>
<span bitTypography="body1" class="tw-text-muted tw-text-sm">{{
(drawerDetails.atRiskMemberDetails.length > 0
? "atRiskMembersDescription"
: "atRiskMembersDescriptionNone"
) | i18n
}}</span>
<ng-container *ngIf="drawerDetails.atRiskMemberDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">{{ "email" | i18n }}</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</div>
<ng-container *ngFor="let member of drawerDetails.atRiskMemberDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ member.email }}</div>
<div>{{ member.atRiskPasswordCount }}</div>
</div>
</ng-container>
</ng-container>
</bit-drawer-body>
</ng-container>
@if (dataService.isActiveDrawerType(drawerTypes.AppAtRiskMembers)) {
<bit-drawer-header title="{{ drawerDetails.appAtRiskMembers.applicationName }}">
</bit-drawer-header>
<bit-drawer-body>
<div bitTypography="body1" class="tw-mb-2">
{{ "atRiskMembersWithCount" | i18n: drawerDetails.appAtRiskMembers.members.length }}
</div>
<ng-container *ngFor="let member of dataService.atRiskMemberDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div bitTypography="body1" class="tw-text-muted tw-text-sm tw-mb-2">
{{
(drawerDetails.appAtRiskMembers.members.length > 0
? "atRiskMembersDescriptionWithApp"
: "atRiskMembersDescriptionWithAppNone"
) | i18n: drawerDetails.appAtRiskMembers.applicationName
}}
</div>
<div class="tw-mt-1">
<ng-container *ngFor="let member of drawerDetails.appAtRiskMembers.members">
<div>{{ member.email }}</div>
<div>{{ member.atRiskPasswordCount }}</div>
</div>
</ng-container>
</ng-container>
</bit-drawer-body>
</ng-container>
<ng-container *ngIf="dataService.isActiveDrawerType(drawerTypes.AppAtRiskMembers)">
<bit-drawer-header title="{{ dataService.appAtRiskMembers.applicationName }}">
</bit-drawer-header>
<bit-drawer-body>
<div bitTypography="body1" class="tw-mb-2">
{{ "atRiskMembersWithCount" | i18n: dataService.appAtRiskMembers.members.length }}
</div>
<div bitTypography="body1" class="tw-text-muted tw-text-sm tw-mb-2">
{{
(dataService.appAtRiskMembers.members.length > 0
? "atRiskMembersDescriptionWithApp"
: "atRiskMembersDescriptionWithAppNone"
) | i18n: dataService.appAtRiskMembers.applicationName
}}
</div>
<div class="tw-mt-1">
<ng-container *ngFor="let member of dataService.appAtRiskMembers.members">
<div>{{ member.email }}</div>
</ng-container>
</div>
</bit-drawer-body>
</ng-container>
<ng-container *ngIf="dataService.isActiveDrawerType(drawerTypes.OrgAtRiskApps)">
<bit-drawer-header
title="{{ 'atRiskApplicationsWithCount' | i18n: dataService.atRiskAppDetails.length }}"
>
</bit-drawer-header>
<bit-drawer-body>
<span bitTypography="body2" class="tw-text-muted tw-text-sm">{{
(dataService.atRiskAppDetails.length > 0
? "atRiskApplicationsDescription"
: "atRiskApplicationsDescriptionNone"
) | i18n
}}</span>
<ng-container *ngIf="dataService.atRiskAppDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "application" | i18n }}
</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</ng-container>
</div>
<ng-container *ngFor="let app of dataService.atRiskAppDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ app.applicationName }}</div>
<div>{{ app.atRiskPasswordCount }}</div>
</bit-drawer-body>
}
@if (dataService.isActiveDrawerType(drawerTypes.OrgAtRiskApps)) {
<bit-drawer-header
title="{{ 'atRiskApplicationsWithCount' | i18n: drawerDetails.atRiskAppDetails.length }}"
>
</bit-drawer-header>
<bit-drawer-body>
<span bitTypography="body2" class="tw-text-muted tw-text-sm">{{
(drawerDetails.atRiskAppDetails.length > 0
? "atRiskApplicationsDescription"
: "atRiskApplicationsDescriptionNone"
) | i18n
}}</span>
<ng-container *ngIf="drawerDetails.atRiskAppDetails.length > 0">
<div class="tw-flex tw-justify-between tw-mt-2 tw-text-muted">
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "application" | i18n }}
</div>
<div bitTypography="body2" class="tw-text-sm tw-font-bold">
{{ "atRiskPasswords" | i18n }}
</div>
</div>
<ng-container *ngFor="let app of drawerDetails.atRiskAppDetails">
<div class="tw-flex tw-justify-between tw-mt-2">
<div>{{ app.applicationName }}</div>
<div>{{ app.atRiskPasswordCount }}</div>
</div>
</ng-container>
</ng-container>
</ng-container>
</bit-drawer-body>
</ng-container>
</bit-drawer>
</bit-drawer-body>
}
</bit-drawer>
}
</ng-container>

View File

@@ -61,6 +61,9 @@ export enum RiskInsightsTabType {
],
})
export class RiskInsightsComponent implements OnInit {
private destroyRef = inject(DestroyRef);
private _isDrawerOpen: boolean = false;
tabIndex: RiskInsightsTabType = RiskInsightsTabType.AllApps;
isRiskInsightsActivityTabFeatureEnabled: boolean = false;
@@ -73,7 +76,6 @@ export class RiskInsightsComponent implements OnInit {
notifiedMembersCount: number = 0;
private organizationId: OrganizationId = "" as OrganizationId;
private destroyRef = inject(DestroyRef);
isLoading$: Observable<boolean> = new Observable<boolean>();
isRefreshing$: Observable<boolean> = new Observable<boolean>();
@@ -136,6 +138,13 @@ export class RiskInsightsComponent implements OnInit {
);
},
});
// Subscribe to drawer state changes
this.dataService.drawerDetails$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((details) => {
this._isDrawerOpen = details.open;
});
}
/**
@@ -163,4 +172,27 @@ export class RiskInsightsComponent implements OnInit {
get drawerTypes(): typeof DrawerType {
return DrawerType;
}
/**
* Special case getter for syncing drawer state from service to component.
* This allows the template to use two-way binding while staying reactive.
*/
get isDrawerOpen() {
return this._isDrawerOpen;
}
/**
* Special case setter for syncing drawer state from component to service.
* When the drawer component closes the drawer, this syncs the state back to the service.
*/
set isDrawerOpen(value: boolean) {
if (this._isDrawerOpen !== value) {
this._isDrawerOpen = value;
// Close the drawer in the service if the drawer component closed the drawer
if (!value) {
this.dataService.closeDrawer();
}
}
}
}