Files
fusion/packages/dashboard/app/utils/nativeStructureDrag.ts
gsxdsm ec52a9131a FN-8859: add native roadmap embeds to chat
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>
2026-08-08 23:54:40 -07:00

67 lines
2.7 KiB
TypeScript

import type { NativeStructureRef } from "@fusion/core";
export const NATIVE_STRUCTURE_DRAG_MIME = "application/x-fusion-native-structure";
const NATIVE_STRUCTURE_KINDS: readonly NativeStructureRef["kind"][] = [
"mission",
"milestone",
"goal",
"research-finding",
"eval-result",
"roadmap-item",
];
/**
* FNXC:NativeStructureEmbed 2026-08-09-05:13:
* Mail and every native-structure owner exchange this one DataTransfer protocol rather than
* inventing per-view payloads. It accepts every persisted preview kind, including roadmap items.
*/
export function serializeNativeStructureRef(dataTransfer: DataTransfer, ref: NativeStructureRef): void {
dataTransfer.setData(NATIVE_STRUCTURE_DRAG_MIME, JSON.stringify(ref));
dataTransfer.setData("text/plain", `fusion://${ref.kind}/${ref.id}`);
dataTransfer.effectAllowed = "copy";
}
/**
* FNXC:RoadmapNativeStructureDrag 2026-08-09-05:13:
* Roadmap feature rows already own text/plain for feature:<id> reordering, so this helper writes
* only the host MIME and preserves their payload and effectAllowed. Its result lets a row that is
* draggable for reordering widen effectAllowed only when a native structure payload was attached.
*/
export function attachNativeStructureRefToDrag(dataTransfer: DataTransfer, ref: NativeStructureRef): boolean {
if (!isNativeStructureDragEnabled()) return false;
dataTransfer.setData(NATIVE_STRUCTURE_DRAG_MIME, JSON.stringify(ref));
return true;
}
/** Reads and validates an untrusted browser drag payload. */
export function readNativeStructureRef(dataTransfer: DataTransfer): NativeStructureRef | null {
try {
const parsed: unknown = JSON.parse(dataTransfer.getData(NATIVE_STRUCTURE_DRAG_MIME));
if (
typeof parsed !== "object" ||
parsed === null ||
!NATIVE_STRUCTURE_KINDS.includes((parsed as NativeStructureRef).kind) ||
typeof (parsed as NativeStructureRef).id !== "string" ||
!(parsed as NativeStructureRef).id.trim() ||
((parsed as NativeStructureRef).projectId !== undefined && typeof (parsed as NativeStructureRef).projectId !== "string")
) {
return null;
}
const { kind, id, projectId } = parsed as NativeStructureRef;
return projectId === undefined ? { kind, id } : { kind, id, projectId };
} catch {
return null;
}
}
/** True only for drags emitted by serializeNativeStructureRef. */
export function hasNativeStructureDrag(dataTransfer: DataTransfer): boolean {
return Array.from(dataTransfer.types).includes(NATIVE_STRUCTURE_DRAG_MIME);
}
/** Native HTML dragging on touch-primary devices competes with scrolling; use the composer picker there. */
export function isNativeStructureDragEnabled(): boolean {
return typeof window === "undefined" || !window.matchMedia("(pointer: coarse)").matches;
}