diff --git a/.changeset/fn-8476-coding-ideas-move.md b/.changeset/fn-8476-coding-ideas-move.md new file mode 100644 index 0000000000..22f63a89f3 --- /dev/null +++ b/.changeset/fn-8476-coding-ideas-move.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Restore the Coding Ideas detail action to move parked ideas to Todo. +category: fix +dev: Detail workflow move targets now resolve independently from supplied custom-field definitions. diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 95f156841c..5c489184e1 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -78,6 +78,7 @@ import { getRelativeTimeBucket } from "../utils/relativeTimeAgo"; import { isReviewBudgetExhaustedApproval } from "../utils/reviewBudgetApproval"; import { ACTIVE_STATUSES, resolveEffectiveExecutor, resolveEffectivePlanning, resolveEffectiveValidator, type ModelSelection } from "./effective-model-resolution"; import { TaskContextMenu, buildTaskActionMenuModel, getTaskPrAutomationLabel } from "./TaskContextMenu"; +import type { TaskContextMenuColumnFlags, TaskContextMenuColumnMetadata } from "./TaskContextMenu"; import { FLOATING_WINDOW_GEOMETRY_CHANGE_EVENT } from "./FloatingWindow"; import { useFileBrowser } from "../context/FileBrowserContext"; import type { DetailTaskInitialActionRequest } from "../hooks/useModalManager"; @@ -384,9 +385,9 @@ export interface TaskDetailModalProps { /** Project setting: true restores Chat-first tab order/default; false or missing uses Activity-first. */ taskDetailChatFirst?: boolean; /** Pre-resolved workflow field defs for this task's workflow (U13/KTD-14). - * When provided (e.g. threaded from a Board that already holds the payload) - * the modal skips its own board-workflows fetch entirely. Falls back to the - * self-fetch when absent (e.g. modal opened from non-board contexts). */ + * When provided, these remain authoritative for custom-field rendering. + * Move metadata still resolves independently because field definitions do not + * identify the selected workflow's ordered columns. */ workflowFieldDefs?: WorkflowFieldDefinition[] | null; } @@ -502,13 +503,27 @@ function normalizeTaskPriorityValue(priority: Task["priority"]): TaskPriority { : DEFAULT_TASK_PRIORITY; } -function resolveTaskWorkflowMetadata(payload: BoardWorkflowsPayload, taskId: string): { id: string; name: string; icon?: string; fields: WorkflowFieldDefinition[] | null } | null { +interface TaskWorkflowMetadata { + id: string; + name: string; + icon?: string; + fields: WorkflowFieldDefinition[] | null; + moveColumns: TaskContextMenuColumnMetadata[]; + currentColumnFlags?: TaskContextMenuColumnFlags; +} + +function resolveTaskWorkflowMetadata(payload: BoardWorkflowsPayload, task: Pick): TaskWorkflowMetadata | null { if (payload.flagEnabled !== true) return null; - const workflowId = payload.taskWorkflowIds[taskId] ?? payload.defaultWorkflowId; + const workflowId = payload.taskWorkflowIds[task.id] ?? payload.defaultWorkflowId; const workflow = payload.workflows.find((candidate) => candidate.id === workflowId); const name = workflow?.name?.trim(); if (!workflow || !name) return null; - return { id: workflow.id, name, icon: workflow.icon, fields: workflow.fields ?? null }; + + const moveColumns = workflow.columns + .filter((column) => column.flags.hiddenFromBoard !== true) + .map((column) => ({ id: column.id as ColumnId, label: column.name, flags: column.flags })); + const currentColumnFlags = moveColumns.find((column) => column.id === task.column)?.flags; + return { id: workflow.id, name, icon: workflow.icon, fields: workflow.fields ?? null, moveColumns, currentColumnFlags }; } function normalizeExecutionModeValue(executionMode: Task["executionMode"]): "standard" | "fast" { @@ -931,13 +946,18 @@ export function TaskDetailContent({ /* FNXC:WorkflowBadges 2026-06-29-00:00: Task details need a stable workflow-name badge because aggregate Board cards can mix tasks from multiple workflows. Resolve the badge name and custom field definitions from the same board-workflows payload so detail headers do not issue duplicate workflow-metadata fetches. + + FNXC:CodingIdeasWorkflow 2026-07-21-00:00: + Coding (Ideas) intake cards need selected-workflow columns to derive their truthful + move target through TaskContextMenu. Callers may supply field definitions, but fields + cannot encode ordered columns, so resolve move metadata independently without replacing + the caller-owned field definitions. */ const [taskWorkflowBadge, setTaskWorkflowBadge] = useState<{ id: string; name: string; icon?: string } | null>(null); + const [workflowMoveMetadata, setWorkflowMoveMetadata] = useState | null>(null); // Custom field definitions (U13/KTD-14). Resolved for this task's workflow // from the board-workflows payload; absent when the workflow declares none, // in which case the fields section renders nothing (today's UI byte-identical). - // When `workflowFieldDefsProp` is provided by the caller (e.g. the Board - // already holds the payload) we skip the self-fetch entirely. const [customFieldDefs, setCustomFieldDefs] = useState( workflowFieldDefsProp !== undefined ? (workflowFieldDefsProp ?? null) : null, ); @@ -949,40 +969,45 @@ export function TaskDetailContent({ setCustomFieldValues(task.customFields ?? {}); }, [task.id, task.customFields]); - // Resolve this task's workflow field definitions and display name once per task. Skipped when - // the caller supplies `workflowFieldDefs` directly (Board context). Best-effort: - // a failed fetch (or flag-OFF empty payload) leaves defs/name null → no section/badge. + // Resolve selected-workflow display and move metadata once per task. A supplied fields prop + // avoids a duplicate field lookup, but cannot replace this ordered-column lookup. useEffect(() => { - if (workflowFieldDefsProp !== undefined) { - // Prop-driven path: keep in sync if the prop changes (task switch etc.). - setCustomFieldDefs(workflowFieldDefsProp ?? null); - setTaskWorkflowBadge(null); - return; - } /* FNXC:WorkflowBadges 2026-06-29-16:48: - Mounted task-detail hosts can swap from one task to another (List split-pane, right dock, floating windows). Clear the previous workflow badge before the shared board-workflows lookup resolves so aggregate-board context never shows a stale cross-workflow label. + Mounted task-detail hosts can swap from one task to another (List split-pane, right dock, floating windows). Clear the previous workflow badge and move metadata before the shared board-workflows lookup resolves so aggregate-board context never shows stale cross-workflow labels or targets. */ - setCustomFieldDefs(null); + if (workflowFieldDefsProp !== undefined) { + setCustomFieldDefs(workflowFieldDefsProp ?? null); + } else { + setCustomFieldDefs(null); + } setTaskWorkflowBadge(null); + setWorkflowMoveMetadata(null); let cancelled = false; void fetchBoardWorkflows(projectId) .then((payload) => { if (cancelled) return; - const metadata = resolveTaskWorkflowMetadata(payload, task.id); - setCustomFieldDefs(metadata?.fields ?? null); + const metadata = resolveTaskWorkflowMetadata(payload, task); + if (workflowFieldDefsProp === undefined) { + setCustomFieldDefs(metadata?.fields ?? null); + } setTaskWorkflowBadge(metadata ? { id: metadata.id, name: metadata.name, icon: metadata.icon } : null); + setWorkflowMoveMetadata(metadata ? { + moveColumns: metadata.moveColumns, + currentColumnFlags: metadata.currentColumnFlags, + } : null); }) .catch(() => { if (!cancelled) { - setCustomFieldDefs(null); + if (workflowFieldDefsProp === undefined) setCustomFieldDefs(null); setTaskWorkflowBadge(null); + setWorkflowMoveMetadata(null); } }); return () => { cancelled = true; }; - }, [task.id, projectId, workflowFieldDefsProp]); + }, [task, projectId, workflowFieldDefsProp]); /* FNXC:PlannerOversight 2026-07-04-17:00: @@ -3516,6 +3541,8 @@ export function TaskDetailContent({ task, t, columnLabel, + currentColumnFlags: workflowMoveMetadata?.currentColumnFlags, + workflowMoveColumns: workflowMoveMetadata?.moveColumns, canRetryTask, hasDuplicateHandler: Boolean(onDuplicateTask), hasRetryHandler: Boolean(onRetryTask), @@ -3540,6 +3567,7 @@ export function TaskDetailContent({ task, t, columnLabel, + workflowMoveMetadata, canRetryTask, onDuplicateTask, onRetryTask, @@ -3561,7 +3589,8 @@ export function TaskDetailContent({ handleCheckPrStatus, handleBypassReview, ]); - const primaryMoveTransition = taskActionMenuModel.moveTransitions[0]?.column; + const primaryMoveAction = taskActionMenuModel.moveTransitions[0]; + const primaryMoveTransition = primaryMoveAction?.column; const secondaryMoveTransitions = taskActionMenuModel.moveTransitions.slice(1); const hasSecondaryMoveOptions = secondaryMoveTransitions.length > 0; const reviewAction = taskActionMenuModel.reviewAction; @@ -6334,12 +6363,12 @@ export function TaskDetailContent({ onClick={handleMoveButtonClick} onKeyDown={handleMoveButtonKeyDown} disabled={!primaryMoveTransition} - aria-label={primaryMoveTransition ? t("taskDetail.move.moveTo", "Move to {{column}}", { column: columnLabel(primaryMoveTransition) }) : undefined} + aria-label={primaryMoveAction?.primaryLabel} aria-haspopup={hasSecondaryMoveOptions ? "menu" : undefined} aria-expanded={hasSecondaryMoveOptions ? showMoveMenu : undefined} > - {t("taskDetail.move.moveTo", "Move to {{column}}", { column: primaryMoveTransition ? columnLabel(primaryMoveTransition) : "" })} + {primaryMoveAction?.primaryLabel ?? t("taskDetail.move.moveTo", "Move to {{column}}", { column: "" })} {hasSecondaryMoveOptions && (