feat(FN-3265): wire Insights create-task to real task creation with mobile
Merges three features: wires the Insights view's "create task" action to real task creation (FN-3265) with tests, adds mobile-responsive collapse behavior in AgentsView when selecting live runs (FN-3271), and expands regression test coverage for worker budget handling (FN-3263). Changes span the das Fusion-Task-Id: FN-3265
This commit is contained in:
@@ -470,6 +470,24 @@ function AppInner() {
|
||||
closeTaskDetail: modalManager.closeDetailTask,
|
||||
});
|
||||
|
||||
const handleInsightTaskCreate = useCallback(
|
||||
async ({ insightId, title, description }: { insightId: string; title: string; description: string }) => {
|
||||
await createTask({
|
||||
title,
|
||||
description,
|
||||
column: "triage",
|
||||
source: {
|
||||
sourceType: "dashboard_ui",
|
||||
sourceMetadata: {
|
||||
origin: "insights",
|
||||
insightId,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[createTask],
|
||||
);
|
||||
|
||||
// Task handlers
|
||||
const {
|
||||
handleBoardQuickCreate,
|
||||
@@ -782,6 +800,7 @@ function AppInner() {
|
||||
projectId={currentProject?.id}
|
||||
addToast={addToast}
|
||||
onClose={() => handleChangeTaskView("board")}
|
||||
onCreateTask={handleInsightTaskCreate}
|
||||
/>
|
||||
</Suspense>
|
||||
</PageErrorBoundary>
|
||||
|
||||
@@ -30,7 +30,7 @@ interface InsightsViewProps {
|
||||
projectId?: string;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
onClose?: () => void;
|
||||
onCreateTask?: (title: string, description: string) => void;
|
||||
onCreateTask?: (payload: { insightId: string; title: string; description: string }) => Promise<void>;
|
||||
}
|
||||
|
||||
const CATEGORY_ICONS: Record<InsightCategory, React.ComponentType<{ size?: number; className?: string }>> = {
|
||||
@@ -142,13 +142,25 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
|
||||
try {
|
||||
setStatusMessage(`Creating task from "${title}"...`);
|
||||
setStatusType("info");
|
||||
const taskData = await createTaskFromInsight(id);
|
||||
if (taskData && onCreateTask) {
|
||||
onCreateTask(taskData.title, taskData.description);
|
||||
|
||||
if (!onCreateTask) {
|
||||
throw new Error("Task creation is unavailable in this view");
|
||||
}
|
||||
|
||||
const taskData = await createTaskFromInsight(id);
|
||||
if (!taskData) {
|
||||
throw new Error("Failed to prepare task payload from insight");
|
||||
}
|
||||
|
||||
await onCreateTask({
|
||||
insightId: id,
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
});
|
||||
|
||||
setStatusMessage(`Task created from "${title}"`);
|
||||
setStatusType("success");
|
||||
addToast(`Task created: ${taskData?.title ?? title}`, "success");
|
||||
addToast(`Task created: ${taskData.title}`, "success");
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to create task";
|
||||
setStatusMessage(message);
|
||||
|
||||
@@ -63,9 +63,11 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
});
|
||||
});
|
||||
|
||||
const mockCreateTask = vi.fn();
|
||||
|
||||
const mockUseTasks = vi.fn(() => ({
|
||||
tasks: [],
|
||||
createTask: vi.fn(),
|
||||
createTask: mockCreateTask,
|
||||
moveTask: vi.fn(),
|
||||
deleteTask: vi.fn(),
|
||||
mergeTask: vi.fn(),
|
||||
@@ -83,6 +85,27 @@ vi.mock("../../hooks/useTasks", () => ({
|
||||
}));
|
||||
|
||||
// Mock useRemoteNodeData
|
||||
const mockUseInsights = vi.fn(() => ({
|
||||
sections: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
latestRun: null,
|
||||
isRunInFlight: false,
|
||||
runError: null,
|
||||
refresh: vi.fn(),
|
||||
runInsights: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
dismissStates: new Map(),
|
||||
createTaskStates: new Map(),
|
||||
totalCount: 0,
|
||||
dismissedCount: 0,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useInsights", () => ({
|
||||
useInsights: (..._args: unknown[]) => mockUseInsights(),
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useRemoteNodeData", () => ({
|
||||
useRemoteNodeData: vi.fn(() => ({
|
||||
projects: [],
|
||||
@@ -464,10 +487,11 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockSubscribeSse.mockReset();
|
||||
mockSubscribeSse.mockReturnValue(vi.fn());
|
||||
mockCreateTask.mockReset();
|
||||
mockUseTasks.mockReset();
|
||||
mockUseTasks.mockImplementation(() => ({
|
||||
tasks: [],
|
||||
createTask: vi.fn(),
|
||||
createTask: mockCreateTask,
|
||||
moveTask: vi.fn(),
|
||||
deleteTask: vi.fn(),
|
||||
mergeTask: vi.fn(),
|
||||
@@ -516,6 +540,23 @@ beforeEach(() => {
|
||||
mockGetSkippedSteps.mockReturnValue([]);
|
||||
mockGetStepData.mockReset();
|
||||
mockGetStepData.mockReturnValue(null);
|
||||
mockUseInsights.mockReset();
|
||||
mockUseInsights.mockImplementation(() => ({
|
||||
sections: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
latestRun: null,
|
||||
isRunInFlight: false,
|
||||
runError: null,
|
||||
refresh: vi.fn(),
|
||||
runInsights: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
createTask: vi.fn(),
|
||||
dismissStates: new Map(),
|
||||
createTaskStates: new Map(),
|
||||
totalCount: 0,
|
||||
dismissedCount: 0,
|
||||
}));
|
||||
});
|
||||
|
||||
describe("App backend-unreachable first-run flow", () => {
|
||||
@@ -1707,6 +1748,80 @@ describe("App view switching", () => {
|
||||
expect(document.querySelector(".agents-view")).toBeNull();
|
||||
});
|
||||
|
||||
it("creates a real triage task from insights using dashboard task creation flow", async () => {
|
||||
mockUseInsights.mockImplementation(() => ({
|
||||
sections: [
|
||||
{
|
||||
category: "features",
|
||||
label: "Features",
|
||||
items: [
|
||||
{
|
||||
id: "INS-1",
|
||||
projectId: DEFAULT_PROJECT_ID,
|
||||
title: "Insight title",
|
||||
content: "Insight content",
|
||||
category: "features",
|
||||
status: "generated",
|
||||
fingerprint: "fp-ins-1",
|
||||
provenance: { trigger: "manual" },
|
||||
lastRunId: null,
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
error: null,
|
||||
latestRun: null,
|
||||
isRunInFlight: false,
|
||||
runError: null,
|
||||
refresh: vi.fn(),
|
||||
runInsights: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
createTask: vi.fn().mockResolvedValue({
|
||||
title: "Task from insight",
|
||||
description: "Use this insight as a task description",
|
||||
}),
|
||||
dismissStates: new Map(),
|
||||
createTaskStates: new Map(),
|
||||
totalCount: 1,
|
||||
dismissedCount: 0,
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("view-toggle-overflow-trigger")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("view-toggle-overflow-trigger"));
|
||||
fireEvent.click(screen.getByTestId("view-overflow-insights"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("create-task-INS-1")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId("create-task-INS-1"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateTask).toHaveBeenCalledWith({
|
||||
title: "Task from insight",
|
||||
description: "Use this insight as a task description",
|
||||
column: "triage",
|
||||
source: {
|
||||
sourceType: "dashboard_ui",
|
||||
sourceMetadata: {
|
||||
origin: "insights",
|
||||
insightId: "INS-1",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("persists insights view preference to localStorage", async () => {
|
||||
localStorage.removeItem(taskViewStorageKey());
|
||||
|
||||
|
||||
@@ -68,22 +68,15 @@ vi.mock("lucide-react", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the createTask API
|
||||
vi.mock("../../api", () => ({
|
||||
createTask: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useInsights } from "../../hooks/useInsights";
|
||||
import { createTask } from "../../api";
|
||||
|
||||
const mockUseInsights = vi.mocked(useInsights);
|
||||
const mockCreateTask = vi.mocked(createTask);
|
||||
|
||||
describe("InsightsView", () => {
|
||||
const defaultProps = {
|
||||
addToast: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
onCreateTask: vi.fn(),
|
||||
onCreateTask: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const mockSections = [
|
||||
@@ -722,6 +715,7 @@ describe("InsightsView", () => {
|
||||
|
||||
it("should trigger create task on insight action click", async () => {
|
||||
const createTaskFn = vi.fn().mockResolvedValue({ title: "New Task", description: "Task description" });
|
||||
const onCreateTask = vi.fn().mockResolvedValue(undefined);
|
||||
const sectionsWithInsight = [
|
||||
{
|
||||
category: "features" as const,
|
||||
@@ -764,7 +758,7 @@ describe("InsightsView", () => {
|
||||
dismissedCount: 0,
|
||||
});
|
||||
|
||||
render(<InsightsView {...defaultProps} />);
|
||||
render(<InsightsView {...defaultProps} onCreateTask={onCreateTask} />);
|
||||
|
||||
const createButton = screen.getByTestId("create-task-INS-1");
|
||||
await act(async () => {
|
||||
@@ -772,7 +766,71 @@ describe("InsightsView", () => {
|
||||
});
|
||||
|
||||
expect(createTaskFn).toHaveBeenCalledWith("INS-1");
|
||||
expect(defaultProps.onCreateTask).toHaveBeenCalledWith("New Task", "Task description");
|
||||
expect(onCreateTask).toHaveBeenCalledWith({
|
||||
insightId: "INS-1",
|
||||
title: "New Task",
|
||||
description: "Task description",
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.addToast).toHaveBeenCalledWith("Task created: New Task", "success");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error and skips success toast when app-level task creation fails", async () => {
|
||||
const createTaskFn = vi.fn().mockResolvedValue({ title: "New Task", description: "Task description" });
|
||||
const onCreateTask = vi.fn().mockRejectedValue(new Error("Task creation is unavailable in this view"));
|
||||
const sectionsWithInsight = [
|
||||
{
|
||||
category: "features" as const,
|
||||
label: "Features",
|
||||
items: [
|
||||
{
|
||||
id: "INS-1",
|
||||
projectId: "test",
|
||||
title: "Test Insight",
|
||||
content: "Content",
|
||||
category: "features" as const,
|
||||
status: "generated" as const,
|
||||
fingerprint: "fp1",
|
||||
provenance: { trigger: "manual" as const },
|
||||
lastRunId: null,
|
||||
createdAt: "2024-01-01T00:00:00Z",
|
||||
updatedAt: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
...mockSections.slice(1),
|
||||
];
|
||||
|
||||
mockUseInsights.mockReturnValue({
|
||||
sections: sectionsWithInsight,
|
||||
loading: false,
|
||||
error: null,
|
||||
latestRun: null,
|
||||
isRunInFlight: false,
|
||||
runError: null,
|
||||
refresh: vi.fn(),
|
||||
runInsights: vi.fn(),
|
||||
dismiss: vi.fn(),
|
||||
createTask: createTaskFn,
|
||||
dismissStates: new Map(),
|
||||
createTaskStates: new Map(),
|
||||
totalCount: 1,
|
||||
dismissedCount: 0,
|
||||
});
|
||||
|
||||
render(<InsightsView {...defaultProps} onCreateTask={onCreateTask} />);
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId("create-task-INS-1"));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(defaultProps.addToast).toHaveBeenCalledWith("Task creation is unavailable in this view", "error");
|
||||
});
|
||||
expect(defaultProps.addToast).not.toHaveBeenCalledWith("Task created: New Task", "success");
|
||||
});
|
||||
|
||||
it("should show toast on run success", async () => {
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* Insights REST API Routes
|
||||
*
|
||||
* Provides CRUD endpoints for project insights and insight generation runs.
|
||||
* Also includes action endpoints for running insight generation and creating tasks from insights.
|
||||
* Also includes action endpoints for running insight generation and preparing task payload drafts from insights.
|
||||
*
|
||||
* Endpoints:
|
||||
* - Insights: GET /, GET /:id, PATCH /:id, DELETE /:id
|
||||
* - Runs: GET /runs, POST /runs, GET /runs/:id
|
||||
* - Actions: POST /run (trigger manual run), POST /:id/dismiss, POST /:id/create-task
|
||||
* - Actions: POST /run (trigger manual run), POST /:id/dismiss, POST /:id/create-task (returns suggested task title/description draft)
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
|
||||
Reference in New Issue
Block a user