feat(FN-883): add workflow step default-on support with task creation opt-out

- Add defaultOn field to WorkflowStep model, API endpoints (GET/POST/PATCH), and WorkflowStepManager UI
- Add skip-default-steps checkbox to TaskForm and NewTaskModal for explicit opt-out of default workflow steps
- Pre-select default workflow steps on new task creation forms automatically
- Add CSS styling for skip-default-steps toggle row
- Add comprehensive tests for WorkflowStepManager, TaskForm, and NewTaskModal components
- Add route tests for PATCH workflow-step endpoint and default selection logic
- Update README and task-structure reference docs
This commit is contained in:
gsxdsm
2026-04-05 10:20:42 -07:00
parent b8bc5469ef
commit ee6aa8f183
11 changed files with 528 additions and 5 deletions

View File

@@ -512,4 +512,100 @@ describe("NewTaskModal", () => {
});
});
});
// DefaultOn workflow step handling (FN-883)
describe("defaultOn workflow step handling", () => {
it("sends undefined enabledWorkflowSteps when no defaultOn steps and user hasn't interacted", 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: "" },
]);
const { props } = renderNewTaskModal();
await waitFor(() => {
expect(screen.getByTestId("workflow-step-checkbox-WS-001")).toBeTruthy();
});
// Don't interact with workflow steps at all
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "No interaction task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
enabledWorkflowSteps: undefined,
}),
);
});
});
it("sends empty array when user explicitly deselects all defaultOn 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, defaultOn: true, createdAt: "", updatedAt: "" },
]);
const { props } = renderNewTaskModal();
await waitFor(() => {
expect(screen.getByTestId("workflow-step-checkbox-WS-001")).toBeTruthy();
});
// Wait for auto-selection to happen
await waitFor(() => {
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
});
// User explicitly deselects the auto-selected step
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
fireEvent.click(checkbox);
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Deselected task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
enabledWorkflowSteps: [],
}),
);
});
});
it("sends defaultOn step IDs when user doesn't modify the auto-selected 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, defaultOn: true, createdAt: "", updatedAt: "" },
{ id: "WS-002", name: "Security", description: "Check security", prompt: "Check", enabled: true, defaultOn: false, createdAt: "", updatedAt: "" },
]);
const { props } = renderNewTaskModal();
await waitFor(() => {
expect(screen.getByTestId("workflow-step-checkbox-WS-001")).toBeTruthy();
});
// Wait for auto-selection to happen
await waitFor(() => {
const checkbox = screen.getByTestId("workflow-step-checkbox-WS-001").querySelector('input[type="checkbox"]') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
});
// Don't modify the selection — just submit.
// Since user hasn't explicitly changed steps, the explicitlySet flag is false,
// so the modal sends undefined (backend applies its own defaults)
fireEvent.change(screen.getByLabelText(/Description/i), { target: { value: "Auto-selected task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
enabledWorkflowSteps: undefined,
}),
);
});
});
});
});