fix: multiselect dropdown not visible (#3044)
* Fixed refresh button and refresh parameter * Refactored Multiselect component and implemented custom values on it * Fixed default values * Made Backend support combobox on Dropdown and Multiselect
This commit is contained in:
parent
4e9367badf
commit
7fa3d33d8a
9 changed files with 261 additions and 325 deletions
|
|
@ -135,17 +135,14 @@ class RangeMixin(BaseModel):
|
||||||
class DropDownMixin(BaseModel):
|
class DropDownMixin(BaseModel):
|
||||||
options: Optional[list[str]] = None
|
options: Optional[list[str]] = None
|
||||||
"""List of options for the field. Only used when is_list=True. Default is an empty list."""
|
"""List of options for the field. Only used when is_list=True. Default is an empty list."""
|
||||||
|
combobox: CoalesceBool = False
|
||||||
|
"""Variable that defines if the user can insert custom values in the dropdown."""
|
||||||
|
|
||||||
|
|
||||||
class MultilineMixin(BaseModel):
|
class MultilineMixin(BaseModel):
|
||||||
multiline: CoalesceBool = True
|
multiline: CoalesceBool = True
|
||||||
|
|
||||||
|
|
||||||
class ComboboxMixin(BaseModel):
|
|
||||||
combobox: CoalesceBool = False
|
|
||||||
"""Variable that defines if the user can insert custom values in the dropdown."""
|
|
||||||
|
|
||||||
|
|
||||||
class TableMixin(BaseModel):
|
class TableMixin(BaseModel):
|
||||||
table_schema: Optional[TableSchema | list[Column]] = None
|
table_schema: Optional[TableSchema | list[Column]] = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ from .input_mixin import (
|
||||||
BaseInputMixin,
|
BaseInputMixin,
|
||||||
DatabaseLoadMixin,
|
DatabaseLoadMixin,
|
||||||
DropDownMixin,
|
DropDownMixin,
|
||||||
ComboboxMixin,
|
|
||||||
FieldTypes,
|
FieldTypes,
|
||||||
FileMixin,
|
FileMixin,
|
||||||
InputTraceMixin,
|
InputTraceMixin,
|
||||||
|
|
@ -307,7 +306,7 @@ class DictInput(BaseInputMixin, ListableInputMixin, InputTraceMixin):
|
||||||
value: Optional[dict] = {}
|
value: Optional[dict] = {}
|
||||||
|
|
||||||
|
|
||||||
class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ComboboxMixin):
|
class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin):
|
||||||
"""
|
"""
|
||||||
Represents a dropdown input field.
|
Represents a dropdown input field.
|
||||||
|
|
||||||
|
|
@ -316,7 +315,7 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ComboboxM
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
field_type (Optional[SerializableFieldTypes]): The field type of the input. Defaults to FieldTypes.TEXT.
|
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.
|
Default is None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -341,6 +340,18 @@ class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, Metada
|
||||||
field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT
|
field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT
|
||||||
options: list[str] = Field(default_factory=list)
|
options: list[str] = Field(default_factory=list)
|
||||||
is_list: bool = Field(default=True, serialization_alias="list")
|
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):
|
class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixin):
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,16 @@
|
||||||
"use client";
|
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||||
|
import Fuse from "fuse.js";
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import isEqual from "lodash.isequal";
|
import { MultiselectComponentType } from "../../types/components";
|
||||||
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 { cn } from "../../utils/utils";
|
import { cn } from "../../utils/utils";
|
||||||
import { Badge } from "../ui/badge";
|
import { default as ForwardedIconComponent } from "../genericIconComponent";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
|
||||||
} from "../ui/command";
|
} from "../ui/command";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
|
|
@ -26,306 +18,213 @@ import {
|
||||||
PopoverContentWithoutPortal,
|
PopoverContentWithoutPortal,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "../ui/popover";
|
} from "../ui/popover";
|
||||||
import { Separator } from "../ui/separator";
|
|
||||||
|
|
||||||
const MultiselectBadgeWrapper = ({
|
export default function MultiselectComponent({
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
value,
|
value,
|
||||||
variant,
|
options: defaultOptions,
|
||||||
className,
|
combobox,
|
||||||
onDelete,
|
onSelect,
|
||||||
}: {
|
|
||||||
value: MultiselectValue;
|
|
||||||
variant: MultiselectProps<MultiselectValue>["variant"];
|
|
||||||
className: MultiselectProps<MultiselectValue>["className"];
|
|
||||||
onDelete: ({ value }: { value: MultiselectValue }) => void;
|
|
||||||
}) => {
|
|
||||||
const badgeRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleDelete = (
|
|
||||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
|
||||||
) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
onDelete({ value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
className={cn(
|
|
||||||
"overflow-hidden rounded-sm p-0 font-normal",
|
|
||||||
multiselectVariants({ variant, className }),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div id="content" className="p-1 pr-0" ref={badgeRef}>
|
|
||||||
{value?.label}
|
|
||||||
</div>
|
|
||||||
<div id="spacer" className="p-1" />
|
|
||||||
<div
|
|
||||||
id="delete"
|
|
||||||
className="flex items-center justify-center px-1 hover:bg-red-300/80"
|
|
||||||
style={{
|
|
||||||
minHeight: `${badgeRef?.current?.clientHeight}px`,
|
|
||||||
}}
|
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
|
||||||
<XCircle
|
|
||||||
className="h-4 min-h-4 w-4 min-w-4 cursor-pointer"
|
|
||||||
style={{
|
|
||||||
minHeight: `${badgeRef?.current?.clientHeight}px`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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<T>
|
|
||||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "value">,
|
|
||||||
VariantProps<typeof multiselectVariants> {
|
|
||||||
options: T[];
|
|
||||||
onValueChange: (value: T[]) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
asChild?: boolean;
|
|
||||||
className?: string;
|
|
||||||
editNode?: boolean;
|
|
||||||
values?: T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Multiselect = forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
MultiselectProps<MultiselectValue>
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
options = [],
|
|
||||||
onValueChange,
|
|
||||||
variant,
|
|
||||||
placeholder = "Select options",
|
|
||||||
asChild = false,
|
|
||||||
className,
|
|
||||||
editNode = false,
|
editNode = false,
|
||||||
values,
|
id = "",
|
||||||
...props
|
children,
|
||||||
},
|
}: MultiselectComponentType): JSX.Element {
|
||||||
ref,
|
const [open, setOpen] = useState(children ? true : false);
|
||||||
) => {
|
|
||||||
// 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<MultiselectValue[]>(
|
const refButton = useRef<HTMLButtonElement>(null);
|
||||||
value || [],
|
|
||||||
|
const PopoverContentDropdown =
|
||||||
|
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
||||||
|
|
||||||
|
const [customValues, setCustomValues] = useState<string[]>([]);
|
||||||
|
const [searchValue, setSearchValue] = useState("");
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState(defaultOptions);
|
||||||
|
const [onlySelected, setOnlySelected] = useState(false);
|
||||||
|
|
||||||
|
const [options, setOptions] = useState<string[]>(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<HTMLButtonElement>(ref);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!!value && value?.length > 0 && !isEqual(selectedValues, value)) {
|
searchRoleByTerm(searchValue);
|
||||||
setSelectedValues(value);
|
}, [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("");
|
||||||
}
|
}
|
||||||
}, [value, selectedValues]);
|
}, [open]);
|
||||||
|
|
||||||
const handleInputKeyDown = (
|
|
||||||
event: React.KeyboardEvent<HTMLInputElement>,
|
|
||||||
) => {
|
|
||||||
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";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
<>
|
||||||
|
{Object.keys(options ?? [])?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<Popover open={open} onOpenChange={children ? () => {} : setOpen}>
|
||||||
|
{children ? (
|
||||||
|
<PopoverAnchor>{children}</PopoverAnchor>
|
||||||
|
) : (
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
ref={combinedRef}
|
disabled={disabled}
|
||||||
{...props}
|
|
||||||
onClick={handleTogglePopover}
|
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="xs"
|
size="xs"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
|
ref={refButton}
|
||||||
|
aria-expanded={open}
|
||||||
|
data-testid={`${id ?? ""}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
editNode
|
editNode
|
||||||
? "multiselect-component-outline"
|
? "dropdown-component-outline"
|
||||||
: "multiselect-component-false-outline",
|
: "dropdown-component-false-outline",
|
||||||
"w-full justify-between font-normal",
|
"w-full justify-between font-normal",
|
||||||
editNode ? "input-edit-node" : "py-2",
|
editNode ? "input-edit-node" : "py-2",
|
||||||
className,
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedValues?.length > 0 ? (
|
<span
|
||||||
<div className="flex w-full items-center justify-between">
|
className="truncate"
|
||||||
<div className="flex flex-wrap items-center">
|
data-testid={`value-dropdown-` + id}
|
||||||
{selectedValues?.map((selectedValue) => {
|
>
|
||||||
return (
|
{value &&
|
||||||
<MultiselectBadgeWrapper
|
value.length > 0 &&
|
||||||
value={selectedValue}
|
options.find((option) => value.includes(option))
|
||||||
onDelete={toggleOption}
|
? value.join(", ")
|
||||||
variant={variant}
|
: "Choose an option..."}
|
||||||
className={className}
|
|
||||||
key={selectedValue.value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<XIcon
|
|
||||||
className="mx-2 cursor-pointer rounded-md text-sm text-muted-foreground ring-offset-background transition-colors hover:bg-secondary-foreground/5 hover:text-accent-foreground hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:hover:bg-background/10"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleClear();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="flex h-full min-h-6"
|
|
||||||
/>
|
|
||||||
<ChevronDown className="mx-2 h-4 cursor-pointer rounded-md text-sm text-muted-foreground ring-offset-background transition-colors hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mx-auto flex w-full items-center justify-between">
|
|
||||||
<span className="mx-3 text-sm text-muted-foreground">
|
|
||||||
{placeholder}
|
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground hover:text-accent-foreground" />
|
|
||||||
</div>
|
<ForwardedIconComponent
|
||||||
)}
|
name="ChevronsUpDown"
|
||||||
|
className="ml-2 h-4 w-4 shrink-0 opacity-50"
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContentMultiselect
|
|
||||||
id="multiselect-content"
|
|
||||||
side="bottom"
|
|
||||||
className={cn(
|
|
||||||
`nocopy nowheel nopan nodelete nodrag noundo w-full p-0`,
|
|
||||||
)}
|
)}
|
||||||
style={{
|
<PopoverContentDropdown
|
||||||
minWidth: popoverContentMultiselectMinWidth,
|
onOpenAutoFocus={(event) => {
|
||||||
maxWidth: popoverContentMultiselectMinWidth,
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
align="start"
|
side="bottom"
|
||||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
avoidCollisions={!!children}
|
||||||
|
className="noflow nowheel nopan nodelete nodrag p-0"
|
||||||
|
style={
|
||||||
|
children
|
||||||
|
? {}
|
||||||
|
: { minWidth: refButton?.current?.clientWidth ?? "200px" }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<div className="flex items-center border-b px-3">
|
||||||
placeholder="Search"
|
<ForwardedIconComponent
|
||||||
onKeyDown={handleInputKeyDown}
|
name="search"
|
||||||
className="h-9"
|
className="mr-2 h-4 w-4 shrink-0 opacity-50"
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<input
|
||||||
<CommandEmpty>No results found.</CommandEmpty>
|
onChange={(event) => {
|
||||||
<CommandGroup>
|
setSearchValue(event.target.value);
|
||||||
{value?.map((option) => {
|
searchRoleByTerm(event.target.value);
|
||||||
const isSelected = !!selectedValues.find(
|
}}
|
||||||
(sv) => sv.value === option.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"
|
||||||
return (
|
/>
|
||||||
<CommandItem
|
<Button
|
||||||
key={option.value}
|
unstyled
|
||||||
onSelect={() => toggleOption({ value: option })}
|
className="ml-2"
|
||||||
className="cursor-pointer"
|
onClick={() => setOnlySelected((old) => !old)}
|
||||||
>
|
>
|
||||||
<div
|
<ForwardedIconComponent
|
||||||
|
className="h-4 w-4"
|
||||||
|
name={onlySelected ? "CheckCheck" : "Check"}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CommandList className="overflow-y-scroll">
|
||||||
|
<CommandEmpty>No values found.</CommandEmpty>
|
||||||
|
<CommandGroup defaultChecked={false}>
|
||||||
|
{filteredOptions?.map((option, id) => (
|
||||||
|
<CommandItem
|
||||||
|
key={id}
|
||||||
|
value={option}
|
||||||
|
onSelect={(currentValue) => {
|
||||||
|
if (value.includes(currentValue)) {
|
||||||
|
onSelect(value.filter((v) => v !== currentValue));
|
||||||
|
} else {
|
||||||
|
onSelect([...value, currentValue]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="items-center truncate"
|
||||||
|
data-testid={`${option}-${id ?? ""}-option`}
|
||||||
|
>
|
||||||
|
{customValues.includes(option) ||
|
||||||
|
searchValue === option ? (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Text:
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{option}
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Check"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
"ml-auto h-4 w-4 text-primary",
|
||||||
isSelected
|
value.includes(option)
|
||||||
? "bg-primary text-primary-foreground"
|
? "opacity-100"
|
||||||
: "opacity-50 [&_svg]:invisible",
|
: "opacity-0",
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<CheckIcon className="h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</CommandItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CommandGroup>
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandGroup>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{selectedValues?.length > 0 && (
|
|
||||||
<>
|
|
||||||
<CommandItem
|
|
||||||
onSelect={handleClear}
|
|
||||||
className="flex-1 cursor-pointer justify-center"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</CommandItem>
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="flex h-full min-h-6"
|
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<CommandSeparator />
|
|
||||||
<CommandItem
|
|
||||||
onSelect={() => setIsPopoverOpen(false)}
|
|
||||||
className="flex-1 cursor-pointer justify-center"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</div>
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</Command>
|
</Command>
|
||||||
</PopoverContentMultiselect>
|
</PopoverContentDropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{(!isLoading && (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm italic">
|
||||||
|
No parameters are available for display.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)) || (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm italic">Loading...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|
||||||
Multiselect.displayName = "Multiselect";
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export function RefreshParameterComponent({
|
||||||
templateData,
|
templateData,
|
||||||
disabled,
|
disabled,
|
||||||
nodeClass,
|
nodeClass,
|
||||||
|
editNode,
|
||||||
handleNodeClass,
|
handleNodeClass,
|
||||||
nodeId,
|
nodeId,
|
||||||
name,
|
name,
|
||||||
|
|
@ -29,12 +30,13 @@ export function RefreshParameterComponent({
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<div className="w-full">{children}</div>
|
{children}
|
||||||
{templateData.refresh_button && (
|
{templateData.refresh_button && (
|
||||||
<div className="w-1/6">
|
<div className="shrink-0 flex-col">
|
||||||
<RefreshButton
|
<RefreshButton
|
||||||
isLoading={postTemplateValue.isPending}
|
isLoading={postTemplateValue.isPending}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
editNode={editNode}
|
||||||
button_text={templateData.refresh_button_text}
|
button_text={templateData.refresh_button_text}
|
||||||
handleUpdateValues={handleRefreshButtonPress}
|
handleUpdateValues={handleRefreshButtonPress}
|
||||||
id={"refresh-button-" + name}
|
id={"refresh-button-" + name}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { InputFieldType } from "@/types/api";
|
||||||
import Dropdown from "../../../dropdownComponent";
|
import Dropdown from "../../../dropdownComponent";
|
||||||
import InputGlobalComponent from "../../../inputGlobalComponent";
|
import InputGlobalComponent from "../../../inputGlobalComponent";
|
||||||
import InputListComponent from "../../../inputListComponent";
|
import InputListComponent from "../../../inputListComponent";
|
||||||
import { Multiselect } from "../../../multiselectComponent";
|
import MultiselectComponent from "../../../multiselectComponent";
|
||||||
import TextAreaComponent from "../../../textAreaComponent";
|
import TextAreaComponent from "../../../textAreaComponent";
|
||||||
|
|
||||||
export function StrRenderComponent({
|
export function StrRenderComponent({
|
||||||
|
|
@ -33,7 +33,7 @@ export function StrRenderComponent({
|
||||||
componentName={name ?? undefined}
|
componentName={name ?? undefined}
|
||||||
editNode={editNode}
|
editNode={editNode}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={!value || value === "" ? [""] : value}
|
value={value || [""]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
id={`inputlist_${id}`}
|
id={`inputlist_${id}`}
|
||||||
/>
|
/>
|
||||||
|
|
@ -67,13 +67,22 @@ export function StrRenderComponent({
|
||||||
|
|
||||||
if (!!templateData.options && !!templateData?.list) {
|
if (!!templateData.options && !!templateData?.list) {
|
||||||
return (
|
return (
|
||||||
<Multiselect
|
<MultiselectComponent
|
||||||
editNode={editNode}
|
editNode={editNode}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={templateData.options || []}
|
options={
|
||||||
values={[value ?? "Choose an option"]}
|
(Array.isArray(templateData.options)
|
||||||
|
? templateData.options
|
||||||
|
: [templateData.options]) || []
|
||||||
|
}
|
||||||
|
combobox={templateData.combobox}
|
||||||
|
value={
|
||||||
|
(Array.isArray(templateData.value)
|
||||||
|
? templateData.value
|
||||||
|
: [templateData.value]) || []
|
||||||
|
}
|
||||||
id={`multiselect_${id}`}
|
id={`multiselect_${id}`}
|
||||||
onValueChange={onChange}
|
onSelect={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +94,7 @@ export function StrRenderComponent({
|
||||||
options={templateData.options}
|
options={templateData.options}
|
||||||
onSelect={onChange}
|
onSelect={onChange}
|
||||||
combobox={templateData.combobox}
|
combobox={templateData.combobox}
|
||||||
value={value ?? "Choose an option"}
|
value={value || ""}
|
||||||
id={`dropdown_${id}`}
|
id={`dropdown_${id}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ export function ParameterRenderComponent({
|
||||||
templateData={templateData}
|
templateData={templateData}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
|
editNode={editNode}
|
||||||
nodeClass={nodeClass}
|
nodeClass={nodeClass}
|
||||||
handleNodeClass={handleNodeClass}
|
handleNodeClass={handleNodeClass}
|
||||||
name={name}
|
name={name}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,11 @@ function RefreshButton({
|
||||||
handleUpdateValues();
|
handleUpdateValues();
|
||||||
};
|
};
|
||||||
|
|
||||||
const classNames = cn(className, disabled ? "cursor-not-allowed" : "");
|
const classNames = cn(
|
||||||
|
className,
|
||||||
|
disabled ? "cursor-not-allowed" : "",
|
||||||
|
!editNode ? "py-2.5 px-3" : "px-2 py-1",
|
||||||
|
);
|
||||||
|
|
||||||
// icon class name should take into account the disabled state and the loading state
|
// icon class name should take into account the disabled state and the loading state
|
||||||
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
|
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
|
||||||
|
|
@ -37,7 +41,7 @@ function RefreshButton({
|
||||||
className={classNames}
|
className={classNames}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
id={id}
|
id={id}
|
||||||
size={editNode ? "sm" : "default"}
|
size={"icon"}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
{button_text && <span className="mr-1">{button_text}</span>}
|
{button_text && <span className="mr-1">{button_text}</span>}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,17 @@ export type DropDownComponentType = {
|
||||||
id?: string;
|
id?: string;
|
||||||
children?: ReactNode;
|
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 = {
|
export type ParameterComponentType = {
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
data: NodeDataType;
|
data: NodeDataType;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
Braces,
|
Braces,
|
||||||
BrainCircuit,
|
BrainCircuit,
|
||||||
Check,
|
Check,
|
||||||
|
CheckCheck,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
|
|
@ -390,6 +391,7 @@ export const nodeIconsLucide: iconsType = {
|
||||||
IFixitLoader: IFixIcon,
|
IFixitLoader: IFixIcon,
|
||||||
CrewAI: CrewAiIcon,
|
CrewAI: CrewAiIcon,
|
||||||
Meta: MetaIcon,
|
Meta: MetaIcon,
|
||||||
|
CheckCheck,
|
||||||
Midjorney: MidjourneyIcon,
|
Midjorney: MidjourneyIcon,
|
||||||
MongoDBAtlasVectorSearch: MongoDBIcon,
|
MongoDBAtlasVectorSearch: MongoDBIcon,
|
||||||
MongoDB: MongoDBIcon,
|
MongoDB: MongoDBIcon,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue