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..2fda9b292de52 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 = {}; 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; 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 3fc5f83551988..ab2271eefd8ae 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,57 @@ 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(null); + const currentValue = + // eslint-disable-next-line unicorn/no-null + 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) => { + 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 ( - 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 - } - > - - - - - {selectOptions.items.map((option) => ( - - {option.label} - - ))} - - + value={currentValue} + /> ); };