From 404e04989a137e8d0f4abc931b8dee53a117021c Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Thu, 10 Apr 2025 10:04:54 -0300 Subject: [PATCH] feat: adds new queryInput with separator and dialog (#7458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Query Input and Mixin on backend * Adds Query on supported types * Adds types for query modal and component * Adds size for new query modal * Adds query modal * Adds query component * Adds query component on parameter render * [autofix.ci] apply automated fixes * ✨ (switch-case-size.ts): Update height value to 'h-fit' for 'small-query' case to improve responsiveness ✨ (queryInputComponent.spec.ts): Add unit test for user interaction with query input component, including updating code and testing functionality * Fixed handle not working on Query Input --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: cristhianzl --- src/backend/base/langflow/inputs/__init__.py | 2 + .../base/langflow/inputs/input_mixin.py | 6 + src/backend/base/langflow/inputs/inputs.py | 18 +++ src/backend/base/langflow/io/__init__.py | 2 + .../components/queryComponent/index.tsx | 151 ++++++++++++++++++ .../core/parameterRenderComponent/index.tsx | 11 ++ .../core/parameterRenderComponent/types.ts | 6 + src/frontend/src/constants/constants.ts | 1 + .../baseModal/helpers/switch-case-size.ts | 4 + src/frontend/src/modals/baseModal/index.tsx | 1 + src/frontend/src/modals/queryModal/index.tsx | 79 +++++++++ src/frontend/src/types/api/index.ts | 1 + src/frontend/src/types/components/index.ts | 11 ++ .../core/unit/queryInputComponent.spec.ts | 141 ++++++++++++++++ 14 files changed, 434 insertions(+) create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/queryComponent/index.tsx create mode 100644 src/frontend/src/modals/queryModal/index.tsx create mode 100644 src/frontend/tests/core/unit/queryInputComponent.spec.ts diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index eb0941e87..92cd8841f 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -21,6 +21,7 @@ from .inputs import ( MultiselectInput, NestedDictInput, PromptInput, + QueryInput, SecretStrInput, SliderInput, SortableListInput, @@ -54,6 +55,7 @@ __all__ = [ "MultiselectInput", "NestedDictInput", "PromptInput", + "QueryInput", "SecretStrInput", "SliderInput", "SortableListInput", diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index afba497a1..6efac9d9f 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -35,6 +35,7 @@ class FieldTypes(str, Enum): LINK = "link" SLIDER = "slider" TAB = "tab" + QUERY = "query" SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)] @@ -142,6 +143,11 @@ class AuthMixin(BaseModel): auth_tooltip: str | None = Field(default="") +class QueryMixin(BaseModel): + separator: str | None = Field(default=None) + """Separator for the query input. Defaults to None.""" + + # Specific mixin for fields needing file interaction class FileMixin(BaseModel): file_path: list[str] | str | None = Field(default="") diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 6be2d3a3d..530aa2fbc 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -24,6 +24,7 @@ from .input_mixin import ( ListableInputMixin, MetadataTraceMixin, MultilineMixin, + QueryMixin, RangeMixin, SerializableFieldTypes, SliderMixin, @@ -492,6 +493,22 @@ class AuthInput(BaseInputMixin, AuthMixin, MetadataTraceMixin): show: bool = False +class QueryInput(MessageTextInput, QueryMixin): + """Represents a query input field. + + This class represents an query input field and provides functionality for handling search values. + It inherits from the `BaseInputMixin` and `QueryMixin` classes. + + Attributes: + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.SEARCH. + separator (str | None): The separator for the query input. Defaults to None. + value (str): The value for the query input. Defaults to an empty string. + """ + + field_type: SerializableFieldTypes = FieldTypes.QUERY + separator: str | None = Field(default=None) + + class SortableListInput(BaseInputMixin, SortableListMixin, MetadataTraceMixin, ToolModeMixin): """Represents a list selection input field. @@ -606,6 +623,7 @@ class DefaultPromptField(Input): InputTypes: TypeAlias = ( Input | AuthInput + | QueryInput | DefaultPromptField | BoolInput | DataInput diff --git a/src/backend/base/langflow/io/__init__.py b/src/backend/base/langflow/io/__init__.py index 92bb4c0eb..86339e7de 100644 --- a/src/backend/base/langflow/io/__init__.py +++ b/src/backend/base/langflow/io/__init__.py @@ -19,6 +19,7 @@ from langflow.inputs import ( MultiselectInput, NestedDictInput, PromptInput, + QueryInput, SecretStrInput, SliderInput, StrInput, @@ -50,6 +51,7 @@ __all__ = [ "NestedDictInput", "Output", "PromptInput", + "QueryInput", "SecretStrInput", "SliderInput", "StrInput", diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/queryComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/queryComponent/index.tsx new file mode 100644 index 000000000..22110d81a --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/queryComponent/index.tsx @@ -0,0 +1,151 @@ +import { GRADIENT_CLASS } from "@/constants/constants"; +import QueryModal from "@/modals/queryModal"; +import { useRef, useState } from "react"; +import { cn } from "../../../../../utils/utils"; +import IconComponent from "../../../../common/genericIconComponent"; +import { Input } from "../../../../ui/input"; +import { getPlaceholder } from "../../helpers/get-placeholder-disabled"; +import { InputProps, QueryComponentType } from "../../types"; +import { getIconName } from "../inputComponent/components/helpers/get-icon-name"; + +const inputClasses = { + base: ({ isFocused }: { isFocused: boolean }) => + `w-full ${isFocused ? "" : "pr-3"}`, + editNode: "input-edit-node", + normal: ({ isFocused }: { isFocused: boolean }) => + `primary-input ${isFocused ? "text-primary" : "text-muted-foreground"}`, + disabled: "disabled-state", + password: "password", +}; + +const externalLinkIconClasses = { + gradient: ({ + disabled, + editNode, + }: { + disabled: boolean; + editNode: boolean; + }) => + disabled + ? "" + : editNode + ? "gradient-fade-input-edit-node" + : "gradient-fade-input", + background: ({ + disabled, + editNode, + }: { + disabled: boolean; + editNode: boolean; + }) => + disabled + ? "" + : editNode + ? "background-fade-input-edit-node" + : "background-fade-input", + icon: "icons-parameters-comp absolute right-3 h-4 w-4 shrink-0", + editNodeTop: "top-[-1.4rem] h-5", + normalTop: "top-[-2.1rem] h-7", + iconTop: "top-[-1.7rem]", +}; + +export default function QueryComponent({ + value, + disabled, + handleOnNewValue, + editNode = false, + id = "", + placeholder, + isToolMode = false, + display_name, + info, + separator, +}: InputProps): JSX.Element { + const inputRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); + + const getInputClassName = () => { + return cn( + inputClasses.base({ isFocused }), + editNode ? inputClasses.editNode : inputClasses.normal({ isFocused }), + disabled && inputClasses.disabled, + isFocused && "pr-10", + ); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + handleOnNewValue({ value: e.target.value }); + }; + + const renderIcon = () => ( +
+ {!disabled && !isFocused && ( + + ); + + return ( +
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + id={id} + data-testid={id} + value={disabled ? "" : value} + onChange={handleInputChange} + disabled={disabled} + className={getInputClassName()} + placeholder={getPlaceholder(disabled, placeholder)} + aria-label={disabled ? value : undefined} + ref={inputRef} + type={"text"} + /> + + handleOnNewValue({ value: newValue })} + disabled={disabled} + > +
{renderIcon()}
+
+
+ ); +} diff --git a/src/frontend/src/components/core/parameterRenderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/index.tsx index 898af1908..45eda0702 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/index.tsx @@ -17,6 +17,7 @@ import KeypairListComponent from "./components/keypairListComponent"; import LinkComponent from "./components/linkComponent"; import MultiselectComponent from "./components/multiselectComponent"; import PromptAreaComponent from "./components/promptComponent"; +import QueryComponent from "./components/queryComponent"; import { RefreshParameterComponent } from "./components/refreshParameterComponent"; import SortableListComponent from "./components/sortableListComponent"; import { StrRenderComponent } from "./components/strRenderComponent"; @@ -257,6 +258,16 @@ export function ParameterRenderComponent({ id={`tab_${id}`} /> ); + case "query": + return ( + + ); default: return ; } diff --git a/src/frontend/src/components/core/parameterRenderComponent/types.ts b/src/frontend/src/components/core/parameterRenderComponent/types.ts index 3a8d21b5a..a9c4404a4 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/types.ts +++ b/src/frontend/src/components/core/parameterRenderComponent/types.ts @@ -104,6 +104,12 @@ export type TextAreaComponentType = { updateVisibility?: () => void; }; +export type QueryComponentType = { + display_name: string; + info: string; + separator?: string; +}; + export type InputGlobalComponentType = { load_from_db: boolean | undefined; password: boolean | undefined; diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index b101f7fc8..cfe5f46a3 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -658,6 +658,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([ "sortableList", "connect", "auth", + "query", ]); export const FLEX_VIEW_TYPES = ["bool"]; diff --git a/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts b/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts index 8882dd9d8..c34d5584a 100644 --- a/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts +++ b/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts @@ -18,6 +18,10 @@ export const switchCaseModalSize = (size: string) => { minWidth = "min-w-[40vw]"; height = "h-[40vh]"; break; + case "small-query": + minWidth = "min-w-[35vw]"; + height = "h-fit"; + break; case "medium-small-tall": minWidth = "min-w-[50vw]"; height = "h-[70vh]"; diff --git a/src/frontend/src/modals/baseModal/index.tsx b/src/frontend/src/modals/baseModal/index.tsx index 0b9178ad2..851ee7ef6 100644 --- a/src/frontend/src/modals/baseModal/index.tsx +++ b/src/frontend/src/modals/baseModal/index.tsx @@ -171,6 +171,7 @@ interface BaseModalProps { | "retangular" | "smaller" | "small" + | "small-query" | "medium" | "medium-tall" | "large" diff --git a/src/frontend/src/modals/queryModal/index.tsx b/src/frontend/src/modals/queryModal/index.tsx new file mode 100644 index 000000000..fdb84cf22 --- /dev/null +++ b/src/frontend/src/modals/queryModal/index.tsx @@ -0,0 +1,79 @@ +import { useEffect, useRef, useState } from "react"; +import { Textarea } from "../../components/ui/textarea"; +import { + EDIT_TEXT_PLACEHOLDER, + TEXT_DIALOG_TITLE, +} from "../../constants/constants"; +import { queryModalPropsType } from "../../types/components"; +import { handleKeyDown } from "../../utils/reactflowUtils"; +import { classNames } from "../../utils/utils"; +import BaseModal from "../baseModal"; + +export default function QueryModal({ + value, + setValue, + title, + description, + placeholder, + children, + disabled, +}: queryModalPropsType): JSX.Element { + const [modalOpen, setModalOpen] = useState(false); + const [inputValue, setInputValue] = useState(value); + + const textRef = useRef(null); + useEffect(() => { + if (typeof value === "string") setInputValue(value); + }, [value, modalOpen]); + + return ( + {}} + open={modalOpen} + setOpen={setModalOpen} + size="small-query" + > + + {children} + + +
+
+ {title ?? TEXT_DIALOG_TITLE} +
+
+
+ +
+