ae27830893
- crm.messenger.directory limit 200 -> 100 (be-crm caps at 100, was 400 Bad Request) in both the inbox + messaging adapters. - CrmInboxAdapter.downloadAttachment via crm.media.presignDownload, so mail attachments are downloadable. Pull SDK 0.1.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
5.5 KiB
TypeScript
122 lines
5.5 KiB
TypeScript
// 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,
|
|
MailAttachment,
|
|
MailMessage,
|
|
MailPerson,
|
|
} from "@insignia/iios-messaging-ui";
|
|
import type { DataDoor } from "./crm-messaging-adapter";
|
|
|
|
const MAX_ATTACHMENT_BYTES = 26 * 1024 * 1024; // matches IIOS's cap
|
|
|
|
// Some types (notably .md) have no OS-registered MIME, so the browser reports an empty file.type.
|
|
const EXT_MIME: Record<string, string> = {
|
|
md: "text/markdown", markdown: "text/markdown", html: "text/html", htm: "text/html", txt: "text/plain", csv: "text/csv",
|
|
};
|
|
function mimeForFile(file: File): string {
|
|
if (file.type) return file.type;
|
|
const ext = file.name.toLowerCase().split(".").pop() ?? "";
|
|
return EXT_MIME[ext] ?? "application/octet-stream";
|
|
}
|
|
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
|
|
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, attachment?: MailAttachment): Promise<void> {
|
|
await this.sdk.command("crm.mail.reply", {
|
|
threadId,
|
|
content,
|
|
...(attachment ? { attachment: { filename: attachment.filename ?? "attachment", contentRef: attachment.contentRef, mimeType: attachment.mimeType, sizeBytes: attachment.sizeBytes } } : {}),
|
|
});
|
|
}
|
|
|
|
async uploadAttachment(file: File): Promise<MailAttachment> {
|
|
if (file.size > MAX_ATTACHMENT_BYTES) throw new Error("File is too large (max 25 MB).");
|
|
const mime = mimeForFile(file);
|
|
const { objectKey, uploadUrl } = await this.sdk.command<{ objectKey: string; uploadUrl: string }>("crm.media.presignUpload", { mime, sizeBytes: file.size });
|
|
const res = await fetch(uploadUrl, { method: "PUT", body: file });
|
|
if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
|
|
return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name };
|
|
}
|
|
|
|
async downloadAttachment(attachment: MailAttachment): Promise<string> {
|
|
const { url } = await this.sdk.command<{ url: string }>("crm.media.presignDownload", {
|
|
contentRef: attachment.contentRef,
|
|
...(attachment.mimeType ? { mime: attachment.mimeType } : {}),
|
|
});
|
|
return url;
|
|
}
|
|
|
|
async directory(): Promise<MailPerson[]> {
|
|
const rows = await this.sdk.query<DirectoryDTO[]>("crm.messenger.directory", { kind: "all", limit: 100 });
|
|
return rows.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind }));
|
|
}
|
|
|
|
async composeInternal(recipientUserId: string, subject: string, text: string, attachments?: MailAttachment[]): Promise<void> {
|
|
await this.sdk.command("crm.mail.internal", { recipientUserId, subject, text, html: `<p>${escapeHtml(text)}</p>`, ...attachmentsVar(attachments) });
|
|
}
|
|
|
|
async composeExternal(target: string, subject: string, text: string, attachments?: MailAttachment[]): Promise<void> {
|
|
await this.sdk.command("crm.mail.send", { target, subject, text, html: `<p>${escapeHtml(text)}</p>`, ...attachmentsVar(attachments) });
|
|
}
|
|
}
|
|
|
|
function attachmentsVar(attachments?: MailAttachment[]): { attachments?: Array<{ filename: string; contentRef: string; mimeType: string; sizeBytes: number }> } {
|
|
if (!attachments || attachments.length === 0) return {};
|
|
return { attachments: attachments.map((a) => ({ filename: a.filename ?? "attachment", contentRef: a.contentRef, mimeType: a.mimeType, sizeBytes: a.sizeBytes })) };
|
|
}
|