From 1dddf6e393af38e356ed14077bf949331ccb1845 Mon Sep 17 00:00:00 2001 From: nagasrisai <59650078+nagasrisai@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:06:07 +0530 Subject: [PATCH 01/13] add search input to FieldDropdown so dag params enums are filterable --- .../components/FlexibleForm/FieldDropdown.tsx | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) 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 3fc5f83551988..8db34a576758f 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { Input } from "@chakra-ui/react"; import { createListCollection } from "@chakra-ui/react/collection"; -import { useRef } from "react"; +import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Select } from "src/components/ui"; @@ -39,25 +40,31 @@ const labelLookup = ( return key === null ? "null" : String(key); }; + const enumTypes = ["string", "number", "integer"]; export const FieldDropdown = ({ name, namespace = "default", onUpdate }: FlexibleFormElementProps) => { const { t: translate } = useTranslation("components"); const { disabled, paramsDict, setParamsDict } = useParamStore(namespace); const param = paramsDict[name] ?? paramPlaceholder; + const [searchQuery, setSearchQuery] = useState(""); + + const allItems = + 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 selectOptions = createListCollection({ - items: - param.schema.enum?.map((value) => { - // Convert null to string constant for zag-js compatibility - const stringValue = String(value ?? NULL_STRING_VALUE); + const selectOptions = createListCollection({ items: allItems }); - return { - label: labelLookup(value, param.schema.values_display), - value: stringValue, - }; - }) ?? [], - }); + const filteredItems = searchQuery + ? allItems.filter((opt) => opt.label.toLowerCase().includes(searchQuery.toLowerCase())) + : allItems; const contentRef = useRef(null); @@ -87,6 +94,11 @@ export const FieldDropdown = ({ name, namespace = "default", onUpdate }: Flexibl disabled={disabled} id={`element_${name}`} name={`element_${name}`} + onOpenChange={({ open }) => { + if (!open) { + setSearchQuery(""); + } + }} onValueChange={(event) => handleChange(event.value)} ref={contentRef} size="sm" @@ -102,7 +114,16 @@ export const FieldDropdown = ({ name, namespace = "default", onUpdate }: Flexibl - {selectOptions.items.map((option) => ( + setSearchQuery(e.target.value)} + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + placeholder={translate("flexibleForm.searchPlaceholder")} + size="sm" + value={searchQuery} + /> + {filteredItems.map((option) => ( {option.label} From 013d992b9e30f5277706a61d2d581594a8c9ae24 Mon Sep 17 00:00:00 2001 From: nagasrisai <59650078+nagasrisai@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:06:08 +0530 Subject: [PATCH 02/13] add tests for search/filter behaviour in FieldDropdown --- .../FlexibleForm/FieldDropdown.test.tsx | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 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 ee9eb5f094bca..1c6e37b06dbe5 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 @@ -17,6 +17,7 @@ * under the License. */ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, it, expect, beforeEach, vi } from "vitest"; import { Wrapper } from "src/utils/Wrapper"; @@ -175,9 +176,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; const selectedString = "6"; @@ -186,4 +184,49 @@ describe("FieldDropdown", () => { expect(original).toBe(6); expect(typeof original).toBe("number"); }); + + it("renders a search input inside the dropdown content", () => { + mockParamsDict.test_param = { + schema: { + enum: ["apple", "banana", "cherry", "date", "elderberry"], + type: "string", + }, + value: "apple", + }; + + render(, { + wrapper: Wrapper, + }); + + // The search input is rendered inside the dropdown content + const searchInput = screen.getByPlaceholderText("Search options"); + + expect(searchInput).toBeDefined(); + }); + + it("filters displayed options as the user types in the search input", async () => { + mockParamsDict.test_param = { + schema: { + enum: ["apple", "banana", "apricot", "cherry"], + type: "string", + }, + value: "apple", + }; + + const user = userEvent.setup(); + + render(, { + wrapper: Wrapper, + }); + + const searchInput = screen.getByPlaceholderText("Search options"); + + await user.type(searchInput, "ap"); + + // After typing "ap", only "apple" and "apricot" should be visible; "banana" and "cherry" should not + expect(screen.queryByText("banana")).toBeNull(); + expect(screen.queryByText("cherry")).toBeNull(); + expect(screen.getByText("apple")).toBeDefined(); + expect(screen.getByText("apricot")).toBeDefined(); + }); }); From 0a62eb303f71e495306fab19075bc6fe86b4bc31 Mon Sep 17 00:00:00 2001 From: nagasrisai <59650078+nagasrisai@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:06:19 +0530 Subject: [PATCH 03/13] add searchPlaceholder translation key for dropdown search input --- .../src/airflow/ui/public/i18n/locales/en/components.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json index 67e348ca11e35..c68fe441c5bb5 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json @@ -78,7 +78,8 @@ "validationErrorArrayNotArray": "Value must be an array.", "validationErrorArrayNotNumbers": "All elements in the array must be numbers.", "validationErrorArrayNotObject": "All elements in the array must be objects.", - "validationErrorRequired": "This field is required" + "validationErrorRequired": "This field is required", + "searchPlaceholder": "Search options" }, "graph": { "directionDown": "Top to Bottom", From d93310bf504db3e3b773e99a2637c79dc2f4e576 Mon Sep 17 00:00:00 2001 From: nagasrisai <59650078+nagasrisai@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:00:41 +0530 Subject: [PATCH 04/13] switch FieldDropdown to chakra-react-select for built-in search support --- .../components/FlexibleForm/FieldDropdown.tsx | 101 ++++++------------ 1 file changed, 34 insertions(+), 67 deletions(-) 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 8db34a576758f..0a3ee45d57da1 100644 --- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx +++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FieldDropdown.tsx @@ -16,12 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import { Input } from "@chakra-ui/react"; -import { createListCollection } from "@chakra-ui/react/collection"; -import { useRef, useState } 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 "."; @@ -43,92 +40,62 @@ const labelLookup = ( const enumTypes = ["string", "number", "integer"]; +type Option = { + label: string; + value: string; +}; + export const FieldDropdown = ({ name, namespace = "default", onUpdate }: FlexibleFormElementProps) => { const { t: translate } = useTranslation("components"); const { disabled, paramsDict, setParamsDict } = useParamStore(namespace); const param = paramsDict[name] ?? paramPlaceholder; - const [searchQuery, setSearchQuery] = useState(""); - - const allItems = - 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 selectOptions = createListCollection({ items: allItems }); - - const filteredItems = searchQuery - ? allItems.filter((opt) => opt.label.toLowerCase().includes(searchQuery.toLowerCase())) - : allItems; - - const contentRef = useRef(null); - - const handleChange = ([value]: Array) => { + const options: Array