feat(FN-3737): add even realities integration research report
Adds a new Even Realities integration research report to the documentation. Fusion-Task-Id: FN-3737
This commit is contained in:
@@ -10,8 +10,8 @@
|
||||
"import": "./src/index.ts"
|
||||
},
|
||||
"./dashboard-view": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
"types": "./src/dashboard-view.tsx",
|
||||
"import": "./src/dashboard-view.tsx"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { definePlugin } from "@fusion/plugin-sdk";
|
||||
import { validatePluginManifest } from "@fusion/core";
|
||||
import plugin, { DependencyGraphDashboardView } from "../index";
|
||||
import plugin from "../index";
|
||||
import { DependencyGraphDashboardView } from "../dashboard-view";
|
||||
import { getPluginViewId } from "../../../../packages/dashboard/app/plugins/pluginViewRegistry";
|
||||
|
||||
vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { mkdtempSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { PluginLoader, PluginStore } from "@fusion/core";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import plugin from "../index";
|
||||
|
||||
const testDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(testDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("dependency graph plugin index", () => {
|
||||
it("exports node-importable plugin metadata", () => {
|
||||
expect(plugin).toBeDefined();
|
||||
expect(plugin.manifest.id).toBe("fusion-plugin-dependency-graph");
|
||||
expect(plugin.dashboardViews?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
viewId: "graph",
|
||||
componentPath: "./dashboard-view",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("loads src/index.ts via Node dynamic import", async () => {
|
||||
const moduleUrl = pathToFileURL(join(process.cwd(), "src/index.ts")).href;
|
||||
const module = await import(moduleUrl);
|
||||
expect(module.default?.manifest?.id).toBe("fusion-plugin-dependency-graph");
|
||||
});
|
||||
|
||||
it("is loadable by PluginLoader without throwing", async () => {
|
||||
const rootDir = mkdtempSync(join(tmpdir(), "fn-3737-plugin-loader-"));
|
||||
testDirs.push(rootDir);
|
||||
|
||||
const pluginStore = new PluginStore(rootDir, { inMemoryDb: true, centralGlobalDir: rootDir });
|
||||
await pluginStore.init();
|
||||
|
||||
const pluginPath = join(process.cwd(), "src/index.ts");
|
||||
await pluginStore.registerPlugin({ manifest: plugin.manifest, path: pluginPath });
|
||||
|
||||
const loader = new PluginLoader({
|
||||
pluginStore,
|
||||
taskStore: { logActivity: async () => undefined } as never,
|
||||
pluginDirs: [dirname(dirname(pluginPath))],
|
||||
});
|
||||
|
||||
await loader.loadPlugin(plugin.manifest.id);
|
||||
|
||||
const loaded = loader.getPlugin(plugin.manifest.id);
|
||||
expect(loaded?.state).toBe("started");
|
||||
expect(loaded?.dashboardViews?.[0]).toEqual(
|
||||
expect.objectContaining({ viewId: "graph", componentPath: "./dashboard-view" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
||||
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
|
||||
import { createElement } from "react";
|
||||
import { DependencyGraph } from "./DependencyGraph";
|
||||
|
||||
function createWorkflowStepNameLookup(workflowSteps: WorkflowStep[] | undefined): ReadonlyMap<string, string> {
|
||||
return new Map((workflowSteps ?? []).map((step) => [step.id, step.name] as const));
|
||||
}
|
||||
|
||||
export function DependencyGraphDashboardView({ context }: { context?: PluginDashboardViewContext }) {
|
||||
return createElement(DependencyGraph, {
|
||||
tasks: context?.tasks ?? [],
|
||||
projectId: context?.projectId,
|
||||
workflowStepNameLookup: createWorkflowStepNameLookup(context?.workflowSteps),
|
||||
onOpenDetail: context?.openTaskDetail as ((task: Task | TaskDetail) => void) | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export { DependencyGraph };
|
||||
@@ -1,8 +1,4 @@
|
||||
import type { Task, TaskDetail, WorkflowStep } from "@fusion/core";
|
||||
import type { PluginDashboardViewContext } from "@fusion/dashboard/app/plugins/types";
|
||||
import { definePlugin } from "@fusion/plugin-sdk";
|
||||
import { createElement } from "react";
|
||||
import { DependencyGraph } from "./DependencyGraph";
|
||||
|
||||
const plugin = definePlugin({
|
||||
manifest: {
|
||||
@@ -25,18 +21,4 @@ const plugin = definePlugin({
|
||||
],
|
||||
});
|
||||
|
||||
function createWorkflowStepNameLookup(workflowSteps: WorkflowStep[] | undefined): ReadonlyMap<string, string> {
|
||||
return new Map((workflowSteps ?? []).map((step) => [step.id, step.name] as const));
|
||||
}
|
||||
|
||||
export function DependencyGraphDashboardView({ context }: { context?: PluginDashboardViewContext }) {
|
||||
return createElement(DependencyGraph, {
|
||||
tasks: context?.tasks ?? [],
|
||||
projectId: context?.projectId,
|
||||
workflowStepNameLookup: createWorkflowStepNameLookup(context?.workflowSteps),
|
||||
onOpenDetail: context?.openTaskDetail as ((task: Task | TaskDetail) => void) | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export default plugin;
|
||||
export { DependencyGraph };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PluginRouteDefinition } from "@fusion/core";
|
||||
import { SUGGESTION_TIMEOUT_MS } from "./roadmap-suggestions.js";
|
||||
export declare function createRoadmapPluginRoutes(): PluginRouteDefinition[];
|
||||
export { SUGGESTION_TIMEOUT_MS };
|
||||
//# sourceMappingURL=roadmap-routes.d.ts.map
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"roadmap-routes.d.ts","sourceRoot":"","sources":["roadmap-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,qBAAqB,EAAuB,MAAM,cAAc,CAAC;AAiG9F,wBAAgB,yBAAyB,IAAI,qBAAqB,EAAE,CAyRnE;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
||||
{"version":3,"file":"roadmap-routes.d.ts","sourceRoot":"","sources":["roadmap-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,qBAAqB,EAAuB,MAAM,cAAc,CAAC;AAG9F,OAAO,EAQL,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAsFlC,wBAAgB,yBAAyB,IAAI,qBAAqB,EAAE,CAyRnE;AAED,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
|
||||
File diff suppressed because one or more lines are too long
@@ -14,6 +14,7 @@ import {
|
||||
validateFeatureSuggestionInput,
|
||||
validateSuggestionInput,
|
||||
ValidationError as SuggestionValidationError,
|
||||
SUGGESTION_TIMEOUT_MS,
|
||||
} from "./roadmap-suggestions.js";
|
||||
|
||||
const roadmapStoreCache = new WeakMap<object, RoadmapStore>();
|
||||
@@ -39,6 +40,11 @@ function asRequest(req: unknown): RouteRequest {
|
||||
return req as RouteRequest;
|
||||
}
|
||||
|
||||
function paramValue(value: string | string[] | undefined): string {
|
||||
if (Array.isArray(value)) return value[0] ?? "";
|
||||
return value ?? "";
|
||||
}
|
||||
|
||||
function badRequest(message: string): PluginRouteResponse {
|
||||
return { status: 400, body: { error: message } };
|
||||
}
|
||||
@@ -129,8 +135,8 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
method: "GET",
|
||||
path: "/roadmaps/:roadmapId",
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
const roadmap = roadmapStore.getRoadmapWithHierarchy(req.params.roadmapId);
|
||||
return roadmap ? roadmap : notFound(`Roadmap ${req.params.roadmapId} not found`);
|
||||
const roadmap = roadmapStore.getRoadmapWithHierarchy(paramValue(req.params.roadmapId));
|
||||
return roadmap ? roadmap : notFound(`Roadmap ${paramValue(req.params.roadmapId)} not found`);
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -139,7 +145,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
const body = req.body as { title?: string; description?: string };
|
||||
try {
|
||||
return roadmapStore.updateRoadmap(req.params.roadmapId, {
|
||||
return roadmapStore.updateRoadmap(paramValue(req.params.roadmapId), {
|
||||
title: body.title !== undefined ? validateTitle(body.title) : undefined,
|
||||
description: body.description !== undefined ? validateDescription(body.description) : undefined,
|
||||
});
|
||||
@@ -149,7 +155,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
}),
|
||||
},
|
||||
{ method: "DELETE", path: "/roadmaps/:roadmapId", handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
roadmapStore.deleteRoadmap(req.params.roadmapId);
|
||||
roadmapStore.deleteRoadmap(paramValue(req.params.roadmapId));
|
||||
return noContent();
|
||||
}) },
|
||||
{
|
||||
@@ -160,7 +166,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
try {
|
||||
return {
|
||||
status: 201,
|
||||
body: roadmapStore.createMilestone(req.params.roadmapId, {
|
||||
body: roadmapStore.createMilestone(paramValue(req.params.roadmapId), {
|
||||
title: validateTitle(body?.title),
|
||||
description: validateDescription(body?.description),
|
||||
}),
|
||||
@@ -176,7 +182,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
try {
|
||||
const body = req.body as { orderedMilestoneIds: string[] };
|
||||
roadmapStore.reorderMilestones({ roadmapId: req.params.roadmapId, orderedMilestoneIds: validateStringArray(body?.orderedMilestoneIds, "orderedMilestoneIds") });
|
||||
roadmapStore.reorderMilestones({ roadmapId: paramValue(req.params.roadmapId), orderedMilestoneIds: validateStringArray(body?.orderedMilestoneIds, "orderedMilestoneIds") });
|
||||
return noContent();
|
||||
} catch (error) {
|
||||
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
||||
@@ -189,7 +195,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
const body = req.body as { title?: string; description?: string };
|
||||
try {
|
||||
return roadmapStore.updateMilestone(req.params.milestoneId, {
|
||||
return roadmapStore.updateMilestone(paramValue(req.params.milestoneId), {
|
||||
title: body.title !== undefined ? validateTitle(body.title) : undefined,
|
||||
description: body.description !== undefined ? validateDescription(body.description) : undefined,
|
||||
});
|
||||
@@ -199,7 +205,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
}),
|
||||
},
|
||||
{ method: "DELETE", path: "/roadmaps/milestones/:milestoneId", handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
roadmapStore.deleteMilestone(req.params.milestoneId);
|
||||
roadmapStore.deleteMilestone(paramValue(req.params.milestoneId));
|
||||
return noContent();
|
||||
}) },
|
||||
{
|
||||
@@ -210,7 +216,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
try {
|
||||
return {
|
||||
status: 201,
|
||||
body: roadmapStore.createFeature(req.params.milestoneId, {
|
||||
body: roadmapStore.createFeature(paramValue(req.params.milestoneId), {
|
||||
title: validateTitle(body?.title),
|
||||
description: validateDescription(body?.description),
|
||||
}),
|
||||
@@ -226,9 +232,9 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
try {
|
||||
const body = req.body as { orderedFeatureIds: string[] };
|
||||
const milestone = roadmapStore.getMilestone(req.params.milestoneId);
|
||||
if (!milestone) return notFound(`Milestone ${req.params.milestoneId} not found`);
|
||||
roadmapStore.reorderFeatures({ roadmapId: milestone.roadmapId, milestoneId: req.params.milestoneId, orderedFeatureIds: validateStringArray(body?.orderedFeatureIds, "orderedFeatureIds") });
|
||||
const milestone = roadmapStore.getMilestone(paramValue(req.params.milestoneId));
|
||||
if (!milestone) return notFound(`Milestone ${paramValue(req.params.milestoneId)} not found`);
|
||||
roadmapStore.reorderFeatures({ roadmapId: milestone.roadmapId, milestoneId: paramValue(req.params.milestoneId), orderedFeatureIds: validateStringArray(body?.orderedFeatureIds, "orderedFeatureIds") });
|
||||
return noContent();
|
||||
} catch (error) {
|
||||
return badRequest(error instanceof Error ? error.message : "Invalid input");
|
||||
@@ -241,7 +247,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
const body = req.body as { title?: string; description?: string };
|
||||
try {
|
||||
return roadmapStore.updateFeature(req.params.featureId, {
|
||||
return roadmapStore.updateFeature(paramValue(req.params.featureId), {
|
||||
title: body.title !== undefined ? validateTitle(body.title) : undefined,
|
||||
description: body.description !== undefined ? validateDescription(body.description) : undefined,
|
||||
});
|
||||
@@ -251,7 +257,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
}),
|
||||
},
|
||||
{ method: "DELETE", path: "/roadmaps/features/:featureId", handler: routeHandler((req, _ctx, roadmapStore) => {
|
||||
roadmapStore.deleteFeature(req.params.featureId);
|
||||
roadmapStore.deleteFeature(paramValue(req.params.featureId));
|
||||
return noContent();
|
||||
}) },
|
||||
{
|
||||
@@ -262,8 +268,8 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
if (!body?.targetMilestoneId) return badRequest("targetMilestoneId is required");
|
||||
if (typeof body.targetIndex !== "number") return badRequest("targetIndex must be a number");
|
||||
|
||||
const feature = roadmapStore.getFeature(req.params.featureId);
|
||||
if (!feature) return notFound(`Feature ${req.params.featureId} not found`);
|
||||
const feature = roadmapStore.getFeature(paramValue(req.params.featureId));
|
||||
if (!feature) return notFound(`Feature ${paramValue(req.params.featureId)} not found`);
|
||||
const fromMilestone = roadmapStore.getMilestone(feature.milestoneId);
|
||||
if (!fromMilestone) return notFound(`Source milestone ${feature.milestoneId} not found`);
|
||||
const toMilestone = roadmapStore.getMilestone(body.targetMilestoneId);
|
||||
@@ -271,7 +277,7 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
|
||||
roadmapStore.moveFeature({
|
||||
roadmapId: fromMilestone.roadmapId,
|
||||
featureId: req.params.featureId,
|
||||
featureId: paramValue(req.params.featureId),
|
||||
fromMilestoneId: feature.milestoneId,
|
||||
toMilestoneId: body.targetMilestoneId,
|
||||
targetOrderIndex: body.targetIndex,
|
||||
@@ -284,8 +290,8 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
method: "POST",
|
||||
path: "/roadmaps/:roadmapId/suggestions/milestones",
|
||||
handler: routeHandler(async (req, ctx, roadmapStore) => {
|
||||
const roadmap = roadmapStore.getRoadmap(req.params.roadmapId);
|
||||
if (!roadmap) return notFound(`Roadmap ${req.params.roadmapId} not found`);
|
||||
const roadmap = roadmapStore.getRoadmap(paramValue(req.params.roadmapId));
|
||||
if (!roadmap) return notFound(`Roadmap ${paramValue(req.params.roadmapId)} not found`);
|
||||
|
||||
try {
|
||||
validateSuggestionInput(req.body);
|
||||
@@ -318,8 +324,8 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
method: "POST",
|
||||
path: "/roadmaps/milestones/:milestoneId/suggestions/features",
|
||||
handler: routeHandler(async (req, ctx, roadmapStore) => {
|
||||
const milestone = roadmapStore.getMilestone(req.params.milestoneId);
|
||||
if (!milestone) return notFound(`Milestone ${req.params.milestoneId} not found`);
|
||||
const milestone = roadmapStore.getMilestone(paramValue(req.params.milestoneId));
|
||||
if (!milestone) return notFound(`Milestone ${paramValue(req.params.milestoneId)} not found`);
|
||||
const roadmap = roadmapStore.getRoadmap(milestone.roadmapId);
|
||||
if (!roadmap) return notFound(`Roadmap ${milestone.roadmapId} not found`);
|
||||
|
||||
@@ -360,25 +366,25 @@ export function createRoadmapPluginRoutes(): PluginRouteDefinition[] {
|
||||
{
|
||||
method: "GET",
|
||||
path: "/roadmaps/:roadmapId/export",
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getRoadmapExport(req.params.roadmapId)),
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getRoadmapExport(paramValue(req.params.roadmapId))),
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/roadmaps/:roadmapId/handoff",
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => ({
|
||||
mission: roadmapStore.getMissionPlanningHandoff(req.params.roadmapId),
|
||||
features: roadmapStore.listFeatureTaskPlanningHandoffs(req.params.roadmapId),
|
||||
mission: roadmapStore.getMissionPlanningHandoff(paramValue(req.params.roadmapId)),
|
||||
features: roadmapStore.listFeatureTaskPlanningHandoffs(paramValue(req.params.roadmapId)),
|
||||
})),
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/roadmaps/:roadmapId/handoff/mission",
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getMissionPlanningHandoff(req.params.roadmapId)),
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getMissionPlanningHandoff(paramValue(req.params.roadmapId))),
|
||||
},
|
||||
{
|
||||
method: "GET",
|
||||
path: "/roadmaps/:roadmapId/milestones/:milestoneId/features/:featureId/handoff/task",
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getRoadmapFeatureHandoff(req.params.roadmapId, req.params.milestoneId, req.params.featureId)),
|
||||
handler: routeHandler((req, _ctx, roadmapStore) => roadmapStore.getRoadmapFeatureHandoff(paramValue(req.params.roadmapId), paramValue(req.params.milestoneId), paramValue(req.params.featureId))),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ export async function generateMilestoneSuggestions(
|
||||
}
|
||||
})(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new ServiceUnavailableError("AI suggestion generation timed out. Please try again.")), SUGGESTION_TIMEOUT_MS),
|
||||
globalThis.setTimeout(() => reject(new ServiceUnavailableError("AI suggestion generation timed out. Please try again.")), SUGGESTION_TIMEOUT_MS),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -336,7 +336,7 @@ export async function generateFeatureSuggestions(
|
||||
}
|
||||
})(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new ServiceUnavailableError("AI suggestion generation timed out. Please try again.")), SUGGESTION_TIMEOUT_MS),
|
||||
globalThis.setTimeout(() => reject(new ServiceUnavailableError("AI suggestion generation timed out. Please try again.")), SUGGESTION_TIMEOUT_MS),
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { createRoadmapPluginRoutes } from "../routes/roadmap-routes.js";
|
||||
import type { PluginRouteDefinition } from "@fusion/core";
|
||||
|
||||
export declare function createRoadmapPluginRoutes(): PluginRouteDefinition[];
|
||||
|
||||
Reference in New Issue
Block a user