feat(FN-2333): merge fusion/fn-2333
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import {
|
||||
Mail,
|
||||
Send,
|
||||
@@ -73,9 +73,18 @@ function formatTimestamp(ts: string): string {
|
||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
function participantLabel(id: string, type: ParticipantType): string {
|
||||
function participantLabel(
|
||||
id: string,
|
||||
type: ParticipantType,
|
||||
agentNamesById?: ReadonlyMap<string, string>,
|
||||
): string {
|
||||
if (type === "user") return id === "dashboard" ? "You" : `User: ${id}`;
|
||||
if (type === "agent") return `Agent: ${id}`;
|
||||
if (type === "agent") {
|
||||
const name = agentNamesById?.get(id)?.trim();
|
||||
if (!name) return `Agent: ${id}`;
|
||||
if (name === id) return `Agent: ${id}`;
|
||||
return `Agent: ${name} (${id})`;
|
||||
}
|
||||
return "System";
|
||||
}
|
||||
|
||||
@@ -145,6 +154,15 @@ export function MailboxView({
|
||||
const [agentMailbox, setAgentMailbox] = useState<AgentMailboxResponse | null>(null);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
|
||||
const agentNamesById = useMemo(
|
||||
() => new Map(agents.map((agent) => [agent.id, agent.name ?? ""])),
|
||||
[agents],
|
||||
);
|
||||
const getParticipantLabel = useCallback(
|
||||
(id: string, type: ParticipantType) => participantLabel(id, type, agentNamesById),
|
||||
[agentNamesById],
|
||||
);
|
||||
|
||||
// ── Data fetching ─────────────────────────────────────────────────────
|
||||
|
||||
const loadInbox = useCallback(async () => {
|
||||
@@ -488,14 +506,14 @@ export function MailboxView({
|
||||
<span className="mailbox-participant-label">From:</span>
|
||||
<span className="mailbox-participant-value">
|
||||
{selectedMessage.fromType === "agent" ? <Bot size={14} /> : <User size={14} />}
|
||||
{participantLabel(selectedMessage.fromId, selectedMessage.fromType)}
|
||||
{getParticipantLabel(selectedMessage.fromId, selectedMessage.fromType)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mailbox-participant">
|
||||
<span className="mailbox-participant-label">To:</span>
|
||||
<span className="mailbox-participant-value">
|
||||
{selectedMessage.toType === "agent" ? <Bot size={14} /> : <User size={14} />}
|
||||
{participantLabel(selectedMessage.toId, selectedMessage.toType)}
|
||||
{getParticipantLabel(selectedMessage.toId, selectedMessage.toType)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -509,7 +527,7 @@ export function MailboxView({
|
||||
className={`mailbox-conversation-msg ${msg.id === selectedMessage.id ? "current" : ""}`}
|
||||
>
|
||||
<div className="mailbox-conversation-msg-header">
|
||||
<span>{participantLabel(msg.fromId, msg.fromType)}</span>
|
||||
<span>{getParticipantLabel(msg.fromId, msg.fromType)}</span>
|
||||
<span className="mailbox-message-time">{formatTimestamp(msg.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mailbox-conversation-msg-body">{msg.content}</div>
|
||||
@@ -566,7 +584,7 @@ export function MailboxView({
|
||||
<div className="mailbox-item-content">
|
||||
<div className="mailbox-item-header">
|
||||
<span className="mailbox-item-from">
|
||||
{participantLabel(group.fromId, group.fromType)}
|
||||
{getParticipantLabel(group.fromId, group.fromType)}
|
||||
</span>
|
||||
<span className="mailbox-item-time">
|
||||
{formatTimestamp(group.latestMessage.createdAt)}
|
||||
@@ -612,7 +630,7 @@ export function MailboxView({
|
||||
<div className="mailbox-item-content">
|
||||
<div className="mailbox-item-header">
|
||||
<span className="mailbox-item-to">
|
||||
To: {participantLabel(msg.toId, msg.toType)}
|
||||
To: {getParticipantLabel(msg.toId, msg.toType)}
|
||||
</span>
|
||||
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
|
||||
</div>
|
||||
@@ -716,9 +734,7 @@ export function MailboxView({
|
||||
<div className="mailbox-item-content">
|
||||
<div className="mailbox-item-header">
|
||||
<span className="mailbox-item-from">
|
||||
{msg.fromType === "agent"
|
||||
? participantLabel(msg.toId, msg.toType)
|
||||
: participantLabel(msg.fromId, msg.fromType)}
|
||||
{getParticipantLabel(msg.fromId, msg.fromType)}
|
||||
</span>
|
||||
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
|
||||
</div>
|
||||
@@ -739,7 +755,7 @@ export function MailboxView({
|
||||
<div className="mailbox-item-content">
|
||||
<div className="mailbox-item-header">
|
||||
<span className="mailbox-item-to">
|
||||
To: {participantLabel(msg.toId, msg.toType)}
|
||||
To: {getParticipantLabel(msg.toId, msg.toType)}
|
||||
</span>
|
||||
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,25 @@ const mockOutboxMessage: Message = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const mockAgentToAgentMessage: Message = {
|
||||
id: "msg-004",
|
||||
fromId: "agent-001",
|
||||
fromType: "agent",
|
||||
toId: "agent-002",
|
||||
toType: "agent",
|
||||
content: "Agent to agent ping.",
|
||||
type: "agent-to-agent",
|
||||
read: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const mockUnknownAgentMessage: Message = {
|
||||
...mockMessage,
|
||||
id: "msg-005",
|
||||
fromId: "agent-999",
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
addToast: vi.fn(),
|
||||
};
|
||||
@@ -190,6 +209,32 @@ describe("MailboxView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders known agent senders by name in inbox conversation rows", async () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [mockMessage],
|
||||
unreadCount: 1,
|
||||
});
|
||||
|
||||
render(<MailboxView {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Agent: Test Agent 1 (agent-001)")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to stable agent identifier when agent metadata is missing", async () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [mockUnknownAgentMessage],
|
||||
unreadCount: 1,
|
||||
});
|
||||
|
||||
render(<MailboxView {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Agent: agent-999")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("groups messages by sender and shows unread count per group", async () => {
|
||||
const secondMessage = { ...mockMessage, id: "msg-003", read: false };
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
@@ -295,6 +340,30 @@ describe("MailboxView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows agent names in message detail participant rows", async () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [mockAgentToAgentMessage],
|
||||
unreadCount: 1,
|
||||
});
|
||||
mockFetchConversation.mockResolvedValue([mockAgentToAgentMessage]);
|
||||
mockMarkMessageRead.mockResolvedValue(undefined);
|
||||
|
||||
render(<MailboxView {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("mailbox-conversation-agent:agent-001")).toBeDefined();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId("mailbox-conversation-agent:agent-001"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Agent: Test Agent 1 (agent-001)")).toBeDefined();
|
||||
expect(screen.getByText("Agent: Test Agent 2 (agent-002)")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("marks message as read when opening unread message", async () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [mockMessage],
|
||||
@@ -560,6 +629,47 @@ describe("MailboxView", () => {
|
||||
expect(agentsComposeButton).toHaveClass("btn", "btn-sm", "btn-secondary", "mailbox-compose-btn");
|
||||
});
|
||||
|
||||
it("shows agent sender names in agent inbox rows", async () => {
|
||||
const agentInboxMessage: Message = {
|
||||
id: "msg-agent-inbox",
|
||||
fromId: "agent-002",
|
||||
fromType: "agent",
|
||||
toId: "agent-001",
|
||||
toType: "agent",
|
||||
content: "Hello from another agent",
|
||||
type: "agent-to-agent",
|
||||
read: false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [],
|
||||
unreadCount: 0,
|
||||
});
|
||||
mockFetchAgentMailbox.mockResolvedValue({
|
||||
ownerId: "agent-001",
|
||||
ownerType: "agent",
|
||||
unreadCount: 1,
|
||||
messages: [agentInboxMessage],
|
||||
inbox: [agentInboxMessage],
|
||||
outbox: [],
|
||||
});
|
||||
|
||||
render(<MailboxView {...defaultProps} />);
|
||||
|
||||
const agentsTab = screen.getByTestId("mailbox-tab-agents");
|
||||
await act(async () => {
|
||||
fireEvent.click(agentsTab);
|
||||
});
|
||||
|
||||
fireEvent.change(screen.getByTestId("mailbox-agent-select"), { target: { value: "agent-001" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Agent: Test Agent 2 (agent-002)")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("switches to outbox view when clicking outbox sub-tab", async () => {
|
||||
mockFetchInbox.mockResolvedValue({
|
||||
messages: [],
|
||||
|
||||
Reference in New Issue
Block a user