FN-7914: fix mobile prompt fullscreen editor Expand button hidden behind workflow sheet
Fixes the workflow editor's fullscreen prompt overlay having an inert-looking mobile Expand button because a static z-index of 10000 sat below the mobile workflow editor's full-screen FloatingWindow sheet (10100+ shared floating-stack band). - Have the fullscreen prompt overlay claim a fresh shared floating-stack z-index via nextFloatingZ() when opened, instead of relying on the static CSS z-index: 10000 fallback - Track the claimed z-index in new promptFullscreenZ state, applying it as an inline style on the portaled overlay and clearing it on collapse/Escape - Update FNXC comments in WorkflowNodeEditor.css and floatingWindowStack.ts documenting the shared z-index contract and why the static value is now only a fallback - Add regression tests asserting the prompt fullscreen overlay's z-index exceeds the workflow editor's floating window z-index on desktop, mobile prompt nodes, mobile gate nodes, and after re-opening after collapse Files changed: .../app/components/WorkflowNodeEditor.css | 1 + .../app/components/WorkflowNodeEditor.tsx | 25 ++++++- .../__tests__/WorkflowNodeEditor.test.tsx | 81 ++++++++++++++++++++++ .../app/components/floatingWindowStack.ts | 2 +- 4 files changed, 106 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-7914 Fusion-Task-Lineage: e230309a-3b53-4e42-9e63-7516597b6c24 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
@@ -1179,6 +1179,7 @@ Keep logical overflow and tab intrinsic-size declarations beside the physical fa
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
/* FNXC:WorkflowEditor 2026-07-12-00:00: Keep this as a static fallback only; WorkflowNodeEditor applies an inline shared floating-stack z-index when the prompt overlay opens so mobile FloatingWindow sheets cannot cover it. */
|
||||
z-index: 10000;
|
||||
background: var(--surface);
|
||||
padding: max(var(--space-lg), env(safe-area-inset-top, 0))
|
||||
|
||||
@@ -108,6 +108,7 @@ import { WorkflowSettingsPanel } from "./WorkflowSettingsPanel";
|
||||
import type { WorkflowFieldDefinition, WorkflowSettingDefinition } from "../api";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { FloatingWindow } from "./FloatingWindow";
|
||||
import { nextFloatingZ } from "./floatingWindowStack";
|
||||
import { MobileWorkflowGraphView } from "./MobileWorkflowGraphView";
|
||||
import {
|
||||
buildMobileWorkflowGraph,
|
||||
@@ -2412,14 +2413,29 @@ function InnerEditor({
|
||||
const mobileNodeDetailStage = isMobileMode && selectedNodeHasInspector && !inspectorCollapsed;
|
||||
const mobileEdgeDetailStage = isMobileMode && selectedEdge !== null;
|
||||
const [isPromptExpanded, setIsPromptExpanded] = useState(false);
|
||||
const [promptFullscreenZ, setPromptFullscreenZ] = useState<number | null>(null);
|
||||
const handleTogglePromptExpand = useCallback(() => {
|
||||
setIsPromptExpanded((prev) => !prev);
|
||||
}, []);
|
||||
if (isPromptExpanded) {
|
||||
setIsPromptExpanded(false);
|
||||
setPromptFullscreenZ(null);
|
||||
return;
|
||||
}
|
||||
setPromptFullscreenZ(nextFloatingZ());
|
||||
setIsPromptExpanded(true);
|
||||
}, [isPromptExpanded]);
|
||||
useEffect(() => {
|
||||
if (!isPromptExpanded) {
|
||||
if (promptFullscreenZ !== null) setPromptFullscreenZ(null);
|
||||
return;
|
||||
}
|
||||
if (promptFullscreenZ === null) setPromptFullscreenZ(nextFloatingZ());
|
||||
}, [isPromptExpanded, promptFullscreenZ]);
|
||||
const handlePromptFullscreenKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!isPromptExpanded || e.key !== "Escape") return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsPromptExpanded(false);
|
||||
setPromptFullscreenZ(null);
|
||||
}, [isPromptExpanded]);
|
||||
const selectedNodePromptValue =
|
||||
selectedNode && (selectedNode.data.kind === "prompt" || selectedNode.data.kind === "gate")
|
||||
@@ -2725,11 +2741,16 @@ function InnerEditor({
|
||||
};
|
||||
}, [overrideColumnBinding, agents.length, projectId, addToast, t]);
|
||||
|
||||
/*
|
||||
FNXC:WorkflowEditor 2026-07-12-00:00:
|
||||
The fullscreen prompt editor is portaled beside the FloatingWindow that launched it, so it must claim a fresh shared floating-stack z-index when opened. A static z-index of 10000 is below the workflow editor's 10100+ full-screen mobile FloatingWindow sheet and makes the mobile Expand button appear inert because the sheet covers the overlay.
|
||||
*/
|
||||
const promptFullscreenOverlay =
|
||||
isPromptExpanded && (selectedNode?.data.kind === "prompt" || selectedNode?.data.kind === "gate")
|
||||
? createPortal(
|
||||
<div
|
||||
className="wf-prompt-editor wf-prompt-editor--fullscreen"
|
||||
style={promptFullscreenZ !== null ? { zIndex: promptFullscreenZ } : undefined}
|
||||
onKeyDown={handlePromptFullscreenKeyDown}
|
||||
>
|
||||
<div className="wf-prompt-fullscreen-header">
|
||||
|
||||
@@ -113,6 +113,23 @@ function getPromptFullscreenTextarea() {
|
||||
return within(overlay!).getByLabelText("Prompt") as HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
function getWorkflowEditorFloatingOverlay() {
|
||||
return document.body.querySelector('[data-testid="floating-window-overlay-workflow-node-editor"]') as HTMLElement | null;
|
||||
}
|
||||
|
||||
function zIndexOf(element: HTMLElement) {
|
||||
const value = element.style.zIndex || window.getComputedStyle(element).zIndex;
|
||||
return Number.parseInt(value, 10);
|
||||
}
|
||||
|
||||
function expectPromptOverlayAboveWorkflowWindow() {
|
||||
const workflowWindow = getWorkflowEditorFloatingOverlay();
|
||||
const promptOverlay = getPromptFullscreenOverlay();
|
||||
expect(workflowWindow).toBeInTheDocument();
|
||||
expect(promptOverlay).toBeInTheDocument();
|
||||
expect(zIndexOf(promptOverlay!)).toBeGreaterThan(zIndexOf(workflowWindow!));
|
||||
}
|
||||
|
||||
function defineElementMetric(element: Element, property: "clientWidth" | "scrollWidth", value: number) {
|
||||
Object.defineProperty(element, property, { configurable: true, value });
|
||||
}
|
||||
@@ -1395,6 +1412,53 @@ describe("WorkflowNodeEditor", () => {
|
||||
expect(screen.getByRole("button", { name: "Expand prompt editor" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stacks the fullscreen prompt editor above the workflow floating window on desktop", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
|
||||
await screen.findByText("Save");
|
||||
const workflowWindow = getWorkflowEditorFloatingOverlay();
|
||||
expect(workflowWindow).toBeInTheDocument();
|
||||
expect(zIndexOf(workflowWindow!)).toBeGreaterThan(10000);
|
||||
|
||||
fireEvent.click(await screen.findByTestId("wf-node-prompt"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Expand prompt editor" }));
|
||||
|
||||
expectPromptOverlayAboveWorkflowWindow();
|
||||
});
|
||||
|
||||
it("stacks the fullscreen prompt editor above the mobile workflow sheet for prompt nodes", async () => {
|
||||
mockWorkflowEditorViewport("mobile");
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Custom" }));
|
||||
fireEvent.click(await screen.findByTestId("wf-node-prompt"));
|
||||
|
||||
const inspector = await screen.findByTestId("wf-node-inspector");
|
||||
fireEvent.click(within(inspector).getByRole("button", { name: "Expand prompt editor" }));
|
||||
|
||||
expectPromptOverlayAboveWorkflowWindow();
|
||||
});
|
||||
|
||||
it("stacks the fullscreen prompt editor above the mobile workflow sheet for empty gate prompts", async () => {
|
||||
mockWorkflowEditorViewport("mobile");
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([def()]);
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} />);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "QA" }));
|
||||
fireEvent.click(await screen.findByTestId("wf-node-gate"));
|
||||
|
||||
const inspector = await screen.findByTestId("wf-node-inspector");
|
||||
fireEvent.click(within(inspector).getByRole("button", { name: "Expand prompt editor" }));
|
||||
|
||||
expect(within(getPromptFullscreenOverlay()!).getByLabelText("Prompt")).toHaveValue("");
|
||||
expectPromptOverlayAboveWorkflowWindow();
|
||||
});
|
||||
|
||||
it("collapses the fullscreen prompt editor on Escape", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
|
||||
|
||||
@@ -1571,6 +1635,7 @@ describe("WorkflowNodeEditor", () => {
|
||||
const fullscreenPromptEditor = getPromptFullscreenOverlay();
|
||||
expect(fullscreenPromptEditor).toBeInTheDocument();
|
||||
expect(fullscreenPromptEditor).toHaveClass("wf-prompt-editor--fullscreen");
|
||||
expectPromptOverlayAboveWorkflowWindow();
|
||||
|
||||
fireEvent.click(within(fullscreenPromptEditor!).getByRole("button", { name: "Collapse prompt editor" }));
|
||||
|
||||
@@ -1664,6 +1729,22 @@ describe("WorkflowNodeEditor — embedded presentation", () => {
|
||||
expect(document.body.querySelector(".floating-window__resize-handle")).toBeNull();
|
||||
});
|
||||
|
||||
it("opens the prompt fullscreen editor from embedded workflows without floating chrome", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([v2Def()]);
|
||||
|
||||
render(<WorkflowNodeEditor isOpen onClose={() => {}} addToast={() => {}} presentation="embedded" />);
|
||||
|
||||
await screen.findByText("Save");
|
||||
expect(getWorkflowEditorFloatingOverlay()).toBeNull();
|
||||
|
||||
fireEvent.click(await screen.findByTestId("wf-node-prompt"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Expand prompt editor" }));
|
||||
|
||||
const fullscreenPromptEditor = getPromptFullscreenOverlay();
|
||||
expect(fullscreenPromptEditor).toBeInTheDocument();
|
||||
expect(zIndexOf(fullscreenPromptEditor!)).toBeGreaterThan(10000);
|
||||
});
|
||||
|
||||
it("does not dismiss on Escape in embedded mode", async () => {
|
||||
const onClose = vi.fn();
|
||||
const { container } = render(
|
||||
|
||||
@@ -3,7 +3,7 @@ FNXC:FloatingWindow 2026-06-22-21:30:
|
||||
SHARED floating-utility z-index stack. This is the ONE source of z-index for utility floating modals in the dashboard (FloatingWindow utility callers, the right-dock pop-out, the floating terminal, the floating New Task dialog) so they interoperate in a SINGLE stack instead of each type owning a private counter. Utility windows claim `nextFloatingZ()` on mount/open and again on every panel pointerdown/focus, so the most-recently-interacted utility window is always on top REGARDLESS of type.
|
||||
|
||||
FNXC:FloatingWindow 2026-06-22-22:30:
|
||||
Base band sits at 10100+ — ABOVE the page overlay/popover band (log viewer, workflow-editor modal, selection popover, fullscreen overlay at z 10000-10001) so a utility floating window the user is dragging is never painted over by those. Transient top-right toasts are bumped to 10500 (styles.css) so system feedback still shows above a dragged utility window. The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them. CRITICAL: every floating modal must be portaled to document.body so this shared z is compared in ONE root stacking context (an inline panel cannot beat siblings outside its own context no matter its z).
|
||||
Base band sits at 10100+ — ABOVE the page overlay/popover band (log viewer, workflow-editor modal, selection popover, static fullscreen fallbacks at z 10000-10001) so a utility floating window the user is dragging is never painted over by those. Transient top-right toasts are bumped to 10500 (styles.css) so system feedback still shows above a dragged utility window. The workflow prompt fullscreen overlay is itself a floating utility surface and claims `nextFloatingZ()` when opened, because a static z 10000 fallback is hidden by the workflow editor's full-screen mobile FloatingWindow sheet. The counter is module-level and intentionally monotonic: it only ever climbs, which is fine for a session-length dashboard. All floating overlays are `pointer-events: none` (click-through) so raising panels into this shared band never traps clicks on the page behind them. CRITICAL: every floating modal must be portaled to document.body so this shared z is compared in ONE root stacking context (an inline panel cannot beat siblings outside its own context no matter its z).
|
||||
|
||||
FNXC:TaskPopupLayer 2026-07-04-18:36:
|
||||
Task-detail popups are ordinary board/task-detail surfaces, not global utilities. Keep their focus stack in a lower board-layer band so task popups can raise among themselves without covering terminal, right-dock expand, Quick Chat, file browser, workflow editor, or other utility windows that intentionally use the 10100+ band.
|
||||
|
||||
Reference in New Issue
Block a user