feat: Allow dropdown to add new values when they don't exist in options list (#7641)

* 🔧 (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 <deon.sanchez@datastax.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2025-04-22 21:17:15 -03:00 • committed by GitHub
commit f213f487a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 67 additions and 20 deletions

View file

@ -1,4 +1,5 @@
import LoadingTextComponent from "@/components/common/loadingTextComponent"; 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 { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent"; import NodeDialog from "@/CustomNodes/GenericNode/components/NodeDialogComponent";
import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template"; import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template";
@ -54,7 +55,6 @@ export default function Dropdown({
dialogInputs, dialogInputs,
handleOnNewValue, handleOnNewValue,
toggle, toggle,
hasRefreshButton,
...baseInputProps ...baseInputProps
}: BaseInputProps & DropDownComponent): JSX.Element { }: BaseInputProps & DropDownComponent): JSX.Element {
const validOptions = useMemo( const validOptions = useMemo(
@ -66,12 +66,20 @@ export default function Dropdown({
const [open, setOpen] = useState(children ? true : false); const [open, setOpen] = useState(children ? true : false);
const [openDialog, setOpenDialog] = useState(false); const [openDialog, setOpenDialog] = useState(false);
const [customValue, setCustomValue] = useState(""); 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 [filteredMetadata, setFilteredMetadata] = useState(optionsMetaData);
const [refreshOptions, setRefreshOptions] = useState(false); const [refreshOptions, setRefreshOptions] = useState(false);
const refButton = useRef<HTMLButtonElement>(null); const refButton = useRef<HTMLButtonElement>(null);
value = useMemo(() => { 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)) { if (!options.includes(value) && !filteredOptions.includes(value)) {
return null; return null;
} }
@ -112,24 +120,50 @@ export default function Dropdown({
if (!value) { if (!value) {
// If search is cleared, show all options // 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); setFilteredMetadata(optionsMetaData);
return; return;
} }
// Search existing options // Search existing options
const searchValues = fuse.search(value); 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 // Update filteredOptions with the search results
setFilteredOptions(filtered); setFilteredOptions(filtered);
// Update filteredMetadata to match the filtered options // Update filteredMetadata to match the filtered options
if (optionsMetaData) { if (optionsMetaData) {
const newMetadata = filtered.map((option) => { const newMetadata = filtered
const originalIndex = validOptions.indexOf(option); .filter((option) => validOptions.includes(option)) // Only map metadata for valid options
return optionsMetaData[originalIndex]; .map((option) => {
}); const originalIndex = validOptions.indexOf(option);
return optionsMetaData[originalIndex];
});
setFilteredMetadata(newMetadata); setFilteredMetadata(newMetadata);
} else { } else {
setFilteredMetadata(optionsMetaData); setFilteredMetadata(optionsMetaData);
@ -181,7 +215,17 @@ export default function Dropdown({
useEffect(() => { useEffect(() => {
if (open) { 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(""); setCustomValue("");
} }
}, [open, validOptions]); }, [open, validOptions]);
@ -233,14 +277,11 @@ export default function Dropdown({
editNode editNode
? "dropdown-component-outline input-edit-node" ? "dropdown-component-outline input-edit-node"
: "dropdown-component-false-outline py-2", : "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",
)} )}
> >
<span <span
className={cn( className="flex w-full items-center gap-2 overflow-hidden"
"flex w-full items-center gap-2 overflow-hidden",
hasRefreshButton && "max-w-[11rem]",
)}
data-testid={`value-dropdown-${id}`} data-testid={`value-dropdown-${id}`}
> >
{optionsMetaData?.[ {optionsMetaData?.[
@ -256,17 +297,23 @@ export default function Dropdown({
/> />
)} )}
<span className="truncate"> <span className="truncate">
{value && filteredOptions.includes(value) {disabled ? (
? value RECEIVING_INPUT_VALUE
: placeholderName}{" "} ) : (
<>
{value && filteredOptions.includes(value)
? value
: SELECT_AN_OPTION}{" "}
</>
)}
</span> </span>
</span> </span>
<ForwardedIconComponent <ForwardedIconComponent
name="ChevronsUpDown" name={disabled ? "Lock" : "ChevronsUpDown"}
className={cn( className={cn(
"ml-2 h-4 w-4 shrink-0 text-foreground", "ml-2 h-4 w-4 shrink-0 text-foreground",
disabled disabled
? "hover:text-placeholder-foreground" ? "text-placeholder-foreground hover:text-placeholder-foreground"
: "hover:text-foreground", : "hover:text-foreground",
)} )}
/> />

View file

@ -1005,7 +1005,7 @@ export const GRADIENT_CLASS_DISABLED =
"linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))"; "linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))";
export const RECEIVING_INPUT_VALUE = "Receiving input"; 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; export const ICON_STROKE_WIDTH = 1.5;