feat(FN-836): add workflow step reorder UX in TaskForm
- Add drag-and-drop and arrow-button reordering for workflow steps in TaskForm - Add CSS styles for reorder controls and drag states - Add comprehensive tests for reorder behavior in TaskForm, NewTaskModal, and TaskDetailModal - Simplify planning.ts by removing redundant JSON parse/stringify logic - Update AGENTS.md and README with workflow step ordering documentation - Remove unused changeset and dashboard README section
This commit is contained in:
@@ -4,7 +4,7 @@ import type { ToastType } from "../hooks/useToast";
|
||||
import { fetchModels, fetchSettings, fetchWorkflowSteps, refineText, getRefineErrorMessage, updateGlobalSettings, type RefinementType, type ModelInfo } from "../api";
|
||||
import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/modelPresets";
|
||||
import { CustomModelDropdown } from "./CustomModelDropdown";
|
||||
import { Sparkles, Globe } from "lucide-react";
|
||||
import { Sparkles, Globe, ChevronUp, ChevronDown, X } from "lucide-react";
|
||||
|
||||
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
|
||||
@@ -293,6 +293,35 @@ export function TaskForm({
|
||||
}
|
||||
}, [favoriteModels, favoriteProviders]);
|
||||
|
||||
// Workflow step reorder helpers
|
||||
const moveWorkflowStepUp = useCallback((index: number) => {
|
||||
if (index <= 0) return;
|
||||
const updated = [...selectedWorkflowSteps];
|
||||
[updated[index - 1], updated[index]] = [updated[index], updated[index - 1]];
|
||||
onWorkflowStepsChange(updated);
|
||||
}, [selectedWorkflowSteps, onWorkflowStepsChange]);
|
||||
|
||||
const moveWorkflowStepDown = useCallback((index: number) => {
|
||||
if (index >= selectedWorkflowSteps.length - 1) return;
|
||||
const updated = [...selectedWorkflowSteps];
|
||||
[updated[index], updated[index + 1]] = [updated[index + 1], updated[index]];
|
||||
onWorkflowStepsChange(updated);
|
||||
}, [selectedWorkflowSteps, onWorkflowStepsChange]);
|
||||
|
||||
const removeWorkflowStep = useCallback((stepId: string) => {
|
||||
onWorkflowStepsChange(selectedWorkflowSteps.filter((id) => id !== stepId));
|
||||
}, [selectedWorkflowSteps, onWorkflowStepsChange]);
|
||||
|
||||
// Build a lookup for step names (includes both fetched steps and built-in browser-verification)
|
||||
const workflowStepLookup = new Map<string, { name: string; description: string }>();
|
||||
for (const step of workflowSteps) {
|
||||
workflowStepLookup.set(step.id, { name: step.name, description: step.description });
|
||||
}
|
||||
workflowStepLookup.set("browser-verification", {
|
||||
name: "Browser Verification",
|
||||
description: "Verify web application functionality using browser automation (agent-browser)",
|
||||
});
|
||||
|
||||
const availableDeps = tasks
|
||||
.filter((t) => !dependencies.includes(t.id))
|
||||
.sort((a, b) => {
|
||||
@@ -655,6 +684,54 @@ export function TaskForm({
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Selected steps — execution order with reorder controls */}
|
||||
{selectedWorkflowSteps.length > 1 && (
|
||||
<div className="workflow-step-order" data-testid="workflow-step-order">
|
||||
<small className="workflow-step-order-label">Execution order:</small>
|
||||
{selectedWorkflowSteps.map((stepId, index) => {
|
||||
const stepInfo = workflowStepLookup.get(stepId);
|
||||
return (
|
||||
<div key={stepId} className="workflow-step-order-item" data-testid={`workflow-step-order-item-${stepId}`}>
|
||||
<span className="workflow-step-order-number">{index + 1}</span>
|
||||
<span className="workflow-step-order-name">{stepInfo?.name || stepId}</span>
|
||||
<div className="workflow-step-order-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => moveWorkflowStepUp(index)}
|
||||
disabled={disabled || index === 0}
|
||||
data-testid={`workflow-step-move-up-${stepId}`}
|
||||
title="Move up"
|
||||
>
|
||||
<ChevronUp size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => moveWorkflowStepDown(index)}
|
||||
disabled={disabled || index === selectedWorkflowSteps.length - 1}
|
||||
data-testid={`workflow-step-move-down-${stepId}`}
|
||||
title="Move down"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => removeWorkflowStep(stepId)}
|
||||
disabled={disabled}
|
||||
data-testid={`workflow-step-remove-${stepId}`}
|
||||
title="Remove"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
|
||||
@@ -7,6 +7,9 @@ import type { Task, Column } from "@fusion/core";
|
||||
vi.mock("lucide-react", () => ({
|
||||
Sparkles: () => null,
|
||||
Globe: () => null,
|
||||
ChevronUp: () => null,
|
||||
ChevronDown: () => null,
|
||||
X: () => null,
|
||||
}));
|
||||
|
||||
// Mock the api module
|
||||
@@ -415,4 +418,98 @@ describe("NewTaskModal", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Workflow step ordering tests (FN-836)
|
||||
describe("workflow step ordering", () => {
|
||||
it("sends ordered enabledWorkflowSteps in create payload when steps are selected in order", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const { props } = renderNewTaskModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-checkbox-WS-001")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Select WS-001, then WS-002 — order should be preserved
|
||||
const checkbox1 = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(checkbox1);
|
||||
|
||||
const checkbox2 = screen.getByTestId("workflow-step-checkbox-WS-002").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(checkbox2);
|
||||
|
||||
// Type description and submit
|
||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Ordered task" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreateTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabledWorkflowSteps: ["WS-001", "WS-002"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("sends reordered enabledWorkflowSteps after user reorders steps", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const { props } = renderNewTaskModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-checkbox-WS-001")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Select WS-001, then WS-002
|
||||
const checkbox1 = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(checkbox1);
|
||||
|
||||
const checkbox2 = screen.getByTestId("workflow-step-checkbox-WS-002").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(checkbox2);
|
||||
|
||||
// Now reorder: move WS-002 up
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-move-up-WS-002")).toBeTruthy();
|
||||
});
|
||||
fireEvent.click(screen.getByTestId("workflow-step-move-up-WS-002"));
|
||||
|
||||
// Type description and submit
|
||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Reordered task" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreateTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabledWorkflowSteps: ["WS-002", "WS-001"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("sends browser-verification and custom steps in selected order", async () => {
|
||||
const { props } = renderNewTaskModal();
|
||||
|
||||
// Select browser-verification first, then nothing else — order is just one
|
||||
const bvCheckbox = screen.getByTestId("browser-verification-checkbox").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(bvCheckbox);
|
||||
|
||||
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "BV task" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.onCreateTask).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabledWorkflowSteps: ["browser-verification"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,9 @@ vi.mock("lucide-react", () => ({
|
||||
RefreshCw: () => null,
|
||||
Plus: () => null,
|
||||
MessageSquare: () => null,
|
||||
ChevronUp: () => null,
|
||||
ChevronDown: () => null,
|
||||
X: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("../../hooks/useAgentLogs", () => ({
|
||||
@@ -3435,4 +3438,54 @@ describe("TaskDetailModal", () => {
|
||||
expect(screen.queryByText("Commits")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Workflow step ordering in edit mode (FN-836)", () => {
|
||||
it("sends ordered enabledWorkflowSteps when saving with reordered steps", async () => {
|
||||
const { updateTask, fetchWorkflowSteps } = await import("../../api");
|
||||
const mockUpdate = vi.mocked(updateTask);
|
||||
mockUpdate.mockResolvedValueOnce({ id: "FN-001" } as Task);
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask({
|
||||
id: "FN-001",
|
||||
column: "triage",
|
||||
title: "Test",
|
||||
description: "Desc",
|
||||
enabledWorkflowSteps: ["WS-001", "WS-002"],
|
||||
})}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Enter edit mode
|
||||
fireEvent.click(container.querySelector(".modal-edit-btn")!);
|
||||
|
||||
// Wait for workflow steps to load and reorder controls to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Move WS-002 up (swap with WS-001)
|
||||
fireEvent.click(screen.getByTestId("workflow-step-move-up-WS-002"));
|
||||
|
||||
// Save
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", expect.objectContaining({
|
||||
enabledWorkflowSteps: ["WS-002", "WS-001"],
|
||||
}), undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,9 @@ import type { Task, Column } from "@fusion/core";
|
||||
vi.mock("lucide-react", () => ({
|
||||
Sparkles: () => null,
|
||||
Globe: () => null,
|
||||
ChevronUp: () => null,
|
||||
ChevronDown: () => null,
|
||||
X: () => null,
|
||||
}));
|
||||
|
||||
// Mock the api module
|
||||
@@ -533,3 +536,206 @@ describe("TaskForm preset selection (FN-819)", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskForm workflow step reordering (FN-836)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("does not show reorder controls when no steps are selected", () => {
|
||||
renderTaskForm({ selectedWorkflowSteps: [] });
|
||||
expect(screen.queryByTestId("workflow-step-order")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not show reorder controls when only one step is selected", () => {
|
||||
renderTaskForm({ selectedWorkflowSteps: ["browser-verification"] });
|
||||
expect(screen.queryByTestId("workflow-step-order")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows reorder controls when two or more steps are selected", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("workflow-step-order-item-WS-001")).toBeTruthy();
|
||||
expect(screen.getByTestId("workflow-step-order-item-WS-002")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows numbered execution order", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("1")).toBeTruthy();
|
||||
expect(screen.getByText("2")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("disables move-up button on first step", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
const moveUpFirst = screen.getByTestId("workflow-step-move-up-WS-001") as HTMLButtonElement;
|
||||
expect(moveUpFirst.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("disables move-down button on last step", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
const moveDownLast = screen.getByTestId("workflow-step-move-down-WS-002") as HTMLButtonElement;
|
||||
expect(moveDownLast.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("calls onWorkflowStepsChange with swapped order when move-up is clicked", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const onWorkflowStepsChange = vi.fn();
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"], onWorkflowStepsChange });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Move WS-002 up (swap with WS-001)
|
||||
fireEvent.click(screen.getByTestId("workflow-step-move-up-WS-002"));
|
||||
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-002", "WS-001"]);
|
||||
});
|
||||
|
||||
it("calls onWorkflowStepsChange with swapped order when move-down is clicked", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const onWorkflowStepsChange = vi.fn();
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"], onWorkflowStepsChange });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Move WS-001 down (swap with WS-002)
|
||||
fireEvent.click(screen.getByTestId("workflow-step-move-down-WS-001"));
|
||||
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-002", "WS-001"]);
|
||||
});
|
||||
|
||||
it("calls onWorkflowStepsChange with step removed when remove button is clicked", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const onWorkflowStepsChange = vi.fn();
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"], onWorkflowStepsChange });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Remove WS-001
|
||||
fireEvent.click(screen.getByTestId("workflow-step-remove-WS-001"));
|
||||
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-002"]);
|
||||
});
|
||||
|
||||
it("shows browser-verification step name in reorder list", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "browser-verification"] });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// browser-verification should show its friendly name in the reorder list
|
||||
const orderItem1 = screen.getByTestId("workflow-step-order-item-WS-001");
|
||||
const orderItem2 = screen.getByTestId("workflow-step-order-item-browser-verification");
|
||||
expect(orderItem1.textContent).toContain("QA Check");
|
||||
expect(orderItem2.textContent).toContain("Browser Verification");
|
||||
});
|
||||
|
||||
it("preserves order when adding a new step via checkbox after reorder", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-003", name: "Doc Review", description: "Check docs", prompt: "Check docs", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const onWorkflowStepsChange = vi.fn();
|
||||
// Start with WS-001, WS-002
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002"], onWorkflowStepsChange });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Click checkbox to add WS-003 — it should be appended
|
||||
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-003").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-001", "WS-002", "WS-003"]);
|
||||
});
|
||||
|
||||
it("preserves order when removing a step via checkbox (not reorder remove)", async () => {
|
||||
const { fetchWorkflowSteps } = await import("../../api");
|
||||
vi.mocked(fetchWorkflowSteps).mockResolvedValueOnce([
|
||||
{ id: "WS-001", name: "QA Check", description: "Run tests", prompt: "Check tests", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-002", name: "Security Audit", description: "Check security", prompt: "Check security", enabled: true, createdAt: "", updatedAt: "" },
|
||||
{ id: "WS-003", name: "Doc Review", description: "Check docs", prompt: "Check docs", enabled: true, createdAt: "", updatedAt: "" },
|
||||
]);
|
||||
|
||||
const onWorkflowStepsChange = vi.fn();
|
||||
renderTaskForm({ selectedWorkflowSteps: ["WS-001", "WS-002", "WS-003"], onWorkflowStepsChange });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("workflow-step-order")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Uncheck WS-002 via checkbox
|
||||
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-002").querySelector('input[type="checkbox"]') as HTMLInputElement;
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(onWorkflowStepsChange).toHaveBeenCalledWith(["WS-001", "WS-003"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11335,6 +11335,99 @@ html .column.drag-over * {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
/* Workflow step reorder controls */
|
||||
.workflow-step-order {
|
||||
margin-top: var(--space-sm);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.workflow-step-order-label {
|
||||
display: block;
|
||||
margin-bottom: var(--space-xs);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.workflow-step-order-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: 4px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.workflow-step-order-item + .workflow-step-order-item {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
margin-top: 2px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.workflow-step-order-item:hover {
|
||||
background: var(--card-hover);
|
||||
}
|
||||
|
||||
.workflow-step-order-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workflow-step-order-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.workflow-step-order-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workflow-step-order-actions .btn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.workflow-step-order-actions .btn-icon:hover:not(:disabled) {
|
||||
background: var(--card-hover);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.workflow-step-order-actions .btn-icon:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.new-task-modal .form-group small {
|
||||
display: block;
|
||||
margin-top: var(--space-sm);
|
||||
|
||||
Reference in New Issue
Block a user