Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1dddf6e
add search input to FieldDropdown so dag params enums are filterable
nagasrisai Mar 18, 2026
013d992
add tests for search/filter behaviour in FieldDropdown
nagasrisai Mar 18, 2026
0a62eb3
add searchPlaceholder translation key for dropdown search input
nagasrisai Mar 18, 2026
4202146
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 18, 2026
a80d0f8
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 18, 2026
d93310b
switch FieldDropdown to chakra-react-select for built-in search support
nagasrisai Mar 18, 2026
7327084
update FieldDropdown tests to match chakra-react-select implementation
nagasrisai Mar 18, 2026
4c3f00d
remove unused searchPlaceholder key from components.json
nagasrisai Mar 18, 2026
457d656
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 19, 2026
ce0d887
add demo screenshot for PR review
nagasrisai Mar 19, 2026
320486d
Add screenshot: searchable dropdown in action
nagasrisai Mar 19, 2026
8deee59
Add screenshot: full dropdown list
nagasrisai Mar 19, 2026
3e8c02e
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 19, 2026
e2333fb
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 19, 2026
e27d261
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 19, 2026
029f3cd
remove committed screenshot trigger-dialog-dropdown.png
nagasrisai Mar 19, 2026
09048b0
remove committed screenshot trigger-dialog-search.png
nagasrisai Mar 19, 2026
3e47853
remove demo screenshot from docs
nagasrisai Mar 19, 2026
a30e8c7
Merge branch 'main' into fix/searchable-dag-params-dropdown
nagasrisai Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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];
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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", () => {
Expand All @@ -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
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ".";
Expand All @@ -39,75 +37,65 @@ const labelLookup = (

return key === null ? "null" : String(key);
};

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 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 options: Array<Option> =
param.schema.enum?.map((value) => ({
label: labelLookup(value, param.schema.values_display),
value: String(value ?? NULL_STRING_VALUE),
})) ?? [];

return {
label: labelLookup(value, param.schema.values_display),
value: stringValue,
};
}) ?? [],
});
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 contentRef = useRef<HTMLDivElement | null>(null);

const handleChange = ([value]: Array<string>) => {
const handleChange = (selected: SingleValue<Option>) => {
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<Option>
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}
/>
);
};
Loading