improve validations on parameter run ui (#4000)

Co-authored-by: Jonathan Dobson <jon.m.dobson@gmail.com>
This commit is contained in:
Celal Zamanoglu
2025-11-20 19:44:58 +03:00
committed by GitHub
parent 6ef5473e57
commit 5fc9435ef3
8 changed files with 276 additions and 94 deletions

View File

@@ -1,7 +1,7 @@
import { AxiosError } from "axios";
import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { type FieldErrors, useForm } from "react-hook-form";
import { useNavigate, useParams } from "react-router-dom";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { z } from "zod";
@@ -99,6 +99,34 @@ function parseValuesForWorkflowRun(
) {
return [key, value.s3uri];
}
// Convert boolean values to strings for backend storage
if (
parameter?.workflow_parameter_type === "boolean" &&
typeof value === "boolean"
) {
return [key, String(value)];
}
if (parameter?.workflow_parameter_type === "string") {
if (value === null || value === undefined) {
return [key, ""];
}
return [key, String(value)];
}
if (
parameter?.workflow_parameter_type === "integer" ||
parameter?.workflow_parameter_type === "float"
) {
if (
value === null ||
value === undefined ||
(typeof value === "number" && Number.isNaN(value))
) {
return [key, ""];
}
return [key, String(value)];
}
return [key, value];
}),
);
@@ -211,6 +239,8 @@ function RunWorkflowForm({
const { data: workflow } = useWorkflowQuery({ workflowPermanentId });
const form = useForm<RunWorkflowFormType>({
mode: "onTouched",
reValidateMode: "onChange",
defaultValues: {
...initialValues,
webhookCallbackUrl: initialSettings.webhookCallbackUrl,
@@ -268,6 +298,7 @@ function RunWorkflowForm({
unknown
> | null>(null);
const [cacheKeyValue, setCacheKeyValue] = useState<string>("");
const [isFormReset, setIsFormReset] = useState(false);
const cacheKey = workflow?.cache_key ?? "default";
useEffect(() => {
@@ -297,10 +328,41 @@ function RunWorkflowForm({
setHasCode(Object.keys(blockScripts ?? {}).length > 0);
}, [blockScripts]);
// Watch form changes and update run parameters without triggering validation
useEffect(() => {
onChange(form.getValues());
const subscription = form.watch((values) => {
onChange(values as RunWorkflowFormType);
});
return () => subscription.unsubscribe();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [form]);
}, []);
// Reset form with initial values after all fields are registered
useEffect(() => {
form.reset({
...initialValues,
webhookCallbackUrl: initialSettings.webhookCallbackUrl,
proxyLocation: initialSettings.proxyLocation,
browserSessionId: null,
cdpAddress: initialSettings.cdpAddress,
maxScreenshotScrolls: initialSettings.maxScreenshotScrolls,
extraHttpHeaders: initialSettings.extraHttpHeaders
? JSON.stringify(initialSettings.extraHttpHeaders)
: null,
runWithCode: workflow?.run_with === "code",
aiFallback: workflow?.ai_fallback ?? true,
});
setIsFormReset(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Trigger validation after form is reset and re-rendered
useEffect(() => {
if (isFormReset) {
form.trigger();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFormReset]);
// if we're coming from debugger, block scripts may already be cached; let's ensure we bust it
// on mount
@@ -360,6 +422,22 @@ function RunWorkflowForm({
setRunParameters(parsedParameters);
}
const handleInvalid = (errors: FieldErrors<RunWorkflowFormType>) => {
const hasBlockingErrors = workflowParameters.some(
(param) =>
(param.workflow_parameter_type === "boolean" ||
param.workflow_parameter_type === "integer" ||
param.workflow_parameter_type === "float" ||
param.workflow_parameter_type === "file_url" ||
param.workflow_parameter_type === "json") &&
errors[param.key],
);
if (!hasBlockingErrors) {
onSubmit(form.getValues());
}
};
if (!workflowPermanentId || !workflow) {
return <div>Invalid workflow</div>;
}
@@ -367,8 +445,7 @@ function RunWorkflowForm({
return (
<Form {...form}>
<form
onChange={form.handleSubmit(onChange)}
onSubmit={form.handleSubmit(onSubmit)}
onSubmit={form.handleSubmit(onSubmit, handleInvalid)}
className="space-y-8"
>
<div className="space-y-8 rounded-lg bg-slate-elevation3 px-6 py-5">
@@ -383,19 +460,74 @@ function RunWorkflowForm({
name={parameter.key}
rules={{
validate: (value) => {
if (
parameter.workflow_parameter_type === "json" &&
typeof value === "string"
) {
try {
JSON.parse(value);
return true;
} catch (e) {
return "Invalid JSON";
if (parameter.workflow_parameter_type === "json") {
if (value === null || value === undefined) {
return "This field is required";
}
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed === "") {
return "This field is required";
}
try {
JSON.parse(trimmed);
return true;
} catch (e) {
return "Invalid JSON";
}
}
return;
}
if (value === null) {
return "This field is required";
// Boolean parameters are required - show error and block submission
if (parameter.workflow_parameter_type === "boolean") {
if (value === null || value === undefined) {
return "This field is required";
}
return;
}
// Numeric parameters are required - show error and block submission
if (
parameter.workflow_parameter_type === "integer" ||
parameter.workflow_parameter_type === "float"
) {
if (
value === null ||
value === undefined ||
Number.isNaN(value)
) {
return "This field is required";
}
return;
}
if (parameter.workflow_parameter_type === "file_url") {
if (
value === null ||
value === undefined ||
(typeof value === "string" && value.trim() === "") ||
(typeof value === "object" &&
value !== null &&
"s3uri" in value &&
!value.s3uri)
) {
return "This field is required";
}
return;
}
// For string parameters, show warning but don't block
if (
parameter.workflow_parameter_type === "string" &&
(value === null || value === "")
) {
return "Warning: you left this field empty";
}
// For all other non-boolean types, show warning but don't block
if (value === null || value === undefined) {
return "Warning: you left this field empty";
}
},
}}
@@ -403,7 +535,7 @@ function RunWorkflowForm({
return (
<FormItem>
<div className="flex gap-16">
<FormLabel>
<FormLabel className="!text-slate-50">
<div className="w-72">
<div className="flex items-center gap-2 text-lg">
{parameter.key}
@@ -423,11 +555,27 @@ function RunWorkflowForm({
<WorkflowParameterInput
type={parameter.workflow_parameter_type}
value={field.value}
onChange={field.onChange}
onChange={(value) => {
field.onChange(value);
form.trigger(parameter.key);
}}
/>
</FormControl>
{form.formState.errors[parameter.key] && (
<div className="text-destructive">
<div
className={`text-xs ${
parameter.workflow_parameter_type ===
"boolean" ||
parameter.workflow_parameter_type ===
"integer" ||
parameter.workflow_parameter_type === "float" ||
parameter.workflow_parameter_type ===
"file_url" ||
parameter.workflow_parameter_type === "json"
? "text-destructive"
: "text-warning"
}`}
>
{form.formState.errors[parameter.key]?.message}
</div>
)}

View File

@@ -1,9 +1,14 @@
import { FileInputValue, FileUpload } from "@/components/FileUpload";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { CodeEditor } from "./components/CodeEditor";
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
import { Label } from "@/components/ui/label";
import { WorkflowParameterValueType } from "./types/workflowTypes";
import { CredentialSelector } from "./components/CredentialSelector";
@@ -60,16 +65,19 @@ function WorkflowParameterInput({ type, value, onChange }: Props) {
}
if (type === "boolean") {
const checked = typeof value === "boolean" ? value : Boolean(value);
return (
<div className="flex items-center gap-2">
<Checkbox
checked={checked}
onCheckedChange={(checked) => onChange(checked)}
className="block"
/>
<Label>{value ? "True" : "False"}</Label>
</div>
<Select
value={value === null ? "" : String(value)}
onValueChange={(v) => onChange(v === "true")}
>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select value..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">True</SelectItem>
<SelectItem value="false">False</SelectItem>
</SelectContent>
</Select>
);
}

View File

@@ -116,6 +116,26 @@ function convertToParametersYAML(
| CredentialParameterYAML
| undefined => {
if (parameter.parameterType === WorkflowEditorParameterTypes.Workflow) {
// Convert boolean default values to strings for backend
let defaultValue = parameter.defaultValue;
if (
parameter.dataType === "boolean" &&
typeof parameter.defaultValue === "boolean"
) {
defaultValue = String(parameter.defaultValue);
}
if (
(parameter.dataType === "integer" ||
parameter.dataType === "float") &&
(typeof parameter.defaultValue === "number" ||
typeof parameter.defaultValue === "string")
) {
defaultValue =
parameter.defaultValue === null
? parameter.defaultValue
: String(parameter.defaultValue);
}
return {
parameter_type: WorkflowParameterTypes.Workflow,
key: parameter.key,
@@ -123,7 +143,7 @@ function convertToParametersYAML(
workflow_parameter_type: parameter.dataType,
...(parameter.defaultValue === null
? {}
: { default_value: parameter.defaultValue }),
: { default_value: defaultValue }),
};
} else if (
parameter.parameterType === WorkflowEditorParameterTypes.Context

View File

@@ -538,11 +538,32 @@ function WorkflowParameterEditPanel({
return;
}
}
const defaultValue =
let defaultValue = defaultValueState.defaultValue;
// Handle JSON parsing
if (
parameterType === "json" &&
typeof defaultValueState.defaultValue === "string"
? JSON.parse(defaultValueState.defaultValue)
: defaultValueState.defaultValue;
) {
defaultValue = JSON.parse(defaultValueState.defaultValue);
}
// Convert boolean to string for backend storage
else if (
parameterType === "boolean" &&
typeof defaultValueState.defaultValue === "boolean"
) {
defaultValue = String(defaultValueState.defaultValue);
}
// Convert numeric defaults to strings for backend storage
else if (
(parameterType === "integer" ||
parameterType === "float") &&
(typeof defaultValueState.defaultValue === "number" ||
typeof defaultValueState.defaultValue === "string")
) {
defaultValue = String(defaultValueState.defaultValue);
}
onSave({
key,
parameterType: "workflow",

View File

@@ -1852,11 +1852,30 @@ function convertParametersToParameterYAML(
};
}
case WorkflowParameterTypes.Workflow: {
// Convert default values to strings for backend when needed
let defaultValue = parameter.default_value;
if (
parameter.workflow_parameter_type === "boolean" &&
typeof parameter.default_value === "boolean"
) {
defaultValue = String(parameter.default_value);
} else if (
(parameter.workflow_parameter_type === "integer" ||
parameter.workflow_parameter_type === "float") &&
(typeof parameter.default_value === "number" ||
typeof parameter.default_value === "string")
) {
defaultValue =
parameter.default_value === null
? parameter.default_value
: String(parameter.default_value);
}
return {
...base,
parameter_type: WorkflowParameterTypes.Workflow,
workflow_parameter_type: parameter.workflow_parameter_type,
default_value: parameter.default_value,
default_value: defaultValue,
};
}
case WorkflowParameterTypes.Credential: {

View File

@@ -12,31 +12,24 @@ export const getInitialValues = (
? location.state.data
: workflowParameters?.reduce(
(acc, curr) => {
if (curr.workflow_parameter_type === "json") {
if (typeof curr.default_value === "string") {
acc[curr.key] = curr.default_value;
const hasDefaultValue =
curr.default_value !== null && curr.default_value !== undefined;
if (hasDefaultValue) {
// Handle JSON parameters
if (curr.workflow_parameter_type === "json") {
if (typeof curr.default_value === "string") {
acc[curr.key] = curr.default_value;
} else {
acc[curr.key] = JSON.stringify(curr.default_value, null, 2);
}
return acc;
}
if (curr.default_value) {
acc[curr.key] = JSON.stringify(curr.default_value, null, 2);
if (curr.workflow_parameter_type === "boolean") {
// Backend stores as strings, convert to boolean for frontend
acc[curr.key] =
curr.default_value === "true" || curr.default_value === true;
return acc;
}
}
if (
curr.default_value &&
curr.workflow_parameter_type === "boolean"
) {
acc[curr.key] = Boolean(curr.default_value);
return acc;
}
if (
curr.default_value === null &&
curr.workflow_parameter_type === "string"
) {
acc[curr.key] = "";
return acc;
}
if (curr.default_value) {
acc[curr.key] = curr.default_value;
return acc;
}