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}
+ />
);
};