1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00
Files
browser/libs/components/src/anon-layout/anon-layout-wrapper.component.ts
2025-10-21 18:52:40 +02:00

191 lines
6.5 KiB
TypeScript

import { ChangeDetectorRef, Component, DestroyRef, inject, OnInit } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router";
import { Subject, filter, of, switchMap, tap } from "rxjs";
import { Icon } from "@bitwarden/assets/svg";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Translation } from "../dialog";
import { AnonLayoutWrapperDataService } from "./anon-layout-wrapper-data.service";
import { AnonLayoutComponent, AnonLayoutMaxWidth } from "./anon-layout.component";
export interface AnonLayoutWrapperData {
/**
* The optional title of the page.
* If a string is provided, it will be presented as is (ex: Organization name)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageTitle?: string | Translation | null;
/**
* The optional subtitle of the page.
* If a string is provided, it will be presented as is (ex: user's email)
* If a Translation object (supports placeholders) is provided, it will be translated
*/
pageSubtitle?: string | Translation | null;
/**
* The icon to display on the page. Pass null to hide the icon.
*/
pageIcon: Icon | null;
/**
* Optional flag to either show the optional environment selector (false) or just a readonly hostname (true).
*/
showReadonlyHostname?: boolean;
/**
* Optional flag to set the max-width of the page. Defaults to 'md' if not provided.
*/
maxWidth?: AnonLayoutMaxWidth;
/**
* Hide the card that wraps the default content. Defaults to false.
*/
hideCardWrapper?: boolean;
/**
* Hides the background illustration. Defaults to false.
*/
hideBackgroundIllustration?: boolean;
}
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
templateUrl: "anon-layout-wrapper.component.html",
imports: [AnonLayoutComponent, RouterModule],
})
export class AnonLayoutWrapperComponent implements OnInit {
private destroy$ = new Subject<void>();
protected pageTitle?: string | null;
protected pageSubtitle?: string | null;
protected pageIcon: Icon | null = null;
protected showReadonlyHostname?: boolean | null;
protected maxWidth?: AnonLayoutMaxWidth | null;
protected hideCardWrapper?: boolean | null;
protected hideBackgroundIllustration?: boolean | null;
constructor(
private router: Router,
private route: ActivatedRoute,
private i18nService: I18nService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private changeDetectorRef: ChangeDetectorRef,
) {}
private readonly destroyRef = inject(DestroyRef);
ngOnInit(): void {
// Set the initial page data on load
this.setAnonLayoutWrapperDataFromRouteData(this.route.snapshot.firstChild?.data);
// Listen for page changes and update the page data appropriately
this.listenForPageDataChanges();
this.listenForServiceDataChanges();
}
private listenForPageDataChanges() {
this.router.events
.pipe(
filter((event) => event instanceof NavigationEnd),
// reset page data on page changes
tap(() => this.resetPageData()),
switchMap(() => this.route.firstChild?.data || of(null)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((firstChildRouteData: Data | null) => {
this.setAnonLayoutWrapperDataFromRouteData(firstChildRouteData);
});
}
private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData?: Data | null) {
if (!firstChildRouteData) {
return;
}
if (firstChildRouteData["pageTitle"] !== undefined) {
this.pageTitle = this.handleStringOrTranslation(firstChildRouteData["pageTitle"]);
}
if (firstChildRouteData["pageSubtitle"] !== undefined) {
this.pageSubtitle = this.handleStringOrTranslation(firstChildRouteData["pageSubtitle"]);
}
if (firstChildRouteData["pageIcon"] !== undefined) {
this.pageIcon = firstChildRouteData["pageIcon"];
}
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
this.hideCardWrapper = Boolean(firstChildRouteData["hideCardWrapper"]);
this.hideBackgroundIllustration = Boolean(firstChildRouteData["hideBackgroundIllustration"]);
}
private listenForServiceDataChanges() {
this.anonLayoutWrapperDataService
.anonLayoutWrapperData$()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data: Partial<AnonLayoutWrapperData>) => {
this.setAnonLayoutWrapperData(data);
});
}
private setAnonLayoutWrapperData(data: Partial<AnonLayoutWrapperData>) {
if (!data) {
return;
}
// Null emissions are used to reset the page data as all fields are optional.
if (data.pageTitle !== undefined) {
this.pageTitle =
data.pageTitle !== null ? this.handleStringOrTranslation(data.pageTitle) : null;
}
if (data.pageSubtitle !== undefined) {
this.pageSubtitle =
data.pageSubtitle !== null ? this.handleStringOrTranslation(data.pageSubtitle) : null;
}
if (data.pageIcon !== undefined) {
this.pageIcon = data.pageIcon !== null ? data.pageIcon : null;
}
if (data.showReadonlyHostname !== undefined) {
this.showReadonlyHostname = data.showReadonlyHostname;
}
if (data.hideCardWrapper !== undefined) {
this.hideCardWrapper = data.hideCardWrapper;
}
if (data.hideBackgroundIllustration !== undefined) {
this.hideBackgroundIllustration = data.hideBackgroundIllustration;
}
if (data.maxWidth !== undefined) {
this.maxWidth = data.maxWidth;
}
// Manually fire change detection to avoid ExpressionChangedAfterItHasBeenCheckedError
// when setting the page data from a service
this.changeDetectorRef.detectChanges();
}
private handleStringOrTranslation(value: string | Translation): string {
if (typeof value === "string") {
// If it's a string, return it as is
return value;
}
// If it's a Translation object, translate it
return this.i18nService.t(value.key, ...(value.placeholders ?? []));
}
private resetPageData() {
this.pageTitle = null;
this.pageSubtitle = null;
this.pageIcon = null;
this.showReadonlyHostname = null;
this.maxWidth = null;
this.hideCardWrapper = null;
this.hideBackgroundIllustration = null;
}
}