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 { 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<HTMLButtonElement>(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",
)}
>
<span
className={cn(
"flex w-full items-center gap-2 overflow-hidden",
hasRefreshButton && "max-w-[11rem]",
)}
className="flex w-full items-center gap-2 overflow-hidden"
data-testid={`value-dropdown-${id}`}
>
{optionsMetaData?.[
@ -256,17 +297,23 @@ export default function Dropdown({
/>
)}
<span className="truncate">
{value && filteredOptions.includes(value)
? value
: placeholderName}{" "}
{disabled ? (
RECEIVING_INPUT_VALUE
) : (
<>
{value && filteredOptions.includes(value)
? value
: SELECT_AN_OPTION}{" "}
</>
)}
</span>
</span>
<ForwardedIconComponent
name="ChevronsUpDown"
name={disabled ? "Lock" : "ChevronsUpDown"}
className={cn(
"ml-2 h-4 w-4 shrink-0 text-foreground",
disabled
? "hover:text-placeholder-foreground"
? "text-placeholder-foreground hover:text-placeholder-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)))";
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;