From 3c8329d7fdf26e176cd9a2e404a85555e6a5b54c Mon Sep 17 00:00:00 2001 From: Hatton Date: Sun, 19 Jul 2026 18:56:10 -0600 Subject: [PATCH] Guard async toolbox/dialog callbacks against torn-down DOM / missing opts (BL-16574) - TextBoxProperties.fillInLanguageNames(): the getBookLangs callback is async, so the Text Box Properties dialog may already be closed and its elements removed from the DOM by the time it resolves. Bail out instead of throwing on a null getElementById result (Sentry BLOOM-DESKTOP-FFJ). - LeveledReaderTool.beginRestoreSettings(): opts can be undefined when a book has no saved leveled-reader state. Optional-chain the lookup so it falls through to the default-level path instead of throwing an unhandled rejection that aborts tool activation (Sentry BLOOM-DESKTOP-FFH). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../TextBoxProperties/TextBoxProperties.ts | 18 ++++++++++++------ .../leveledReader/leveledReaderTool.tsx | 8 ++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/BloomBrowserUI/bookEdit/TextBoxProperties/TextBoxProperties.ts b/src/BloomBrowserUI/bookEdit/TextBoxProperties/TextBoxProperties.ts index 68228fd3b482..0a317d00fd5c 100644 --- a/src/BloomBrowserUI/bookEdit/TextBoxProperties/TextBoxProperties.ts +++ b/src/BloomBrowserUI/bookEdit/TextBoxProperties/TextBoxProperties.ts @@ -152,12 +152,18 @@ export default class TextBoxProperties { } fillInLanguageNames() { get("editView/getBookLangs", (result) => { - document.getElementById("tbprop-lang1")!.innerText = - "(" + result.data.V + ")"; - document.getElementById("tbprop-lang2")!.innerText = - "(" + result.data.N1 + ")"; - document.getElementById("tbprop-lang3")!.innerText = - "(" + result.data.N2 + ")"; + // This is an async callback: by the time getBookLangs returns, the dialog may + // already have been closed and these elements removed from the DOM. In that case + // getElementById returns null; bail rather than throw (Sentry BLOOM-DESKTOP-FFJ). + // Guard all three (not just the first): don't rely on an assumption about the + // order in which the dialog's elements are torn down. + const lang1 = document.getElementById("tbprop-lang1"); + const lang2 = document.getElementById("tbprop-lang2"); + const lang3 = document.getElementById("tbprop-lang3"); + if (!lang1 || !lang2 || !lang3) return; + lang1.innerText = "(" + result.data.V + ")"; + lang2.innerText = "(" + result.data.N1 + ")"; + lang3.innerText = "(" + result.data.N2 + ")"; }); } diff --git a/src/BloomBrowserUI/bookEdit/toolbox/readers/leveledReader/leveledReaderTool.tsx b/src/BloomBrowserUI/bookEdit/toolbox/readers/leveledReader/leveledReaderTool.tsx index 31157f7928ba..17c434c65f0f 100644 --- a/src/BloomBrowserUI/bookEdit/toolbox/readers/leveledReader/leveledReaderTool.tsx +++ b/src/BloomBrowserUI/bookEdit/toolbox/readers/leveledReader/leveledReaderTool.tsx @@ -33,9 +33,13 @@ export class LeveledReaderTool extends ToolboxToolReactAdaptor { public beginRestoreSettings(opts: string): JQueryPromise { return beginInitializeLeveledReaderTool().then(() => { const restoreDone = $.Deferred(); + // opts can be undefined/null when the tool is activated for a book that has no + // saved leveled-reader settings. Guard before indexing so we fall through to the + // default-level path instead of throwing an unhandled promise rejection that + // aborts tool activation (Sentry BLOOM-DESKTOP-FFH). const leveledReaderState = ( - opts as unknown as Record - )["leveledReaderState"]; + opts as unknown as Record | undefined + )?.["leveledReaderState"]; if (leveledReaderState) { // The true passed here prevents re-saving the state we just read. // One non-obvious implication is that simply opening a level-4 book