From 2b90498d24d7f4bdb7d44454dcc1ef8583a33202 Mon Sep 17 00:00:00 2001 From: anserwaseem Date: Thu, 11 Sep 2025 21:35:29 +0500 Subject: [PATCH] add support in Dropdown for creating new options using allowCreateOptions prop --- .changeset/quiet-bulldogs-tan.md | 6 + .../src/ensemble/screens/forms.yaml | 14 ++ .../runtime/src/widgets/Form/Dropdown.tsx | 136 ++++++++++++++++-- packages/runtime/src/widgets/Form/Form.tsx | 43 +++++- .../widgets/Form/__tests__/Dropdown.test.tsx | 107 ++++++++++++++ 5 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 .changeset/quiet-bulldogs-tan.md diff --git a/.changeset/quiet-bulldogs-tan.md b/.changeset/quiet-bulldogs-tan.md new file mode 100644 index 000000000..989bc3f97 --- /dev/null +++ b/.changeset/quiet-bulldogs-tan.md @@ -0,0 +1,6 @@ +--- +"@ensembleui/react-kitchen-sink": patch +"@ensembleui/react-runtime": patch +--- + +add support in Dropdown for creating new options using allowCreateOptions prop diff --git a/apps/kitchen-sink/src/ensemble/screens/forms.yaml b/apps/kitchen-sink/src/ensemble/screens/forms.yaml index 2c6fb9e63..fe0717ec8 100644 --- a/apps/kitchen-sink/src/ensemble/screens/forms.yaml +++ b/apps/kitchen-sink/src/ensemble/screens/forms.yaml @@ -165,6 +165,20 @@ View: onChange: executeCode: | console.log('dropdown changed', value); + - Dropdown: + id: dropdownWithCreateOptions + label: Dropdown with create options + hintText: Try typing "New Option" + allowCreateOptions: true + value: option2 + items: + - label: Option 1 + value: option1 + - label: Option 2 + value: option2 + onChange: + executeCode: | + console.log('dropdownWithCreateOptions changed', value); # If you omit id, the form value key will be the label - TextInput: id: formTextInput diff --git a/packages/runtime/src/widgets/Form/Dropdown.tsx b/packages/runtime/src/widgets/Form/Dropdown.tsx index 81d873b77..52d028fa4 100644 --- a/packages/runtime/src/widgets/Form/Dropdown.tsx +++ b/packages/runtime/src/widgets/Form/Dropdown.tsx @@ -1,5 +1,12 @@ -import { Select, Form } from "antd"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Select, Form, Space } from "antd"; +import { PlusCircleOutlined } from "@ant-design/icons"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { CustomScopeProvider, evaluate, @@ -26,6 +33,7 @@ import { getComponentStyles } from "../../shared/styles"; import type { HasBorder } from "../../shared/hasSchema"; import type { FormInputProps } from "./types"; import { EnsembleFormItem } from "./FormItem"; +import { updateFieldValue } from "./Form"; const widgetName = "Dropdown"; @@ -56,6 +64,7 @@ export type DropdownProps = { autoComplete: Expression; hintStyle?: EnsembleWidgetStyles; panel?: { [key: string]: unknown }; + allowCreateOptions?: boolean; } & EnsembleWidgetProps & HasItemTemplate & { "item-template"?: { value: Expression }; @@ -64,11 +73,49 @@ export type DropdownProps = { const DropdownRenderer = ( menu: React.ReactElement, panel?: { [key: string]: unknown }, + newOption?: string, + onAddNewOption?: (value: string) => void, ): React.ReactElement => { const panelOption = useMemo(() => { return panel ? EnsembleRuntime.render([unwrapWidget(panel)]) : null; }, []); + // if we have a new option to add, show custom content along with the menu + if (newOption) { + return ( + <> +
+ There are no matches +
+ onAddNewOption?.(newOption)} + > + + + {`Add "${newOption}"`} + + + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {panelOption} +
+ + ); + } + return ( <> {menu} @@ -89,8 +136,10 @@ const Dropdown: React.FC = (props) => { const [selectedValue, setSelectedValue] = useState< string | number | undefined >(); - + const [newOption, setNewOption] = useState(""); const [isOpen, setIsOpen] = useState(false); + const [items, setItems] = useState([]); + const initializedRef = useRef(false); const { "item-template": itemTemplate, onChange, @@ -124,8 +173,21 @@ const Dropdown: React.FC = (props) => { (value?: number | string) => { setSelectedValue(value); onChangeAction?.callback({ value }); + + // if the selected value is a new option that doesn't exist in items, add it + if (value && values?.allowCreateOptions) { + const optionExists = items.find((item) => item.value === value); + if (!optionExists) { + const newItem: SelectOption = { + label: value.toString(), + value: value, + }; + setItems((prevItems) => [...prevItems, newItem]); + } + } + setNewOption(""); }, - [onChangeAction?.callback], + [onChangeAction?.callback, values?.allowCreateOptions, items], ); const onItemSelectAction = useEnsembleAction(onItemSelect); @@ -137,16 +199,60 @@ const Dropdown: React.FC = (props) => { [onItemSelectAction?.callback], ); + const handleSearch = (value: string): void => { + if (!values?.allowCreateOptions) { + return; + } + + const isOptionExist = items.find((option) => + option.label.toString().toLowerCase().includes(value.toLowerCase()), + ); + + if (!isOptionExist && value.trim()) { + setNewOption(value); + } else { + setNewOption(""); + } + }; + + const handleAddNewOption = useCallback( + (value: string) => { + const newItem: SelectOption = { + label: value, + value: value, + }; + setItems((prevItems) => [...prevItems, newItem]); + + setSelectedValue(value); + onChangeAction?.callback({ value }); + + // trigger form's onChange action + updateFieldValue(formInstance, values?.id ?? values?.label ?? "", value); + + setNewOption(""); + setIsOpen(false); + }, + [onChangeAction?.callback], + ); + const { namedData } = useTemplateData({ data: itemTemplate?.data, name: itemTemplate?.name, }); + // initialize items from props only once + useEffect(() => { + if (values?.items && !initializedRef.current) { + setItems(values.items); + initializedRef.current = true; + } + }, [values?.items]); + const options = useMemo(() => { let dropdownOptions: React.ReactNode[] | null = null; - if (values?.items) { - const tempOptions = values.items.map((item) => { + if (items.length > 0) { + const tempOptions = items.map((item) => { if (item.type === "group") { // Render a group item with sub-items return ( @@ -166,7 +272,7 @@ const Dropdown: React.FC = (props) => { } return ( @@ -206,7 +312,7 @@ const Dropdown: React.FC = (props) => { } return dropdownOptions; - }, [values?.items, namedData, itemTemplate]); + }, [items, namedData, itemTemplate]); const { backgroundColor: _, ...formItemStyles } = values?.styles ?? {}; @@ -303,17 +409,24 @@ const Dropdown: React.FC = (props) => {