FN-6765: fix mobile nav mailbox duplication

Keep Mailbox as a single top-level entry while Todos stays in overflow navigation.

- Remove the duplicate Mailbox action from the mobile More sheet while preserving its primary-tab badges.
- Cover desktop, tablet, and mobile navigation placement expectations for Todos and Mailbox.
- Update dashboard docs to describe compact Header and mobile bottom-nav mailbox indicators.

Files changed:
 docs/dashboard-guide.md                            |  2 +-
 .../mobile-feature-access-regression.test.tsx      |  3 +-
 packages/dashboard/app/components/MobileNavBar.tsx | 22 +++---------
 .../app/components/__tests__/Header.test.tsx       | 19 +++++++----
 .../app/components/__tests__/MobileNavBar.test.tsx | 39 ++++++++++++++++++----
 5 files changed, 52 insertions(+), 33 deletions(-)

Fusion-Task-Id: FN-6765
Fusion-Task-Lineage: e32f271e-7dab-43e0-b824-da598641aa77
This commit is contained in:
gsxdsm
2026-06-20 01:34:51 -07:00
parent 10208ecd63
commit 7559883940
5 changed files with 52 additions and 33 deletions

View File

@@ -334,7 +334,7 @@ Mailbox view shows inbox/outbox communication threads and unread state.
- reply rows in the mailbox modal can expand inline to show the replied-to message context for easier thread reading
- mailbox now includes an **Approvals** tab with pending and history filters (`approved` / `denied` / `completed`), approval detail context, and inline approve/deny actions for pending requests
- in the **Agents** tab, the agent selector now includes **All agents**, which shows one combined agent-to-agent stream (with sender + recipient labels); selecting a specific agent still shows Inbox/Outbox subtabs
- mailbox entry points now show unread/pending indicators: the desktop Header mailbox toggle shows a pending-approval dot first or an unread dot when unread mail exists without pending approvals, while Header overflow + Mobile mailbox entry points continue to surface mailbox badges/dots
- mailbox entry points now show unread/pending indicators: the desktop/tablet Header mailbox toggle shows a pending-approval dot first or an unread dot when unread mail exists without pending approvals, the mobile bottom-nav Mailbox tab carries the mobile badges/dots, and the compact Header actions overflow keeps a Mailbox entry only when the mobile bottom nav is disabled
- approval lifecycle SSE events (`approval:requested`, `approval:updated`, `approval:decided`) trigger mailbox approvals refresh without manual reload
- when a task newly enters `awaiting-approval`, the app shows a persistent approval banner above project content with an **Open Mailbox** CTA; dismissals are remembered per approval item until that item advances or a different one arrives
- when a task first transitions into `done`, the dashboard shows a one-time **Enjoying Fusion?** GitHub star prompt in the project view; clicking **Star on GitHub** or dismissing the card marks it shown in browser `localStorage`, so it does not reappear on reload or later task completions

View File

@@ -170,7 +170,8 @@ describe("Mobile Feature Access Regression Guard", () => {
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getByTestId("mobile-more-item-mailbox")).toBeDefined();
expect(screen.getByTestId("mobile-nav-tab-mailbox")).toBeDefined();
expect(screen.queryByTestId("mobile-more-item-mailbox")).toBeNull();
expect(screen.getByTestId("mobile-more-item-git")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-terminal")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-files")).toBeDefined();

View File

@@ -138,7 +138,6 @@ export function MobileNavBar({
keyboardOpen = false,
onOpenSettings,
onOpenActivityLog,
onOpenMailbox,
mailboxUnreadCount = 0,
mailboxPendingApprovalCount = 0,
chatHasUnreadResponse = false,
@@ -375,6 +374,11 @@ export function MobileNavBar({
</button>
{/*
FNXC:Navigation 2026-06-19-12:30:
Mailbox is a top-level mobile tab only and must not be duplicated in the three-dot More sheet; Todos lives only in the three-dot overflow/More menu, never the main tab list.
Keep unread and pending-approval indicators on this surviving Mailbox tab so removing the More-sheet duplicate does not hide mailbox state.
*/}
<button
type="button"
className={`mobile-nav-tab${view === "mailbox" ? " mobile-nav-tab--active" : ""}`}
@@ -470,22 +474,6 @@ export function MobileNavBar({
</div>
) : null}
<button
type="button"
className="mobile-more-item"
data-testid="mobile-more-item-mailbox"
onClick={() => handleMoreAction(onOpenMailbox)}
>
<Mail />
<span>{t("nav.mailbox", "Mailbox")}</span>
{mailboxUnreadCount > 0 && (
<span className="mobile-more-item-badge mobile-more-item-badge--unread">{formatCount(mailboxUnreadCount)}</span>
)}
{mailboxPendingApprovalCount > 0 && (
<span className="mobile-more-item-badge">{formatCount(mailboxPendingApprovalCount)}</span>
)}
</button>
<button
type="button"
className="mobile-more-item"

View File

@@ -540,14 +540,19 @@ describe("Header", () => {
});
describe("todos navigation", () => {
it("shows Todos only in More views on desktop when enabled", () => {
renderHeader({ onChangeView: noop, onOpenTodos: vi.fn(), todosEnabled: true }, "desktop");
expect(screen.queryByTestId("todos-toggle-btn")).toBeNull();
for (const tier of ["desktop", "tablet"] as const) {
it(`shows Todos only in More views and Mailbox only top-level on ${tier}`, () => {
renderHeader({ onChangeView: noop, onOpenTodos: vi.fn(), todosEnabled: true }, tier);
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getAllByText("Todos")).toHaveLength(1);
expect(screen.getByTestId("view-overflow-todos")).toBeInTheDocument();
});
expect(screen.queryByTestId("todos-toggle-btn")).toBeNull();
expect(screen.getByTitle("Mailbox view")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
expect(screen.getAllByText("Todos")).toHaveLength(1);
expect(screen.getByTestId("view-overflow-todos")).toBeInTheDocument();
expect(screen.queryByTestId("view-overflow-mailbox")).toBeNull();
});
}
it("does not show Todos entry in More views when disabled", () => {
renderHeader({ onChangeView: noop, onOpenTodos: vi.fn(), todosEnabled: false }, "desktop");

View File

@@ -121,6 +121,30 @@ describe("MobileNavBar", () => {
expect(onOpenTodos).toHaveBeenCalled();
});
it("Mailbox is a primary tab and is not duplicated in the More sheet", () => {
render(<MobileNavBar {...createDefaultProps()} mailboxUnreadCount={3} mailboxPendingApprovalCount={1} />);
expect(screen.getByTestId("mobile-nav-tab-mailbox")).toBeInTheDocument();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.queryByTestId("mobile-more-item-mailbox")).toBeNull();
});
it("Todos lives only in the More sheet, never a primary tab", () => {
render(
<MobileNavBar
{...createDefaultProps()}
onOpenTodos={vi.fn()}
experimentalFeatures={{ todoView: true }}
/>,
);
expect(screen.queryByTestId("mobile-nav-tab-todos")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getByTestId("mobile-more-item-todos")).toBeInTheDocument();
});
it("shows secrets in More and routes to secrets view", () => {
const props = createDefaultProps();
render(<MobileNavBar {...props} />);
@@ -304,15 +328,15 @@ describe("MobileNavBar", () => {
expect(badge?.textContent).toBe("5");
});
it("shows matching mailbox unread badge in the More sheet", () => {
it("keeps mailbox unread badge on the primary tab only", () => {
render(<MobileNavBar {...createDefaultProps()} mailboxUnreadCount={7} />);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
const tabBadge = screen.getByTestId("mobile-nav-tab-mailbox").querySelector(".mobile-nav-tab-badge");
expect(tabBadge).toBeDefined();
expect(tabBadge?.textContent).toBe("7");
const moreItemBadge = screen.getByTestId("mobile-more-item-mailbox").querySelector(".mobile-more-item-badge");
expect(moreItemBadge).toBeDefined();
expect(moreItemBadge?.className).toContain("mobile-more-item-badge--unread");
expect(moreItemBadge?.textContent).toBe("7");
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.queryByTestId("mobile-more-item-mailbox")).toBeNull();
});
it("tasks tab calls onChangeView with 'board' when coming from a non-tasks view", () => {
@@ -423,7 +447,8 @@ describe("MobileNavBar", () => {
render(<MobileNavBar {...createDefaultProps()} />);
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getByTestId("mobile-more-item-mailbox")).toBeDefined();
expect(screen.queryByTestId("mobile-more-item-mailbox")).toBeNull();
expect(screen.getByTestId("mobile-nav-tab-mailbox")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-activity")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-git")).toBeDefined();
expect(screen.getByTestId("mobile-more-item-terminal")).toBeDefined();