FN-7924: Add View task link to artifact-registration mail notifications
Artifact-registration mail messages now expose the task that produced the artifact so users can jump straight to it from the mailbox. - MailboxArtifactAttachment renders a "View task" button when message.metadata.taskId is present and an onOpenTask handler is supplied, alongside the existing Open artifact affordance - MailboxModal and MailboxView thread taskId metadata and onOpenTask through to MailboxArtifactAttachment for both the message-list and detail-pane renders - MainContent wires MailboxView's onOpenTask to the shared fetchTaskDetail -> openDetailTask path, with a toast on failure, so mailbox reuses the existing task-detail flow - docs/dashboard-guide.md documents the new View task affordance for artifact notifications - adds a minor changeset for @runfusion/fusion describing the new mail notification behavior - extends MailboxArtifactAttachment and MailboxView tests to cover the new taskId/onOpenTask wiring Files changed: .changeset/fn-7924-artifact-mail-view-task-link.md | 7 ++++ docs/dashboard-guide.md | 2 +- .../app/components/MailboxArtifactAttachment.tsx | 20 ++++++++++ packages/dashboard/app/components/MailboxModal.css | 2 +- packages/dashboard/app/components/MailboxModal.tsx | 6 +++ packages/dashboard/app/components/MailboxView.tsx | 6 +++ .../__tests__/MailboxArtifactAttachment.test.tsx | 35 ++++++++++++++++- .../app/components/__tests__/MailboxView.test.tsx | 45 +++++++++++++++++++++- .../app/components/dashboard/MainContent.tsx | 6 +++ 9 files changed, 124 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-7924 Fusion-Task-Lineage: 800102e1-9025-40da-8130-9ab0e8acd747 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7924-artifact-mail-view-task-link.md
Normal file
7
.changeset/fn-7924-artifact-mail-view-task-link.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Artifact-registration mail notifications now include a "View task" link to open the producing task.
|
||||||
|
category: feature
|
||||||
|
dev: MailboxArtifactAttachment renders a metadata-driven View-task affordance (message.metadata.taskId + onOpenTask); MainContent wires MailboxView's onOpenTask via fetchTaskDetail -> openDetailTask.
|
||||||
@@ -627,7 +627,7 @@ Mailbox view shows inbox/outbox communication threads and unread state.
|
|||||||
- Inbox renders one row per message (no sender-based collapsing)
|
- Inbox renders one row per message (no sender-based collapsing)
|
||||||
- clicking a message in the Mail tab opens the task detail pane with full message content and conversation context
|
- clicking a message in the Mail tab opens the task detail pane with full message content and conversation context
|
||||||
- reply rows in the mailbox modal can expand inline to show the replied-to message context for easier thread reading
|
- reply rows in the mailbox modal can expand inline to show the replied-to message context for easier thread reading
|
||||||
- when an agent or dashboard chat session registers an artifact with `fn_artifact_register`, Fusion sends a best-effort `system` → user inbox message announcing the new artifact (for example, `New image artifact registered: <title>`) with metadata for `artifactId`, `artifactType`, `title`, optional `mimeType`, `authorId`, and optional `taskId`; notification delivery is informational and never blocks or rolls back the artifact registration. Artifact notifications are actionable in message detail views: image artifacts show an inline preview plus **Open artifact**, while video/audio/document/other artifacts show an **Open artifact** link to the managed media URL.
|
- when an agent or dashboard chat session registers an artifact with `fn_artifact_register`, Fusion sends a best-effort `system` → user inbox message announcing the new artifact (for example, `New image artifact registered: <title>`) with metadata for `artifactId`, `artifactType`, `title`, optional `mimeType`, `authorId`, and optional `taskId`; notification delivery is informational and never blocks or rolls back the artifact registration. Artifact notifications are actionable in message detail views: image artifacts show an inline preview plus **Open artifact**, while video/audio/document/other artifacts show an **Open artifact** link to the managed media URL. When `taskId` metadata is present, the same artifact block also shows **View task** so users can open the producing task detail directly from the mailbox.
|
||||||
- on first engine startup under Fusion `0.59.x`, each project receives one best-effort `system` inbox notice about the upcoming embedded-Postgres storage migration with the Discord help link; `metadata.kind = "postgres-migration-notice"` prevents duplicates across restarts.
|
- on first engine startup under Fusion `0.59.x`, each project receives one best-effort `system` inbox notice about the upcoming embedded-Postgres storage migration with the Discord help link; `metadata.kind = "postgres-migration-notice"` prevents duplicates across restarts.
|
||||||
- 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
|
- 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
|
||||||
- for approvals gated by an agent's permission policy (permanent agents and task-worker heartbeats), the Approvals detail pane renders the gated action's real payload — tool name, shell command line or structured arguments, and working directory when present — instead of only a generic "Agent gated action for `<tool>`" summary; a stateless heartbeat retrying the same gated command reuses the existing pending approval instead of creating a duplicate (FN-7609)
|
- for approvals gated by an agent's permission policy (permanent agents and task-worker heartbeats), the Approvals detail pane renders the gated action's real payload — tool name, shell command line or structured arguments, and working directory when present — instead of only a generic "Agent gated action for `<tool>`" summary; a stateless heartbeat retrying the same gated command reuses the existing pending approval instead of creating a duplicate (FN-7609)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface MailboxArtifactAttachmentProps {
|
|||||||
title?: unknown;
|
title?: unknown;
|
||||||
mimeType?: unknown;
|
mimeType?: unknown;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
|
taskId?: unknown;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readString(value: unknown): string | undefined {
|
function readString(value: unknown): string | undefined {
|
||||||
@@ -23,6 +25,9 @@ function readArtifactType(value: unknown): ArtifactType | "unknown" {
|
|||||||
/**
|
/**
|
||||||
* FNXC:ArtifactRegistry 2026-07-12-00:00:
|
* FNXC:ArtifactRegistry 2026-07-12-00:00:
|
||||||
* Artifact-registration mail messages must expose the artifact announced by message.metadata. Render image artifacts inline, keep every type reachable through artifactMediaUrl(projectId-aware), and render nothing when metadata has no artifactId so ordinary messages keep their exact layout.
|
* Artifact-registration mail messages must expose the artifact announced by message.metadata. Render image artifacts inline, keep every type reachable through artifactMediaUrl(projectId-aware), and render nothing when metadata has no artifactId so ordinary messages keep their exact layout.
|
||||||
|
*
|
||||||
|
* FNXC:ArtifactRegistry 2026-07-12-00:00:
|
||||||
|
* Artifact-registration mail messages must also expose the producing task when message.metadata.taskId is paired with an onOpenTask handler. Render no task affordance when either side is absent so artifact-only and ordinary messages do not gain empty shells.
|
||||||
*/
|
*/
|
||||||
export const MailboxArtifactAttachment = memo(function MailboxArtifactAttachment({
|
export const MailboxArtifactAttachment = memo(function MailboxArtifactAttachment({
|
||||||
artifactId,
|
artifactId,
|
||||||
@@ -30,11 +35,14 @@ export const MailboxArtifactAttachment = memo(function MailboxArtifactAttachment
|
|||||||
title,
|
title,
|
||||||
mimeType,
|
mimeType,
|
||||||
projectId,
|
projectId,
|
||||||
|
taskId,
|
||||||
|
onOpenTask,
|
||||||
}: MailboxArtifactAttachmentProps) {
|
}: MailboxArtifactAttachmentProps) {
|
||||||
const id = readString(artifactId);
|
const id = readString(artifactId);
|
||||||
const type = readArtifactType(artifactType);
|
const type = readArtifactType(artifactType);
|
||||||
const label = readString(title) ?? "artifact";
|
const label = readString(title) ?? "artifact";
|
||||||
const mediaMimeType = readString(mimeType);
|
const mediaMimeType = readString(mimeType);
|
||||||
|
const task = readString(taskId);
|
||||||
const [imageFailed, setImageFailed] = useState(false);
|
const [imageFailed, setImageFailed] = useState(false);
|
||||||
const mediaUrl = useMemo(() => id ? artifactMediaUrl(id, projectId) : "", [id, projectId]);
|
const mediaUrl = useMemo(() => id ? artifactMediaUrl(id, projectId) : "", [id, projectId]);
|
||||||
|
|
||||||
@@ -51,6 +59,17 @@ export const MailboxArtifactAttachment = memo(function MailboxArtifactAttachment
|
|||||||
Open artifact
|
Open artifact
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
const taskLink = task && onOpenTask ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="mailbox-artifact-attachment__link btn"
|
||||||
|
aria-label={`View task: ${task}`}
|
||||||
|
data-testid="mailbox-artifact-view-task"
|
||||||
|
onClick={() => onOpenTask(task)}
|
||||||
|
>
|
||||||
|
View task
|
||||||
|
</button>
|
||||||
|
) : null;
|
||||||
|
|
||||||
let preview: ReactNode = null;
|
let preview: ReactNode = null;
|
||||||
if (type === "image" && !imageFailed) {
|
if (type === "image" && !imageFailed) {
|
||||||
@@ -97,6 +116,7 @@ export const MailboxArtifactAttachment = memo(function MailboxArtifactAttachment
|
|||||||
{preview}
|
{preview}
|
||||||
<div className="mailbox-artifact-attachment__actions">
|
<div className="mailbox-artifact-attachment__actions">
|
||||||
{openLink}
|
{openLink}
|
||||||
|
{taskLink}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -323,7 +323,7 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:ArtifactRegistry 2026-07-12-00:00:
|
FNXC:ArtifactRegistry 2026-07-12-00:00:
|
||||||
Artifact-registration mail messages now render metadata-driven media affordances directly below the markdown body. The block must share mailbox spacing/radius tokens and collapse entirely for messages without artifactId metadata.
|
Artifact-registration mail messages now render metadata-driven media and producing-task affordances directly below the markdown body. The Open artifact link and View task button share the same mailbox action styling, spacing/radius tokens, and collapse entirely for messages without artifactId metadata.
|
||||||
*/
|
*/
|
||||||
.mailbox-artifact-attachment {
|
.mailbox-artifact-attachment {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ interface MailboxModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast?: (msg: string, type?: "success" | "error") => void;
|
addToast?: (msg: string, type?: "success" | "error") => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
agents?: Agent[];
|
agents?: Agent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +170,7 @@ export function MailboxModal({
|
|||||||
onClose,
|
onClose,
|
||||||
projectId,
|
projectId,
|
||||||
addToast,
|
addToast,
|
||||||
|
onOpenTask,
|
||||||
agents = [],
|
agents = [],
|
||||||
}: MailboxModalProps) {
|
}: MailboxModalProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -895,6 +897,8 @@ export function MailboxModal({
|
|||||||
title={msg.metadata?.title}
|
title={msg.metadata?.title}
|
||||||
mimeType={msg.metadata?.mimeType}
|
mimeType={msg.metadata?.mimeType}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
taskId={msg.metadata?.taskId}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -924,6 +928,8 @@ export function MailboxModal({
|
|||||||
title={selectedMessage.metadata?.title}
|
title={selectedMessage.metadata?.title}
|
||||||
mimeType={selectedMessage.metadata?.mimeType}
|
mimeType={selectedMessage.metadata?.mimeType}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
taskId={selectedMessage.metadata?.taskId}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ type MailboxTab = "inbox" | "outbox" | "agents" | "approvals";
|
|||||||
interface MailboxViewProps {
|
interface MailboxViewProps {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
addToast?: (msg: string, type?: "success" | "error") => void;
|
addToast?: (msg: string, type?: "success" | "error") => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
/** Callback when unread count changes (for header badge updates) */
|
/** Callback when unread count changes (for header badge updates) */
|
||||||
onUnreadCountChange?: (count: number) => void;
|
onUnreadCountChange?: (count: number) => void;
|
||||||
}
|
}
|
||||||
@@ -213,6 +214,7 @@ function buildReplyThread(messages: Message[], selectedMessage: Message): Messag
|
|||||||
export function MailboxView({
|
export function MailboxView({
|
||||||
projectId,
|
projectId,
|
||||||
addToast,
|
addToast,
|
||||||
|
onOpenTask,
|
||||||
onUnreadCountChange,
|
onUnreadCountChange,
|
||||||
}: MailboxViewProps) {
|
}: MailboxViewProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
@@ -880,6 +882,8 @@ export function MailboxView({
|
|||||||
title={msg.metadata?.title}
|
title={msg.metadata?.title}
|
||||||
mimeType={msg.metadata?.mimeType}
|
mimeType={msg.metadata?.mimeType}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
taskId={msg.metadata?.taskId}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -904,6 +908,8 @@ export function MailboxView({
|
|||||||
title={selectedMessage.metadata?.title}
|
title={selectedMessage.metadata?.title}
|
||||||
mimeType={selectedMessage.metadata?.mimeType}
|
mimeType={selectedMessage.metadata?.mimeType}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
taskId={selectedMessage.metadata?.taskId}
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -27,6 +27,38 @@ describe("MailboxArtifactAttachment", () => {
|
|||||||
expect(screen.getByRole("link", { name: "Open artifact: Screenshot" })).toHaveAttribute("href", "/api/artifacts/art-image/media?projectId=proj-1");
|
expect(screen.getByRole("link", { name: "Open artifact: Screenshot" })).toHaveAttribute("href", "/api/artifacts/art-image/media?projectId=proj-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders a View task affordance when task metadata and a handler are present", () => {
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
|
render(
|
||||||
|
<MailboxArtifactAttachment
|
||||||
|
artifactId="art-image"
|
||||||
|
artifactType="image"
|
||||||
|
title="Screenshot"
|
||||||
|
taskId="FN-1234"
|
||||||
|
onOpenTask={onOpenTask}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-artifact-view-task"));
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "View task: FN-1234" })).toHaveTextContent("View task");
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-1234");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render a View task affordance without an open-task handler", () => {
|
||||||
|
render(<MailboxArtifactAttachment artifactId="art-image" artifactType="image" title="Screenshot" taskId="FN-1234" />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("mailbox-artifact-view-task")).toBeNull();
|
||||||
|
expect(screen.getByRole("link", { name: "Open artifact: Screenshot" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render a View task affordance without task metadata", () => {
|
||||||
|
render(<MailboxArtifactAttachment artifactId="art-image" artifactType="image" title="Screenshot" onOpenTask={vi.fn()} />);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("mailbox-artifact-view-task")).toBeNull();
|
||||||
|
expect(screen.getByRole("link", { name: "Open artifact: Screenshot" })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
["document", "Spec"],
|
["document", "Spec"],
|
||||||
["other", "Archive"],
|
["other", "Archive"],
|
||||||
@@ -55,11 +87,12 @@ describe("MailboxArtifactAttachment", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("degrades image load failures to the open artifact link", () => {
|
it("degrades image load failures to the open artifact link", () => {
|
||||||
render(<MailboxArtifactAttachment artifactId="art-broken" artifactType="image" title="Broken screenshot" />);
|
render(<MailboxArtifactAttachment artifactId="art-broken" artifactType="image" title="Broken screenshot" taskId="FN-1234" onOpenTask={vi.fn()} />);
|
||||||
|
|
||||||
fireEvent.error(screen.getByRole("img", { name: "Broken screenshot" }));
|
fireEvent.error(screen.getByRole("img", { name: "Broken screenshot" }));
|
||||||
|
|
||||||
expect(screen.queryByRole("img", { name: "Broken screenshot" })).toBeNull();
|
expect(screen.queryByRole("img", { name: "Broken screenshot" })).toBeNull();
|
||||||
expect(screen.getByRole("link", { name: "Open artifact: Broken screenshot" })).toHaveAttribute("href", "/api/artifacts/art-broken/media");
|
expect(screen.getByRole("link", { name: "Open artifact: Broken screenshot" })).toHaveAttribute("href", "/api/artifacts/art-broken/media");
|
||||||
|
expect(screen.getByTestId("mailbox-artifact-view-task")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -819,13 +819,15 @@ describe("MailboxView", () => {
|
|||||||
artifactType: "image",
|
artifactType: "image",
|
||||||
title: "Mailbox Screenshot",
|
title: "Mailbox Screenshot",
|
||||||
mimeType: "image/png",
|
mimeType: "image/png",
|
||||||
|
taskId: "FN-1234",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
mockFetchInbox.mockResolvedValue(makeInboxResponse([artifactMessage], 1));
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([artifactMessage], 1));
|
||||||
mockFetchConversation.mockResolvedValue([artifactMessage]);
|
mockFetchConversation.mockResolvedValue([artifactMessage]);
|
||||||
mockMarkMessageRead.mockResolvedValue({ ...artifactMessage, read: true });
|
mockMarkMessageRead.mockResolvedValue({ ...artifactMessage, read: true });
|
||||||
|
|
||||||
render(<MailboxView {...defaultProps} projectId="project-a" />);
|
render(<MailboxView {...defaultProps} projectId="project-a" onOpenTask={onOpenTask} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||||
@@ -840,6 +842,39 @@ describe("MailboxView", () => {
|
|||||||
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("img", { name: "Mailbox Screenshot" })).toHaveAttribute("src", "/api/artifacts/art-mailbox-image/media?projectId=project-a");
|
expect(screen.getByRole("img", { name: "Mailbox Screenshot" })).toHaveAttribute("src", "/api/artifacts/art-mailbox-image/media?projectId=project-a");
|
||||||
expect(screen.getByRole("link", { name: "Open artifact: Mailbox Screenshot" })).toHaveAttribute("href", "/api/artifacts/art-mailbox-image/media?projectId=project-a");
|
expect(screen.getByRole("link", { name: "Open artifact: Mailbox Screenshot" })).toHaveAttribute("href", "/api/artifacts/art-mailbox-image/media?projectId=project-a");
|
||||||
|
expect(screen.getByTestId("mailbox-artifact-view-task")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-artifact-view-task"));
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-1234");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render a View task affordance for artifact messages without task metadata", async () => {
|
||||||
|
const artifactMessage: Message = {
|
||||||
|
...mockMessage,
|
||||||
|
metadata: {
|
||||||
|
artifactId: "art-mailbox-image",
|
||||||
|
artifactType: "image",
|
||||||
|
title: "Mailbox Screenshot",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([artifactMessage], 1));
|
||||||
|
mockFetchConversation.mockResolvedValue([artifactMessage]);
|
||||||
|
mockMarkMessageRead.mockResolvedValue({ ...artifactMessage, read: true });
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} onOpenTask={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-item-msg-001"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("mailbox-artifact-view-task")).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -879,14 +914,16 @@ describe("MailboxView", () => {
|
|||||||
artifactId: "art-thread-image",
|
artifactId: "art-thread-image",
|
||||||
artifactType: "image",
|
artifactType: "image",
|
||||||
title: "Thread Image",
|
title: "Thread Image",
|
||||||
|
taskId: "FN-5678",
|
||||||
},
|
},
|
||||||
read: true,
|
read: true,
|
||||||
};
|
};
|
||||||
|
const onOpenTask = vi.fn();
|
||||||
mockFetchInbox.mockResolvedValue(makeInboxResponse([rootMessage], 1));
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([rootMessage], 1));
|
||||||
mockFetchConversation.mockResolvedValue([rootMessage, artifactReply]);
|
mockFetchConversation.mockResolvedValue([rootMessage, artifactReply]);
|
||||||
mockMarkMessageRead.mockResolvedValue({ ...rootMessage, read: true });
|
mockMarkMessageRead.mockResolvedValue({ ...rootMessage, read: true });
|
||||||
|
|
||||||
render(<MailboxView {...defaultProps} />);
|
render(<MailboxView {...defaultProps} onOpenTask={onOpenTask} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("mailbox-item-msg-artifact-root")).toBeDefined();
|
expect(screen.getByTestId("mailbox-item-msg-artifact-root")).toBeDefined();
|
||||||
@@ -900,7 +937,11 @@ describe("MailboxView", () => {
|
|||||||
expect(screen.getByTestId("mailbox-conversation")).toBeInTheDocument();
|
expect(screen.getByTestId("mailbox-conversation")).toBeInTheDocument();
|
||||||
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
expect(screen.getByTestId("mailbox-artifact-attachment")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("img", { name: "Thread Image" })).toHaveAttribute("src", "/api/artifacts/art-thread-image/media");
|
expect(screen.getByRole("img", { name: "Thread Image" })).toHaveAttribute("src", "/api/artifacts/art-thread-image/media");
|
||||||
|
expect(screen.getByTestId("mailbox-artifact-view-task")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-artifact-view-task"));
|
||||||
|
expect(onOpenTask).toHaveBeenCalledWith("FN-5678");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps list pane visible alongside detail pane on desktop/tablet", async () => {
|
it("keeps list pane visible alongside detail pane on desktop/tablet", async () => {
|
||||||
|
|||||||
@@ -358,6 +358,12 @@ export function MainContent({
|
|||||||
<MailboxView
|
<MailboxView
|
||||||
projectId={currentProject?.id}
|
projectId={currentProject?.id}
|
||||||
addToast={addToast}
|
addToast={addToast}
|
||||||
|
/* FNXC:ArtifactRegistry 2026-07-12-00:00: Artifact-registration mail notifications open their producing task through the shared task-detail fetch path so the mailbox does not invent a separate deep-link scheme. */
|
||||||
|
onOpenTask={(taskId) => {
|
||||||
|
void fetchTaskDetail(taskId, currentProject?.id)
|
||||||
|
.then((task) => openDetailTask(task as TaskDetail))
|
||||||
|
.catch(() => addToast?.("Failed to open task", "error"));
|
||||||
|
}}
|
||||||
onUnreadCountChange={setMailboxUnreadCount}
|
onUnreadCountChange={setMailboxUnreadCount}
|
||||||
/>
|
/>
|
||||||
</PageErrorBoundary>
|
</PageErrorBoundary>
|
||||||
|
|||||||
Reference in New Issue
Block a user