feat(dashboard): select a custom workflow when creating a task
New-task modal and the quick-add card (expanded) now expose a workflow picker; the chosen workflow is applied to the new task via selectTaskWorkflow right after creation, non-blocking on failure.
This commit is contained in:
@@ -5,7 +5,7 @@ import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp, Bot, Max
|
||||
import { DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, type Task, type TaskPriority, type Settings } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, DuplicateCandidatesError } from "../api";
|
||||
import { checkDuplicateTasks, fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings, fetchAgents, selectTaskWorkflow, DuplicateCandidatesError } from "../api";
|
||||
import type { CreateTaskInput, ModelInfo, Agent, NodeInfo, DuplicateMatch } from "../api";
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
import { ModelSelectionModal } from "./ModelSelectionModal";
|
||||
@@ -13,6 +13,7 @@ import { NodeHealthDot } from "./NodeHealthDot";
|
||||
import { DuplicateWarningModal } from "./DuplicateWarningModal";
|
||||
import { applyPresetToSelection } from "../utils/modelPresets";
|
||||
import { getScopedItem, removeScopedItem, setScopedItem } from "../utils/projectStorage";
|
||||
import { WorkflowSelector } from "./WorkflowSelector";
|
||||
|
||||
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
const STORAGE_KEY = "kb-inline-create-text";
|
||||
@@ -114,6 +115,7 @@ export function InlineCreateCard({
|
||||
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// isDescriptionExpanded controls fullscreen description editing mode
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
@@ -338,6 +340,19 @@ export function InlineCreateCard({
|
||||
try {
|
||||
const task = await onSubmit(input);
|
||||
|
||||
// Apply custom workflow if selected (non-blocking — task already exists)
|
||||
if (selectedWorkflowId) {
|
||||
try {
|
||||
await selectTaskWorkflow(task.id, selectedWorkflowId, projectId);
|
||||
} catch (err) {
|
||||
if (addToast) {
|
||||
addToast(getErrorMessage(err) || "Failed to apply workflow", "error");
|
||||
} else {
|
||||
console.warn("Failed to apply workflow:", getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upload pending images as attachments
|
||||
if (pendingImages.length > 0) {
|
||||
const failures: string[] = [];
|
||||
@@ -376,6 +391,7 @@ export function InlineCreateCard({
|
||||
setShowAgentPicker(false);
|
||||
setIsModelModalOpen(false);
|
||||
setShowPresets(false);
|
||||
setSelectedWorkflowId(null);
|
||||
addToast(`Created ${task.id}`, "success");
|
||||
|
||||
// Collapse and clear localStorage after successful task creation
|
||||
@@ -400,6 +416,7 @@ export function InlineCreateCard({
|
||||
onSubmit,
|
||||
addToast,
|
||||
projectId,
|
||||
selectedWorkflowId,
|
||||
]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
@@ -1008,6 +1025,15 @@ export function InlineCreateCard({
|
||||
{browserVerification ? "Browser Verify ✓" : "Browser Verify"}
|
||||
</button>
|
||||
|
||||
<WorkflowSelector
|
||||
value={selectedWorkflowId}
|
||||
onChange={(id) => setSelectedWorkflowId(id)}
|
||||
projectId={projectId}
|
||||
addToast={addToast}
|
||||
label="Workflow"
|
||||
disabled={submitting}
|
||||
/>
|
||||
|
||||
<label className="inline-create-priority-wrap" htmlFor="inline-create-priority-select">
|
||||
<span className="visually-hidden">Priority</span>
|
||||
<select
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { DEFAULT_TASK_PRIORITY, type Task, type TaskCreateInput, type TaskPriority } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { uploadAttachment } from "../api";
|
||||
import { uploadAttachment, selectTaskWorkflow } from "../api";
|
||||
import { Bot } from "lucide-react";
|
||||
import { useSetupReadiness } from "../hooks/useSetupReadiness";
|
||||
import { SetupWarningBanner } from "./SetupWarningBanner";
|
||||
@@ -15,6 +15,7 @@ import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
|
||||
import { useNodes } from "../hooks/useNodes";
|
||||
import { useViewportMode } from "../hooks/useViewportMode";
|
||||
import { useAgentsMapCache } from "../hooks/useAgentsMapCache";
|
||||
import { WorkflowSelector } from "./WorkflowSelector";
|
||||
|
||||
interface NewTaskModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -53,6 +54,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string>("");
|
||||
const [presetMode, setPresetMode] = useState<"default" | "preset" | "custom">("default");
|
||||
const [hasDirtyState, setHasDirtyState] = useState(false);
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
|
||||
const [selectedWorkflowSteps, setSelectedWorkflowSteps] = useState<string[]>([]);
|
||||
const [workflowStepsExplicitlySet, setWorkflowStepsExplicitlySet] = useState(false);
|
||||
const [reviewLevel, setReviewLevel] = useState<number | undefined>(undefined);
|
||||
@@ -265,6 +267,15 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
|
||||
const task = await onCreateTask(createInput);
|
||||
|
||||
// Apply custom workflow if selected (non-blocking — task already exists)
|
||||
if (selectedWorkflowId) {
|
||||
try {
|
||||
await selectTaskWorkflow(task.id, selectedWorkflowId, projectId);
|
||||
} catch (err) {
|
||||
addToast(getErrorMessage(err) || "Failed to apply workflow", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Upload pending images as attachments
|
||||
if (pendingImages.length > 0) {
|
||||
const failures: string[] = [];
|
||||
@@ -291,6 +302,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
setThinkingLevel("");
|
||||
setSelectedPresetId("");
|
||||
setPresetMode("default");
|
||||
setSelectedWorkflowId(null);
|
||||
setSelectedWorkflowSteps([]);
|
||||
setWorkflowStepsExplicitlySet(false);
|
||||
setSelectedAgentId(null);
|
||||
@@ -310,7 +322,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [description, dependencies, pendingImages, executorModel, validatorModel, planningModel, thinkingLevel, isSubmitting, githubRepoOverrideInvalid, hasInvalidBranchSelection, onCreateTask, addToast, onClose, projectId, presetMode, selectedPresetId, selectedWorkflowSteps, workflowStepsExplicitlySet, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed]);
|
||||
}, [description, dependencies, pendingImages, executorModel, validatorModel, planningModel, thinkingLevel, isSubmitting, githubRepoOverrideInvalid, hasInvalidBranchSelection, onCreateTask, addToast, onClose, projectId, presetMode, selectedPresetId, selectedWorkflowId, selectedWorkflowSteps, workflowStepsExplicitlySet, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed]);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
@@ -452,6 +464,17 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Custom Workflow */}
|
||||
<div className="form-group">
|
||||
<WorkflowSelector
|
||||
value={selectedWorkflowId}
|
||||
onChange={(id) => setSelectedWorkflowId(id)}
|
||||
projectId={projectId}
|
||||
addToast={addToast}
|
||||
label="Custom workflow"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@ vi.mock("lucide-react", () => ({
|
||||
Bot: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
Workflow: () => null,
|
||||
}));
|
||||
|
||||
// Mock WorkflowSelector to avoid API calls in tests
|
||||
vi.mock("../WorkflowSelector", () => ({
|
||||
WorkflowSelector: ({ label }: { label?: string }) => <div data-testid="workflow-selector">{label ?? "Workflow"}</div>,
|
||||
}));
|
||||
|
||||
// Mock the api module
|
||||
@@ -35,6 +41,7 @@ vi.mock("../../api", () => ({
|
||||
refineText: vi.fn(),
|
||||
getRefineErrorMessage: vi.fn((err) => err?.message || "Failed to refine text. Please try again."),
|
||||
updateGlobalSettings: vi.fn().mockResolvedValue({}),
|
||||
selectTaskWorkflow: vi.fn().mockResolvedValue({ workflowId: null, enabledWorkflowSteps: [] }),
|
||||
}));
|
||||
|
||||
const mockConfirm = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user