fix: clarify required automation fields (#1312)

This commit is contained in:
Carlos Florêncio
2026-06-08 20:06:02 +01:00
committed by GitHub
parent 5d6fe29293
commit 2b068fbbf2
4 changed files with 159 additions and 6 deletions
@@ -30,6 +30,7 @@ import { PromptSection } from "./prompt-section";
import { ConfigSection } from "./config-section";
import { RunsSection } from "./runs-section";
import { WebhookCreatedDialog } from "./webhook-created-dialog";
import { RequiredFieldLabel } from "./required-field-label";
import {
type CreatedWebhookDetails,
type FormState,
@@ -353,14 +354,21 @@ export function AutomationEditor({ workspaceId, automationId }: AutomationEditor
return (
<div className="max-w-3xl space-y-6" data-testid="automation-editor">
<div className="space-y-2">
<Label htmlFor="automation-name">Name</Label>
<RequiredFieldLabel htmlFor="automation-name">Name</RequiredFieldLabel>
<Input
id="automation-name"
data-testid="automation-name-input"
value={form.name}
onChange={(e) => updateField("name", e.target.value)}
placeholder="Automation name"
aria-describedby={!form.name.trim() ? "automation-name-help" : undefined}
aria-invalid={!form.name.trim() ? true : undefined}
/>
{!form.name.trim() && (
<p id="automation-name-help" className="text-xs text-muted-foreground">
Enter an automation name to enable saving.
</p>
)}
</div>
<Separator />
<WhenSection
@@ -0,0 +1,99 @@
import { cleanup, render, screen } from "@testing-library/react";
import type { ComponentProps } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
const mockState = {
workflows: {
items: [{ id: "workflow-1", name: "Build" }],
},
agentProfiles: {
items: [],
},
executors: {
items: [],
},
};
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: typeof mockState) => unknown) => selector(mockState),
}));
vi.mock("@/hooks/domains/settings/use-settings-data", () => ({
useSettingsData: vi.fn(),
}));
vi.mock("@/hooks/use-workflows", () => ({
useWorkflows: vi.fn(),
}));
vi.mock("@/hooks/domains/workspace/use-repositories", () => ({
useRepositories: () => ({ repositories: [] }),
}));
vi.mock("@/app/actions/workspaces", () => ({
discoverRepositoriesAction: vi.fn().mockResolvedValue({ repositories: [] }),
}));
vi.mock("@/lib/api/domains/workflow-api", () => ({
listWorkflowSteps: vi.fn().mockResolvedValue({ steps: [] }),
}));
import { ConfigSection } from "./config-section";
function renderConfigSection(overrides: Partial<ComponentProps<typeof ConfigSection>> = {}) {
return render(
<ConfigSection
workspaceId="workspace-1"
workflowId=""
workflowStepId=""
agentProfileId=""
executorProfileId=""
repositorySelection={{ kind: "none" }}
executionMode="task"
conditionType={null}
onWorkflowChange={() => {}}
onStepChange={() => {}}
onAgentProfileChange={() => {}}
onExecutorProfileChange={() => {}}
onRepositoryChange={() => {}}
onExecutionModeChange={() => {}}
{...overrides}
/>,
);
}
describe("ConfigSection", () => {
afterEach(cleanup);
it("marks task workflow fields as required and explains missing selections", () => {
renderConfigSection();
screen.getByText("Workflow");
screen.getByText("Workflow Step");
expect(screen.getAllByText("required")).toHaveLength(2);
screen.getByText("Select a workflow to enable saving.");
screen.getByText("Select a workflow before choosing a step.");
expect(screen.getByTestId("workflow-selector").getAttribute("aria-describedby")).toBe(
"workflow-selector-help",
);
});
it("changes step help text once a workflow is selected", () => {
renderConfigSection({ workflowId: "workflow-1" });
expect(screen.queryByText("Select a workflow to enable saving.")).toBeNull();
expect(screen.queryByText("Select a workflow before choosing a step.")).toBeNull();
screen.getByText("Select a workflow step to enable saving.");
expect(screen.getByTestId("workflow-step-selector").getAttribute("aria-describedby")).toBe(
"workflow-step-selector-help",
);
});
it("hides workflow required markers in run mode", () => {
renderConfigSection({ executionMode: "run" });
expect(screen.queryByText("Workflow")).toBeNull();
expect(screen.queryByText("Workflow Step")).toBeNull();
expect(screen.queryAllByText("required")).toHaveLength(0);
});
});
@@ -11,6 +11,7 @@ import { discoverRepositoriesAction } from "@/app/actions/workspaces";
import { listWorkflowSteps } from "@/lib/api/domains/workflow-api";
import type { LocalRepository, Repository } from "@/lib/types/http";
import type { ExecutionMode, TriggerType } from "@/lib/types/automation";
import { RequiredFieldLabel } from "./required-field-label";
// RepositorySelection mirrors the task-create dialog's two-tier model: a
// registered workspace repository (keyed by id) OR a filesystem-discovered
@@ -121,6 +122,12 @@ function useDiscoveredRepositories(workspaceId: string) {
type StepOption = { id: string; name: string };
function getWorkflowStepHelpText(workflowId: string, workflowStepId: string): string | undefined {
if (!workflowId) return "Select a workflow before choosing a step.";
if (!workflowStepId) return "Select a workflow step to enable saving.";
return undefined;
}
function useWorkflowSteps(workflowId: string) {
const [steps, setSteps] = useState<StepOption[]>([]);
@@ -190,7 +197,7 @@ export function ConfigSection({
<Label className="text-xs uppercase tracking-wider text-muted-foreground">
Configuration
</Label>
<div className="grid grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<SelectField
testId="execution-mode-selector"
label="Execution Mode"
@@ -269,20 +276,23 @@ function WorkflowFields({
<SelectField
testId="workflow-selector"
label="Workflow"
required
value={workflowId}
onChange={onWorkflowChange}
placeholder="Select workflow"
items={workflows.map((w) => ({ id: w.id, label: w.name }))}
helpText={!hasWorkflow ? "Select a workflow to enable saving." : undefined}
/>
<SelectField
testId="workflow-step-selector"
label="Workflow Step"
required
value={workflowStepId}
onChange={onStepChange}
placeholder={hasWorkflow ? "Select step" : "Pick a workflow first"}
items={steps.map((s) => ({ id: s.id, label: s.name }))}
disabled={!hasWorkflow}
helpText={hasWorkflow ? undefined : "Pick a workflow above to load its steps."}
helpText={getWorkflowStepHelpText(workflowId, workflowStepId)}
/>
</>
);
@@ -297,6 +307,7 @@ function SelectField({
items,
disabled,
helpText,
required,
}: {
testId?: string;
label: string;
@@ -306,12 +317,24 @@ function SelectField({
items: Array<{ id: string; label: string }>;
disabled?: boolean;
helpText?: string;
required?: boolean;
}) {
const invalid = required && !value;
const helpId = testId && helpText ? `${testId}-help` : undefined;
return (
<div className="space-y-1.5">
<Label className="text-xs">{label}</Label>
{required ? (
<RequiredFieldLabel className="text-xs">{label}</RequiredFieldLabel>
) : (
<Label className="text-xs">{label}</Label>
)}
<Select value={value || undefined} onValueChange={onChange} disabled={disabled}>
<SelectTrigger data-testid={testId} className="cursor-pointer">
<SelectTrigger
data-testid={testId}
className="cursor-pointer"
aria-describedby={invalid ? helpId : undefined}
aria-invalid={invalid ? true : undefined}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
@@ -322,7 +345,11 @@ function SelectField({
))}
</SelectContent>
</Select>
{helpText && <p className="text-[10px] text-muted-foreground">{helpText}</p>}
{helpText && (
<p id={helpId} className="text-[10px] text-muted-foreground">
{helpText}
</p>
)}
</div>
);
}
@@ -0,0 +1,19 @@
import type { ComponentProps } from "react";
import { Label } from "@kandev/ui/label";
import { cn } from "@/lib/utils";
export function RequiredFieldLabel({
children,
className,
...props
}: ComponentProps<typeof Label>) {
return (
<Label className={cn("gap-1.5", className)} {...props}>
<span>{children}</span>
<span aria-hidden className="text-destructive">
*
</span>
<span className="sr-only">required</span>
</Label>
);
}