FN-6846: move header search after workflow slot

Move the desktop and tablet search toggle to follow the workflow portal slot.\n\n- Reorder the non-mobile header search button after the workflow portal mount point.\n- Preserve the mobile search trigger position and behavior.\n- Add header tests covering empty, populated, tablet, and mobile slot behavior.\n\nFiles changed:\n packages/dashboard/app/components/Header.tsx       | 29 ++++++++-------\n .../app/components/__tests__/Header.test.tsx       | 42 ++++++++++++++++++++--\n 2 files changed, 56 insertions(+), 15 deletions(-)

Fusion-Task-Id: FN-6846

Fusion-Task-Lineage: 294100b1-76ae-4c46-b4b3-e4c83e4a55f8
This commit is contained in:
gsxdsm
2026-06-21 11:35:17 -07:00
parent c9146e15b2
commit 1c6fe544e2
2 changed files with 56 additions and 15 deletions

View File

@@ -928,19 +928,6 @@ export function Header({
</button>
)}
{/* Desktop/Tablet Search Toggle - show icon when search is available but hidden */}
{canShowNonMobileSearchToggle && (
<button
className="btn-icon"
onClick={handleNonMobileSearchToggle}
title={t("header.openSearch", "Open search")}
aria-label={t("header.openSearch", "Open search")}
data-testid="desktop-header-search-btn"
>
<Search size={16} />
</button>
)}
{/* Usage button on mobile when mobile bottom nav is active */}
{isMobile && hideFullNav && onOpenUsage && (
<button
@@ -961,6 +948,22 @@ export function Header({
/>
)}
{/**
* FNXC:Header 2026-06-21-00:00:
* Desktop and tablet header search must render after the workflow portal slot so a populated WorkflowSwitcher appears left of the search icon while preserving the mobile search trigger's existing position and behavior.
*/}
{canShowNonMobileSearchToggle && (
<button
className="btn-icon"
onClick={handleNonMobileSearchToggle}
title={t("header.openSearch", "Open search")}
aria-label={t("header.openSearch", "Open search")}
data-testid="desktop-header-search-btn"
>
<Search size={16} />
</button>
)}
{/* View Toggle - always inline, even on mobile */}
{!hideFullNav && !hideHeaderViewNav && onChangeView && (
<div className="view-toggle">

View File

@@ -1076,6 +1076,39 @@ describe("Header", () => {
expect(screen.getByTestId("desktop-header-search-btn")).toBeDefined();
});
it("renders the desktop search toggle after the empty workflow portal slot", () => {
renderHeader({ onSearchChange: vi.fn(), onChangeView: noop, view: "board", leftSidebarNavActive: true }, "desktop");
const workflowSlot = screen.getByTestId("header-workflow-slot");
const searchToggle = screen.getByTestId("desktop-header-search-btn");
expect(screen.getAllByTestId("desktop-header-search-btn")).toHaveLength(1);
expect(workflowSlot.compareDocumentPosition(searchToggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it("keeps the desktop search toggle after a populated workflow portal slot", () => {
renderHeader({ onSearchChange: vi.fn(), onChangeView: noop, view: "board", leftSidebarNavActive: true }, "desktop");
const workflowSlot = screen.getByTestId("header-workflow-slot");
const workflowSwitcher = document.createElement("button");
workflowSwitcher.type = "button";
workflowSwitcher.dataset.testid = "mock-workflow-switcher";
workflowSwitcher.textContent = "Coding workflow";
workflowSlot.appendChild(workflowSwitcher);
const searchToggle = screen.getByTestId("desktop-header-search-btn");
expect(screen.getAllByTestId("desktop-header-search-btn")).toHaveLength(1);
expect(workflowSlot.compareDocumentPosition(searchToggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(workflowSwitcher.compareDocumentPosition(searchToggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it("keeps the tablet search toggle after the workflow portal slot", () => {
renderHeader({ onSearchChange: vi.fn(), onChangeView: noop, view: "board", leftSidebarNavActive: true }, "tablet");
const workflowSlot = screen.getByTestId("header-workflow-slot");
const searchToggle = screen.getByTestId("desktop-header-search-btn");
expect(screen.getAllByTestId("desktop-header-search-btn")).toHaveLength(1);
expect(workflowSlot.compareDocumentPosition(searchToggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it("does not render search toggle when view is 'agents'", () => {
renderHeader({ onSearchChange: vi.fn(), view: "agents" });
expect(screen.queryByTestId("desktop-header-search-btn")).toBeNull();
@@ -1371,9 +1404,14 @@ describe("Header", () => {
it("can open mobile search when mobileNavEnabled is true", () => {
renderHeader({ view: "board", searchQuery: "", onSearchChange: vi.fn(), onChangeView: noop }, "mobile");
// Should show the trigger button
expect(screen.getByTestId("mobile-header-search-btn")).toBeDefined();
// Expanded search should not be visible initially
const mobileSearchTrigger = screen.getByTestId("mobile-header-search-btn");
expect(mobileSearchTrigger).toBeDefined();
expect(screen.queryByTestId("header-workflow-slot")).toBeNull();
expect(screen.queryByTestId("desktop-header-search-btn")).toBeNull();
// Expanded search should not be visible initially, then opens from the unchanged mobile trigger.
expect(screen.queryByPlaceholderText("Search tasks...")).toBeNull();
fireEvent.click(mobileSearchTrigger);
expect(screen.getByPlaceholderText("Search tasks...")).toBeDefined();
});
it("closes mobile search and clears query when close button clicked with mobileNavEnabled", () => {