feat(inbox): migrate CRM Inbox to @insignia/iios-messaging-ui SDK

Replace the bespoke in-CRM inbox with the SDK's <Inbox>, driven by a new
CrmInboxAdapter over the be-crm data door (crm.inbox.* + crm.mail.*).
Demo mode falls back to the SDK MockInboxAdapter. Maps --miu-* tokens for
.miu-inbox to the CRM theme.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 00:54:03 +05:30
parent 413f8d43b6
commit facc50e82f
6 changed files with 382 additions and 4 deletions
+82
View File
@@ -0,0 +1,82 @@
// The CRM's InboxAdapter — the SDK <Inbox> rendered over the be-crm data door
// (crm.inbox.* + crm.mail.*). Folds mail threads into the unified inbox exactly as the old
// inbox-api did; the CRM keeps auth/tenancy server-side.
import type {
InboxAdapter,
InboxItem,
InboxState,
MailMessage,
MailPerson,
} from "@insignia/iios-messaging-ui";
import type { DataDoor } from "./crm-messaging-adapter";
interface InboxItemDTO {
id: string; kind: string; state: InboxState; title: string; summary?: string; priority: string; threadId?: string; createdAt: string;
}
interface MailThreadDTO { threadId: string; subject: string | null; participants: string[]; unread: number; lastMessage?: string; lastAt?: string }
interface MailMessageDTO {
interactionId: string; actorId: string | null; kind: string; occurredAt: string;
html: string | null; text: string | null;
attachment: { contentRef: string; mimeType: string; sizeBytes: number; filename: string | null } | null;
}
interface DirectoryDTO { id: string; displayName: string; kind: "staff" | "customer" }
const escapeHtml = (s: string): string => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
export class CrmInboxAdapter implements InboxAdapter {
constructor(private readonly sdk: DataDoor) {}
async listInbox(state?: InboxState): Promise<InboxItem[]> {
const showMail = !state || state === "OPEN";
const [items, mail] = await Promise.all([
this.sdk.query<InboxItemDTO[]>("crm.inbox.list", state ? { state } : {}),
showMail ? this.sdk.query<MailThreadDTO[]>("crm.mail.list", {}) : Promise.resolve([] as MailThreadDTO[]),
]);
const mailItems: InboxItem[] = mail.map((t) => ({
id: `mail:${t.threadId}`,
kind: "MAIL",
state: "OPEN",
title: t.subject || "(no subject)",
...(t.lastMessage ? { summary: t.lastMessage } : {}),
priority: t.unread > 0 ? "HIGH" : "LOW",
threadId: t.threadId,
createdAt: t.lastAt ?? "",
}));
return [...mailItems, ...items].sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
}
async transition(id: string, state: InboxState): Promise<void> {
await this.sdk.command("crm.inbox.transition", { id, state });
}
async mailHistory(threadId: string): Promise<MailMessage[]> {
const rows = await this.sdk.query<MailMessageDTO[]>("crm.mail.history", { threadId });
return rows.map((m) => ({
id: m.interactionId,
actorId: m.actorId,
kind: m.kind,
at: m.occurredAt,
html: m.html,
text: m.text,
attachment: m.attachment,
}));
}
async mailReply(threadId: string, content: string): Promise<void> {
await this.sdk.command("crm.mail.reply", { threadId, content });
}
async directory(): Promise<MailPerson[]> {
const rows = await this.sdk.query<DirectoryDTO[]>("crm.messenger.directory", { kind: "all", limit: 200 });
return rows.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind }));
}
async composeInternal(recipientUserId: string, subject: string, text: string): Promise<void> {
await this.sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `<p>${escapeHtml(text)}</p>` });
}
async composeExternal(target: string, subject: string, text: string): Promise<void> {
await this.sdk.command("crm.mail.send", { target, subject, text, html: `<p>${escapeHtml(text)}</p>` });
}
}