Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/BloomBrowserUI/app/EditTabPane.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from "react";
import { css } from "@emotion/react";
import { get } from "../utils/bloomApi";
import { setToolboxEnabledHandler } from "../bookEdit/workspaceRoot";

interface IEditFrameSources {
pageListSrc: string;
Expand All @@ -22,6 +23,7 @@ export const EditTabPane: React.FunctionComponent<{ active: boolean }> = (
defaultEditFrameSources,
);
const [toolboxIsShowing, setToolboxIsShowing] = React.useState(true);
const [toolboxIsEnabled, setToolboxIsEnabled] = React.useState(true);

// Request edit-frame URLs from C# when edit mode becomes active so app.tsx controls iframe rendering.
React.useEffect(() => {
Expand All @@ -39,6 +41,20 @@ export const EditTabPane: React.FunctionComponent<{ active: boolean }> = (
});
}, [props.active]);

React.useEffect(() => {
// We will use this to disable and hide the toolbox when in Change Layout mode (BL-16069)
setToolboxEnabledHandler((enabled) => {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Toolbox visibility is not restored when re-enabled. When entering Change Layout mode, toolboxIsShowing is set to false, but when enabled becomes true again the handler doesn't restore it. This means after exiting Change Layout mode the toolbox stays hidden even though the toggle is re-enabled.

Consider saving the previous toolboxIsShowing value and restoring it when re-enabled.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/BloomBrowserUI/app/EditTabPane.tsx, line 46:

<comment>Toolbox visibility is not restored when re-enabled. When entering Change Layout mode, `toolboxIsShowing` is set to `false`, but when `enabled` becomes `true` again the handler doesn't restore it. This means after exiting Change Layout mode the toolbox stays hidden even though the toggle is re-enabled.

Consider saving the previous `toolboxIsShowing` value and restoring it when re-enabled.</comment>

<file context>
@@ -39,6 +41,20 @@ export const EditTabPane: React.FunctionComponent<{ active: boolean }> = (
 
+    React.useEffect(() => {
+        // We will use this to disable and hide the toolbox when in Change Layout mode (BL-16069)
+        setToolboxEnabledHandler((enabled) => {
+            setToolboxIsEnabled(enabled);
+            if (!enabled) {
</file context>
Fix with Cubic

setToolboxIsEnabled(enabled);
if (!enabled) {
setToolboxIsShowing(false);
}
});
Comment on lines +46 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Toolbox showing state not restored when toolbox is re-enabled after exiting Change Layout mode

When the toolbox is disabled (entering Change Layout mode), the handler at EditTabPane.tsx:48-50 sets toolboxIsShowing to false. However, when the toolbox is re-enabled (exiting Change Layout mode), the handler only calls setToolboxIsEnabled(true) — it never restores toolboxIsShowing back to true. This means the toolbox remains hidden after leaving Change Layout mode, even though it was visible before entering it. The user must manually click the toggle label to show the toolbox again.

The frameSources fetch that initializes toolboxIsShowing from the server (EditTabPane.tsx:40) only runs when props.active changes, not on page reloads within the edit tab, so the page reload triggered by postThatMightNavigate in origami.ts:191 does not restore the state either.

Suggested change
setToolboxEnabledHandler((enabled) => {
setToolboxIsEnabled(enabled);
if (!enabled) {
setToolboxIsShowing(false);
}
});
setToolboxEnabledHandler((enabled) => {
setToolboxIsEnabled(enabled);
if (!enabled) {
setToolboxIsShowing(false);
} else {
setToolboxIsShowing(true);
}
});
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return () => {
setToolboxEnabledHandler(undefined);
};
}, []);
Comment on lines +44 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Handler registration uses stale closure for reading state but this doesn't affect current code

The handler registered in the useEffect([], []) at line 44-56 captures the initial values of any local variables. However, the handler only calls state setters (setToolboxIsEnabled, setToolboxIsShowing) which are stable references and work correctly regardless of closure staleness. If future code needs to read toolboxIsShowing inside this handler (e.g., to save it before hiding), a useRef would be needed to avoid stale reads. This isn't a bug today but is worth noting for the suggested fix of BUG-0001 — a naive fix that tries to restore the previous showing state by reading toolboxIsShowing inside the handler would get the stale initial value.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


return (
<div
css={css`
Expand Down Expand Up @@ -88,6 +104,15 @@ export const EditTabPane: React.FunctionComponent<{ active: boolean }> = (
top: 3px;
}

.pure-toggle-label.toolbox-disabled {
opacity: 0.45;
cursor: default;
}

.pure-toggle-label.toolbox-disabled .pure-toggle-icon {
filter: grayscale(1);
}

.pure-drawer {
position: absolute;
top: 0;
Expand Down Expand Up @@ -123,14 +148,21 @@ export const EditTabPane: React.FunctionComponent<{ active: boolean }> = (
id="pure-toggle-right"
data-toggle="right"
checked={toolboxIsShowing}
disabled={!toolboxIsEnabled}
onChange={(event) => {
if (!toolboxIsEnabled) {
return;
}
setToolboxIsShowing(event.currentTarget.checked);
}}
/>
<label
className="pure-toggle-label"
className={`pure-toggle-label${
toolboxIsEnabled ? "" : " toolbox-disabled"
}`}
htmlFor="pure-toggle-right"
data-toggle-label="right"
aria-disabled={!toolboxIsEnabled}
>
<span className="pure-toggle-icon" />
</label>
Expand Down
1 change: 1 addition & 0 deletions src/BloomBrowserUI/bookEdit/js/AbovePageControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function updateAbovePageControls(

export function resetAbovePageControls(): void {
currentState = defaultState;
getWorkspaceBundleExports().setToolboxEnabled(true);

const container = document.getElementsByClassName(
"above-page-control-container",
Expand Down
15 changes: 11 additions & 4 deletions src/BloomBrowserUI/bookEdit/js/origami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "../../lib/jquery.i18n.custom";
import { splitPane } from "../../lib/split-pane/split-pane";
import { kCanvasToolId } from "../toolbox/toolIds";
import { updateAbovePageControls } from "./AbovePageControls";
import { getWorkspaceBundleExports } from "./workspaceFrames";

$(() => {
splitPane($("div.split-pane"));
Expand All @@ -32,15 +33,19 @@ export function setupOrigami() {
)[0] as HTMLElement | undefined;
if (bloomPage) {
const showChangeLayoutModeControls = customPages.length > 0;
const isChangeLayoutMode = $(".marginBox").hasClass(
"origami-layout-mode",
);
updateAbovePageControls({
isGamePage:
bloomPage?.getAttribute("data-tool-id") === "game",
showChangeLayoutModeToggle: showChangeLayoutModeControls,
isChangeLayoutMode: $(".marginBox").hasClass(
"origami-layout-mode",
),
isChangeLayoutMode,
onChangeLayoutModeToggle: handleChangeLayoutModeToggle,
});
getWorkspaceBundleExports().setToolboxEnabled(
!isChangeLayoutMode,
);
}
// I'm not clear why the rest of this needs to wait until we have
// the two results, but none of the controls shows up if we leave it all
Expand Down Expand Up @@ -190,9 +195,11 @@ function changeLayoutModeToggleClickHandler() {

function handleChangeLayoutModeToggle() {
changeLayoutModeToggleClickHandler();
const isChangeLayoutMode = $(".marginBox").hasClass("origami-layout-mode");
updateAbovePageControls({
isChangeLayoutMode: $(".marginBox").hasClass("origami-layout-mode"),
isChangeLayoutMode,
});
getWorkspaceBundleExports().setToolboxEnabled(!isChangeLayoutMode);
}

function GetTextBoxPropertiesDialog() {
Expand Down
19 changes: 19 additions & 0 deletions src/BloomBrowserUI/bookEdit/workspaceRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface IWorkspaceExports {
options: JQueryUI.DialogOptions,
): JQuery;
closeDialog(id: string): void;
setToolboxEnabled(enabled: boolean): void;
toolboxIsShowing(): boolean;
doWhenToolboxLoaded(
task: (toolboxFrameExports: IToolboxFrameExports) => unknown,
Expand Down Expand Up @@ -188,6 +189,22 @@ export function toolboxIsShowing() {
return checkbox ? checkbox.checked : true;
}

// The toolbox should be disabled whenever we are in Change Layout mode (BL-16069)
let toolboxEnabled = true;
let toolboxEnabledHandler: ((enabled: boolean) => void) | undefined;

export function setToolboxEnabledHandler(
handler: ((enabled: boolean) => void) | undefined,
): void {
toolboxEnabledHandler = handler;
toolboxEnabledHandler?.(toolboxEnabled);
}

export function setToolboxEnabled(enabled: boolean): void {
toolboxEnabled = enabled;
toolboxEnabledHandler?.(enabled);
}
Comment on lines +196 to +206
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Missing comments on new exported functions setToolboxEnabledHandler and setToolboxEnabled

Per AGENTS.md: "All public methods should have a comment. So should most private ones!" Both setToolboxEnabledHandler (line 196) and setToolboxEnabled (line 203) are exported (public) functions without individual doc comments. The comment at line 192 is a section-level comment for the variable declarations, not a function-level doc comment for either function.

Suggested change
export function setToolboxEnabledHandler(
handler: ((enabled: boolean) => void) | undefined,
): void {
toolboxEnabledHandler = handler;
toolboxEnabledHandler?.(toolboxEnabled);
}
export function setToolboxEnabled(enabled: boolean): void {
toolboxEnabled = enabled;
toolboxEnabledHandler?.(enabled);
}
// Registers a handler that will be called whenever the toolbox enabled state changes.
// The handler is immediately invoked with the current enabled state when registered.
export function setToolboxEnabledHandler(
handler: ((enabled: boolean) => void) | undefined,
): void {
toolboxEnabledHandler = handler;
toolboxEnabledHandler?.(toolboxEnabled);
}
// Sets whether the toolbox is enabled. When disabled (e.g. in Change Layout mode),
// the toolbox toggle is grayed out and the toolbox panel is hidden.
export function setToolboxEnabled(enabled: boolean): void {
toolboxEnabled = enabled;
toolboxEnabledHandler?.(enabled);
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// Do this task when the toolbox is loaded. If it isn't already, we set a timeout and do it when we can.
// (The value passed to the task function will be the value from getToolboxBundleExports(). Unfortunately we
// haven't yet managed to declare a type for that, so I can't easily specify it here.)
Expand Down Expand Up @@ -359,6 +376,7 @@ interface WorkspaceBundleApi {
switchContentPage: typeof switchContentPage;
showDialog: typeof showDialog;
closeDialog: typeof closeDialog;
setToolboxEnabled: typeof setToolboxEnabled;
toolboxIsShowing: typeof toolboxIsShowing;
doWhenToolboxLoaded: typeof doWhenToolboxLoaded;
canUndo: typeof canUndo;
Expand Down Expand Up @@ -399,6 +417,7 @@ window.workspaceBundle = {
switchContentPage,
showDialog,
closeDialog,
setToolboxEnabled,
toolboxIsShowing,
doWhenToolboxLoaded,
canUndo,
Expand Down