From 42af48af59811f8832aa6023b4d7dc2f0e4f441b Mon Sep 17 00:00:00 2001 From: Nathaniel Paulus Date: Thu, 23 Jan 2025 16:21:38 -0500 Subject: [PATCH] Add robust error dialog for when digest cycle is broken --- .../error-dialog/error-dialog.component.ts | 77 ++++++++++++++++++- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-dialog/error-dialog.component.ts b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-dialog/error-dialog.component.ts index 84f93f97be..45c54bce77 100644 --- a/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-dialog/error-dialog.component.ts +++ b/src/SIL.XForge.Scripture/ClientApp/src/xforge-common/error-dialog/error-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { browserLinks, getLinkHTML, issuesEmailTemplate, supportedBrowser } from 'xforge-common/utils'; import { environment } from '../../environments/environment'; @@ -14,9 +14,11 @@ export interface ErrorAlertData { templateUrl: './error-dialog.component.html', styleUrls: ['./error-dialog.component.scss'] }) -export class ErrorDialogComponent { +export class ErrorDialogComponent implements OnInit { + initComplete = false; showDetails = false; browserUnsupported = !supportedBrowser(); + outsideAngularErrorDialog?: OutsideAngularErrorDialog; issueEmailLink = getLinkHTML(environment.issueEmail, issuesEmailTemplate(this.data.eventId)); @@ -24,9 +26,78 @@ export class ErrorDialogComponent { public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: ErrorAlertData, readonly i18n: I18nService - ) {} + ) { + // If the dialog doesn't init in the expected time, open the outsideAngularErrorDialog + setTimeout(() => { + if (!this.initComplete) this.openOutsideAngularErrorDialog(); + }, 100); + } get browserLinks(): { chromeLink: string; firefoxLink: string; safariLink: string } { return browserLinks(); } + + ngOnInit(): void { + this.initComplete = true; + // In the unlikely event that this dialog didn't init in the expected time, but eventually did init, and the + // outsideAngularErrorDialog was wrongly created, close it. + if (this.outsideAngularErrorDialog != null) { + this.outsideAngularErrorDialog.close(); + } + } + + openOutsideAngularErrorDialog(): void { + this.outsideAngularErrorDialog = new OutsideAngularErrorDialog( + this.data.message, + issuesEmailTemplate(this.data.eventId) + ); + } +} + +/** + * This is a last-ditch error dialog for when an error prevents Angular from rendering the error dialog, such as when + * there's an error happening in every Angular digest cycle. + * It is not internationalized, and is not intended to look like the normal error dialog. + */ +class OutsideAngularErrorDialog { + private dialogElement: HTMLDialogElement; + + constructor(message: string, linkUrl: string) { + // Elements + this.dialogElement = document.createElement('dialog'); + const titleElement = document.createElement('h1'); + const messageElement = document.createElement('p'); + const issueLinkWrapper = document.createElement('p'); + const issueLinkElement = document.createElement('a'); + const closeElement = document.createElement('button'); + + // Text content + titleElement.textContent = 'An error has occurred'; + messageElement.textContent = `Error: ${message}`; + issueLinkElement.textContent = `Report error to ${environment.issueEmail}`; + closeElement.textContent = 'Close'; + + // Report issue link + issueLinkElement.href = linkUrl; + issueLinkElement.target = '_blank'; + issueLinkWrapper.appendChild(issueLinkElement); + + // Close button + closeElement.onclick = () => this.close(); + + // Assemble element tree + this.dialogElement.appendChild(titleElement); + this.dialogElement.appendChild(messageElement); + this.dialogElement.appendChild(issueLinkWrapper); + this.dialogElement.appendChild(closeElement); + + // Create and open dialog + document.body.appendChild(this.dialogElement); + this.dialogElement.showModal(); + } + + close(): void { + this.dialogElement.close(); + this.dialogElement.remove(); + } }