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:
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
/*
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user