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>
112 lines
4.9 KiB
TypeScript
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");
|
|
}
|
|
}
|
|
}
|
|
}
|