Frontend Feature: Multiselect ui-component and merge refs hook (#2405)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
feed388857
commit
180e475cc3
10 changed files with 488 additions and 47 deletions
|
|
@ -171,7 +171,7 @@ def add_new_custom_field(
|
|||
field_config["load_from_db"] = True
|
||||
field_config["input_types"] = ["Text"]
|
||||
|
||||
# If options is a list, then it's a dropdown
|
||||
# If options is a list, then it's a dropdown or multiselect
|
||||
# If options is None, then it's a list of strings
|
||||
is_list = isinstance(field_config.get("options"), list)
|
||||
field_config["is_list"] = is_list or field_config.get("list", False) or field_contains_list
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from .inputs import (
|
|||
DataInput,
|
||||
DictInput,
|
||||
DropdownInput,
|
||||
MultiselectInput,
|
||||
FileInput,
|
||||
FloatInput,
|
||||
HandleInput,
|
||||
|
|
@ -21,6 +22,7 @@ __all__ = [
|
|||
"DataInput",
|
||||
"DictInput",
|
||||
"DropdownInput",
|
||||
"MultiselectInput",
|
||||
"FileInput",
|
||||
"FloatInput",
|
||||
"HandleInput",
|
||||
|
|
|
|||
|
|
@ -289,6 +289,23 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin):
|
|||
options: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, MetadataTraceMixin):
|
||||
"""
|
||||
Represents a multiselect input field.
|
||||
|
||||
This class represents a multiselect input field and provides functionality for handling multiselect values.
|
||||
It inherits from the `BaseInputMixin`, `ListableInputMixin` and `DropDownMixin` classes.
|
||||
|
||||
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.
|
||||
Default is None.
|
||||
"""
|
||||
|
||||
field_type: Optional[SerializableFieldTypes] = FieldTypes.TEXT
|
||||
options: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FileInput(BaseInputMixin, ListableInputMixin, FileMixin, MetadataTraceMixin):
|
||||
"""
|
||||
Represents a file field.
|
||||
|
|
@ -308,6 +325,7 @@ InputTypes = Union[
|
|||
DataInput,
|
||||
DictInput,
|
||||
DropdownInput,
|
||||
MultiselectInput,
|
||||
FileInput,
|
||||
FloatInput,
|
||||
HandleInput,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from langflow.inputs import (
|
|||
DataInput,
|
||||
DictInput,
|
||||
DropdownInput,
|
||||
MultiselectInput,
|
||||
FileInput,
|
||||
FloatInput,
|
||||
HandleInput,
|
||||
|
|
@ -22,6 +23,7 @@ __all__ = [
|
|||
"DataInput",
|
||||
"DictInput",
|
||||
"DropdownInput",
|
||||
"MultiselectInput",
|
||||
"FileInput",
|
||||
"FloatInput",
|
||||
"HandleInput",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import InputGlobalComponent from "../../../../components/inputGlobalComponent";
|
|||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import IntComponent from "../../../../components/intComponent";
|
||||
import KeypairListComponent from "../../../../components/keypairListComponent";
|
||||
import { Multiselect } from "../../../../components/multiselectComponent";
|
||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||
import ShadTooltip from "../../../../components/shadTooltipComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
|
|
@ -558,6 +559,23 @@ export default function ParameterComponent({
|
|||
)}
|
||||
</div>
|
||||
</Case>
|
||||
<Case
|
||||
condition={
|
||||
type === "str" &&
|
||||
!!data.node?.template[name]?.options &&
|
||||
!!data.node?.template[name]?.list
|
||||
}
|
||||
>
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<Multiselect
|
||||
disabled={disabled}
|
||||
options={data?.node?.template?.[name]?.options || []}
|
||||
value={data?.node?.template?.[name]?.value || []}
|
||||
id={"multiselect-" + name}
|
||||
onValueChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case condition={left === true && type === "code"}>
|
||||
<div className="mt-2 w-full">
|
||||
|
|
|
|||
35
src/frontend/src/CustomNodes/hooks/use-merge-refs.tsx
Normal file
35
src/frontend/src/CustomNodes/hooks/use-merge-refs.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { MutableRefObject, useEffect, useRef } from "react";
|
||||
|
||||
type Ref<T> = MutableRefObject<T | null> | ((instance: T | null) => void);
|
||||
|
||||
export function isRefCallback<T>(
|
||||
ref: Ref<T>,
|
||||
): ref is (instance: T | null) => void {
|
||||
return typeof ref === "function";
|
||||
}
|
||||
|
||||
export function isRefObject<T>(ref: Ref<T>): ref is MutableRefObject<T> {
|
||||
return !isRefCallback(ref);
|
||||
}
|
||||
|
||||
function useMergeRefs<T>(
|
||||
...refs: (Ref<T> | null | undefined)[]
|
||||
): Ref<T | null> {
|
||||
const targetRef: Ref<T | null> = useRef<T | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
refs.forEach((ref) => {
|
||||
if (!ref) return;
|
||||
|
||||
if (typeof ref === "function") {
|
||||
ref(targetRef.current);
|
||||
} else {
|
||||
ref.current = targetRef.current as T;
|
||||
}
|
||||
});
|
||||
}, [refs]);
|
||||
|
||||
return targetRef;
|
||||
}
|
||||
|
||||
export default useMergeRefs;
|
||||
326
src/frontend/src/components/multiselectComponent/index.tsx
Normal file
326
src/frontend/src/components/multiselectComponent/index.tsx
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
"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 { cn } from "../../utils/utils";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "../ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverContentWithoutPortal,
|
||||
PopoverTrigger,
|
||||
} from "../ui/popover";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
const MultiselectBadgeWrapper = ({
|
||||
value,
|
||||
variant,
|
||||
className,
|
||||
onDelete,
|
||||
}: {
|
||||
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;
|
||||
value?: T[];
|
||||
}
|
||||
|
||||
export const Multiselect = forwardRef<
|
||||
HTMLButtonElement,
|
||||
MultiselectProps<MultiselectValue>
|
||||
>(
|
||||
(
|
||||
{
|
||||
options = [],
|
||||
onValueChange,
|
||||
variant,
|
||||
placeholder = "Select options",
|
||||
asChild = false,
|
||||
className,
|
||||
editNode = false,
|
||||
value,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [selectedValues, setSelectedValues] = useState<MultiselectValue[]>(
|
||||
value || [],
|
||||
);
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const combinedRef = useMergeRefs<HTMLButtonElement>(ref);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!value && value?.length > 0 && !isEqual(selectedValues, value)) {
|
||||
setSelectedValues(value);
|
||||
}
|
||||
}, [value, selectedValues]);
|
||||
|
||||
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 (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={combinedRef}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
variant="primary"
|
||||
size="xs"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
editNode
|
||||
? "multiselect-component-outline"
|
||||
: "multiselect-component-false-outline",
|
||||
"w-full justify-between font-normal",
|
||||
editNode ? "input-edit-node" : "py-2",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedValues?.length > 0 ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex flex-wrap items-center">
|
||||
{selectedValues?.map((selectedValue) => {
|
||||
return (
|
||||
<MultiselectBadgeWrapper
|
||||
value={selectedValue}
|
||||
onDelete={toggleOption}
|
||||
variant={variant}
|
||||
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>
|
||||
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground hover:text-accent-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContentMultiselect
|
||||
id="multiselect-content"
|
||||
side="bottom"
|
||||
className={cn(
|
||||
`nocopy nowheel nopan nodelete nodrag noundo w-full p-0`,
|
||||
)}
|
||||
style={{
|
||||
minWidth: popoverContentMultiselectMinWidth,
|
||||
maxWidth: popoverContentMultiselectMinWidth,
|
||||
}}
|
||||
align="start"
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
className="h-9"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options?.map((option) => {
|
||||
const isSelected = !!selectedValues.find(
|
||||
(sv) => sv.value === option.value,
|
||||
);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => toggleOption({ value: option })}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||
isSelected
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentMultiselect>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Multiselect.displayName = "Multiselect";
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import Dropdown from "../../../dropdownComponent";
|
||||
import InputGlobalComponent from "../../../inputGlobalComponent";
|
||||
import InputListComponent from "../../../inputListComponent";
|
||||
import { Multiselect } from "../../../multiselectComponent";
|
||||
import TextAreaComponent from "../../../textAreaComponent";
|
||||
|
||||
export function renderStrType({
|
||||
templateData,
|
||||
templateValue,
|
||||
disabled,
|
||||
handleOnNewValue,
|
||||
}) {
|
||||
if (!templateData.options) {
|
||||
return templateData?.list ? (
|
||||
<InputListComponent
|
||||
componentName={templateData.key ?? undefined}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={!templateValue || templateValue === "" ? [""] : templateValue}
|
||||
onChange={(value: string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
) : templateData.multiline ? (
|
||||
<TextAreaComponent
|
||||
id={"textarea-edit-" + templateData.name}
|
||||
data-testid={"textarea-edit-" + templateData.name}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputGlobalComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
onChange={(value, dbValue, snapshot) =>
|
||||
handleOnNewValue(value, templateData.key, dbValue)
|
||||
}
|
||||
name={templateData.key}
|
||||
data={templateData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!!templateData.options && !!templateData?.list) {
|
||||
return (
|
||||
<Multiselect
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
options={templateData.options || []}
|
||||
value={templateValue ?? "Choose an option"}
|
||||
id={"multiselect-" + templateData.name}
|
||||
onValueChange={(value) => handleOnNewValue(value, templateData.key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!!templateData.options) {
|
||||
return (
|
||||
<Dropdown
|
||||
editNode={true}
|
||||
options={templateData.options}
|
||||
onSelect={(value) => handleOnNewValue(value, templateData.key)}
|
||||
value={templateValue ?? "Choose an option"}
|
||||
id={"dropdown-edit-" + templateData.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import KeypairListComponent from "../../../keypairListComponent";
|
|||
import PromptAreaComponent from "../../../promptComponent";
|
||||
import TextAreaComponent from "../../../textAreaComponent";
|
||||
import ToggleShadComponent from "../../../toggleShadComponent";
|
||||
import { renderStrType } from "./cellTypeStr";
|
||||
|
||||
export default function TableNodeCellRender({
|
||||
node: { data },
|
||||
|
|
@ -75,52 +76,12 @@ export default function TableNodeCellRender({
|
|||
function getCellType() {
|
||||
switch (templateData.type) {
|
||||
case "str":
|
||||
if (!templateData.options) {
|
||||
return templateData?.list ? (
|
||||
<InputListComponent
|
||||
componentName={templateData.key ?? undefined}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!templateValue || templateValue === "" ? [""] : templateValue
|
||||
}
|
||||
onChange={(value: string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
) : templateData.multiline ? (
|
||||
<TextAreaComponent
|
||||
id={"textarea-edit-" + templateData.name}
|
||||
data-testid={"textarea-edit-" + templateData.name}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputGlobalComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
onChange={(value, dbValue, snapshot) =>
|
||||
handleOnNewValue(value, templateData.key, dbValue)
|
||||
}
|
||||
name={templateData.key}
|
||||
data={templateData}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Dropdown
|
||||
editNode={true}
|
||||
options={templateData.options}
|
||||
onSelect={(value) => handleOnNewValue(value, templateData.key)}
|
||||
value={templateValue ?? "Choose an option"}
|
||||
id={"dropdown-edit-" + templateData.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return renderStrType({
|
||||
templateData,
|
||||
templateValue,
|
||||
disabled,
|
||||
handleOnNewValue,
|
||||
});
|
||||
|
||||
case "NestedDict":
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -546,6 +546,13 @@
|
|||
@apply h-5 w-5 text-primary;
|
||||
}
|
||||
|
||||
.multiselect-component-outline {
|
||||
@apply input-edit-node relative pr-8;
|
||||
}
|
||||
.multiselect-component-false-outline {
|
||||
@apply primary-input py-2 pl-3 pr-10 text-left;
|
||||
}
|
||||
|
||||
.edit-flow-arrangement {
|
||||
@apply flex justify-between;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue