Enable roadmap structures to open, preview, and embed directly from chat and dashboard views. - Register roadmap views as native structures with chat-reference support. - Add composer previews, in-place dashboard embedding, and drag-safe interactions. - Document the native-structure plugin contract and add coverage across supported surfaces. Files changed: .changeset/fn-8859-roadmap-native-structure-embeds.md | 7 + docs/PLUGIN_AUTHORING.md | 1 + docs/dashboard-guide.md | 6 +- packages/dashboard/app/components/MessageComposer.tsx | 16 +- packages/dashboard/app/components/__tests__/MessageComposer.test.tsx | 102 ++++++++++- packages/dashboard/app/components/__tests__/overflowViewRegistry.test.tsx | 68 +++++++- packages/dashboard/app/components/dashboard/MainContent.tsx | 22 ++- packages/dashboard/app/components/dashboard/__tests__/MainContent.native-structure-roadmap.test.tsx | 188 +++++++++++++++++++++ packages/dashboard/app/components/nativeStructureChatRef.ts | 4 +- packages/dashboard/app/components/overflowViewRegistry.tsx | 34 ++-- packages/dashboard/app/plugins/types.ts | 10 +- packages/dashboard/app/utils/__tests__/nativeStructureDrag.test.ts | 46 +++-- packages/dashboard/app/utils/nativeStructureDrag.ts | 18 +- plugins/fusion-plugin-roadmap/src/dashboard-view.tsx | 7 +- plugins/fusion-plugin-roadmap/src/dashboard/RoadmapsView.tsx | 22 ++- plugins/fusion-plugin-roadmap/src/dashboard/__tests__/RoadmapsView.test.tsx | 51 ++++++ 16 files changed, 560 insertions(+), 42 deletions(-) Fusion-Task-Id: FN-8859 Fusion-Task-Lineage: 99a7292b-2a06-4325-8131-ca74431214e3 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
83 lines
3.5 KiB
TypeScript
83 lines
3.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { NativeStructureRef } from "@fusion/core";
|
|
import { attachNativeStructureRefToDrag, hasNativeStructureDrag, isNativeStructureDragEnabled, 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 refsByKind: { [Kind in NativeStructureRef["kind"]]: NativeStructureRef & { kind: Kind } } = {
|
|
mission: { kind: "mission", id: "M-1" },
|
|
milestone: { kind: "milestone", id: "MS-1" },
|
|
goal: { kind: "goal", id: "G-1" },
|
|
"research-finding": { kind: "research-finding", id: "INS-1" },
|
|
"eval-result": { kind: "eval-result", id: "E-1" },
|
|
"roadmap-item": { kind: "roadmap-item", id: "F-1" },
|
|
};
|
|
const refs = Object.values(refsByKind);
|
|
|
|
describe("nativeStructureDrag", () => {
|
|
afterEach(() => vi.unstubAllGlobals());
|
|
|
|
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: "unknown", id: "R-1" }));
|
|
expect(readNativeStructureRef(missing)).toBeNull();
|
|
expect(readNativeStructureRef(malformed)).toBeNull();
|
|
expect(readNativeStructureRef(unsupported)).toBeNull();
|
|
});
|
|
|
|
it("attaches a native payload without replacing an existing reorder payload", () => {
|
|
const dataTransfer = transfer();
|
|
dataTransfer.setData("text/plain", "feature:F-1");
|
|
dataTransfer.effectAllowed = "move";
|
|
|
|
expect(attachNativeStructureRefToDrag(dataTransfer, refsByKind["roadmap-item"])).toBe(true);
|
|
expect(dataTransfer.getData("text/plain")).toBe("feature:F-1");
|
|
expect(dataTransfer.effectAllowed).toBe("move");
|
|
expect(hasNativeStructureDrag(dataTransfer)).toBe(true);
|
|
});
|
|
|
|
it("does not attach native payloads on coarse pointers", () => {
|
|
vi.stubGlobal("matchMedia", vi.fn().mockReturnValue({ matches: true }));
|
|
const dataTransfer = transfer();
|
|
dataTransfer.setData("text/plain", "feature:F-1");
|
|
|
|
expect(isNativeStructureDragEnabled()).toBe(false);
|
|
expect(attachNativeStructureRefToDrag(dataTransfer, refsByKind["roadmap-item"])).toBe(false);
|
|
expect(dataTransfer.getData(NATIVE_STRUCTURE_DRAG_MIME)).toBe("");
|
|
expect(dataTransfer.getData("text/plain")).toBe("feature:F-1");
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|