From 2669bb17e7c5e1bfc8331e58255bbcb3098f5799 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 25 Jun 2026 11:03:48 -0700 Subject: [PATCH] FN-6980: fix inherited workflow details Fix task workflow details so inherited board defaults render consistently in the live Workflow tab. - Resolve null task workflow selections through board workflow mappings or the project default for read-only detail surfaces. - Load workflow graphs and optional steps from the effective workflow while preserving explicit selector state. - Add coverage for default inheritance, explicit custom workflows, cleared selections, stale workflow ids, and task detail progress fixtures. Files changed: .../app/components/WorkflowResultsTab.tsx | 60 ++++++++--- ...skDetailModal.models-progress-workflow.test.tsx | 63 +++++++++++ .../__tests__/TaskDetailModal.test-helpers.ts | 23 ++++ .../__tests__/WorkflowResultsTab.test.tsx | 118 ++++++++++++++++++++- 4 files changed, 243 insertions(+), 21 deletions(-) Fusion-Task-Id: FN-6980 Fusion-Task-Lineage: 2ee9ca68-4053-4d69-a4da-e2bfc22cfe43 --- .../app/components/WorkflowResultsTab.tsx | 60 ++++++--- ...ailModal.models-progress-workflow.test.tsx | 63 ++++++++++ .../__tests__/TaskDetailModal.test-helpers.ts | 23 ++++ .../__tests__/WorkflowResultsTab.test.tsx | 118 +++++++++++++++++- 4 files changed, 243 insertions(+), 21 deletions(-) diff --git a/packages/dashboard/app/components/WorkflowResultsTab.tsx b/packages/dashboard/app/components/WorkflowResultsTab.tsx index a87efbc6dc..6d7a05aa14 100644 --- a/packages/dashboard/app/components/WorkflowResultsTab.tsx +++ b/packages/dashboard/app/components/WorkflowResultsTab.tsx @@ -13,7 +13,7 @@ import remarkGfm from "remark-gfm"; import { ReactFlow, ReactFlowProvider } from "@xyflow/react"; import type { AgentLogEntry, Settings, Task, TaskDetail, WorkflowDefinition, WorkflowStep, WorkflowStepResult, ResolvedWorkflowOptionalStep } from "@fusion/core"; import { getErrorMessage, resolveTaskExecutionModel, resolveTaskPlanningModel, resolveTaskValidatorModel } from "@fusion/core"; -import { approveTaskWorkflowCli, fetchWorkflow, fetchWorkflows, fetchWorkflowSteps, fetchTaskWorkflow, fetchWorkflowOptionalSteps, selectTaskWorkflow, submitTaskWorkflowInput } from "../api"; +import { approveTaskWorkflowCli, fetchBoardWorkflows, fetchWorkflow, fetchWorkflows, fetchWorkflowSteps, fetchTaskWorkflow, fetchWorkflowOptionalSteps, selectTaskWorkflow, submitTaskWorkflowInput } from "../api"; import { WorkflowSelector } from "./WorkflowSelector"; import { phaseBadge } from "./workflow-phase-badge"; import { useAgentLogs } from "../hooks/useAgentLogs"; @@ -139,12 +139,12 @@ function getOutputPreview(output: string): string { // and the optional-steps dropdown). Imported above. function getWorkflowName( - selectedWorkflowId: string | null, + workflowId: string | null, workflows: WorkflowDefinition[], t: ReturnType["t"], ): string { - if (!selectedWorkflowId) return t("app:workflow.defaultWorkflow", "Default"); - const match = workflows.find((workflow) => workflow.id === selectedWorkflowId); + if (!workflowId) return t("app:workflow.noWorkflowAssigned", "No workflow assigned"); + const match = workflows.find((workflow) => workflow.id === workflowId); return match?.name || t("app:workflow.customWorkflowFallback", "Custom workflow"); } @@ -323,6 +323,7 @@ export function WorkflowResultsTab({ const [allWorkflowSteps, setAllWorkflowSteps] = useState([]); const [optionalWorkflowSteps, setOptionalWorkflowSteps] = useState([]); const [workflowDefinitions, setWorkflowDefinitions] = useState([]); + const [boardWorkflowFallbackId, setBoardWorkflowFallbackId] = useState(null); const [isEditing, setIsEditing] = useState(false); const [selectedWorkflowId, setSelectedWorkflowId] = useState(null); const [resumeError, setResumeError] = useState(null); @@ -387,13 +388,38 @@ export function WorkflowResultsTab({ }, [projectId]); useEffect(() => { - if (!graphExpanded || !selectedWorkflowId || workflowGraphCache[selectedWorkflowId]) return; + let cancelled = false; + setBoardWorkflowFallbackId(null); + fetchBoardWorkflows(projectId) + .then((payload) => { + if (cancelled) return; + const mappedWorkflowId = payload.taskWorkflowIds?.[taskId] || null; + const defaultWorkflowId = payload.defaultWorkflowId || null; + setBoardWorkflowFallbackId(payload.flagEnabled ? (mappedWorkflowId ?? defaultWorkflowId) : null); + }) + .catch(() => { + if (!cancelled) setBoardWorkflowFallbackId(null); + }); + return () => { + cancelled = true; + }; + }, [taskId, projectId]); + + /* + FNXC:TaskWorkflowDetails 2026-06-24-09:50: + A null per-task workflow selection means "inherit the board workflow" when board-workflows supplies a task mapping or project default. Use this effective id for read-only task-detail surfaces while keeping the explicit selection value for WorkflowSelector. + */ + const effectiveWorkflowId = selectedWorkflowId ?? boardWorkflowFallbackId; + const graphCacheKey = effectiveWorkflowId ? `${projectId ?? ""}::${effectiveWorkflowId}` : null; + + useEffect(() => { + if (!graphExpanded || !effectiveWorkflowId || !graphCacheKey || workflowGraphCache[graphCacheKey]) return; let cancelled = false; setWorkflowGraphLoading(true); - fetchWorkflow(selectedWorkflowId, projectId) + fetchWorkflow(effectiveWorkflowId, projectId) .then((definition) => { if (!cancelled) { - setWorkflowGraphCache((prev) => ({ ...prev, [selectedWorkflowId]: definition })); + setWorkflowGraphCache((prev) => ({ ...prev, [graphCacheKey]: definition })); } }) .catch(() => { @@ -405,7 +431,7 @@ export function WorkflowResultsTab({ return () => { cancelled = true; }; - }, [graphExpanded, selectedWorkflowId, projectId, workflowGraphCache]); + }, [graphExpanded, effectiveWorkflowId, graphCacheKey, projectId, workflowGraphCache]); // Check if any result has pending status const hasPendingStep = results.some((r) => r.status === "pending"); @@ -436,11 +462,13 @@ export function WorkflowResultsTab({ }; }, [projectId]); - const effectiveOptionalStepsWorkflowId = selectedWorkflowId || "builtin:coding"; - useEffect(() => { + if (!effectiveWorkflowId) { + setOptionalWorkflowSteps([]); + return; + } let cancelled = false; - fetchWorkflowOptionalSteps(effectiveOptionalStepsWorkflowId, projectId) + fetchWorkflowOptionalSteps(effectiveWorkflowId, projectId) .then((steps) => { if (!cancelled) setOptionalWorkflowSteps(steps); }) @@ -450,7 +478,7 @@ export function WorkflowResultsTab({ return () => { cancelled = true; }; - }, [effectiveOptionalStepsWorkflowId, projectId]); + }, [effectiveWorkflowId, projectId]); const selectedWorkflowSteps = enabledWorkflowSteps ?? []; @@ -574,11 +602,11 @@ export function WorkflowResultsTab({ }); }, [selectedWorkflowSteps, workflowStepLookup, t]); - const workflowName = useMemo(() => getWorkflowName(selectedWorkflowId, workflowDefinitions, t), [selectedWorkflowId, workflowDefinitions, t]); + const workflowName = useMemo(() => getWorkflowName(effectiveWorkflowId, workflowDefinitions, t), [effectiveWorkflowId, workflowDefinitions, t]); const executionPhase = useMemo(() => getExecutionPhase(task, taskStatus, taskPausedReason, results, t), [task, taskStatus, taskPausedReason, results, t]); const aggregateResult = useMemo(() => getAggregateWorkflowResult(results, t), [results, t]); const completedStepCount = useMemo(() => results.filter((result) => ["passed", "skipped", "failed", "advisory_failure"].includes(result.status)).length, [results]); - const graphWorkflow = selectedWorkflowId ? workflowGraphCache[selectedWorkflowId] : undefined; + const graphWorkflow = graphCacheKey ? workflowGraphCache[graphCacheKey] : undefined; const graphFlow = useMemo(() => (graphWorkflow ? irToFlow(graphWorkflow) : null), [graphWorkflow]); const effectiveExecutor = useMemo(() => (task ? resolveTaskExecutionModel(task, settings) : undefined), [task, settings]); const effectiveValidator = useMemo(() => (task ? resolveTaskValidatorModel(task, settings) : undefined), [task, settings]); @@ -1011,7 +1039,7 @@ export function WorkflowResultsTab({ {graphExpanded && (
- {!selectedWorkflowId ? ( + {!effectiveWorkflowId ? (

{t("app:workflow.noWorkflowAssigned", "No workflow assigned")}

@@ -1050,7 +1078,7 @@ export function WorkflowResultsTab({

{t("app:workflow.workflowName", "Workflow")}

- {canEdit && selectedWorkflowId && onEditWorkflow && ( + {canEdit && effectiveWorkflowId && onEditWorkflow && (