Mail composers can attach native structures by drag-and-drop and generate report narratives with AI. - Add a shared native-structure drag protocol and attachable previews across Mail source views. - Add a disposable compose-chat panel that drafts narratives around attached embeds. - Cover drag behavior and compose-chat session lifecycle, and document the Mail interaction. Files changed: .changeset/fn-8293-mail-drag-compose-chat.md | 7 ++ docs/dashboard-guide.md | 3 + .../dashboard/app/components/ComposeChatPanel.css | 33 ++++++ .../dashboard/app/components/ComposeChatPanel.tsx | 117 +++++++++++++++++++++ packages/dashboard/app/components/EvalsView.tsx | 3 +- packages/dashboard/app/components/GoalsView.tsx | 6 ++ packages/dashboard/app/components/InsightsView.tsx | 8 +- .../dashboard/app/components/MessageComposer.css | 22 ++++ .../dashboard/app/components/MessageComposer.tsx | 92 ++++++++++++++-- .../dashboard/app/components/MissionManager.tsx | 11 +- .../components/__tests__/ComposeChatPanel.test.tsx | 102 ++++++++++++++++++ .../components/__tests__/MessageComposer.test.tsx | 25 +++++ .../utils/__tests__/nativeStructureDrag.test.ts | 56 ++++++++++ .../dashboard/app/utils/nativeStructureDrag.ts | 54 ++++++++++ 14 files changed, 525 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-8293 Fusion-Task-Lineage: 9864e76c-2a0d-41f6-b7f2-de8dd63710b5 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
57 lines
2.1 KiB
TypeScript
57 lines
2.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { NativeStructureRef } from "@fusion/core";
|
|
import { hasNativeStructureDrag, NATIVE_STRUCTURE_DRAG_MIME, readNativeStructureRef, serializeNativeStructureRef } from "../nativeStructureDrag";
|
|
|
|
function transfer(): DataTransfer {
|
|
const data = new Map<string, string>();
|
|
return {
|
|
types: [] as unknown as DOMStringList,
|
|
effectAllowed: "none",
|
|
setData(type: string, value: string) {
|
|
data.set(type, value);
|
|
(this.types as unknown as string[]) = [...data.keys()];
|
|
},
|
|
getData(type: string) {
|
|
return data.get(type) ?? "";
|
|
},
|
|
} as unknown as DataTransfer;
|
|
}
|
|
|
|
const refs: NativeStructureRef[] = [
|
|
{ kind: "mission", id: "M-1" },
|
|
{ kind: "milestone", id: "MS-1" },
|
|
{ kind: "goal", id: "G-1" },
|
|
{ kind: "research-finding", id: "INS-1" },
|
|
{ kind: "eval-result", id: "E-1" },
|
|
];
|
|
|
|
describe("nativeStructureDrag", () => {
|
|
it.each(refs)("round-trips $kind references", (ref) => {
|
|
const dataTransfer = transfer();
|
|
serializeNativeStructureRef(dataTransfer, ref);
|
|
expect(readNativeStructureRef(dataTransfer)).toEqual(ref);
|
|
expect(dataTransfer.getData("text/plain")).toBe(`fusion://${ref.kind}/${ref.id}`);
|
|
expect(dataTransfer.effectAllowed).toBe("copy");
|
|
});
|
|
|
|
it("rejects missing or malformed payloads", () => {
|
|
const missing = transfer();
|
|
const malformed = transfer();
|
|
malformed.setData(NATIVE_STRUCTURE_DRAG_MIME, "not json");
|
|
const unsupported = transfer();
|
|
unsupported.setData(NATIVE_STRUCTURE_DRAG_MIME, JSON.stringify({ kind: "roadmap-item", id: "R-1" }));
|
|
expect(readNativeStructureRef(missing)).toBeNull();
|
|
expect(readNativeStructureRef(malformed)).toBeNull();
|
|
expect(readNativeStructureRef(unsupported)).toBeNull();
|
|
});
|
|
|
|
it("guards only its own custom MIME type", () => {
|
|
const nativeDrag = transfer();
|
|
nativeDrag.setData(NATIVE_STRUCTURE_DRAG_MIME, JSON.stringify(refs[0]));
|
|
const fileDrag = transfer();
|
|
fileDrag.setData("text/plain", "file");
|
|
expect(hasNativeStructureDrag(nativeDrag)).toBe(true);
|
|
expect(hasNativeStructureDrag(fileDrag)).toBe(false);
|
|
});
|
|
});
|