diff --git a/apps/web/components/automations/automation-editor.tsx b/apps/web/components/automations/automation-editor.tsx
index 605b632bd3..24564d06e8 100644
--- a/apps/web/components/automations/automation-editor.tsx
+++ b/apps/web/components/automations/automation-editor.tsx
@@ -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 (
-
+
Name
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() && (
+
+ Enter an automation name to enable saving.
+
+ )}
({
+ 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> = {}) {
+ return render(
+ {}}
+ 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);
+ });
+});
diff --git a/apps/web/components/automations/config-section.tsx b/apps/web/components/automations/config-section.tsx
index 2f8000431e..f317b8a956 100644
--- a/apps/web/components/automations/config-section.tsx
+++ b/apps/web/components/automations/config-section.tsx
@@ -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([]);
@@ -190,7 +197,7 @@ export function ConfigSection({
-
+
({ id: w.id, label: w.name }))}
+ helpText={!hasWorkflow ? "Select a workflow to enable saving." : undefined}
/>
({ 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 (
-
+ {required ? (
+
{label}
+ ) : (
+
+ )}
- {helpText &&
{helpText}
}
+ {helpText && (
+
+ {helpText}
+
+ )}
);
}
diff --git a/apps/web/components/automations/required-field-label.tsx b/apps/web/components/automations/required-field-label.tsx
new file mode 100644
index 0000000000..ccca3fc717
--- /dev/null
+++ b/apps/web/components/automations/required-field-label.tsx
@@ -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) {
+ return (
+
+ );
+}