feat(dashboard): move Planning+Missions below Graph (before Agents); confirm dialog renders above floating modals

- LeftSidebarNav: Planning + Missions now sit directly after List/Graph and before Agents.
- ConfirmDialog: portal to document.body and claim the top of the shared floating z-stack on open, so the discard-changes confirm appears ABOVE the floating New Task window (and other floating modals) instead of behind it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-06-22 10:26:14 -07:00
parent d3da3cc301
commit 66925816bd
4 changed files with 54 additions and 36 deletions

View File

@@ -1,6 +1,8 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import type { ConfirmOptions } from "../hooks/useConfirm";
import { nextFloatingZ } from "./floatingWindowStack";
import "./ConfirmDialog.css";
export interface ConfirmDialogProps {
@@ -28,6 +30,16 @@ export function ConfirmDialog({
}: ConfirmDialogProps) {
const { t } = useTranslation("app");
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
/*
FNXC:Confirm 2026-06-23-01:30:
The confirm dialog (e.g. the "discard changes" prompt when cancelling New Task) MUST sit above the floating modal stack. Floating windows (New Task, pop-outs) live at the shared floating z-band (nextFloatingZ) and are portaled to document.body, so a confirm rendered inline at the page .modal-overlay z (~10000) paints BEHIND them. Portal the confirm to body and claim the TOP of the shared stack each time it opens so it always appears over whatever floating window triggered it.
*/
const [overlayZ, setOverlayZ] = useState<number | undefined>(undefined);
useEffect(() => {
if (isOpen) {
setOverlayZ(nextFloatingZ());
}
}, [isOpen]);
useEffect(() => {
if (!isOpen) {
@@ -51,8 +63,8 @@ export function ConfirmDialog({
return null;
}
return (
<div className="modal-overlay open confirm-dialog-overlay" onClick={onCancel}>
return createPortal(
<div className="modal-overlay open confirm-dialog-overlay" onClick={onCancel} style={overlayZ ? { zIndex: overlayZ } : undefined}>
<div
className="modal confirm-dialog"
onClick={(event) => event.stopPropagation()}
@@ -95,6 +107,7 @@ export function ConfirmDialog({
</button>
</div>
</div>
</div>
</div>,
document.body,
);
}

View File

@@ -299,6 +299,28 @@ export function LeftSidebarNav({
onSelect: () => onChangeView("list"),
},
...(graphPluginEntry ? [mapPluginEntry(graphPluginEntry)] : []),
/*
FNXC:Navigation 2026-06-23-01:30:
Planning and Missions sit directly below Graph and above Agents (moved up from after Memory) per user request, so the planning/mission destinations sit next to the structural Board/List/Graph group.
*/
{
id: "planning",
label: t("nav.planning", "Planning"),
view: "planning",
isActive: view === "planning",
icon: Lightbulb,
testId: "sidebar-nav-planning",
onSelect: () => onChangeView("planning"),
},
{
id: "missions",
label: t("nav.missions", "Missions"),
view: "missions",
isActive: view === "missions",
icon: Target,
testId: "sidebar-nav-missions",
onSelect: () => onChangeView("missions"),
},
...(showAgentsTab
? [
{
@@ -343,28 +365,6 @@ export function LeftSidebarNav({
...(experimentalFeatures?.memoryView
? [{ id: "memory", label: t("header.memoryView", "Memory"), view: "memory" as TaskView, isActive: view === "memory", icon: Brain, testId: "sidebar-nav-memory", onSelect: () => onChangeView("memory") }]
: []),
{
id: "planning",
/*
FNXC:Navigation 2026-06-21-00:00:
FN-6886 makes Planning Mode a first-class sidebar destination.
*/
label: t("nav.planning", "Planning"),
view: "planning",
isActive: view === "planning",
icon: Lightbulb,
testId: "sidebar-nav-planning",
onSelect: () => onChangeView("planning"),
},
{
id: "missions",
label: t("nav.missions", "Missions"),
view: "missions",
isActive: view === "missions",
icon: Target,
testId: "sidebar-nav-missions",
onSelect: () => onChangeView("missions"),
},
{
id: "documents",
/*

View File

@@ -65,7 +65,7 @@ describe("ConfirmDialog", () => {
it("calls onCancel when overlay clicked", () => {
const onCancel = vi.fn();
const { container } = render(
render(
<ConfirmDialog
isOpen={true}
options={{ title: "Discard", message: "Discard changes?" }}
@@ -74,7 +74,8 @@ describe("ConfirmDialog", () => {
/>,
);
const overlay = container.querySelector(".modal-overlay");
// FNXC: ConfirmDialog portals to document.body, so query from document (not the render container).
const overlay = document.querySelector(".modal-overlay");
expect(overlay).toBeTruthy();
fireEvent.click(overlay as Element);
expect(onCancel).toHaveBeenCalledTimes(1);
@@ -110,7 +111,7 @@ describe("ConfirmDialog", () => {
});
it("uses compact mobile override classes on overlay and dialog surface", () => {
const { container } = render(
render(
<ConfirmDialog
isOpen={true}
options={{ title: "Discard", message: "Discard changes?" }}
@@ -119,8 +120,9 @@ describe("ConfirmDialog", () => {
/>,
);
expect(container.querySelector(".confirm-dialog-overlay")).toBeTruthy();
expect(container.querySelector(".confirm-dialog.modal")).toBeTruthy();
// FNXC: portaled to document.body — query from document.
expect(document.querySelector(".confirm-dialog-overlay")).toBeTruthy();
expect(document.querySelector(".confirm-dialog.modal")).toBeTruthy();
});
it("does not render checkbox when checkboxLabel is omitted", () => {

View File

@@ -270,13 +270,13 @@ describe("LeftSidebarNav", () => {
"sidebar-nav-command-center",
"sidebar-nav-board",
"sidebar-nav-list",
"sidebar-nav-planning",
"sidebar-nav-missions",
"sidebar-nav-agents",
"sidebar-nav-chat",
"sidebar-nav-mailbox",
"sidebar-nav-skills",
"sidebar-nav-memory",
"sidebar-nav-planning",
"sidebar-nav-missions",
"sidebar-nav-documents",
"sidebar-nav-goals",
"sidebar-nav-automations",
@@ -290,11 +290,14 @@ describe("LeftSidebarNav", () => {
expect(orderedIndices).toEqual([...orderedIndices].sort((a, b) => a - b));
expect(orderedIndices.every((index) => index >= 0)).toBe(true);
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-command-center"))).toBeLessThan(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-agents")));
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-documents"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-missions")) + 1);
// Skills and Memory sit immediately after Mailbox and before Planning.
// FNXC:Navigation 2026-06-23-01:30: Planning + Missions now sit directly after List and before Agents; Documents (Artifacts) follows Memory.
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-planning"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-list")) + 1);
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-missions"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-planning")) + 1);
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-agents"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-missions")) + 1);
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-documents"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-memory")) + 1);
// Skills and Memory sit immediately after Mailbox.
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-skills"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-mailbox")) + 1);
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-memory"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-skills")) + 1);
expect(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-planning"))).toBe(primaryButtons.indexOf(screen.getByTestId("sidebar-nav-memory")) + 1);
const sidebar = screen.getByTestId("left-sidebar-nav");
const footer = screen.getByTestId("sidebar-nav-settings").closest(".left-sidebar-nav__footer");