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

rahulvats pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 23b31280a20 Fix: restore searchable dropdown for DAG params enum 
fields (#63895)
23b31280a20 is described below

commit 23b31280a20117d3ad07eeb5fca966676ec189e5
Author: nagasrisai <[email protected]>
AuthorDate: Mon Mar 23 20:30:12 2026 +0530

    Fix: restore searchable dropdown for DAG params enum fields (#63895)
    
    * add search input to FieldDropdown so dag params enums are filterable
    
    * add tests for search/filter behaviour in FieldDropdown
    
    * add searchPlaceholder translation key for dropdown search input
    
    * switch FieldDropdown to chakra-react-select for built-in search support
    
    * update FieldDropdown tests to match chakra-react-select implementation
    
    * remove unused searchPlaceholder key from components.json
    
    * add demo screenshot for PR review
    
    * Add screenshot: searchable dropdown in action
    
    * Add screenshot: full dropdown list
    
    * remove committed screenshot trigger-dialog-dropdown.png
    
    * remove committed screenshot trigger-dialog-search.png
    
    * remove demo screenshot from docs
    
    * Remove explicit Option type and generic per review feedback
    
    * fix: apply Prettier formatting to FieldDropdown.tsx
    
    Wrap nullish-coalescing expressions inside ternary branches with
    parentheses, and remove the misplaced eslint-disable comment that
    was not directly before a null literal. Both changes match what
    Prettier 3.x requires for `?? null` inside `? :` ternaries.
---
 .../components/FlexibleForm/FieldDropdown.test.tsx | 27 ++------
 .../src/components/FlexibleForm/FieldDropdown.tsx  | 77 +++++++++-------------
 2 files changed, 37 insertions(+), 67 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.test.tsx
 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.test.tsx
index ee9eb5f094b..2fda9b292de 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.test.tsx
@@ -23,7 +23,6 @@ import { Wrapper } from "src/utils/Wrapper";
 
 import { FieldDropdown } from "./FieldDropdown";
 
-// Mock the useParamStore hook
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 const mockParamsDict: Record<string, any> = {};
 const mockSetParamsDict = vi.fn();
@@ -43,7 +42,6 @@ vi.mock("src/queries/useParamStore", () => ({
 
 describe("FieldDropdown", () => {
   beforeEach(() => {
-    // Clear mock params before each test
     Object.keys(mockParamsDict).forEach((key) => {
       // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
       delete mockParamsDict[key];
@@ -65,9 +63,7 @@ describe("FieldDropdown", () => {
       wrapper: Wrapper,
     });
 
-    const select = screen.getByRole("combobox");
-
-    expect(select).toBeDefined();
+    expect(screen.getByRole("combobox")).toBeDefined();
   });
 
   it("displays custom label for null value via values_display", () => {
@@ -90,9 +86,7 @@ describe("FieldDropdown", () => {
       wrapper: Wrapper,
     });
 
-    const select = screen.getByRole("combobox");
-
-    expect(select).toBeDefined();
+    expect(screen.getByRole("combobox")).toBeDefined();
   });
 
   it("handles string enum with null value", () => {
@@ -109,9 +103,7 @@ describe("FieldDropdown", () => {
       wrapper: Wrapper,
     });
 
-    const select = screen.getByRole("combobox");
-
-    expect(select).toBeDefined();
+    expect(screen.getByRole("combobox")).toBeDefined();
   });
 
   it("handles enum with only null value", () => {
@@ -129,9 +121,7 @@ describe("FieldDropdown", () => {
       wrapper: Wrapper,
     });
 
-    const select = screen.getByRole("combobox");
-
-    expect(select).toBeDefined();
+    expect(screen.getByRole("combobox")).toBeDefined();
   });
 
   it("renders when current value is null", () => {
@@ -149,14 +139,10 @@ describe("FieldDropdown", () => {
       wrapper: Wrapper,
     });
 
-    const select = screen.getByRole("combobox");
-
-    expect(select).toBeDefined();
+    expect(screen.getByRole("combobox")).toBeDefined();
   });
 
   it("preserves numeric type when selecting a number enum value (prevents 400 
Bad Request)", () => {
-    // Regression test: jscheffl reported that selecting "Six" from a numeric 
enum
-    // caused a 400 Bad Request because the value was stored as string "6" 
instead of number 6.
     mockParamsDict.test_param = {
       schema: {
         // eslint-disable-next-line unicorn/no-null
@@ -175,9 +161,6 @@ describe("FieldDropdown", () => {
       wrapper: Wrapper,
     });
 
-    // Simulate internal handleChange being called with the string "6" (as 
Select always returns strings)
-    // The component should store the number 6, not the string "6".
-    // We verify by checking the schema enum contains the original number type.
     // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
     const enumValues = mockParamsDict.test_param.schema.enum as Array<number | 
string | null>;
     const selectedString = "6";
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 3fc5f835519..d439dd6032f 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx
@@ -16,11 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { createListCollection } from "@chakra-ui/react/collection";
-import { useRef } from "react";
+import { type SingleValue, Select as ReactSelect } from "chakra-react-select";
 import { useTranslation } from "react-i18next";
 
-import { Select } from "src/components/ui";
 import { paramPlaceholder, useParamStore } from "src/queries/useParamStore";
 
 import type { FlexibleFormElementProps } from ".";
@@ -39,6 +37,7 @@ const labelLookup = (
 
   return key === null ? "null" : String(key);
 };
+
 const enumTypes = ["string", "number", "integer"];
 
 export const FieldDropdown = ({ name, namespace = "default", onUpdate }: 
FlexibleFormElementProps) => {
@@ -46,68 +45,56 @@ export const FieldDropdown = ({ name, namespace = 
"default", onUpdate }: Flexibl
   const { disabled, paramsDict, setParamsDict } = useParamStore(namespace);
   const param = paramsDict[name] ?? paramPlaceholder;
 
-  const selectOptions = createListCollection({
-    items:
-      param.schema.enum?.map((value) => {
-        // Convert null to string constant for zag-js compatibility
-        const stringValue = String(value ?? NULL_STRING_VALUE);
-
-        return {
-          label: labelLookup(value, param.schema.values_display),
-          value: stringValue,
-        };
-      }) ?? [],
-  });
+  const options =
+    param.schema.enum?.map((value) => ({
+      label: labelLookup(value, param.schema.values_display),
+      value: String(value ?? NULL_STRING_VALUE),
+    })) ?? [];
 
-  const contentRef = useRef<HTMLDivElement | null>(null);
+  const currentValue =
+    param.value === null
+      ? (options.find((opt) => opt.value === NULL_STRING_VALUE) ?? null)
+      : enumTypes.includes(typeof param.value)
+        ? (options.find((opt) => opt.value === String(param.value)) ?? null)
+        : // eslint-disable-next-line unicorn/no-null
+          null;
 
-  const handleChange = ([value]: Array<string>) => {
+  const handleChange = (
+    selected: SingleValue<{
+      label: string;
+      value: string;
+    }>,
+  ) => {
     if (paramsDict[name]) {
-      if (value === NULL_STRING_VALUE || value === undefined) {
+      if (!selected || selected.value === NULL_STRING_VALUE) {
         // eslint-disable-next-line unicorn/no-null
         paramsDict[name].value = null;
       } else {
         // Map the string value back to the original typed enum value (e.g. 
number, string)
         // so that backend validation receives the correct type.
         const originalValue = param.schema.enum?.find(
-          (enumVal) => String(enumVal ?? NULL_STRING_VALUE) === value,
+          (enumVal) => String(enumVal ?? NULL_STRING_VALUE) === selected.value,
         );
 
-        paramsDict[name].value = originalValue ?? value;
+        paramsDict[name].value = originalValue ?? selected.value;
       }
     }
 
     setParamsDict(paramsDict);
-    onUpdate(value);
+    onUpdate(selected?.value ?? "");
   };
 
   return (
-    <Select.Root
-      collection={selectOptions}
-      disabled={disabled}
+    <ReactSelect
       id={`element_${name}`}
+      isClearable
+      isDisabled={disabled}
       name={`element_${name}`}
-      onValueChange={(event) => handleChange(event.value)}
-      ref={contentRef}
+      onChange={handleChange}
+      options={options}
+      placeholder={translate("flexibleForm.placeholder")}
       size="sm"
-      value={
-        param.value === null
-          ? [NULL_STRING_VALUE]
-          : enumTypes.includes(typeof param.value)
-            ? [String(param.value as number | string)]
-            : undefined
-      }
-    >
-      <Select.Trigger clearable>
-        <Select.ValueText placeholder={translate("flexibleForm.placeholder")} 
/>
-      </Select.Trigger>
-      <Select.Content portalRef={contentRef}>
-        {selectOptions.items.map((option) => (
-          <Select.Item item={option} key={option.value}>
-            {option.label}
-          </Select.Item>
-        ))}
-      </Select.Content>
-    </Select.Root>
+      value={currentValue}
+    />
   );
 };

Reply via email to