FN-8678: keep mailbox task links in dashboard
Keep valid dashboard task links from mailbox markdown in the current application surface. - Route same-origin task deep links through the shared task-opening handler. - Preserve normal links, modified-click behavior, and desktop/mobile task-detail surfaces. - Cover markdown task navigation in content, modal, and mailbox-view tests. - Add a patch changeset for the mailbox navigation fix. Files changed: .changeset/mailbox-task-links-stay-in-app.md | 7 ++ .../app/components/MailboxMessageContent.tsx | 106 +++++++++++++++++---- packages/dashboard/app/components/MailboxModal.tsx | 2 + packages/dashboard/app/components/MailboxView.tsx | 2 + .../__tests__/MailboxMessageContent.test.tsx | 81 +++++++++++++++- .../app/components/__tests__/MailboxModal.test.tsx | 29 +++++- .../app/components/__tests__/MailboxView.test.tsx | 27 +++++- 7 files changed, 233 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-8678 Fusion-Task-Lineage: c906f173-f78d-4b5a-a0ce-5294cfddcada Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/mailbox-task-links-stay-in-app.md
Normal file
7
.changeset/mailbox-task-links-stay-in-app.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Open task links from mail in the active dashboard view.
|
||||||
|
category: fix
|
||||||
|
dev: Mail markdown task deep links now use the shared task-detail handler.
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { memo } from "react";
|
import { createContext, memo, useContext, useMemo, type ComponentPropsWithoutRef } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import type { PluggableList } from "unified";
|
import type { PluggableList } from "unified";
|
||||||
|
import { isMobileViewport } from "../hooks/useViewportMode";
|
||||||
import { linkifyReactChildren } from "../utils/filePathLinkify";
|
import { linkifyReactChildren } from "../utils/filePathLinkify";
|
||||||
import { sharedRehypePlugins, createMermaidCodeComponent } from "./markdownPipeline";
|
import { sharedRehypePlugins, createMermaidCodeComponent } from "./markdownPipeline";
|
||||||
|
|
||||||
@@ -30,14 +31,74 @@ const mailboxMarkdownComponents: Components = {
|
|||||||
// Code-block override: fenced ```mermaid renders as a diagram; all other code
|
// Code-block override: fenced ```mermaid renders as a diagram; all other code
|
||||||
// keeps default rendering. See createMermaidCodeComponent in markdownPipeline.
|
// keeps default rendering. See createMermaidCodeComponent in markdownPipeline.
|
||||||
code: createMermaidCodeComponent("mailbox-mermaid-diagram"),
|
code: createMermaidCodeComponent("mailbox-mermaid-diagram"),
|
||||||
// Open links in a new tab. Sanitize strips javascript: URLs and event handlers
|
};
|
||||||
// before this runs, so href is safe.
|
|
||||||
a: ({ children, ...props }) => (
|
const TASK_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]*-\d+$/;
|
||||||
<a {...props} target="_blank" rel="noopener noreferrer">
|
const MailboxTaskLinkContext = createContext<((taskId: string) => void) | undefined>(undefined);
|
||||||
|
|
||||||
|
type MailboxMarkdownAnchorProps = ComponentPropsWithoutRef<"a">;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a task id only for same-origin dashboard deep links with a valid task parameter.
|
||||||
|
*
|
||||||
|
* FNXC:MailboxTaskLinks 2026-08-01-07:20:
|
||||||
|
* Mail task links must open in the existing tab; on non-mobile viewports they open the
|
||||||
|
* task detail modal through the shared onOpenTask handler instead of booting another dashboard tab.
|
||||||
|
*/
|
||||||
|
export function parseInAppTaskHref(href: string | undefined): string | null {
|
||||||
|
if (!href || typeof window === "undefined") return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(href, window.location.origin);
|
||||||
|
const taskId = url.searchParams.get("task");
|
||||||
|
return url.origin === window.location.origin && taskId && TASK_ID_PATTERN.test(taskId)
|
||||||
|
? taskId
|
||||||
|
: null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MailboxMarkdownAnchor({ children, ...props }: MailboxMarkdownAnchorProps) {
|
||||||
|
const onOpenTask = useContext(MailboxTaskLinkContext);
|
||||||
|
const taskId = parseInAppTaskHref(props.href);
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
/*
|
||||||
|
FNXC:MailboxTaskLinks 2026-08-01-07:34:
|
||||||
|
Only verified same-origin task deep links stay in the dashboard. Markdown sanitization makes all
|
||||||
|
remaining hrefs safe, while external and non-task links retain their established new-tab behavior.
|
||||||
|
*/
|
||||||
|
return <a {...props} target="_blank" rel="noopener noreferrer">{children}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onOpenTask) return <a {...props}>{children}</a>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
{...props}
|
||||||
|
data-testid="mailbox-task-link"
|
||||||
|
onClick={(event) => {
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
/*
|
||||||
|
FNXC:MailboxTaskLinks 2026-08-01-07:20:
|
||||||
|
Mail task links retain their href for copy and modified-click behavior, but ordinary clicks
|
||||||
|
stay in the existing tab. Both viewport paths call the shared handler: desktop opens the task
|
||||||
|
detail modal and mobile opens its existing full-screen task sheet.
|
||||||
|
*/
|
||||||
|
if (isMobileViewport()) {
|
||||||
|
onOpenTask(taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpenTask(taskId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
),
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const remarkPlugins: PluggableList = [remarkGfm];
|
const remarkPlugins: PluggableList = [remarkGfm];
|
||||||
|
|
||||||
@@ -48,6 +109,12 @@ interface MailboxMessageContentProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
/** Optional data-testid for test selectors. */
|
/** Optional data-testid for test selectors. */
|
||||||
testId?: string;
|
testId?: string;
|
||||||
|
/**
|
||||||
|
* FNXC:MailboxTaskLinks 2026-08-01-07:34:
|
||||||
|
* The supplied dashboard handler preserves existing-tab task navigation and selects the appropriate
|
||||||
|
* task-detail surface for the current viewport.
|
||||||
|
*/
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,19 +132,26 @@ export const MailboxMessageContent = memo(function MailboxMessageContent({
|
|||||||
content,
|
content,
|
||||||
className,
|
className,
|
||||||
testId,
|
testId,
|
||||||
|
onOpenTask,
|
||||||
}: MailboxMessageContentProps) {
|
}: MailboxMessageContentProps) {
|
||||||
|
const markdownComponents = useMemo(
|
||||||
|
() => ({ ...mailboxMarkdownComponents, a: MailboxMarkdownAnchor }),
|
||||||
|
[onOpenTask],
|
||||||
|
);
|
||||||
const wrapperClass = className
|
const wrapperClass = className
|
||||||
? `mailbox-markdown ${className}`
|
? `mailbox-markdown ${className}`
|
||||||
: "mailbox-markdown";
|
: "mailbox-markdown";
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClass} data-testid={testId}>
|
<MailboxTaskLinkContext.Provider value={onOpenTask}>
|
||||||
<ReactMarkdown
|
<div className={wrapperClass} data-testid={testId}>
|
||||||
remarkPlugins={remarkPlugins}
|
<ReactMarkdown
|
||||||
rehypePlugins={sharedRehypePlugins}
|
remarkPlugins={remarkPlugins}
|
||||||
components={mailboxMarkdownComponents}
|
rehypePlugins={sharedRehypePlugins}
|
||||||
>
|
components={markdownComponents}
|
||||||
{content}
|
>
|
||||||
</ReactMarkdown>
|
{content}
|
||||||
</div>
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</MailboxTaskLinkContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -946,6 +946,7 @@ export function MailboxModal({
|
|||||||
<MailboxMessageContent
|
<MailboxMessageContent
|
||||||
content={msg.content}
|
content={msg.content}
|
||||||
className="mailbox-conversation-msg-body"
|
className="mailbox-conversation-msg-body"
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
<MailboxRelatedWorkLink
|
<MailboxRelatedWorkLink
|
||||||
metadata={msg.metadata}
|
metadata={msg.metadata}
|
||||||
@@ -986,6 +987,7 @@ export function MailboxModal({
|
|||||||
content={selectedMessage.content}
|
content={selectedMessage.content}
|
||||||
className="mailbox-message-body"
|
className="mailbox-message-body"
|
||||||
testId="mailbox-message-body"
|
testId="mailbox-message-body"
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
<MailboxRelatedWorkLink
|
<MailboxRelatedWorkLink
|
||||||
metadata={selectedMessage.metadata}
|
metadata={selectedMessage.metadata}
|
||||||
|
|||||||
@@ -961,6 +961,7 @@ export function MailboxView({
|
|||||||
<MailboxMessageContent
|
<MailboxMessageContent
|
||||||
content={msg.content}
|
content={msg.content}
|
||||||
className="mailbox-conversation-msg-body"
|
className="mailbox-conversation-msg-body"
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
<MailboxRelatedWorkLink
|
<MailboxRelatedWorkLink
|
||||||
metadata={msg.metadata}
|
metadata={msg.metadata}
|
||||||
@@ -995,6 +996,7 @@ export function MailboxView({
|
|||||||
content={selectedMessage.content}
|
content={selectedMessage.content}
|
||||||
className="mailbox-message-body"
|
className="mailbox-message-body"
|
||||||
testId="mailbox-message-body"
|
testId="mailbox-message-body"
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
<MailboxRelatedWorkLink
|
<MailboxRelatedWorkLink
|
||||||
metadata={selectedMessage.metadata}
|
metadata={selectedMessage.metadata}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
||||||
import { render, cleanup, screen, waitFor } from "@testing-library/react";
|
import { render, cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { FileBrowserProvider } from "../../context/FileBrowserContext";
|
import { FileBrowserProvider } from "../../context/FileBrowserContext";
|
||||||
import { MailboxMessageContent } from "../MailboxMessageContent";
|
import { MailboxMessageContent, parseInAppTaskHref } from "../MailboxMessageContent";
|
||||||
|
|
||||||
const mailboxModalCss = readFileSync(resolve(__dirname, "../MailboxModal.css"), "utf8");
|
const mailboxModalCss = readFileSync(resolve(__dirname, "../MailboxModal.css"), "utf8");
|
||||||
|
|
||||||
@@ -18,8 +18,17 @@ vi.mock("mermaid", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function stubMobileViewport(matches: boolean) {
|
||||||
|
vi.stubGlobal("matchMedia", vi.fn().mockImplementation(() => ({ matches })));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stubMobileViewport(false);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("MailboxMessageContent", () => {
|
describe("MailboxMessageContent", () => {
|
||||||
@@ -95,6 +104,72 @@ describe("MailboxMessageContent", () => {
|
|||||||
expect(container.querySelector("a.file-path-link")).toBeNull();
|
expect(container.querySelector("a.file-path-link")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("in-app task links", () => {
|
||||||
|
it("parses only same-origin hrefs with valid dashboard task ids", () => {
|
||||||
|
expect(parseInAppTaskHref("/?task=FN-1234")).toBe("FN-1234");
|
||||||
|
expect(parseInAppTaskHref("/?project=project-1&task=KB-002")).toBe("KB-002");
|
||||||
|
expect(parseInAppTaskHref(`${window.location.origin}/?task=FN-1234`)).toBe("FN-1234");
|
||||||
|
expect(parseInAppTaskHref("https://example.com/?task=FN-1234")).toBeNull();
|
||||||
|
expect(parseInAppTaskHref("mailto:operator@example.com?task=FN-1234")).toBeNull();
|
||||||
|
expect(parseInAppTaskHref("/?task=invalid")).toBeNull();
|
||||||
|
expect(parseInAppTaskHref("/?project=project-1")).toBeNull();
|
||||||
|
expect(parseInAppTaskHref("http://[invalid")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens a desktop task link through onOpenTask without navigating", async () => {
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const recordDefaultPrevention = vi.fn((event: MouseEvent) => event.defaultPrevented);
|
||||||
|
document.addEventListener("click", recordDefaultPrevention);
|
||||||
|
render(<MailboxMessageContent content="See [FN-1234](/?task=FN-1234)." onOpenTask={onOpenTask} />);
|
||||||
|
|
||||||
|
const link = screen.getByTestId("mailbox-task-link");
|
||||||
|
expect(link).not.toHaveAttribute("target");
|
||||||
|
expect(link).not.toHaveAttribute("rel");
|
||||||
|
await user.click(link);
|
||||||
|
document.removeEventListener("click", recordDefaultPrevention);
|
||||||
|
|
||||||
|
expect(onOpenTask).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-1234");
|
||||||
|
expect(recordDefaultPrevention).toHaveBeenCalledWith(expect.objectContaining({ defaultPrevented: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps mobile task links in the existing tab through onOpenTask", async () => {
|
||||||
|
stubMobileViewport(true);
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<MailboxMessageContent content="See [FN-1234](/?task=FN-1234)." onOpenTask={onOpenTask} />);
|
||||||
|
|
||||||
|
const link = screen.getByTestId("mailbox-task-link");
|
||||||
|
expect(link).not.toHaveAttribute("target", "_blank");
|
||||||
|
await user.click(link);
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-1234");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps task links as ordinary same-tab anchors without onOpenTask", () => {
|
||||||
|
render(<MailboxMessageContent content="See [FN-1234](/?task=FN-1234)." />);
|
||||||
|
const link = screen.getByRole("link", { name: "FN-1234" });
|
||||||
|
expect(link).not.toHaveAttribute("target");
|
||||||
|
expect(link).not.toHaveAttribute("data-testid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps external and same-origin non-task links in new tabs", () => {
|
||||||
|
render(<MailboxMessageContent content="[external](https://example.com) [dashboard](/settings)" />);
|
||||||
|
for (const link of screen.getAllByRole("link")) {
|
||||||
|
expect(link).toHaveAttribute("target", "_blank");
|
||||||
|
expect(link).toHaveAttribute("rel", "noopener noreferrer");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves modified-click behavior for task links", () => {
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
|
render(<MailboxMessageContent content="See [FN-1234](/?task=FN-1234)." onOpenTask={onOpenTask} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-task-link"), { ctrlKey: true });
|
||||||
|
expect(onOpenTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("defines token-only mailbox markdown anchor styles", () => {
|
it("defines token-only mailbox markdown anchor styles", () => {
|
||||||
const anchorRule = mailboxModalCss.match(/\.mailbox-markdown a\s*\{([^}]*)\}/)?.[1];
|
const anchorRule = mailboxModalCss.match(/\.mailbox-markdown a\s*\{([^}]*)\}/)?.[1];
|
||||||
expect(mailboxModalCss).toMatch(/\.mailbox-markdown a\b/);
|
expect(mailboxModalCss).toMatch(/\.mailbox-markdown a\b/);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { loadAllAppCss } from "../../test/cssFixture";
|
import { loadAllAppCss } from "../../test/cssFixture";
|
||||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor, act, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { MailboxModal } from "../MailboxModal";
|
import { MailboxModal } from "../MailboxModal";
|
||||||
import * as apiModule from "../../api";
|
import * as apiModule from "../../api";
|
||||||
import * as mobileKeyboardModule from "../../hooks/useMobileKeyboard";
|
import * as mobileKeyboardModule from "../../hooks/useMobileKeyboard";
|
||||||
@@ -138,6 +139,7 @@ const defaultProps = {
|
|||||||
describe("MailboxModal", () => {
|
describe("MailboxModal", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -436,6 +438,31 @@ describe("MailboxModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens markdown task links from the selected mobile mail detail in the existing tab", async () => {
|
||||||
|
vi.stubGlobal("matchMedia", vi.fn().mockImplementation(() => ({ matches: true })));
|
||||||
|
const taskMessage: Message = {
|
||||||
|
...mockMessage,
|
||||||
|
id: "msg-modal-markdown-task-link",
|
||||||
|
content: "See [FN-1234](/?task=FN-1234) and [external](https://example.com).",
|
||||||
|
};
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockFetchInbox.mockResolvedValue({ messages: [taskMessage], total: 1, unreadCount: 1 });
|
||||||
|
mockFetchConversation.mockResolvedValue([taskMessage]);
|
||||||
|
mockMarkMessageRead.mockResolvedValue({ ...taskMessage, read: true });
|
||||||
|
|
||||||
|
render(<MailboxModal {...defaultProps} onOpenTask={onOpenTask} />);
|
||||||
|
await user.click(await screen.findByTestId("mailbox-item-msg-modal-markdown-task-link"));
|
||||||
|
|
||||||
|
const detail = await screen.findByTestId("mailbox-message-body");
|
||||||
|
const taskLink = within(detail).getByTestId("mailbox-task-link");
|
||||||
|
expect(taskLink).not.toHaveAttribute("target", "_blank");
|
||||||
|
await user.click(taskLink);
|
||||||
|
expect(onOpenTask).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-1234");
|
||||||
|
expect(within(detail).getByRole("link", { name: "external" })).toHaveAttribute("target", "_blank");
|
||||||
|
});
|
||||||
|
|
||||||
it("opens task-only and planning-clarification related work from modal detail", async () => {
|
it("opens task-only and planning-clarification related work from modal detail", async () => {
|
||||||
const taskMessage: Message = {
|
const taskMessage: Message = {
|
||||||
...mockMessage,
|
...mockMessage,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { useEffect, type ReactNode } from "react";
|
import { useEffect, type ReactNode } from "react";
|
||||||
import { loadAllAppCss } from "../../test/cssFixture";
|
import { loadAllAppCss } from "../../test/cssFixture";
|
||||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor, act, within } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import { MailboxView } from "../MailboxView";
|
import { MailboxView } from "../MailboxView";
|
||||||
import { NavigationHistoryProvider, useNavigationHistory, type UseNavigationHistoryResult } from "../../hooks/useNavigationHistory";
|
import { NavigationHistoryProvider, useNavigationHistory, type UseNavigationHistoryResult } from "../../hooks/useNavigationHistory";
|
||||||
import * as apiModule from "../../api";
|
import * as apiModule from "../../api";
|
||||||
@@ -833,6 +834,30 @@ describe("MailboxView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("opens markdown task links from the selected mail detail in the existing desktop tab", async () => {
|
||||||
|
const taskMessage: Message = {
|
||||||
|
...mockMessage,
|
||||||
|
id: "msg-markdown-task-link",
|
||||||
|
content: "See [FN-1234](/?project=project-1&task=FN-1234) and [external](https://example.com).",
|
||||||
|
};
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([taskMessage], 1));
|
||||||
|
mockFetchConversation.mockResolvedValue([taskMessage]);
|
||||||
|
mockMarkMessageRead.mockResolvedValue({ ...taskMessage, read: true });
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} onOpenTask={onOpenTask} />);
|
||||||
|
await user.click(await screen.findByTestId("mailbox-item-msg-markdown-task-link"));
|
||||||
|
|
||||||
|
const detail = await screen.findByTestId("mailbox-message-body");
|
||||||
|
const taskLink = within(detail).getByTestId("mailbox-task-link");
|
||||||
|
expect(taskLink).not.toHaveAttribute("target", "_blank");
|
||||||
|
await user.click(taskLink);
|
||||||
|
expect(onOpenTask).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-1234");
|
||||||
|
expect(within(detail).getByRole("link", { name: "external" })).toHaveAttribute("target", "_blank");
|
||||||
|
});
|
||||||
|
|
||||||
it("opens task-only and planning-clarification related work from mailbox detail", async () => {
|
it("opens task-only and planning-clarification related work from mailbox detail", async () => {
|
||||||
const taskMessage: Message = {
|
const taskMessage: Message = {
|
||||||
...mockMessage,
|
...mockMessage,
|
||||||
|
|||||||
Reference in New Issue
Block a user