This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 848c1b31140bf7ebc728f5e58306519f994688e9
Author: Aritra Basu <[email protected]>
AuthorDate: Thu May 1 20:37:25 2025 +0530

    Added validation in flexibleform (#49981)
    
    * Added validation in flexibleform
    
    * Updated errors
    
    * Added more validation and disabling button
    
    * Fixed boolean error
    
    * Makes section red on error
    
    (cherry picked from commit 607ff45fa00812b86c52e62dbb3a3229f169ed5d)
---
 .../src/airflow/ui/src/components/ConfigForm.tsx   |  3 ++
 .../src/components/DagActions/RunBackfillForm.tsx  |  6 ++-
 .../components/FlexibleForm/FieldAdvancedArray.tsx | 27 ++++-------
 .../src/components/FlexibleForm/FieldDateTime.tsx  |  4 +-
 .../src/components/FlexibleForm/FieldDropdown.tsx  |  3 +-
 .../components/FlexibleForm/FieldMultiSelect.tsx   |  3 +-
 .../components/FlexibleForm/FieldMultilineText.tsx |  3 +-
 .../ui/src/components/FlexibleForm/FieldNumber.tsx |  3 +-
 .../ui/src/components/FlexibleForm/FieldObject.tsx | 23 ++++------
 .../ui/src/components/FlexibleForm/FieldRow.tsx    | 38 +++++++++++-----
 .../src/components/FlexibleForm/FieldSelector.tsx  | 26 +++++------
 .../ui/src/components/FlexibleForm/FieldString.tsx |  3 +-
 .../components/FlexibleForm/FieldStringArray.tsx   |  3 +-
 .../src/components/FlexibleForm/FlexibleForm.tsx   | 52 ++++++++++++++++++++--
 .../airflow/ui/src/components/FlexibleForm/Row.tsx |  8 +++-
 .../ui/src/components/FlexibleForm/index.tsx       |  1 +
 .../{index.tsx => isParamRequired.tsx}             | 14 +++---
 .../src/components/TriggerDag/TriggerDAGForm.tsx   |  4 +-
 .../ui/src/pages/Connections/ConnectionForm.tsx    |  5 ++-
 19 files changed, 148 insertions(+), 81 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx 
b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
index a216b22de60..c8e75edfa81 100644
--- a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx
@@ -39,6 +39,7 @@ type ConfigFormProps<T extends FieldValues = FieldValues> = {
       date?: unknown;
     }>
   >;
+  readonly setFormError: (error: boolean) => void;
 };
 
 const ConfigForm = <T extends FieldValues = FieldValues>({
@@ -47,6 +48,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({
   errors,
   initialParamsDict,
   setErrors,
+  setFormError,
 }: ConfigFormProps<T>) => {
   const { conf, setConf } = useParamStore();
 
@@ -86,6 +88,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({
       <FlexibleForm
         flexibleFormDefaultSection={flexibleFormDefaultSection}
         initialParamsDict={initialParamsDict}
+        setError={setFormError}
       />
       <Accordion.Item key="advancedOptions" value="advancedOptions">
         <Accordion.ItemTrigger cursor="button">Advanced 
Options</Accordion.ItemTrigger>
diff --git 
a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx 
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 23162df0453..b86aa824c9b 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -49,6 +49,7 @@ type BackfillFormProps = DagRunTriggerParams & 
Omit<BackfillPostBody, "dag_run_c
 const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
   const [unpause, setUnpause] = useState(true);
+  const [formError, setFormError] = useState(false);
   const initialParamsDict = useDagParams(dag.dag_id, true);
   const { conf } = useParamStore();
   const { control, handleSubmit, reset, watch } = useForm<BackfillFormProps>({
@@ -251,6 +252,7 @@ const RunBackfillForm = ({ dag, onClose }: 
RunBackfillFormProps) => {
           errors={errors}
           initialParamsDict={initialParamsDict}
           setErrors={setErrors}
+          setFormError={setFormError}
         />
       </VStack>
       <Box as="footer" display="flex" justifyContent="flex-end" mt={4}>
@@ -259,7 +261,9 @@ const RunBackfillForm = ({ dag, onClose }: 
RunBackfillFormProps) => {
           <Button onClick={() => void handleSubmit(onCancel)()}>Cancel</Button>
           <Button
             colorPalette="blue"
-            disabled={Boolean(errors.date) || isPendingDryRun || 
affectedTasks.total_entries === 0}
+            disabled={
+              Boolean(errors.date) || isPendingDryRun || formError || 
affectedTasks.total_entries === 0
+            }
             loading={isPending}
             onClick={() => void handleSubmit(onSubmit)()}
           >
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
index 80cb46dbdbc..27221204665 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldAdvancedArray.tsx
@@ -16,23 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Text } from "@chakra-ui/react";
-import { useState } from "react";
-
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 import { JsonEditor } from "../JsonEditor";
 
-export const FieldAdvancedArray = ({ name }: FlexibleFormElementProps) => {
+export const FieldAdvancedArray = ({ name, onUpdate }: 
FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
-  const [error, setError] = useState<unknown>(undefined);
   // Determine the expected type based on schema
   const expectedType = param.schema.items?.type ?? "object";
 
   const handleChange = (value: string) => {
-    setError(undefined);
     if (value === "") {
       if (paramsDict[name]) {
         // "undefined" values are removed from params, so we set it to null to 
avoid falling back to DAG defaults.
@@ -64,24 +59,18 @@ export const FieldAdvancedArray = ({ name }: 
FlexibleFormElementProps) => {
         }
 
         setParamsDict(paramsDict);
+        onUpdate(String(parsedValue));
       } catch (_error) {
-        setError(expectedType === "number" ? String(_error).replace("JSON", 
"Array") : _error);
+        onUpdate(undefined, expectedType === "number" ? 
String(_error).replace("JSON", "Array") : _error);
       }
     }
   };
 
   return (
-    <>
-      <JsonEditor
-        id={`element_${name}`}
-        onChange={handleChange}
-        value={JSON.stringify(param.value ?? [], undefined, 2)}
-      />
-      {Boolean(error) ? (
-        <Text color="fg.error" fontSize="xs">
-          {String(error)}
-        </Text>
-      ) : undefined}
-    </>
+    <JsonEditor
+      id={`element_${name}`}
+      onChange={handleChange}
+      value={JSON.stringify(param.value ?? [], undefined, 2)}
+    />
   );
 };
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
index 07911837996..cc8d23184b3 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDateTime.tsx
@@ -23,7 +23,7 @@ import { paramPlaceholder, useParamStore } from 
"src/queries/useParamStore";
 import type { FlexibleFormElementProps } from ".";
 import { DateTimeInput } from "../DateTimeInput";
 
-export const FieldDateTime = ({ name, ...rest }: FlexibleFormElementProps & 
InputProps) => {
+export const FieldDateTime = ({ name, onUpdate, ...rest }: 
FlexibleFormElementProps & InputProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -40,6 +40,7 @@ export const FieldDateTime = ({ name, ...rest }: 
FlexibleFormElementProps & Inpu
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   if (rest.type === "datetime-local") {
@@ -59,6 +60,7 @@ export const FieldDateTime = ({ name, ...rest }: 
FlexibleFormElementProps & Inpu
       id={`element_${name}`}
       name={`element_${name}`}
       onChange={(event) => handleChange(event.target.value)}
+      required={rest.required}
       size="sm"
       type={rest.type}
       value={((param.value ?? "") as string).slice(0, 16)}
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
index a12d4dc366d..de15568c39c 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
@@ -33,7 +33,7 @@ const labelLookup = (key: string, valuesDisplay: 
Record<string, string> | undefi
 };
 const enumTypes = ["string", "number", "integer"];
 
-export const FieldDropdown = ({ name }: FlexibleFormElementProps) => {
+export const FieldDropdown = ({ name, onUpdate }: FlexibleFormElementProps) => 
{
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
@@ -55,6 +55,7 @@ export const FieldDropdown = ({ name }: 
FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
index aa4a4e112f9..e34241037e1 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultiSelect.tsx
@@ -31,7 +31,7 @@ const labelLookup = (key: string, valuesDisplay: 
Record<string, string> | undefi
   return key;
 };
 
-export const FieldMultiSelect = ({ name }: FlexibleFormElementProps) => {
+export const FieldMultiSelect = ({ name, onUpdate }: FlexibleFormElementProps) 
=> {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
@@ -64,6 +64,7 @@ export const FieldMultiSelect = ({ name }: 
FlexibleFormElementProps) => {
       paramsDict[name].value = newValueArray;
     }
     setParamsDict(paramsDict);
+    onUpdate(String(newValueArray));
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
index bfefcb276d8..93c7410bb82 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldMultilineText.tsx
@@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from 
"src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 
-export const FieldMultilineText = ({ name }: FlexibleFormElementProps) => {
+export const FieldMultilineText = ({ name, onUpdate }: 
FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -33,6 +33,7 @@ export const FieldMultilineText = ({ name }: 
FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
index dcaaf37cf56..635f461335c 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldNumber.tsx
@@ -21,7 +21,7 @@ import { paramPlaceholder, useParamStore } from 
"src/queries/useParamStore";
 import type { FlexibleFormElementProps } from ".";
 import { NumberInputField, NumberInputRoot } from "../ui/NumberInput";
 
-export const FieldNumber = ({ name }: FlexibleFormElementProps) => {
+export const FieldNumber = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -40,6 +40,7 @@ export const FieldNumber = ({ name }: 
FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
index fcd7d7d2a16..ce5bf307311 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldObject.tsx
@@ -16,21 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Text } from "@chakra-ui/react";
-import { useState } from "react";
-
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 import { JsonEditor } from "../JsonEditor";
 
-export const FieldObject = ({ name }: FlexibleFormElementProps) => {
+export const FieldObject = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
-  const [error, setError] = useState<unknown>(undefined);
 
   const handleChange = (value: string) => {
-    setError(undefined);
     try {
       // "undefined" values are removed from params, so we set it to null to 
avoid falling back to DAG defaults.
       // eslint-disable-next-line unicorn/no-null
@@ -41,19 +36,17 @@ export const FieldObject = ({ name }: 
FlexibleFormElementProps) => {
       }
 
       setParamsDict(paramsDict);
+      onUpdate(value);
     } catch (_error) {
-      setError(_error);
+      onUpdate("", _error);
     }
   };
 
   return (
-    <>
-      <JsonEditor
-        id={`element_${name}`}
-        onChange={handleChange}
-        value={JSON.stringify(param.value ?? [], undefined, 2)}
-      />
-      {Boolean(error) ? <Text color="fg,.error">{String(error)}</Text> : 
undefined}
-    </>
+    <JsonEditor
+      id={`element_${name}`}
+      onChange={handleChange}
+      value={JSON.stringify(param.value ?? [], undefined, 2)}
+    />
   );
 };
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
index e41d4643f6b..406dbaf18fe 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldRow.tsx
@@ -17,35 +17,52 @@
  * under the License.
  */
 import { Field, Stack } from "@chakra-ui/react";
+import { useState } from "react";
 import Markdown from "react-markdown";
 import remarkGfm from "remark-gfm";
 
-import type { ParamSpec } from "src/queries/useDagParams";
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 import { FieldSelector } from "./FieldSelector";
-
-const isRequired = (param: ParamSpec) =>
-  // The field is required if the schema type is defined.
-  // But if the type "null" is included, then the field is not required.
-  // We assume that "null" is only defined if the type is an array.
-  Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || 
!param.schema.type.includes("null"));
+import { isRequired } from "./isParamRequired";
 
 /** Render a normal form row with a field that is auto-selected */
-export const FieldRow = ({ name }: FlexibleFormElementProps) => {
+export const FieldRow = ({ name, onUpdate: rowOnUpdate }: 
FlexibleFormElementProps) => {
   const { paramsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
+  const [error, setError] = useState<unknown>(
+    isRequired(param) && param.value === null ? "This field is required" : 
undefined,
+  );
+  const [isValid, setIsValid] = useState(!(isRequired(param) && param.value 
=== null));
+
+  // console.log(param);
+
+  const onUpdate = (value?: string, _error?: unknown) => {
+    if (Boolean(_error)) {
+      setIsValid(false);
+      setError(_error);
+      rowOnUpdate(undefined, _error);
+    } else if (isRequired(param) && (!Boolean(value) || value === "")) {
+      setIsValid(false);
+      setError("This field is required");
+      rowOnUpdate(undefined, "This field is required");
+    } else {
+      setIsValid(true);
+      setError(undefined);
+      rowOnUpdate();
+    }
+  };
 
   return (
-    <Field.Root orientation="horizontal" required={isRequired(param)}>
+    <Field.Root invalid={!isValid} orientation="horizontal" 
required={isRequired(param)}>
       <Stack>
         <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
           {param.schema.title ?? name} <Field.RequiredIndicator />
         </Field.Label>
       </Stack>
       <Stack css={{ flexBasis: "70%" }}>
-        <FieldSelector name={name} />
+        <FieldSelector name={name} onUpdate={onUpdate} />
         {param.description === null ? (
           param.schema.description_md === undefined ? undefined : (
             <Field.HelperText>
@@ -55,6 +72,7 @@ export const FieldRow = ({ name }: FlexibleFormElementProps) 
=> {
         ) : (
           <Field.HelperText>{param.description}</Field.HelperText>
         )}
+        {isValid ? undefined : 
<Field.ErrorText>{String(error)}</Field.ErrorText>}
       </Stack>
     </Field.Root>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
index 8665b41aa2b..cd0061987a6 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldSelector.tsx
@@ -87,35 +87,35 @@ const isFieldStringArray = (fieldType: string, fieldSchema: 
ParamSchema) =>
 const isFieldTime = (fieldType: string, fieldSchema: ParamSchema) =>
   fieldType === "string" && fieldSchema.format === "time";
 
-export const FieldSelector = ({ name }: FlexibleFormElementProps) => {
+export const FieldSelector = ({ name, onUpdate }: FlexibleFormElementProps) => 
{
   // FUTURE: Add support for other types as described in AIP-68 via Plugins
   const { initialParamDict } = useParamStore();
   const param = initialParamDict[name] ?? paramPlaceholder;
   const fieldType = inferType(param);
 
   if (isFieldBool(fieldType)) {
-    return <FieldBool name={name} />;
+    return <FieldBool name={name} onUpdate={onUpdate} />;
   } else if (isFieldDateTime(fieldType, param.schema)) {
-    return <FieldDateTime name={name} type="datetime-local" />;
+    return <FieldDateTime name={name} onUpdate={onUpdate} 
type="datetime-local" />;
   } else if (isFieldDate(fieldType, param.schema)) {
-    return <FieldDateTime name={name} type="date" />;
+    return <FieldDateTime name={name} onUpdate={onUpdate} type="date" />;
   } else if (isFieldTime(fieldType, param.schema)) {
-    return <FieldDateTime name={name} type="time" />;
+    return <FieldDateTime name={name} onUpdate={onUpdate} type="time" />;
   } else if (isFieldDropdown(fieldType, param.schema)) {
-    return <FieldDropdown name={name} />;
+    return <FieldDropdown name={name} onUpdate={onUpdate} />;
   } else if (isFieldMultiSelect(fieldType, param.schema)) {
-    return <FieldMultiSelect name={name} />;
+    return <FieldMultiSelect name={name} onUpdate={onUpdate} />;
   } else if (isFieldStringArray(fieldType, param.schema)) {
-    return <FieldStringArray name={name} />;
+    return <FieldStringArray name={name} onUpdate={onUpdate} />;
   } else if (isFieldAdvancedArray(fieldType, param.schema)) {
-    return <FieldAdvancedArray name={name} />;
+    return <FieldAdvancedArray name={name} onUpdate={onUpdate} />;
   } else if (isFieldObject(fieldType)) {
-    return <FieldObject name={name} />;
+    return <FieldObject name={name} onUpdate={onUpdate} />;
   } else if (isFieldNumber(fieldType)) {
-    return <FieldNumber name={name} />;
+    return <FieldNumber name={name} onUpdate={onUpdate} />;
   } else if (isFieldMultilineText(fieldType, param.schema)) {
-    return <FieldMultilineText name={name} />;
+    return <FieldMultilineText name={name} onUpdate={onUpdate} />;
   } else {
-    return <FieldString name={name} />;
+    return <FieldString name={name} onUpdate={onUpdate} />;
   }
 };
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx
index e8cd0185706..8d49e604452 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldString.tsx
@@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from 
"src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 
-export const FieldString = ({ name }: FlexibleFormElementProps) => {
+export const FieldString = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
   const handleChange = (value: string) => {
@@ -33,6 +33,7 @@ export const FieldString = ({ name }: 
FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(value);
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
index f5fc6ac69f5..2078d95276d 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldStringArray.tsx
@@ -22,7 +22,7 @@ import { paramPlaceholder, useParamStore } from 
"src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
 
-export const FieldStringArray = ({ name }: FlexibleFormElementProps) => {
+export const FieldStringArray = ({ name, onUpdate }: FlexibleFormElementProps) 
=> {
   const { paramsDict, setParamsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
@@ -34,6 +34,7 @@ export const FieldStringArray = ({ name }: 
FlexibleFormElementProps) => {
     }
 
     setParamsDict(paramsDict);
+    onUpdate(newValue);
   };
 
   const handleBlur = () => {
diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
index 165e9d6dd19..ae9e60c812f 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
@@ -17,23 +17,44 @@
  * under the License.
  */
 import { Box, Stack, StackSeparator } from "@chakra-ui/react";
-import { useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
 
 import type { ParamsSpec } from "src/queries/useDagParams";
 import { useParamStore } from "src/queries/useParamStore";
 
 import { Accordion } from "../ui";
 import { Row } from "./Row";
+import { isRequired } from "./isParamRequired";
 
 export type FlexibleFormProps = {
   flexibleFormDefaultSection: string;
   initialParamsDict: { paramsDict: ParamsSpec };
   key?: string;
+  setError: (error: boolean) => void;
 };
 
-export const FlexibleForm = ({ flexibleFormDefaultSection, initialParamsDict 
}: FlexibleFormProps) => {
+export const FlexibleForm = ({
+  flexibleFormDefaultSection,
+  initialParamsDict,
+  setError,
+}: FlexibleFormProps) => {
   const { paramsDict: params, setinitialParamDict, setParamsDict } = 
useParamStore();
   const processedSections = new Map();
+  const [sectionError, setSectionError] = useState<Map<string, boolean>>(new 
Map());
+
+  const recheckSection = useCallback(() => {
+    sectionError.clear();
+    Object.entries(params).forEach(([, element]) => {
+      if (
+        isRequired(element) &&
+        (element.value === null || element.value === undefined || 
element.value === "")
+      ) {
+        sectionError.set(element.schema.section ?? flexibleFormDefaultSection, 
true);
+        setSectionError(sectionError);
+      }
+    });
+    console.log("errors1", sectionError);
+  }, [flexibleFormDefaultSection, params, sectionError]);
 
   useEffect(() => {
     // Initialize paramsDict and initialParamDict when modal opens
@@ -54,6 +75,24 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, 
initialParamsDict }:
     [setParamsDict, setinitialParamDict],
   );
 
+  useEffect(
+    () => () => {
+      recheckSection();
+    },
+    [params, recheckSection],
+  );
+
+  const onUpdate = (_value?: string, error?: unknown) => {
+    recheckSection();
+    if (!Boolean(error) && sectionError.size === 0) {
+      setError(false);
+    } else {
+      setError(true);
+    }
+  };
+
+  console.log(sectionError);
+
   return Object.entries(params).some(([, param]) => typeof 
param.schema.section !== "string")
     ? Object.entries(params).map(([, secParam]) => {
         const currentSection = secParam.schema.section ?? 
flexibleFormDefaultSection;
@@ -65,7 +104,12 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, 
initialParamsDict }:
 
           return (
             <Accordion.Item key={currentSection} value={currentSection}>
-              <Accordion.ItemTrigger 
cursor="button">{currentSection}</Accordion.ItemTrigger>
+              <Accordion.ItemTrigger
+                color={sectionError.get(currentSection) ? "red" : undefined}
+                cursor="button"
+              >
+                {currentSection}
+              </Accordion.ItemTrigger>
               <Accordion.ItemContent paddingTop={0}>
                 <Box p={5}>
                   <Stack separator={<StackSeparator />}>
@@ -76,7 +120,7 @@ export const FlexibleForm = ({ flexibleFormDefaultSection, 
initialParamsDict }:
                           (currentSection === flexibleFormDefaultSection && 
!Boolean(param.schema.section)),
                       )
                       .map(([name]) => (
-                        <Row key={name} name={name} />
+                        <Row key={name} name={name} onUpdate={onUpdate} />
                       ))}
                   </Stack>
                 </Box>
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx
index 1f1186124a2..cc71fedb6ee 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/Row.tsx
@@ -26,9 +26,13 @@ import { HiddenInput } from "./HiddenInput";
 const isHidden = (fieldSchema: ParamSchema) => Boolean(fieldSchema.const);
 
 /** Generates a form row */
-export const Row = ({ name }: FlexibleFormElementProps) => {
+export const Row = ({ name, onUpdate }: FlexibleFormElementProps) => {
   const { paramsDict } = useParamStore();
   const param = paramsDict[name] ?? paramPlaceholder;
 
-  return isHidden(param.schema) ? <HiddenInput name={name} /> : <FieldRow 
name={name} />;
+  return isHidden(param.schema) ? (
+    <HiddenInput name={name} onUpdate={onUpdate} />
+  ) : (
+    <FieldRow name={name} onUpdate={onUpdate} />
+  );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
index f7e8ab0d8c4..704b5d956ae 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
@@ -19,6 +19,7 @@
 
 export type FlexibleFormElementProps = {
   readonly name: string;
+  readonly onUpdate: (value?: string, error?: unknown) => void;
 };
 
 export const flexibleFormDefaultSection = "Run Parameters";
diff --git a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx
similarity index 66%
copy from airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
copy to 
airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx
index f7e8ab0d8c4..7a9a0668855 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/index.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/isParamRequired.tsx
@@ -16,12 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { ParamSpec } from "src/queries/useDagParams";
 
-export type FlexibleFormElementProps = {
-  readonly name: string;
-};
-
-export const flexibleFormDefaultSection = "Run Parameters";
-export const flexibleFormExtraFieldSection = "Extra Fields";
-
-export { FlexibleForm } from "./FlexibleForm";
+export const isRequired = (param: ParamSpec) =>
+  // The field is required if the schema type is defined.
+  // But if the type "null" is included, then the field is not required.
+  // We assume that "null" is only defined if the type is an array.
+  Boolean(param.schema.type) && (!Array.isArray(param.schema.type) || 
!param.schema.type.includes("null"));
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
index fdeae4bdb69..e1337f530f6 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -49,6 +49,7 @@ export type DagRunTriggerParams = {
 
 const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string; date?: unknown }>({});
+  const [formError, setFormError] = useState(false);
   const initialParamsDict = useDagParams(dagId, open);
   const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ 
dagId, onSuccessConfirm: onClose });
   const { conf } = useParamStore();
@@ -96,6 +97,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps)
         errors={errors}
         initialParamsDict={initialParamsDict}
         setErrors={setErrors}
+        setFormError={setFormError}
       >
         <Controller
           control={control}
@@ -153,7 +155,7 @@ const TriggerDAGForm = ({ dagId, isPaused, onClose, open }: 
TriggerDAGFormProps)
           <Spacer />
           <Button
             colorPalette="blue"
-            disabled={Boolean(errors.conf) || Boolean(errors.date) || 
isPending}
+            disabled={Boolean(errors.conf) || Boolean(errors.date) || 
formError || isPending}
             onClick={() => void handleSubmit(onSubmit)()}
           >
             <FiPlay /> Trigger
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx 
b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
index f2a47c47b15..6a17f1794f6 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
@@ -68,6 +68,8 @@ const ConnectionForm = ({
   const standardFields = connectionTypeMeta[selectedConnType]?.standard_fields 
?? {};
   const paramsDic = { paramsDict: 
connectionTypeMeta[selectedConnType]?.extra_fields ?? ({} as ParamsSpec) };
 
+  const [formErrors, setFormErrors] = useState(false);
+
   useEffect(() => {
     reset((prevValues) => ({
       ...initialConnection,
@@ -198,6 +200,7 @@ const ConnectionForm = ({
               flexibleFormDefaultSection={flexibleFormExtraFieldSection}
               initialParamsDict={paramsDic}
               key={selectedConnType}
+              setError={setFormErrors}
             />
             <Accordion.Item key="extraJson" value="extraJson">
               <Accordion.ItemTrigger cursor="button">Extra Fields 
JSON</Accordion.ItemTrigger>
@@ -228,7 +231,7 @@ const ConnectionForm = ({
           <Spacer />
           <Button
             colorPalette="blue"
-            disabled={Boolean(errors.conf) || isPending || !isValid}
+            disabled={Boolean(errors.conf) || formErrors || isPending || 
!isValid}
             onClick={() => void handleSubmit(onSubmit)()}
           >
             <FiSave /> Save

Reply via email to