From 36f43d7d2ac165fa3fdf13d3885ed67fafec0cfb Mon Sep 17 00:00:00 2001 From: maaz519 Date: Sat, 18 Jul 2026 16:43:48 +0530 Subject: [PATCH] refactor(inbox): fold mail INTO the Inbox (one unified surface, no Mail tab) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Inbox is the single communication surface — mentions, needs-reply, system alerts, support updates AND the mail behind them, all in one list. Removed the separate Mail tab. - Inbox is now two-pane: the item list (crm.inbox.*) on the left; clicking an item tied to a thread opens its conversation (MailReader) on the right to read + reply. Non-threaded items (e.g. system alerts) show their detail. Item actions (Done/Snooze/Archive) work for every item. - Compose new mail (in-app or email) from the Inbox header. - mail.tsx trimmed to reusable MailReader + NewMailModal (no standalone tab); sidebar + dashboard reverted to no Mail entry. - The existing work-item Inbox stays the notifier; this makes it the reader too. HTML bodies still render in a sandboxed iframe. tsc + next build clean. Co-Authored-By: Claude Opus 4.8 --- src/components/dashboard/dashboard.tsx | 2 - src/components/dashboard/inbox.tsx | 129 ++++++++++++++++++------- src/components/dashboard/mail.tsx | 127 +++++------------------- src/components/dashboard/sidebar.tsx | 5 +- 4 files changed, 121 insertions(+), 142 deletions(-) diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index cdf5fb8..d052a4e 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -16,7 +16,6 @@ import { Rules } from "./rules"; import { AiAssistant } from "./ai-assistant"; import { TeamManagement } from "./team-management"; import { Messenger } from "./messenger"; -import { Mail } from "./mail"; import { Inbox } from "./inbox"; import "../../app/dashboard/dashboard.css"; @@ -49,7 +48,6 @@ export function Dashboard() { : active === "rules" ? : active === "ai" ? : active === "messenger" ? - : active === "mail" ? : active === "inbox" ? : active === "team" ? : } diff --git a/src/components/dashboard/inbox.tsx b/src/components/dashboard/inbox.tsx index 08dbb3d..56b0e15 100644 --- a/src/components/dashboard/inbox.tsx +++ b/src/components/dashboard/inbox.tsx @@ -1,16 +1,17 @@ "use client"; // ============================================================ -// Inbox — a personalized work/awareness feed IIOS projects from -// events (mentions, needs-reply, support updates, …), surfaced via -// the be-crm data door (crm.inbox.*). List + filter by state, and -// mark items done / snoozed / archived. Items are created by IIOS's -// projector, never here. Mock when the Shell isn't configured. +// Inbox — the ONE unified communication surface. It lists everything +// IIOS surfaces for you (mentions, needs-reply, system alerts, support +// updates, …) AND the mail behind them: click an item tied to a thread +// and its conversation opens on the right to read + reply. Compose new +// mail from here too. Items come from crm.inbox.*; threads from crm.mail.*. // ============================================================ -import { useState } from "react"; -import { Btn, Icon, PageHead, Pill } from "./ui"; +import { useEffect, useState } from "react"; +import { Btn, Icon, PageHead, Pill, useToast } from "./ui"; import { useInboxData, type InboxState, type UiInboxItem } from "@/lib/inbox-api"; +import { MailReader, NewMailModal } from "./mail"; const KIND_LABEL: Record = { MENTION: "Mention", NEEDS_REPLY: "Needs reply", NEEDS_REVIEW: "Needs review", NEEDS_APPROVAL: "Needs approval", @@ -23,10 +24,22 @@ const FILTERS: { value: InboxState; label: string }[] = [ export function Inbox() { const [filter, setFilter] = useState("OPEN"); const inbox = useInboxData(filter); + const toast = useToast(); + const [selectedId, setSelectedId] = useState(null); + const [newOpen, setNewOpen] = useState(false); + + useEffect(() => { + if ((!selectedId || !inbox.items.some((i) => i.id === selectedId)) && inbox.items[0]) setSelectedId(inbox.items[0].id); + }, [inbox.items, selectedId]); + + const selected = inbox.items.find((i) => i.id === selectedId) ?? null; return (
- + setNewOpen(true)}>New mail} + /> {!inbox.live && (
Demo mode — running on mock data. It goes live once the Shell + be-crm are connected. @@ -39,47 +52,97 @@ export function Inbox() { ))}
-
- {inbox.loading &&
Loading…
} - {!inbox.loading && inbox.items.length === 0 && ( -
Nothing here — you're all caught up 🎉
- )} - {inbox.items.map((it) => ( - inbox.transition(it.id, "DONE")} - onSnooze={() => inbox.transition(it.id, "SNOOZED")} - onArchive={() => inbox.transition(it.id, "ARCHIVED")} - /> - ))} +
+ {/* Left — the unified item list */} + + + {/* Right — read the mail behind the item, or the item detail */} +
+ {selected ? ( + toast.push({ tone: "error", title: "Failed", desc: m })} + onDone={() => inbox.transition(selected.id, "DONE")} + onSnooze={() => inbox.transition(selected.id, "SNOOZED")} + onArchive={() => inbox.transition(selected.id, "ARCHIVED")} + /> + ) : ( +
+

Select an item to read

+
+ )} +
+ + setNewOpen(false)} + onSent={() => { setNewOpen(false); inbox.refetch(); toast.push({ tone: "success", title: "Sent" }); }} + onError={(m) => toast.push({ tone: "error", title: "Couldn't send", desc: m })} + />
); } -function InboxRow({ it, onDone, onSnooze, onArchive }: { it: UiInboxItem; onDone: () => void; onSnooze: () => void; onArchive: () => void }) { +function ItemRow({ it, active, onClick }: { it: UiInboxItem; active: boolean; onClick: () => void }) { const isMention = it.kind === "MENTION"; return ( -
- - + + ); +} + +function Detail({ it, onError, onDone, onSnooze, onArchive }: { + it: UiInboxItem; onError: (m: string) => void; onDone: () => void; onSnooze: () => void; onArchive: () => void; +}) { + return ( + <> + {/* Item actions bar (works for every item, threaded or not) */} + {it.state === "OPEN" && ( +
Snooze Done Archive
- ) : ( - {it.state.toLowerCase()} )} -
+ + {it.threadId ? ( + // A message/mail item → open the conversation to read + reply. +
+ +
+ ) : ( + // A non-threaded item (e.g. a system alert) → show its detail. +
+
{it.title}
+ {it.summary &&
{it.summary}
} +
+ )} + ); } diff --git a/src/components/dashboard/mail.tsx b/src/components/dashboard/mail.tsx index 8fd4c8e..500d4ce 100644 --- a/src/components/dashboard/mail.tsx +++ b/src/components/dashboard/mail.tsx @@ -1,16 +1,15 @@ "use client"; // ============================================================ -// Mail — a dedicated reader for app-to-app and email messages, -// powered by IIOS via the be-crm data door (crm.mail.*). Distinct -// from Messenger (chat) and from the work-item Inbox: this shows the -// actual mail (subject + rendered body) and lets you read + reply + -// compose. HTML bodies render inside a sandboxed iframe (no scripts). +// Mail components used INSIDE the Inbox (not a separate tab). +// The Inbox is the one unified surface — mentions, system messages +// and mail all live there. These render the mail body + reply, and +// compose a new message. HTML bodies render in a sandboxed iframe. // ============================================================ import { type CSSProperties, useEffect, useState } from "react"; -import { Avatar, Btn, Field, Icon, Modal, PageHead, Pill, useToast } from "./ui"; -import { useMailThreads, useMailThread, useMailCompose, type MailThread, type MailPerson } from "@/lib/mail-api"; +import { Avatar, Btn, Field, Icon, Modal, Pill } from "./ui"; +import { useMailThread, useMailCompose, type MailPerson } from "@/lib/mail-api"; const timeOf = (iso?: string) => { if (!iso) return ""; @@ -23,87 +22,9 @@ const inputStyle: CSSProperties = { background: "var(--panel)", color: "var(--text)", fontSize: 14, outline: "none", }; -export function Mail() { - const list = useMailThreads(); - const toast = useToast(); - const [selected, setSelected] = useState(null); - const [newOpen, setNewOpen] = useState(false); - - useEffect(() => { - if ((!selected || !list.threads.some((t) => t.threadId === selected)) && list.threads[0]) { - setSelected(list.threads[0].threadId); - } - }, [list.threads, selected]); - - const current = list.threads.find((t) => t.threadId === selected) ?? null; - - return ( -
- setNewOpen(true)}>New mail} - /> - {!list.live && ( -
- Demo mode — running on mock data. It goes live once the Shell + be-crm are connected. -
- )} - -
- - -
- {current ? ( - toast.push({ tone: "error", title: "Failed", desc: m })} /> - ) : ( -
-

Select or compose a message

-
- )} -
-
- - setNewOpen(false)} - onSent={() => { setNewOpen(false); list.refetch(); toast.push({ tone: "success", title: "Sent" }); }} - onError={(m) => toast.push({ tone: "error", title: "Couldn't send", desc: m })} - /> -
- ); -} - -function ThreadRow({ t, active, onClick }: { t: MailThread; active: boolean; onClick: () => void }) { - return ( - - ); -} - -function Reader({ thread, onError }: { thread: MailThread; onError: (m: string) => void }) { - const t = useMailThread(thread.threadId); +/** Reader + reply for one mail thread. Used in the Inbox detail pane when an item has a threadId. */ +export function MailReader({ threadId, subject, onError }: { threadId: string; subject: string; onError: (m: string) => void }) { + const t = useMailThread(threadId); const [draft, setDraft] = useState(""); const [sending, setSending] = useState(false); @@ -117,29 +38,26 @@ function Reader({ thread, onError }: { thread: MailThread; onError: (m: string) } return ( - <> -
-
{thread.subject || "(no subject)"}
-
{thread.participants.length} participant(s)
+
+
+
{subject || "(no subject)"}
- -
+
{t.loading && t.messages.length === 0 &&
Loading…
} - {!t.loading && t.messages.length === 0 &&
No messages in this thread.
} + {!t.loading && t.messages.length === 0 &&
No messages.
} {t.messages.map((m) => (
-
+
{m.kind === "EMAIL" ? "Email" : "Reply"}{m.actorId ? ` · ${m.actorId.replace(/^(pp_|cust_)/, "").slice(0, 8)}` : ""} {timeOf(m.occurredAt)}
{m.html - ?