FN-6957: prevent stale mailbox deep links from reselecting messages
Treat mailbox deep links as one-shot navigation so mobile users can switch messages without stale URL state restoring the prior selection. - consume the current mailbox deep link before explicit user navigation, compose, delete, reply, and approval actions - prevent repeated deep-link highlighting and clear stale selections when switching agent mailboxes or subtabs - add regression coverage for modal and full mailbox views, plus a published CLI patch changeset Files changed: .changeset/fn-6957-mailbox-stale-deeplink.md | 5 + packages/dashboard/app/components/MailboxModal.tsx | 56 ++++++--- packages/dashboard/app/components/MailboxView.tsx | 60 +++++++--- .../app/components/__tests__/MailboxModal.test.tsx | 76 ++++++++++++ .../app/components/__tests__/MailboxView.test.tsx | 130 +++++++++++++++++++++ 5 files changed, 293 insertions(+), 34 deletions(-) Fusion-Task-Id: FN-6957 Fusion-Task-Lineage: 2a85c8d1-2163-434a-987f-2e842b847651
This commit is contained in:
5
.changeset/fn-6957-mailbox-stale-deeplink.md
Normal file
5
.changeset/fn-6957-mailbox-stale-deeplink.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix mobile mailbox message selection so stale deep links no longer override the user's selected message.
|
||||||
@@ -198,6 +198,20 @@ export function MailboxModal({
|
|||||||
const [replyContextLoading, setReplyContextLoading] = useState<Record<string, boolean>>({});
|
const [replyContextLoading, setReplyContextLoading] = useState<Record<string, boolean>>({});
|
||||||
const [replyContextErrors, setReplyContextErrors] = useState<Record<string, string>>({});
|
const [replyContextErrors, setReplyContextErrors] = useState<Record<string, string>>({});
|
||||||
const [replyContextCache, setReplyContextCache] = useState<Map<string, Message>>(new Map());
|
const [replyContextCache, setReplyContextCache] = useState<Map<string, Message>>(new Map());
|
||||||
|
const consumedDeepLinkedMessageIdRef = useRef<string | null>(null);
|
||||||
|
const highlightedDeepLinkedMessageIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:MailboxMobile 2026-06-23-10:55:
|
||||||
|
* Modal mailbox deep links are one-shot initializers. Once the user taps Back, changes tabs, composes, deletes, or opens another row, the URL target is stale state and must not win over the explicit mobile selection.
|
||||||
|
*/
|
||||||
|
const consumeCurrentDeepLink = useCallback(() => {
|
||||||
|
const deepLinkedMessageId = getDeepLinkedMessageId();
|
||||||
|
if (deepLinkedMessageId) {
|
||||||
|
consumedDeepLinkedMessageIdRef.current = deepLinkedMessageId;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const skipOpenSpinnerInboxRef = useRef(false);
|
const skipOpenSpinnerInboxRef = useRef(false);
|
||||||
const skipOpenSpinnerOutboxRef = useRef(false);
|
const skipOpenSpinnerOutboxRef = useRef(false);
|
||||||
const agentNamesById = useMemo(() => {
|
const agentNamesById = useMemo(() => {
|
||||||
@@ -383,7 +397,10 @@ export function MailboxModal({
|
|||||||
|
|
||||||
// ── Actions ───────────────────────────────────────────────────────────
|
// ── Actions ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const handleOpenMessage = useCallback(async (message: Message) => {
|
const handleOpenMessage = useCallback(async (message: Message, source: "deep-link" | "user" = "user") => {
|
||||||
|
if (source === "user") {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
|
}
|
||||||
setSelectedMessage(message);
|
setSelectedMessage(message);
|
||||||
setReplyContextExpanded({});
|
setReplyContextExpanded({});
|
||||||
setReplyContextLoading({});
|
setReplyContextLoading({});
|
||||||
@@ -424,7 +441,7 @@ export function MailboxModal({
|
|||||||
} catch {
|
} catch {
|
||||||
setConversationMessages([message]);
|
setConversationMessages([message]);
|
||||||
}
|
}
|
||||||
}, [activeTab, inboxCacheKey, projectId, unreadCountCacheKey]);
|
}, [activeTab, inboxCacheKey, projectId, unreadCountCacheKey, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
// Deep-link: open and highlight a specific message from URL params.
|
// Deep-link: open and highlight a specific message from URL params.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -433,7 +450,7 @@ export function MailboxModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deepLinkedMessageId = getDeepLinkedMessageId();
|
const deepLinkedMessageId = getDeepLinkedMessageId();
|
||||||
if (!deepLinkedMessageId) {
|
if (!deepLinkedMessageId || consumedDeepLinkedMessageIdRef.current === deepLinkedMessageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,7 +467,8 @@ export function MailboxModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleOpenMessage(message);
|
consumedDeepLinkedMessageIdRef.current = deepLinkedMessageId;
|
||||||
|
void handleOpenMessage(message, "deep-link");
|
||||||
}, [isOpen, inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]);
|
}, [isOpen, inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -459,7 +477,7 @@ export function MailboxModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deepLinkedMessageId = getDeepLinkedMessageId();
|
const deepLinkedMessageId = getDeepLinkedMessageId();
|
||||||
if (!deepLinkedMessageId) {
|
if (!deepLinkedMessageId || selectedMessage?.id !== deepLinkedMessageId || highlightedDeepLinkedMessageIdRef.current === deepLinkedMessageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +486,7 @@ export function MailboxModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlightedDeepLinkedMessageIdRef.current = deepLinkedMessageId;
|
||||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
element.classList.add("mailbox-message-highlight");
|
element.classList.add("mailbox-message-highlight");
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
@@ -480,12 +499,13 @@ export function MailboxModal({
|
|||||||
}, [isOpen, selectedMessage, conversationMessages]);
|
}, [isOpen, selectedMessage, conversationMessages]);
|
||||||
|
|
||||||
const handleCloseMessage = useCallback(() => {
|
const handleCloseMessage = useCallback(() => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
setConversationMessages([]);
|
setConversationMessages([]);
|
||||||
setReplyContextExpanded({});
|
setReplyContextExpanded({});
|
||||||
setReplyContextLoading({});
|
setReplyContextLoading({});
|
||||||
setReplyContextErrors({});
|
setReplyContextErrors({});
|
||||||
}, []);
|
}, [consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(async () => {
|
const handleMarkAllRead = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -512,6 +532,7 @@ export function MailboxModal({
|
|||||||
}, [addToast, inboxCacheKey, projectId, unreadCountCacheKey, t]);
|
}, [addToast, inboxCacheKey, projectId, unreadCountCacheKey, t]);
|
||||||
|
|
||||||
const handleDeleteMessage = useCallback(async (id: string) => {
|
const handleDeleteMessage = useCallback(async (id: string) => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
try {
|
try {
|
||||||
await deleteMessage(id, projectId);
|
await deleteMessage(id, projectId);
|
||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
@@ -525,16 +546,17 @@ export function MailboxModal({
|
|||||||
} catch {
|
} catch {
|
||||||
addToast?.(t("mailbox.deleteFailed", "Failed to delete message"), "error");
|
addToast?.(t("mailbox.deleteFailed", "Failed to delete message"), "error");
|
||||||
}
|
}
|
||||||
}, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast, t]);
|
}, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast, t, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleReply = useCallback((message: Message) => {
|
const handleReply = useCallback((message: Message) => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
setComposeRecipient({ id: message.fromId, type: message.fromType });
|
setComposeRecipient({ id: message.fromId, type: message.fromType });
|
||||||
setComposeReplyContext({
|
setComposeReplyContext({
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
preview: messagePreview(message.content, 120),
|
preview: messagePreview(message.content, 120),
|
||||||
});
|
});
|
||||||
setShowComposer(true);
|
setShowComposer(true);
|
||||||
}, []);
|
}, [consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleMessageSent = useCallback(() => {
|
const handleMessageSent = useCallback(() => {
|
||||||
setShowComposer(false);
|
setShowComposer(false);
|
||||||
@@ -548,6 +570,7 @@ export function MailboxModal({
|
|||||||
}, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast, t]);
|
}, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast, t]);
|
||||||
|
|
||||||
const handleOpenCompose = useCallback(() => {
|
const handleOpenCompose = useCallback(() => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
// Pre-fill recipient from selected agent if available
|
// Pre-fill recipient from selected agent if available
|
||||||
if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) {
|
if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) {
|
||||||
setComposeRecipient({ id: selectedAgentId, type: "agent" });
|
setComposeRecipient({ id: selectedAgentId, type: "agent" });
|
||||||
@@ -556,13 +579,14 @@ export function MailboxModal({
|
|||||||
}
|
}
|
||||||
setComposeReplyContext(null);
|
setComposeReplyContext(null);
|
||||||
setShowComposer(true);
|
setShowComposer(true);
|
||||||
}, [activeTab, selectedAgentId]);
|
}, [activeTab, selectedAgentId, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleComposeCancel = useCallback(() => {
|
const handleComposeCancel = useCallback(() => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
setShowComposer(false);
|
setShowComposer(false);
|
||||||
setComposeRecipient(null);
|
setComposeRecipient(null);
|
||||||
setComposeReplyContext(null);
|
setComposeReplyContext(null);
|
||||||
}, []);
|
}, [consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const threadMessages = selectedMessage ? buildReplyThread(conversationMessages, selectedMessage) : [];
|
const threadMessages = selectedMessage ? buildReplyThread(conversationMessages, selectedMessage) : [];
|
||||||
|
|
||||||
@@ -751,7 +775,7 @@ export function MailboxModal({
|
|||||||
<div className="mailbox-tabs" data-testid="mailbox-tabs">
|
<div className="mailbox-tabs" data-testid="mailbox-tabs">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("inbox"); setSelectedMessage(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("inbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-tab-inbox"
|
data-testid="mailbox-tab-inbox"
|
||||||
>
|
>
|
||||||
<InboxIcon size={14} />
|
<InboxIcon size={14} />
|
||||||
@@ -760,7 +784,7 @@ export function MailboxModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("outbox"); setSelectedMessage(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("outbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-tab-outbox"
|
data-testid="mailbox-tab-outbox"
|
||||||
>
|
>
|
||||||
<Send size={14} />
|
<Send size={14} />
|
||||||
@@ -768,7 +792,7 @@ export function MailboxModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("agents"); setSelectedMessage(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("agents"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-tab-agents"
|
data-testid="mailbox-tab-agents"
|
||||||
>
|
>
|
||||||
<Bot size={14} />
|
<Bot size={14} />
|
||||||
@@ -993,7 +1017,7 @@ export function MailboxModal({
|
|||||||
<select
|
<select
|
||||||
className="message-composer-select mailbox-agent-select"
|
className="message-composer-select mailbox-agent-select"
|
||||||
value={selectedAgentId}
|
value={selectedAgentId}
|
||||||
onChange={(e) => { setSelectedAgentId(e.target.value); setAgentSubTab("inbox"); }}
|
onChange={(e) => { consumeCurrentDeepLink(); setSelectedAgentId(e.target.value); setAgentSubTab("inbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-agent-select"
|
data-testid="mailbox-agent-select"
|
||||||
>
|
>
|
||||||
<option value={ALL_AGENTS_MAILBOX_ID}>{t("mailbox.allAgentsOption", "All agents")}</option>
|
<option value={ALL_AGENTS_MAILBOX_ID}>{t("mailbox.allAgentsOption", "All agents")}</option>
|
||||||
@@ -1019,7 +1043,7 @@ export function MailboxModal({
|
|||||||
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
|
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
|
||||||
onClick={() => setAgentSubTab("inbox")}
|
onClick={() => { consumeCurrentDeepLink(); setAgentSubTab("inbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-agent-subtab-inbox"
|
data-testid="mailbox-agent-subtab-inbox"
|
||||||
>
|
>
|
||||||
<InboxIcon size={12} />
|
<InboxIcon size={12} />
|
||||||
@@ -1030,7 +1054,7 @@ export function MailboxModal({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
|
||||||
onClick={() => setAgentSubTab("outbox")}
|
onClick={() => { consumeCurrentDeepLink(); setAgentSubTab("outbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-agent-subtab-outbox"
|
data-testid="mailbox-agent-subtab-outbox"
|
||||||
>
|
>
|
||||||
<Send size={12} />
|
<Send size={12} />
|
||||||
|
|||||||
@@ -235,6 +235,19 @@ export function MailboxView({
|
|||||||
const [selectedApproval, setSelectedApproval] = useState<ApprovalRequestDetail | null>(null);
|
const [selectedApproval, setSelectedApproval] = useState<ApprovalRequestDetail | null>(null);
|
||||||
const [approvalComment, setApprovalComment] = useState("");
|
const [approvalComment, setApprovalComment] = useState("");
|
||||||
const [approvalDecisionLoading, setApprovalDecisionLoading] = useState<false | "approve" | "deny">(false);
|
const [approvalDecisionLoading, setApprovalDecisionLoading] = useState<false | "approve" | "deny">(false);
|
||||||
|
const consumedDeepLinkedMessageIdRef = useRef<string | null>(null);
|
||||||
|
const highlightedDeepLinkedMessageIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FNXC:MailboxMobile 2026-06-23-10:55:
|
||||||
|
* URL mailbox deep links initialize one message selection for reload/share flows, but mobile Back, tab switches, compose/delete/approval actions, and direct row clicks are explicit user navigation. Consume the current URL target before those actions so refresh or conversation effects cannot restore an older message over the user's chosen row.
|
||||||
|
*/
|
||||||
|
const consumeCurrentDeepLink = useCallback(() => {
|
||||||
|
const deepLinkedMessageId = getDeepLinkedMessageId();
|
||||||
|
if (deepLinkedMessageId) {
|
||||||
|
consumedDeepLinkedMessageIdRef.current = deepLinkedMessageId;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const agentNamesById = useMemo(
|
const agentNamesById = useMemo(
|
||||||
() => new Map(agents.map((agent) => [agent.id, agent.name ?? ""])),
|
() => new Map(agents.map((agent) => [agent.id, agent.name ?? ""])),
|
||||||
@@ -574,7 +587,10 @@ export function MailboxView({
|
|||||||
|
|
||||||
// ── Actions ───────────────────────────────────────────────────────────
|
// ── Actions ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const handleOpenMessage = useCallback(async (message: Message) => {
|
const handleOpenMessage = useCallback(async (message: Message, source: "deep-link" | "user" = "user") => {
|
||||||
|
if (source === "user") {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
|
}
|
||||||
setSelectedMessage(message);
|
setSelectedMessage(message);
|
||||||
// Only auto-mark as read when viewing the dashboard user's own inbox.
|
// Only auto-mark as read when viewing the dashboard user's own inbox.
|
||||||
// Browsing another agent's mailbox must not consume their unread messages
|
// Browsing another agent's mailbox must not consume their unread messages
|
||||||
@@ -608,12 +624,12 @@ export function MailboxView({
|
|||||||
} catch {
|
} catch {
|
||||||
setConversationMessages([message]);
|
setConversationMessages([message]);
|
||||||
}
|
}
|
||||||
}, [projectId, unreadCount, onUnreadCountChange, activeTab]);
|
}, [projectId, unreadCount, onUnreadCountChange, activeTab, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
// Deep-link: open and highlight a specific message from URL params.
|
// Deep-link: open and highlight a specific message from URL params.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const deepLinkedMessageId = getDeepLinkedMessageId();
|
const deepLinkedMessageId = getDeepLinkedMessageId();
|
||||||
if (!deepLinkedMessageId) {
|
if (!deepLinkedMessageId || consumedDeepLinkedMessageIdRef.current === deepLinkedMessageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,11 +646,12 @@ export function MailboxView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleOpenMessage(message);
|
consumedDeepLinkedMessageIdRef.current = deepLinkedMessageId;
|
||||||
|
void handleOpenMessage(message, "deep-link");
|
||||||
}, [inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]);
|
}, [inbox, outbox, agentMailbox, allAgentsMailbox, conversationMessages, handleOpenMessage]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const deepLinkedMessageId = getDeepLinkedMessageId();
|
const deepLinkedMessageId = getDeepLinkedMessageId();
|
||||||
if (!deepLinkedMessageId) {
|
if (!deepLinkedMessageId || selectedMessage?.id !== deepLinkedMessageId || highlightedDeepLinkedMessageIdRef.current === deepLinkedMessageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,6 +660,7 @@ export function MailboxView({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
highlightedDeepLinkedMessageIdRef.current = deepLinkedMessageId;
|
||||||
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
element.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
element.classList.add("mailbox-message-highlight");
|
element.classList.add("mailbox-message-highlight");
|
||||||
const timer = window.setTimeout(() => {
|
const timer = window.setTimeout(() => {
|
||||||
@@ -655,9 +673,10 @@ export function MailboxView({
|
|||||||
}, [selectedMessage, conversationMessages]);
|
}, [selectedMessage, conversationMessages]);
|
||||||
|
|
||||||
const handleCloseMessage = useCallback(() => {
|
const handleCloseMessage = useCallback(() => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
setConversationMessages([]);
|
setConversationMessages([]);
|
||||||
}, []);
|
}, [consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(async () => {
|
const handleMarkAllRead = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -680,6 +699,7 @@ export function MailboxView({
|
|||||||
}, [projectId, addToast, onUnreadCountChange]);
|
}, [projectId, addToast, onUnreadCountChange]);
|
||||||
|
|
||||||
const handleDeleteMessage = useCallback(async (id: string) => {
|
const handleDeleteMessage = useCallback(async (id: string) => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
try {
|
try {
|
||||||
await deleteMessage(id, projectId);
|
await deleteMessage(id, projectId);
|
||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
@@ -693,16 +713,17 @@ export function MailboxView({
|
|||||||
} catch {
|
} catch {
|
||||||
addToast?.("Failed to delete message", "error");
|
addToast?.("Failed to delete message", "error");
|
||||||
}
|
}
|
||||||
}, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast]);
|
}, [projectId, activeTab, selectedAgentId, loadInbox, loadOutbox, loadAgentMailbox, loadAllAgentsMailbox, addToast, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleReply = useCallback((message: Message) => {
|
const handleReply = useCallback((message: Message) => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
setComposeRecipient({ id: message.fromId, type: message.fromType });
|
setComposeRecipient({ id: message.fromId, type: message.fromType });
|
||||||
setComposeReplyContext({
|
setComposeReplyContext({
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
preview: messagePreview(message.content, 120),
|
preview: messagePreview(message.content, 120),
|
||||||
});
|
});
|
||||||
setShowComposer(true);
|
setShowComposer(true);
|
||||||
}, []);
|
}, [consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleMessageSent = useCallback(() => {
|
const handleMessageSent = useCallback(() => {
|
||||||
setShowComposer(false);
|
setShowComposer(false);
|
||||||
@@ -717,6 +738,7 @@ export function MailboxView({
|
|||||||
}, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast, refreshUnreadCount]);
|
}, [activeTab, loadOutbox, selectedAgentId, loadAgentMailbox, loadAllAgentsMailbox, addToast, refreshUnreadCount]);
|
||||||
|
|
||||||
const handleOpenCompose = useCallback(() => {
|
const handleOpenCompose = useCallback(() => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
// Pre-fill recipient from selected agent if available
|
// Pre-fill recipient from selected agent if available
|
||||||
if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) {
|
if (activeTab === "agents" && selectedAgentId && selectedAgentId !== ALL_AGENTS_MAILBOX_ID) {
|
||||||
setComposeRecipient({ id: selectedAgentId, type: "agent" });
|
setComposeRecipient({ id: selectedAgentId, type: "agent" });
|
||||||
@@ -725,15 +747,17 @@ export function MailboxView({
|
|||||||
}
|
}
|
||||||
setComposeReplyContext(null);
|
setComposeReplyContext(null);
|
||||||
setShowComposer(true);
|
setShowComposer(true);
|
||||||
}, [activeTab, selectedAgentId]);
|
}, [activeTab, selectedAgentId, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleComposeCancel = useCallback(() => {
|
const handleComposeCancel = useCallback(() => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
setShowComposer(false);
|
setShowComposer(false);
|
||||||
setComposeRecipient(null);
|
setComposeRecipient(null);
|
||||||
setComposeReplyContext(null);
|
setComposeReplyContext(null);
|
||||||
}, []);
|
}, [consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleOpenApproval = useCallback(async (request: ApprovalRequestSummary) => {
|
const handleOpenApproval = useCallback(async (request: ApprovalRequestSummary) => {
|
||||||
|
consumeCurrentDeepLink();
|
||||||
try {
|
try {
|
||||||
const detail = await fetchApprovalDetail(request.id, projectId);
|
const detail = await fetchApprovalDetail(request.id, projectId);
|
||||||
setSelectedApproval(detail);
|
setSelectedApproval(detail);
|
||||||
@@ -741,7 +765,7 @@ export function MailboxView({
|
|||||||
} catch {
|
} catch {
|
||||||
addToast?.("Failed to load approval request", "error");
|
addToast?.("Failed to load approval request", "error");
|
||||||
}
|
}
|
||||||
}, [projectId, addToast]);
|
}, [projectId, addToast, consumeCurrentDeepLink]);
|
||||||
|
|
||||||
const handleApprovalDecision = useCallback(async (decision: "approve" | "deny") => {
|
const handleApprovalDecision = useCallback(async (decision: "approve" | "deny") => {
|
||||||
if (!selectedApproval || approvalDecisionLoading) return;
|
if (!selectedApproval || approvalDecisionLoading) return;
|
||||||
@@ -1003,7 +1027,7 @@ export function MailboxView({
|
|||||||
<select
|
<select
|
||||||
className="message-composer-select mailbox-agent-select"
|
className="message-composer-select mailbox-agent-select"
|
||||||
value={selectedAgentId}
|
value={selectedAgentId}
|
||||||
onChange={(e) => { setSelectedAgentId(e.target.value); setAgentSubTab("inbox"); }}
|
onChange={(e) => { consumeCurrentDeepLink(); setSelectedAgentId(e.target.value); setAgentSubTab("inbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-agent-select"
|
data-testid="mailbox-agent-select"
|
||||||
>
|
>
|
||||||
<option value={ALL_AGENTS_MAILBOX_ID}>{t("mailbox.allAgents", "All agents")}</option>
|
<option value={ALL_AGENTS_MAILBOX_ID}>{t("mailbox.allAgents", "All agents")}</option>
|
||||||
@@ -1028,7 +1052,7 @@ export function MailboxView({
|
|||||||
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
|
<div className="mailbox-agent-subtabs" data-testid="mailbox-agent-subtabs">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "inbox" ? "active" : ""}`}
|
||||||
onClick={() => setAgentSubTab("inbox")}
|
onClick={() => { consumeCurrentDeepLink(); setAgentSubTab("inbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-agent-subtab-inbox"
|
data-testid="mailbox-agent-subtab-inbox"
|
||||||
>
|
>
|
||||||
<InboxIcon size={12} />
|
<InboxIcon size={12} />
|
||||||
@@ -1039,7 +1063,7 @@ export function MailboxView({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-agent-subtab ${agentSubTab === "outbox" ? "active" : ""}`}
|
||||||
onClick={() => setAgentSubTab("outbox")}
|
onClick={() => { consumeCurrentDeepLink(); setAgentSubTab("outbox"); setSelectedMessage(null); }}
|
||||||
data-testid="mailbox-agent-subtab-outbox"
|
data-testid="mailbox-agent-subtab-outbox"
|
||||||
>
|
>
|
||||||
<Send size={12} />
|
<Send size={12} />
|
||||||
@@ -1285,7 +1309,7 @@ export function MailboxView({
|
|||||||
<div className="mailbox-tabs" data-testid="mailbox-tabs">
|
<div className="mailbox-tabs" data-testid="mailbox-tabs">
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "inbox" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("inbox"); setSelectedMessage(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("inbox"); setSelectedMessage(null); setSelectedApproval(null); }}
|
||||||
data-testid="mailbox-tab-inbox"
|
data-testid="mailbox-tab-inbox"
|
||||||
>
|
>
|
||||||
<InboxIcon size={14} />
|
<InboxIcon size={14} />
|
||||||
@@ -1294,7 +1318,7 @@ export function MailboxView({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "outbox" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("outbox"); setSelectedMessage(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("outbox"); setSelectedMessage(null); setSelectedApproval(null); }}
|
||||||
data-testid="mailbox-tab-outbox"
|
data-testid="mailbox-tab-outbox"
|
||||||
>
|
>
|
||||||
<Send size={14} />
|
<Send size={14} />
|
||||||
@@ -1302,7 +1326,7 @@ export function MailboxView({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "agents" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("agents"); setSelectedMessage(null); setSelectedApproval(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("agents"); setSelectedMessage(null); setSelectedApproval(null); }}
|
||||||
data-testid="mailbox-tab-agents"
|
data-testid="mailbox-tab-agents"
|
||||||
>
|
>
|
||||||
<Bot size={14} />
|
<Bot size={14} />
|
||||||
@@ -1310,7 +1334,7 @@ export function MailboxView({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "approvals" ? "active" : ""}`}
|
className={`btn btn-sm btn-secondary mailbox-tab ${activeTab === "approvals" ? "active" : ""}`}
|
||||||
onClick={() => { setActiveTab("approvals"); setSelectedMessage(null); setSelectedApproval(null); }}
|
onClick={() => { consumeCurrentDeepLink(); setActiveTab("approvals"); setSelectedMessage(null); setSelectedApproval(null); }}
|
||||||
data-testid="mailbox-tab-approvals"
|
data-testid="mailbox-tab-approvals"
|
||||||
>
|
>
|
||||||
<CheckCheck size={14} />
|
<CheckCheck size={14} />
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ describe("MailboxModal", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
window.history.replaceState({}, "", "/");
|
||||||
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
// Clear SWR cache between tests so prior runs don't pre-hydrate inbox/outbox
|
// Clear SWR cache between tests so prior runs don't pre-hydrate inbox/outbox
|
||||||
// state and mask the loading/empty/error UI assertions.
|
// state and mask the loading/empty/error UI assertions.
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@@ -514,6 +516,80 @@ describe("MailboxModal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a manually selected modal message after a stale deep link", async () => {
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=msg-001#message-msg-001");
|
||||||
|
const clickedMessage: Message = {
|
||||||
|
...mockReadMessage,
|
||||||
|
read: false,
|
||||||
|
content: "Modal clicked selection body",
|
||||||
|
fromId: mockMessage.fromId,
|
||||||
|
fromType: mockMessage.fromType,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetchInbox.mockResolvedValue({ messages: [mockMessage, clickedMessage], total: 2, unreadCount: 2 });
|
||||||
|
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||||
|
mockMarkMessageRead.mockImplementation(async (messageId) => ({
|
||||||
|
...(messageId === clickedMessage.id ? clickedMessage : mockMessage),
|
||||||
|
read: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<MailboxModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent(mockMessage.content);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-back-to-list"));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-item-msg-002")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-item-msg-002"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Modal clicked selection body");
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toHaveAttribute("id", "message-msg-002");
|
||||||
|
expect(mockMarkMessageRead).toHaveBeenCalledWith("msg-002", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores unknown modal deep links and empty inboxes without selecting stale data", async () => {
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=missing#message-missing");
|
||||||
|
mockFetchInbox.mockResolvedValue({ messages: [], total: 0, unreadCount: 0 });
|
||||||
|
mockFetchConversation.mockResolvedValue([]);
|
||||||
|
|
||||||
|
render(<MailboxModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-inbox-empty")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("mailbox-message-detail")).toBeNull();
|
||||||
|
expect(mockMarkMessageRead).not.toHaveBeenCalled();
|
||||||
|
expect(mockFetchConversation).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps modal tab changes from restoring a consumed mailbox deep link", async () => {
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=msg-001#message-msg-001");
|
||||||
|
mockFetchInbox.mockResolvedValue({ messages: [mockMessage], total: 1, unreadCount: 1 });
|
||||||
|
mockFetchOutbox.mockResolvedValue({ messages: [], total: 0 });
|
||||||
|
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||||
|
mockMarkMessageRead.mockResolvedValue({ ...mockMessage, read: true });
|
||||||
|
|
||||||
|
render(<MailboxModal {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toHaveAttribute("id", "message-msg-001");
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-tab-outbox"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-outbox-empty")).toBeDefined();
|
||||||
|
expect(screen.queryByTestId("mailbox-message-detail")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("shows mark all read button when there are unread messages", async () => {
|
it("shows mark all read button when there are unread messages", async () => {
|
||||||
render(<MailboxModal {...defaultProps} />);
|
render(<MailboxModal {...defaultProps} />);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ describe("MailboxView", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
window.history.replaceState({}, "", "/");
|
||||||
|
Element.prototype.scrollIntoView = vi.fn();
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
sseSubscriptions.length = 0;
|
sseSubscriptions.length = 0;
|
||||||
mockUseViewportMode.mockReturnValue("desktop");
|
mockUseViewportMode.mockReturnValue("desktop");
|
||||||
@@ -867,6 +869,134 @@ describe("MailboxView", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a manually selected mobile message after a stale deep link and refresh", async () => {
|
||||||
|
mockUseViewportMode.mockReturnValue("mobile");
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=msg-001#message-msg-001");
|
||||||
|
const clickedMessage: Message = {
|
||||||
|
...mockReadMessage,
|
||||||
|
read: false,
|
||||||
|
content: "Newer mobile selection body",
|
||||||
|
fromId: mockMessage.fromId,
|
||||||
|
fromType: mockMessage.fromType,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetchInbox
|
||||||
|
.mockResolvedValueOnce(makeInboxResponse([mockMessage, clickedMessage], 1))
|
||||||
|
.mockResolvedValue(makeInboxResponse([mockMessage, clickedMessage], 1));
|
||||||
|
const staleThread = [mockMessage];
|
||||||
|
mockFetchConversation.mockResolvedValue(staleThread);
|
||||||
|
mockMarkMessageRead.mockImplementation(async (messageId) => ({
|
||||||
|
...(messageId === clickedMessage.id ? clickedMessage : mockMessage),
|
||||||
|
read: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent(mockMessage.content);
|
||||||
|
expect(mockMarkMessageRead).toHaveBeenCalledWith("msg-001", undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-back-to-list"));
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-item-msg-002")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-item-msg-002"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Newer mobile selection body");
|
||||||
|
expect(mockMarkMessageRead).toHaveBeenCalledWith("msg-002", undefined);
|
||||||
|
expect(mockFetchConversation).toHaveBeenLastCalledWith(clickedMessage.fromId, clickedMessage.fromType, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
sseSubscriptions.at(-1)?.["message:received"]?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Newer mobile selection body");
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toHaveAttribute("id", "mailbox-detail-message-msg-002");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets desktop split-pane row selection override a stale deep link", async () => {
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=msg-001#message-msg-001");
|
||||||
|
const clickedMessage: Message = {
|
||||||
|
...mockReadMessage,
|
||||||
|
read: false,
|
||||||
|
content: "Desktop selected message body",
|
||||||
|
fromId: mockMessage.fromId,
|
||||||
|
fromType: mockMessage.fromType,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage, clickedMessage], 1));
|
||||||
|
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||||
|
mockMarkMessageRead.mockImplementation(async (messageId) => ({
|
||||||
|
...(messageId === clickedMessage.id ? clickedMessage : mockMessage),
|
||||||
|
read: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent(mockMessage.content);
|
||||||
|
expect(screen.getByTestId("mailbox-inbox-list")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-item-msg-002"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-body")).toHaveTextContent("Desktop selected message body");
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toHaveAttribute("id", "mailbox-detail-message-msg-002");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps tab changes from restoring a consumed mailbox deep link", async () => {
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=msg-001#message-msg-001");
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([mockMessage], 1));
|
||||||
|
mockFetchOutbox.mockResolvedValue(makeOutboxResponse([]));
|
||||||
|
mockFetchConversation.mockResolvedValue([mockMessage]);
|
||||||
|
mockMarkMessageRead.mockResolvedValue({ ...mockMessage, read: true });
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-message-detail")).toHaveAttribute("id", "mailbox-detail-message-msg-001");
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByTestId("mailbox-tab-outbox"));
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-outbox-empty")).toBeDefined();
|
||||||
|
expect(screen.queryByTestId("mailbox-message-detail")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores unknown deep links and empty inboxes without fabricating a selected message", async () => {
|
||||||
|
window.history.replaceState({}, "", "?view=mailbox&mailbox-message=missing#message-missing");
|
||||||
|
mockFetchInbox.mockResolvedValue(makeInboxResponse([], 0));
|
||||||
|
mockFetchConversation.mockResolvedValue([]);
|
||||||
|
|
||||||
|
render(<MailboxView {...defaultProps} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("mailbox-inbox-empty")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("mailbox-message-detail")).toBeNull();
|
||||||
|
expect(mockMarkMessageRead).not.toHaveBeenCalled();
|
||||||
|
expect(mockFetchConversation).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows agent names in message detail participant rows", async () => {
|
it("shows agent names in message detail participant rows", async () => {
|
||||||
mockFetchInbox.mockResolvedValue({
|
mockFetchInbox.mockResolvedValue({
|
||||||
messages: [mockAgentToAgentMessage],
|
messages: [mockAgentToAgentMessage],
|
||||||
|
|||||||
Reference in New Issue
Block a user