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:
gsxdsm
2026-04-04 02:21:56 -07:00
parent 224b0603e8
commit 81e61f76f9
7 changed files with 534 additions and 5 deletions

View File

@@ -1110,6 +1110,8 @@ Tasks store their enabled workflow step IDs in `task.json`:
} }
``` ```
The order of IDs in `enabledWorkflowSteps` determines execution order — the engine iterates the array sequentially. Users can reorder steps in the task create/edit form using ▲/▼ controls when two or more steps are selected.
### API ### API
- `GET /api/workflow-steps` — List all workflow step definitions - `GET /api/workflow-steps` — List all workflow step definitions

View File

@@ -633,10 +633,11 @@ Click **Add** on any template to create a customizable workflow step.
### Using Workflow Steps ### Using Workflow Steps
1. When creating a new task, check the workflow steps you want to run 1. When creating or editing a task, check the workflow steps you want to run
2. After the main task executor completes, each selected workflow step runs automatically 2. **Reorder steps** — When two or more steps are selected, an execution-order panel appears showing the numbered sequence. Use the ▲/▼ buttons to change the order
3. The task only moves to in-review after all workflow steps pass 3. Steps execute sequentially in the saved order — the first selected step runs first, then the next, and so on
4. View results in the **Workflow** tab of the task detail modal 4. The task only moves to in-review after all workflow steps pass
5. View results in the **Workflow** tab of the task detail modal
Workflow step agents use **readonly tools** (no modifications). If a workflow step fails, the task is marked as failed and won't move to in-review. Workflow step agents use **readonly tools** (no modifications). If a workflow step fails, the task is marked as failed and won't move to in-review.

View File

@@ -4,7 +4,7 @@ import type { ToastType } from "../hooks/useToast";
import { fetchModels, fetchSettings, fetchWorkflowSteps, refineText, getRefineErrorMessage, updateGlobalSettings, type RefinementType, type ModelInfo } from "../api"; import { fetchModels, fetchSettings, fetchWorkflowSteps, refineText, getRefineErrorMessage, updateGlobalSettings, type RefinementType, type ModelInfo } from "../api";
import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/modelPresets"; import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/modelPresets";
import { CustomModelDropdown } from "./CustomModelDropdown"; 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"]; const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -293,6 +293,35 @@ export function TaskForm({
} }
}, [favoriteModels, favoriteProviders]); }, [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 const availableDeps = tasks
.filter((t) => !dependencies.includes(t.id)) .filter((t) => !dependencies.includes(t.id))
.sort((a, b) => { .sort((a, b) => {
@@ -655,6 +684,54 @@ export function TaskForm({
</div> </div>
</label> </label>
</div> </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> </div>
{/* Attachments */} {/* Attachments */}

View File

@@ -7,6 +7,9 @@ import type { Task, Column } from "@fusion/core";
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
Sparkles: () => null, Sparkles: () => null,
Globe: () => null, Globe: () => null,
ChevronUp: () => null,
ChevronDown: () => null,
X: () => null,
})); }));
// Mock the api module // 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"],
}),
);
});
});
});
}); });

View File

@@ -35,6 +35,9 @@ vi.mock("lucide-react", () => ({
RefreshCw: () => null, RefreshCw: () => null,
Plus: () => null, Plus: () => null,
MessageSquare: () => null, MessageSquare: () => null,
ChevronUp: () => null,
ChevronDown: () => null,
X: () => null,
})); }));
vi.mock("../../hooks/useAgentLogs", () => ({ vi.mock("../../hooks/useAgentLogs", () => ({
@@ -3435,4 +3438,54 @@ describe("TaskDetailModal", () => {
expect(screen.queryByText("Commits")).toBeNull(); 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);
});
});
});
}); });

View File

@@ -7,6 +7,9 @@ import type { Task, Column } from "@fusion/core";
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
Sparkles: () => null, Sparkles: () => null,
Globe: () => null, Globe: () => null,
ChevronUp: () => null,
ChevronDown: () => null,
X: () => null,
})); }));
// Mock the api module // 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"]);
});
});

View File

@@ -11335,6 +11335,99 @@ html .column.drag-over * {
margin-bottom: 4px !important; 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 { .new-task-modal .form-group small {
display: block; display: block;
margin-top: var(--space-sm); margin-top: var(--space-sm);