fix(FN-3161): migrate roadmap dashboard surface to bundled plugin

- Move RoadmapsView UI, hooks, and tests from dashboard app into fusion-plugin-roadmap
- Replace the built-in roadmaps route/nav wiring with plugin view registration and host rendering
- Add bundled plugin view registration bootstrap in dashboard startup
- Update roadmap plugin build/test config and add FN-3161 removal changeset

Fusion-Task-Id: FN-3161
This commit is contained in:
Fusion
2026-05-08 16:51:04 -07:00
committed by gsxdsm
parent 7cb9ab78fe
commit d42d8ee2f2
33 changed files with 295 additions and 149 deletions

View File

@@ -1,3 +0,0 @@
export function RoadmapsView() {
return null;
}

View File

@@ -1,3 +0,0 @@
import { RoadmapsView } from "./RoadmapsViewBridge.js";
export { RoadmapsView as RoadmapDashboardView };

View File

@@ -0,0 +1,6 @@
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
import { RoadmapsView } from "./dashboard/RoadmapsView.js";
export function RoadmapDashboardView({ context }: { context?: PluginDashboardViewContext }) {
return <RoadmapsView projectId={context?.projectId} addToast={context?.addToast ?? (() => undefined)} />;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
import type {
Roadmap,
RoadmapCreateInput,
RoadmapUpdateInput,
RoadmapMilestone,
RoadmapMilestoneCreateInput,
RoadmapMilestoneUpdateInput,
RoadmapFeature,
RoadmapFeatureCreateInput,
RoadmapFeatureUpdateInput,
RoadmapWithHierarchy,
RoadmapMissionPlanningHandoff,
RoadmapFeatureTaskPlanningHandoff,
} from "../roadmap-types.js";
const BASE = "/api/plugins/roadmap-planner";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${BASE}${path}`, {
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
...init,
});
if (!response.ok) {
let message = `${response.status} ${response.statusText}`;
try {
const body = (await response.json()) as { error?: string };
if (body.error) message = body.error;
} catch {
// ignore
}
throw new Error(message);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
function qp(projectId?: string): string {
return projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
}
export function fetchRoadmaps(projectId?: string): Promise<Roadmap[]> { return request(`/roadmaps${qp(projectId)}`); }
export function fetchRoadmap(roadmapId: string, projectId?: string): Promise<RoadmapWithHierarchy> { return request(`/roadmaps/${roadmapId}${qp(projectId)}`); }
export function createRoadmap(input: RoadmapCreateInput, projectId?: string): Promise<Roadmap> { return request(`/roadmaps${qp(projectId)}`, { method: "POST", body: JSON.stringify({ ...input, projectId }) }); }
export function updateRoadmap(roadmapId: string, updates: RoadmapUpdateInput, projectId?: string): Promise<Roadmap> { return request(`/roadmaps/${roadmapId}${qp(projectId)}`, { method: "PATCH", body: JSON.stringify({ ...updates, projectId }) }); }
export function deleteRoadmap(roadmapId: string, projectId?: string): Promise<void> { return request(`/roadmaps/${roadmapId}${qp(projectId)}`, { method: "DELETE" }); }
export function createRoadmapMilestone(roadmapId: string, input: RoadmapMilestoneCreateInput, projectId?: string): Promise<RoadmapMilestone> { return request(`/roadmaps/${roadmapId}/milestones${qp(projectId)}`, { method: "POST", body: JSON.stringify({ ...input, projectId }) }); }
export function updateRoadmapMilestone(milestoneId: string, updates: RoadmapMilestoneUpdateInput, projectId?: string): Promise<RoadmapMilestone> { return request(`/roadmaps/milestones/${milestoneId}${qp(projectId)}`, { method: "PATCH", body: JSON.stringify({ ...updates, projectId }) }); }
export function deleteRoadmapMilestone(milestoneId: string, projectId?: string): Promise<void> { return request(`/roadmaps/milestones/${milestoneId}${qp(projectId)}`, { method: "DELETE" }); }
export function reorderRoadmapMilestones(roadmapId: string, orderedMilestoneIds: string[], projectId?: string): Promise<void> { return request(`/roadmaps/${roadmapId}/milestones/reorder${qp(projectId)}`, { method: "POST", body: JSON.stringify({ orderedMilestoneIds, projectId }) }); }
export function createRoadmapFeature(milestoneId: string, input: RoadmapFeatureCreateInput, projectId?: string): Promise<RoadmapFeature> { return request(`/roadmaps/milestones/${milestoneId}/features${qp(projectId)}`, { method: "POST", body: JSON.stringify({ ...input, projectId }) }); }
export function updateRoadmapFeature(featureId: string, updates: RoadmapFeatureUpdateInput, projectId?: string): Promise<RoadmapFeature> { return request(`/roadmaps/features/${featureId}${qp(projectId)}`, { method: "PATCH", body: JSON.stringify({ ...updates, projectId }) }); }
export function deleteRoadmapFeature(featureId: string, projectId?: string): Promise<void> { return request(`/roadmaps/features/${featureId}${qp(projectId)}`, { method: "DELETE" }); }
export function reorderRoadmapFeatures(milestoneId: string, orderedFeatureIds: string[], projectId?: string): Promise<void> { return request(`/roadmaps/milestones/${milestoneId}/features/reorder${qp(projectId)}`, { method: "POST", body: JSON.stringify({ orderedFeatureIds, projectId }) }); }
export function moveRoadmapFeature(featureId: string, targetMilestoneId: string, targetIndex: number, projectId?: string): Promise<void> { return request(`/roadmaps/features/${featureId}/move${qp(projectId)}`, { method: "POST", body: JSON.stringify({ targetMilestoneId, targetIndex, projectId }) }); }
export function generateMilestoneSuggestions(roadmapId: string, goalPrompt: string, count = 5, projectId?: string): Promise<{ suggestions: Array<{ title: string; description?: string }> }> {
return request(`/roadmaps/${roadmapId}/suggestions/milestones${qp(projectId)}`, { method: "POST", body: JSON.stringify({ goalPrompt, count, projectId }) });
}
export function generateFeatureSuggestions(milestoneId: string, input?: { prompt?: string; count?: number }, projectId?: string): Promise<{ suggestions: Array<{ title: string; description?: string }> }> {
return request(`/roadmaps/milestones/${milestoneId}/suggestions/features${qp(projectId)}`, { method: "POST", body: JSON.stringify({ ...input, projectId }) });
}
export function fetchRoadmapHandoff(roadmapId: string, projectId?: string): Promise<{ mission: RoadmapMissionPlanningHandoff; features: RoadmapFeatureTaskPlanningHandoff[] }> {
return request(`/roadmaps/${roadmapId}/handoff${qp(projectId)}`);
}

View File

@@ -0,0 +1,7 @@
import "@testing-library/jest-dom/vitest";
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
afterEach(() => {
cleanup();
});

View File

@@ -0,0 +1 @@
export type ToastType = "success" | "error" | "warning" | "info";

View File

@@ -0,0 +1,8 @@
export function useConfirm() {
return {
confirm: async (input: string | { title?: string; message?: string; danger?: boolean }): Promise<boolean> => {
const message = typeof input === "string" ? input : [input.title, input.message].filter(Boolean).join("\n\n");
return window.confirm(message || "Are you sure?");
},
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
import { useEffect, useState } from "react";
export type ViewportMode = "mobile" | "tablet" | "desktop";
function getViewportMode(): ViewportMode {
if (typeof window === "undefined") return "desktop";
if (window.matchMedia("(max-width: 768px)").matches) return "mobile";
if (window.matchMedia("(min-width: 769px) and (max-width: 1024px)").matches) return "tablet";
return "desktop";
}
export function useViewportMode(): ViewportMode {
const [mode, setMode] = useState<ViewportMode>(() => getViewportMode());
useEffect(() => {
const onResize = () => setMode(getViewportMode());
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return mode;
}