Files
fusion/packages/dashboard/src/native-structure-preview.ts
gsxdsm d940bf3d5c FN-8358: add roadmap-item structure previews
Enable native previews and hosted navigation for roadmap items.
- Extend native structure references and API resolution through the roadmap adapter.
- Register the bundled Roadmaps view across desktop and mobile navigation.
- Cover roadmap preview availability, navigation, and route behavior.

Files changed:
 ...n-8358-roadmap-item-native-structure-preview.md |  7 ++++
 docs/dashboard-guide.md                            |  6 ++--
 packages/core/src/types.ts                         | 24 ++++++++-----
 packages/dashboard/app/App.tsx                     | 24 ++++++++-----
 .../dashboard/app/components/LeftSidebarNav.tsx    | 12 +++----
 .../app/components/NativeStructurePreview.tsx      | 10 ++++--
 .../app/components/__tests__/App.test.tsx          | 13 ++++---
 .../app/components/__tests__/Header.test.tsx       |  5 ++-
 .../components/__tests__/LeftSidebarNav.test.tsx   |  5 ++-
 .../app/components/__tests__/MobileNavBar.test.tsx |  4 +--
 .../__tests__/NativeStructurePreview.test.tsx      | 40 ++++++++++++++++------
 .../__tests__/registerBundledPluginViews.test.tsx  | 24 ++++++++++---
 .../app/plugins/registerBundledPluginViews.ts      | 23 +++++++++++++
 packages/dashboard/src/native-structure-preview.ts | 25 +++++++++++++-
 .../native-structure-preview-routes.test.ts        | 31 +++++++++++++++--
 .../src/routes/register-task-workflow-routes.ts    |  4 +--
 packages/dashboard/vite.config.ts                  |  4 +++
 packages/dashboard/vitest.config.ts                |  4 +++
 plugins/fusion-plugin-roadmap/package.json         |  5 +++
 .../fusion-plugin-roadmap/src/dashboard-view.tsx   |  5 +++
 plugins/fusion-plugin-roadmap/src/index.ts         | 14 ++++++--
 21 files changed, 223 insertions(+), 66 deletions(-)

Fusion-Task-Id: FN-8358
Fusion-Task-Lineage: c369ce87-eef0-4d06-bf23-bced9c4363f9
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-19 15:45:45 -07:00

112 lines
4.9 KiB
TypeScript

import { createRoadmapStoreForTaskStore } from "@fusion-plugin-examples/roadmap";
import type {
NativeStructurePreviewPayload,
NativeStructurePreviewResult,
NativeStructureRef,
TaskStore,
} from "@fusion/core";
const KIND_LABELS: Record<NativeStructureRef["kind"], string> = {
mission: "Mission",
milestone: "Milestone",
"research-finding": "Research finding",
"eval-result": "Evaluation result",
goal: "Goal",
"roadmap-item": "Roadmap item",
};
const MAX_EXCERPT_LENGTH = 180;
function unavailable(ref: NativeStructureRef, reason: "missing" | "soft-deleted"): NativeStructurePreviewResult {
return { available: false, kind: ref.kind, id: ref.id, reason };
}
function preview(
ref: NativeStructureRef,
title: string,
excerpt: string,
openTarget: NativeStructurePreviewPayload["openTarget"],
): NativeStructurePreviewPayload {
return { available: true, kind: ref.kind, kindLabel: KIND_LABELS[ref.kind], title, excerpt, openTarget };
}
function text(value: string | null | undefined, fallback: string): string {
const normalized = value?.replace(/\s+/g, " ").trim();
const resolved = normalized || fallback;
return resolved.length <= MAX_EXCERPT_LENGTH
? resolved
: `${resolved.slice(0, MAX_EXCERPT_LENGTH - 1).trimEnd()}…`;
}
/**
* FNXC:NativeStructureEmbed 2026-07-16-12:00:
* This is a read-only projection over the task-scoped stores; it never duplicates structure
* persistence. Existing archived/dismissed lifecycle status supplies `soft-deleted` because no
* target has a tombstone column. Missing and unavailable structures are returned, never thrown;
* eval results and roadmap items have no archive lifecycle and can only be missing.
*
* FNXC:NativeStructureEmbed 2026-07-19-12:45:
* Roadmap-item is a project-scoped, read-only projection through the plugin's supported
* createRoadmapStoreForTaskStore seam. Adapter/layer failures degrade to missing; do not
* duplicate roadmap persistence or reach into plugin store internals.
*/
export async function resolveNativeStructurePreview(
store: TaskStore,
ref: NativeStructureRef,
): Promise<NativeStructurePreviewResult> {
switch (ref.kind) {
case "mission": {
const mission = await store.getMissionStore().getMission(ref.id);
if (!mission) return unavailable(ref, "missing");
if (mission.status === "archived") return unavailable(ref, "soft-deleted");
return preview(ref, mission.title, text(mission.description, `Status: ${mission.status}`), { view: "missions", id: mission.id });
}
case "milestone": {
const missionStore = store.getMissionStore();
const milestone = await missionStore.getMilestone(ref.id);
if (!milestone) return unavailable(ref, "missing");
const mission = await missionStore.getMission(milestone.missionId);
if (!mission) return unavailable(ref, "missing");
if (mission.status === "archived") return unavailable(ref, "soft-deleted");
return preview(ref, milestone.title, text(milestone.description, `Status: ${milestone.status}`), {
view: "missions",
id: milestone.id,
missionId: mission.id,
});
}
case "research-finding": {
const insight = await store.getInsightStore().getInsight(ref.id);
if (!insight) return unavailable(ref, "missing");
if (insight.status === "dismissed" || insight.status === "archived") return unavailable(ref, "soft-deleted");
return preview(ref, insight.title, text(insight.content, `Status: ${insight.status}`), { view: "insights", id: insight.id });
}
case "eval-result": {
const result = await store.getEvalStore().getTaskResult(ref.id);
if (!result) return unavailable(ref, "missing");
const score = result.overallScore === undefined ? "Score unavailable" : `Score: ${result.overallScore}${result.maxScore === undefined ? "" : `/${result.maxScore}`}`;
return preview(ref, result.taskSnapshot.title || result.taskId, text(result.summary ?? result.rationale, score), { view: "evals", id: result.id });
}
case "goal": {
const goal = await store.getGoalStore().getGoal(ref.id);
if (!goal) return unavailable(ref, "missing");
if (goal.status === "archived") return unavailable(ref, "soft-deleted");
return preview(ref, goal.title, text(goal.description, `Status: ${goal.status}`), { view: "goals", id: goal.id });
}
case "roadmap-item": {
try {
const roadmapStore = createRoadmapStoreForTaskStore(store);
const feature = await roadmapStore.getFeature(ref.id);
if (!feature) return unavailable(ref, "missing");
const milestone = await roadmapStore.getMilestone(feature.milestoneId);
return preview(ref, feature.title, text(feature.description, ""), {
view: "roadmaps",
id: feature.id,
milestoneId: feature.milestoneId,
...(milestone ? { roadmapId: milestone.roadmapId } : {}),
});
} catch {
return unavailable(ref, "missing");
}
}
}
}