feat(inbox): wire mail attachments through CrmInboxAdapter
uploadAttachment presigns via crm.media.presignUpload + PUTs bytes to IIOS storage; mailReply/composeInternal/composeExternal forward attachment refs to crm.mail.reply/internal/send (server already supported them). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -1030,7 +1030,7 @@
|
||||
"node_modules/@insignia/iios-messaging-ui": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "file:../iios/packages/iios-messaging-ui/insignia-iios-messaging-ui-0.1.0.tgz",
|
||||
"integrity": "sha512-RoXEI/SgzYM2Zj+XvwNkc3rrDQ5kDa/oqsCo2wkZxwRs3bVHJuRoQR+c713wgaWK+MmqIg0cetXQ//rfJvpBbg==",
|
||||
"integrity": "sha512-qJc7+34VwPcsuaucXcUe7LbW7CAvVcGBUnUR6ktuHHISpGVWzXEni9rjUiBI+Baw3UxvUFbtmIuqT1DTXIeegw==",
|
||||
"peerDependencies": {
|
||||
"@insignia/iios-kernel-client": "*",
|
||||
"react": ">=18"
|
||||
|
||||
@@ -6,11 +6,24 @@ 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;
|
||||
}
|
||||
@@ -63,8 +76,21 @@ export class CrmInboxAdapter implements InboxAdapter {
|
||||
}));
|
||||
}
|
||||
|
||||
async mailReply(threadId: string, content: string): Promise<void> {
|
||||
await this.sdk.command("crm.mail.reply", { threadId, content });
|
||||
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 directory(): Promise<MailPerson[]> {
|
||||
@@ -72,11 +98,16 @@ export class CrmInboxAdapter implements InboxAdapter {
|
||||
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 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): Promise<void> {
|
||||
await this.sdk.command("crm.mail.send", { target, subject, text, html: `<p>${escapeHtml(text)}</p>` });
|
||||
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 })) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user