From f213f487a6df35b6f0452ee2fea3717d389cf91a Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Tue, 22 Apr 2025 21:17:15 -0300 Subject: [PATCH] feat: Allow dropdown to add new values when they don't exist in options list (#7641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 (dropdownComponent/index.tsx): refactor filteredOptions state initialization to include custom values not in validOptions ♻️ (dropdownComponent/index.tsx): refactor value memoization logic to handle custom values and improve performance 🔧 (dropdownComponent/index.tsx): refactor filteredOptions state update logic to handle custom values and improve user experience * 📝 (RenderInputParameters/index.tsx): Remove unnecessary console.log statement 🔧 (dropdownComponent/index.tsx): Add constant RECEIVING_INPUT_VALUE and update styles for disabled state in Dropdown component * ✨ (dropdownComponent/index.tsx): add new constant SELECT_AN_OPTION to improve user experience by providing a default option when no value is selected. * ✨ (constants.ts): add constant SELECT_AN_OPTION to improve user experience by providing a clear message to select an option --------- Co-authored-by: deon-sanchez Co-authored-by: Edwin Jose --- .../core/dropdownComponent/index.tsx | 85 ++++++++++++++----- src/frontend/src/constants/constants.ts | 2 +- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/frontend/src/components/core/dropdownComponent/index.tsx b/src/frontend/src/components/core/dropdownComponent/index.tsx index 818144c3e..cf400b66d 100644 --- a/src/frontend/src/components/core/dropdownComponent/index.tsx +++ b/src/frontend/src/components/core/dropdownComponent/index.tsx @@ -1,4 +1,5 @@ import LoadingTextComponent from "@/components/common/loadingTextComponent"; +import { RECEIVING_INPUT_VALUE, SELECT_AN_OPTION } from "@/constants/constants"; import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value"; import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent"; import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template"; @@ -54,7 +55,6 @@ export default function Dropdown({ dialogInputs, handleOnNewValue, toggle, - hasRefreshButton, ...baseInputProps }: BaseInputProps & DropDownComponent): JSX.Element { const validOptions = useMemo( @@ -66,12 +66,20 @@ export default function Dropdown({ const [open, setOpen] = useState(children ? true : false); const [openDialog, setOpenDialog] = useState(false); const [customValue, setCustomValue] = useState(""); - const [filteredOptions, setFilteredOptions] = useState(validOptions); + const [filteredOptions, setFilteredOptions] = useState(() => { + // Include the current value in filteredOptions if it's a custom value not in validOptions + if (value && !validOptions.includes(value)) { + return [...validOptions, value]; + } + return validOptions; + }); const [filteredMetadata, setFilteredMetadata] = useState(optionsMetaData); const [refreshOptions, setRefreshOptions] = useState(false); const refButton = useRef(null); value = useMemo(() => { + // We should only reset the value if it's not in options and not in filteredOptions + // and not a recently added custom value if (!options.includes(value) && !filteredOptions.includes(value)) { return null; } @@ -112,24 +120,50 @@ export default function Dropdown({ if (!value) { // If search is cleared, show all options - setFilteredOptions(validOptions); + // Preserve any custom values that were in filteredOptions + const customValuesInFiltered = filteredOptions.filter( + (option) => !validOptions.includes(option), + ); + setFilteredOptions([...validOptions, ...customValuesInFiltered]); setFilteredMetadata(optionsMetaData); return; } // Search existing options const searchValues = fuse.search(value); - const filtered = searchValues.map((search) => search.item); + let filtered = searchValues.map((search) => search.item); + + // If the search value exactly matches one of the custom options, include it + const customOptions = filteredOptions.filter( + (option) => !validOptions.includes(option), + ); + const matchingCustomOption = customOptions.find( + (option) => option.toLowerCase() === value.toLowerCase(), + ); + + // Include matching custom options or allow adding the current search if combobox is true + if (matchingCustomOption && !filtered.includes(matchingCustomOption)) { + filtered.push(matchingCustomOption); + } else if ( + combobox && + value && + !filtered.some((opt) => opt.toLowerCase() === value.toLowerCase()) + ) { + // If combobox is enabled and we're typing a new value, include it in the filtered list + filtered = [value, ...filtered]; + } // Update filteredOptions with the search results setFilteredOptions(filtered); // Update filteredMetadata to match the filtered options if (optionsMetaData) { - const newMetadata = filtered.map((option) => { - const originalIndex = validOptions.indexOf(option); - return optionsMetaData[originalIndex]; - }); + const newMetadata = filtered + .filter((option) => validOptions.includes(option)) // Only map metadata for valid options + .map((option) => { + const originalIndex = validOptions.indexOf(option); + return optionsMetaData[originalIndex]; + }); setFilteredMetadata(newMetadata); } else { setFilteredMetadata(optionsMetaData); @@ -181,7 +215,17 @@ export default function Dropdown({ useEffect(() => { if (open) { - setFilteredOptions(validOptions); + // Check if filteredOptions contains any custom values not in validOptions + const customValuesInFiltered = filteredOptions.filter( + (option) => !validOptions.includes(option), + ); + + // If there are custom values, preserve them when resetting filtered options + if (customValuesInFiltered.length > 0) { + setFilteredOptions([...validOptions, ...customValuesInFiltered]); + } else { + setFilteredOptions(validOptions); + } setCustomValue(""); } }, [open, validOptions]); @@ -233,14 +277,11 @@ export default function Dropdown({ editNode ? "dropdown-component-outline input-edit-node" : "dropdown-component-false-outline py-2", - "no-focus-visible w-full justify-between font-normal", + "no-focus-visible w-full justify-between font-normal disabled:bg-muted disabled:text-muted-foreground", )} > {optionsMetaData?.[ @@ -256,17 +297,23 @@ export default function Dropdown({ /> )} - {value && filteredOptions.includes(value) - ? value - : placeholderName}{" "} + {disabled ? ( + RECEIVING_INPUT_VALUE + ) : ( + <> + {value && filteredOptions.includes(value) + ? value + : SELECT_AN_OPTION}{" "} + + )} diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index bea80f179..fced4b93b 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -1005,7 +1005,7 @@ export const GRADIENT_CLASS_DISABLED = "linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))"; export const RECEIVING_INPUT_VALUE = "Receiving input"; -export const SELECT_AN_OPTION = "Select an option..."; +export const SELECT_AN_OPTION = "Select an option"; export const ICON_STROKE_WIDTH = 1.5;