From 2976e421edf19da427f86388f8a4f23de18e1c26 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 29 Jul 2024 17:17:52 -0300 Subject: [PATCH 1/8] Fixed refresh button and refresh parameter --- .../component/refreshParameterComponent/index.tsx | 6 ++++-- .../src/components/parameterRenderComponent/index.tsx | 1 + src/frontend/src/components/ui/refreshButton.tsx | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx index 23139f5776c..8289143f69b 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/refreshParameterComponent/index.tsx @@ -8,6 +8,7 @@ export function RefreshParameterComponent({ templateData, disabled, nodeClass, + editNode, handleNodeClass, nodeId, name, @@ -29,12 +30,13 @@ export function RefreshParameterComponent({ ); return (
-
{children}
+ {children} {templateData.refresh_button && ( -
+
{button_text && {button_text}} From fa9bac48bd804f5b146ba65ae06854a602ed3381 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 29 Jul 2024 17:20:28 -0300 Subject: [PATCH 2/8] Refactored Multiselect component and implemented custom values on it --- .../components/multiselectComponent/index.tsx | 507 +++++++----------- .../component/strRenderComponent/index.tsx | 8 +- src/frontend/src/types/components/index.ts | 11 + src/frontend/src/utils/styleUtils.ts | 2 + 4 files changed, 220 insertions(+), 308 deletions(-) diff --git a/src/frontend/src/components/multiselectComponent/index.tsx b/src/frontend/src/components/multiselectComponent/index.tsx index e294c4edd69..ba8e825d93d 100644 --- a/src/frontend/src/components/multiselectComponent/index.tsx +++ b/src/frontend/src/components/multiselectComponent/index.tsx @@ -1,24 +1,16 @@ -"use client"; - -import { VariantProps, cva } from "class-variance-authority"; -import isEqual from "lodash.isequal"; -import { CheckIcon, ChevronDown, XCircle, XIcon } from "lucide-react"; -import { forwardRef, useEffect, useRef, useState } from "react"; - -import useMergeRefs, { - isRefObject, -} from "../../CustomNodes/hooks/use-merge-refs"; +import { PopoverAnchor } from "@radix-ui/react-popover"; +import Fuse from "fuse.js"; +import { useEffect, useRef, useState } from "react"; +import { MultiselectComponentType } from "../../types/components"; import { cn } from "../../utils/utils"; -import { Badge } from "../ui/badge"; +import { default as ForwardedIconComponent } from "../genericIconComponent"; import { Button } from "../ui/button"; import { Command, CommandEmpty, CommandGroup, - CommandInput, CommandItem, CommandList, - CommandSeparator, } from "../ui/command"; import { Popover, @@ -26,306 +18,213 @@ import { PopoverContentWithoutPortal, PopoverTrigger, } from "../ui/popover"; -import { Separator } from "../ui/separator"; -const MultiselectBadgeWrapper = ({ +export default function MultiselectComponent({ + disabled, + isLoading, value, - variant, - className, - onDelete, -}: { - value: MultiselectValue; - variant: MultiselectProps["variant"]; - className: MultiselectProps["className"]; - onDelete: ({ value }: { value: MultiselectValue }) => void; -}) => { - const badgeRef = useRef(null); - - const handleDelete = ( - event: React.MouseEvent, - ) => { - event.stopPropagation(); - onDelete({ value }); - }; - - return ( - -
- {value?.label} -
-
-
- -
- - ); -}; - -const multiselectVariants = cva("m-1 ", { - variants: { - variant: { - default: - "border-foreground/10 text-foreground bg-card hover:bg-card/80 whitespace-normal", - secondary: - "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80 whitespace-normal", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80 whitespace-normal", - inverted: "inverted", - }, - }, - defaultVariants: { - variant: "default", - }, -}); - -type MultiselectValue = { - label: string; - value: string; -}; - -interface MultiselectProps - extends Omit, "value">, - VariantProps { - options: T[]; - onValueChange: (value: T[]) => void; - placeholder?: string; - asChild?: boolean; - className?: string; - editNode?: boolean; - values?: T[]; -} - -export const Multiselect = forwardRef< - HTMLButtonElement, - MultiselectProps ->( - ( - { - options = [], - onValueChange, - variant, - placeholder = "Select options", - asChild = false, - className, - editNode = false, - values, - ...props - }, - ref, - ) => { - // if elements in values are strings, create the multiselectValue object - // otherwise, use the values as is - const value = Array.isArray(values) - ? values?.map((v) => (typeof v === "string" ? { label: v, value: v } : v)) - : []; - - const [selectedValues, setSelectedValues] = useState( - value || [], + options: defaultOptions, + combobox, + onSelect, + editNode = false, + id = "", + children, +}: MultiselectComponentType): JSX.Element { + const [open, setOpen] = useState(children ? true : false); + + const refButton = useRef(null); + + const PopoverContentDropdown = + children || editNode ? PopoverContent : PopoverContentWithoutPortal; + + const [customValues, setCustomValues] = useState([]); + const [searchValue, setSearchValue] = useState(""); + const [filteredOptions, setFilteredOptions] = useState(defaultOptions); + const [onlySelected, setOnlySelected] = useState(false); + + const [options, setOptions] = useState(defaultOptions); + + const fuseOptions = new Fuse(options, { keys: ["name", "value"] }); + const fuseValues = new Fuse(value, { keys: ["name", "value"] }); + + const searchRoleByTerm = async (v: string) => { + const fuse = onlySelected ? fuseValues : fuseOptions; + const searchValues = fuse.search(v); + let filtered: string[] = searchValues.map((search) => search.item); + if (!filtered.includes(v) && combobox) filtered = [v, ...filtered]; + setFilteredOptions( + v + ? filtered + : onlySelected + ? options.filter((x) => value.includes(x)) + : options, ); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const combinedRef = useMergeRefs(ref); - useEffect(() => { - if (!!value && value?.length > 0 && !isEqual(selectedValues, value)) { - setSelectedValues(value); - } - }, [value, selectedValues]); - - const handleInputKeyDown = ( - event: React.KeyboardEvent, - ) => { - if (event.key === "Enter") { - setIsPopoverOpen(true); - } else if (event.key === "Backspace" && !event.currentTarget.value) { - const newSelectedValues = [...selectedValues]; - newSelectedValues.pop(); - setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); - } - }; - - const toggleOption = ({ value }: { value: MultiselectValue }) => { - const newSelectedValues = !!selectedValues.find( - (v) => v.value === value.value, - ) - ? selectedValues.filter((v) => v.value !== value.value) - : [...selectedValues, value]; - setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); - }; - - const handleClear = () => { - setSelectedValues([]); - onValueChange([]); - }; - - const handleTogglePopover = () => { - setIsPopoverOpen((prev) => !prev); - }; - - const PopoverContentMultiselect = editNode - ? PopoverContent - : PopoverContentWithoutPortal; + }; - const popoverContentMultiselectMinWidth = isRefObject(combinedRef) - ? `${combinedRef?.current?.clientWidth}px` - : "200px"; + useEffect(() => { + searchRoleByTerm(searchValue); + }, [onlySelected]); + + useEffect(() => { + searchRoleByTerm(searchValue); + }, [options]); + + useEffect(() => { + setCustomValues(value.filter((v) => !defaultOptions.includes(v)) ?? []); + setOptions([ + ...value.filter((v) => !defaultOptions.includes(v)), + ...defaultOptions, + ]); + }, [value]); + + useEffect(() => { + if (open) { + setOnlySelected(false); + setSearchValue(""); + searchRoleByTerm(""); + } + }, [open]); - return ( - - - + )} - > - {selectedValues?.length > 0 ? ( -
-
- {selectedValues?.map((selectedValue) => { - return ( - - ); - })} -
-
- { - event.stopPropagation(); - handleClear(); - }} + { + event.preventDefault(); + }} + side="bottom" + avoidCollisions={!!children} + className="noflow nowheel nopan nodelete nodrag p-0" + style={ + children + ? {} + : { minWidth: refButton?.current?.clientWidth ?? "200px" } + } + > + +
+ - { + setSearchValue(event.target.value); + searchRoleByTerm(event.target.value); + }} + placeholder="Search options..." + className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50" /> - +
-
- ) : ( -
- - {placeholder} - - -
- )} - - - setIsPopoverOpen(false)} - > - - - - No results found. - - {value?.map((option) => { - const isSelected = !!selectedValues.find( - (sv) => sv.value === option.value, - ); - return ( - toggleOption({ value: option })} - className="cursor-pointer" - > -
- -
- {option.label} -
- ); - })} -
- - -
- {selectedValues?.length > 0 && ( - <> + + + No values found. + + {filteredOptions?.map((option, id) => ( { + if (value.includes(currentValue)) { + onSelect(value.filter((v) => v !== currentValue)); + } else { + onSelect([...value, currentValue]); + } + }} + className="items-center truncate" + data-testid={`${option}-${id ?? ""}-option`} > - Clear + {customValues.includes(option) || + searchValue === option ? ( + + Text:  + + ) : ( + <> + )} + {option} + - - - )} - - setIsPopoverOpen(false)} - className="flex-1 cursor-pointer justify-center" - > - Close - -
-
-
-
-
- - ); - }, -); - -Multiselect.displayName = "Multiselect"; + ))} + + + + + + + ) : ( + <> + {(!isLoading && ( +
+ + No parameters are available for display. + +
+ )) || ( +
+ Loading... +
+ )} + + )} + + ); +} diff --git a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx index 1c8dad612c5..10067e3c354 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx @@ -3,7 +3,7 @@ import { InputFieldType } from "@/types/api"; import Dropdown from "../../../dropdownComponent"; import InputGlobalComponent from "../../../inputGlobalComponent"; import InputListComponent from "../../../inputListComponent"; -import { Multiselect } from "../../../multiselectComponent"; +import MultiselectComponent from "../../../multiselectComponent"; import TextAreaComponent from "../../../textAreaComponent"; export function StrRenderComponent({ @@ -58,13 +58,13 @@ export function StrRenderComponent({ if (!!templateData.options && !!templateData?.list) { return ( - ); } diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 29eaeb5baf8..ecbbcdca488 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -61,6 +61,17 @@ export type DropDownComponentType = { id?: string; children?: ReactNode; }; +export type MultiselectComponentType = { + disabled?: boolean; + isLoading?: boolean; + value: string[]; + combobox?: boolean; + options: string[]; + onSelect: (value: string[]) => void; + editNode?: boolean; + id?: string; + children?: ReactNode; +}; export type ParameterComponentType = { selected?: boolean; data: NodeDataType; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index f5389f3b984..3bde56e9aef 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -16,6 +16,7 @@ import { Braces, BrainCircuit, Check, + CheckCheck, CheckCircle2, ChevronDown, ChevronLeft, @@ -390,6 +391,7 @@ export const nodeIconsLucide: iconsType = { IFixitLoader: IFixIcon, CrewAI: CrewAiIcon, Meta: MetaIcon, + CheckCheck, Midjorney: MidjourneyIcon, MongoDBAtlasVectorSearch: MongoDBIcon, MongoDB: MongoDBIcon, From 3759795ab32471463a5c6d2374e19fa8be4aa348 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 29 Jul 2024 17:25:29 -0300 Subject: [PATCH 3/8] Fixed default values --- .../component/strRenderComponent/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx index 10067e3c354..72f82ca0d07 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx @@ -62,7 +62,7 @@ export function StrRenderComponent({ editNode={editNode} disabled={disabled} options={templateData.options || []} - value={value ?? "Choose an option"} + value={value || []} id={`multiselect_${id}`} onSelect={onChange} /> @@ -76,7 +76,7 @@ export function StrRenderComponent({ options={templateData.options} onSelect={onChange} combobox={templateData.combobox} - value={value ?? "Choose an option"} + value={value || ""} id={`dropdown_${id}`} /> ); From f0911a262cd1ce471ea27dd274425de49f177e66 Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 29 Jul 2024 17:30:52 -0300 Subject: [PATCH 4/8] Made Backend support combobox on Dropdown and Multiselect --- src/backend/base/langflow/inputs/input_mixin.py | 9 ++------- src/backend/base/langflow/inputs/inputs.py | 4 ++-- .../component/strRenderComponent/index.tsx | 1 + 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 9b9c49db6c5..46881bff725 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -135,16 +135,11 @@ class RangeMixin(BaseModel): class DropDownMixin(BaseModel): options: Optional[list[str]] = None """List of options for the field. Only used when is_list=True. Default is an empty list.""" - - -class MultilineMixin(BaseModel): - multiline: CoalesceBool = True - - -class ComboboxMixin(BaseModel): combobox: CoalesceBool = False """Variable that defines if the user can insert custom values in the dropdown.""" +class MultilineMixin(BaseModel): + multiline: CoalesceBool = True class TableMixin(BaseModel): table_schema: Optional[TableSchema | list[Column]] = None diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index bd81fc3c475..477f01c270b 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -11,7 +11,6 @@ BaseInputMixin, DatabaseLoadMixin, DropDownMixin, - ComboboxMixin, FieldTypes, FileMixin, InputTraceMixin, @@ -293,7 +292,7 @@ class DictInput(BaseInputMixin, ListableInputMixin, InputTraceMixin): value: Optional[dict] = {} -class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ComboboxMixin): +class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin): """ Represents a dropdown input field. @@ -327,6 +326,7 @@ class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, Metada field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT options: list[str] = Field(default_factory=list) is_list: bool = Field(default=True, serialization_alias="list") + combobox: CoalesceBool = False class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixin): diff --git a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx index 72f82ca0d07..48e15d06e43 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx @@ -62,6 +62,7 @@ export function StrRenderComponent({ editNode={editNode} disabled={disabled} options={templateData.options || []} + combobox={templateData.combobox} value={value || []} id={`multiselect_${id}`} onSelect={onChange} From 3237cd82a335a9fe108de50769d3d6f0601839e7 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:38:13 +0000 Subject: [PATCH 5/8] [autofix.ci] apply automated fixes --- src/backend/base/langflow/inputs/input_mixin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 46881bff725..fe7f54f5674 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -138,9 +138,11 @@ class DropDownMixin(BaseModel): combobox: CoalesceBool = False """Variable that defines if the user can insert custom values in the dropdown.""" + class MultilineMixin(BaseModel): multiline: CoalesceBool = True + class TableMixin(BaseModel): table_schema: Optional[TableSchema | list[Column]] = None From 212d4179cadee78285287b007bc8c1886867fe6e Mon Sep 17 00:00:00 2001 From: Lucas Oliveira Date: Mon, 29 Jul 2024 17:44:28 -0300 Subject: [PATCH 6/8] Fixed multiselect breaking if value is string --- src/backend/base/langflow/inputs/inputs.py | 2 +- .../component/strRenderComponent/index.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 20b298cc791..005364cf839 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -315,7 +315,7 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin): Attributes: field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.TEXT. - options (Optional[Union[list[str], Callable]]): List of options for the field. Only used when is_list=True. + options (Optional[Union[list[str], Callable]]): List of options for the field. Default is None. """ diff --git a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx index 7b6d98ff12f..75ee42e2a3a 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx @@ -70,7 +70,11 @@ export function StrRenderComponent({ Date: Mon, 29 Jul 2024 17:47:07 -0300 Subject: [PATCH 7/8] Fixed frontend breaking if value is string instead of list for multiselect component --- .../component/strRenderComponent/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx index 75ee42e2a3a..9f0773f6776 100644 --- a/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx +++ b/src/frontend/src/components/parameterRenderComponent/component/strRenderComponent/index.tsx @@ -33,7 +33,7 @@ export function StrRenderComponent({ componentName={name ?? undefined} editNode={editNode} disabled={disabled} - value={!value || value === "" ? [""] : value} + value={value || [""]} onChange={onChange} id={`inputlist_${id}`} /> @@ -76,7 +76,11 @@ export function StrRenderComponent({ : [templateData.options]) || [] } combobox={templateData.combobox} - value={value || []} + value={ + (Array.isArray(templateData.value) + ? templateData.value + : [templateData.value]) || [] + } id={`multiselect_${id}`} onSelect={onChange} /> From af950b192dd2d2d0bdca53b1b62c1bc57987e77f Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Mon, 29 Jul 2024 17:48:37 -0300 Subject: [PATCH 8/8] feat: Add validation for MultiselectInput value --- src/backend/base/langflow/inputs/inputs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 005364cf839..c8023775f24 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -342,6 +342,17 @@ class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, Metada is_list: bool = Field(default=True, serialization_alias="list") combobox: CoalesceBool = False + @field_validator("value") + @classmethod + def validate_value(cls, v: Any, _info): + # Check if value is a list of dicts + if not isinstance(v, list): + raise ValueError(f"MultiselectInput value must be a list. Value: '{v}'") + for item in v: + if not isinstance(item, str): + raise ValueError(f"MultiselectInput value must be a list of strings. Item: '{item}' is not a string") + return v + class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixin): """