feat: adds new queryInput with separator and dialog (#7458)
* 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 <cristhian.lousa@gmail.com>
This commit is contained in:
parent
6f259c6bee
commit
404e04989a
14 changed files with 434 additions and 0 deletions
|
|
@ -21,6 +21,7 @@ from .inputs import (
|
||||||
MultiselectInput,
|
MultiselectInput,
|
||||||
NestedDictInput,
|
NestedDictInput,
|
||||||
PromptInput,
|
PromptInput,
|
||||||
|
QueryInput,
|
||||||
SecretStrInput,
|
SecretStrInput,
|
||||||
SliderInput,
|
SliderInput,
|
||||||
SortableListInput,
|
SortableListInput,
|
||||||
|
|
@ -54,6 +55,7 @@ __all__ = [
|
||||||
"MultiselectInput",
|
"MultiselectInput",
|
||||||
"NestedDictInput",
|
"NestedDictInput",
|
||||||
"PromptInput",
|
"PromptInput",
|
||||||
|
"QueryInput",
|
||||||
"SecretStrInput",
|
"SecretStrInput",
|
||||||
"SliderInput",
|
"SliderInput",
|
||||||
"SortableListInput",
|
"SortableListInput",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class FieldTypes(str, Enum):
|
||||||
LINK = "link"
|
LINK = "link"
|
||||||
SLIDER = "slider"
|
SLIDER = "slider"
|
||||||
TAB = "tab"
|
TAB = "tab"
|
||||||
|
QUERY = "query"
|
||||||
|
|
||||||
|
|
||||||
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
||||||
|
|
@ -142,6 +143,11 @@ class AuthMixin(BaseModel):
|
||||||
auth_tooltip: str | None = Field(default="")
|
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
|
# Specific mixin for fields needing file interaction
|
||||||
class FileMixin(BaseModel):
|
class FileMixin(BaseModel):
|
||||||
file_path: list[str] | str | None = Field(default="")
|
file_path: list[str] | str | None = Field(default="")
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from .input_mixin import (
|
||||||
ListableInputMixin,
|
ListableInputMixin,
|
||||||
MetadataTraceMixin,
|
MetadataTraceMixin,
|
||||||
MultilineMixin,
|
MultilineMixin,
|
||||||
|
QueryMixin,
|
||||||
RangeMixin,
|
RangeMixin,
|
||||||
SerializableFieldTypes,
|
SerializableFieldTypes,
|
||||||
SliderMixin,
|
SliderMixin,
|
||||||
|
|
@ -492,6 +493,22 @@ class AuthInput(BaseInputMixin, AuthMixin, MetadataTraceMixin):
|
||||||
show: bool = False
|
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):
|
class SortableListInput(BaseInputMixin, SortableListMixin, MetadataTraceMixin, ToolModeMixin):
|
||||||
"""Represents a list selection input field.
|
"""Represents a list selection input field.
|
||||||
|
|
||||||
|
|
@ -606,6 +623,7 @@ class DefaultPromptField(Input):
|
||||||
InputTypes: TypeAlias = (
|
InputTypes: TypeAlias = (
|
||||||
Input
|
Input
|
||||||
| AuthInput
|
| AuthInput
|
||||||
|
| QueryInput
|
||||||
| DefaultPromptField
|
| DefaultPromptField
|
||||||
| BoolInput
|
| BoolInput
|
||||||
| DataInput
|
| DataInput
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from langflow.inputs import (
|
||||||
MultiselectInput,
|
MultiselectInput,
|
||||||
NestedDictInput,
|
NestedDictInput,
|
||||||
PromptInput,
|
PromptInput,
|
||||||
|
QueryInput,
|
||||||
SecretStrInput,
|
SecretStrInput,
|
||||||
SliderInput,
|
SliderInput,
|
||||||
StrInput,
|
StrInput,
|
||||||
|
|
@ -50,6 +51,7 @@ __all__ = [
|
||||||
"NestedDictInput",
|
"NestedDictInput",
|
||||||
"Output",
|
"Output",
|
||||||
"PromptInput",
|
"PromptInput",
|
||||||
|
"QueryInput",
|
||||||
"SecretStrInput",
|
"SecretStrInput",
|
||||||
"SliderInput",
|
"SliderInput",
|
||||||
"StrInput",
|
"StrInput",
|
||||||
|
|
|
||||||
|
|
@ -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<string, QueryComponentType>): JSX.Element {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||||
|
handleOnNewValue({ value: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIcon = () => (
|
||||||
|
<div>
|
||||||
|
{!disabled && !isFocused && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
externalLinkIconClasses.gradient({
|
||||||
|
disabled,
|
||||||
|
editNode,
|
||||||
|
}),
|
||||||
|
editNode
|
||||||
|
? externalLinkIconClasses.editNodeTop
|
||||||
|
: externalLinkIconClasses.normalTop,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: isFocused
|
||||||
|
? undefined
|
||||||
|
: disabled
|
||||||
|
? "bg-background"
|
||||||
|
: GRADIENT_CLASS,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<IconComponent
|
||||||
|
dataTestId={`button_open_text_area_modal_${id}${editNode ? "_advanced" : ""}`}
|
||||||
|
name={getIconName(disabled, "", "", false, isToolMode) || "Scan"}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer bg-background",
|
||||||
|
externalLinkIconClasses.icon,
|
||||||
|
editNode
|
||||||
|
? externalLinkIconClasses.editNodeTop
|
||||||
|
: externalLinkIconClasses.iconTop,
|
||||||
|
disabled
|
||||||
|
? "bg-muted text-placeholder-foreground"
|
||||||
|
: "bg-background text-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("w-full", disabled && "pointer-events-none")}>
|
||||||
|
<Input
|
||||||
|
onFocus={() => 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"}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<QueryModal
|
||||||
|
title={display_name}
|
||||||
|
description={info}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
setValue={(newValue) => handleOnNewValue({ value: newValue })}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<div className="relative w-full">{renderIcon()}</div>
|
||||||
|
</QueryModal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ import KeypairListComponent from "./components/keypairListComponent";
|
||||||
import LinkComponent from "./components/linkComponent";
|
import LinkComponent from "./components/linkComponent";
|
||||||
import MultiselectComponent from "./components/multiselectComponent";
|
import MultiselectComponent from "./components/multiselectComponent";
|
||||||
import PromptAreaComponent from "./components/promptComponent";
|
import PromptAreaComponent from "./components/promptComponent";
|
||||||
|
import QueryComponent from "./components/queryComponent";
|
||||||
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
||||||
import SortableListComponent from "./components/sortableListComponent";
|
import SortableListComponent from "./components/sortableListComponent";
|
||||||
import { StrRenderComponent } from "./components/strRenderComponent";
|
import { StrRenderComponent } from "./components/strRenderComponent";
|
||||||
|
|
@ -257,6 +258,16 @@ export function ParameterRenderComponent({
|
||||||
id={`tab_${id}`}
|
id={`tab_${id}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "query":
|
||||||
|
return (
|
||||||
|
<QueryComponent
|
||||||
|
{...baseInputProps}
|
||||||
|
display_name={templateData.display_name ?? ""}
|
||||||
|
info={templateData.info ?? ""}
|
||||||
|
separator={templateData.separator}
|
||||||
|
id={`query_${id}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <EmptyParameterComponent {...baseInputProps} />;
|
return <EmptyParameterComponent {...baseInputProps} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,12 @@ export type TextAreaComponentType = {
|
||||||
updateVisibility?: () => void;
|
updateVisibility?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QueryComponentType = {
|
||||||
|
display_name: string;
|
||||||
|
info: string;
|
||||||
|
separator?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type InputGlobalComponentType = {
|
export type InputGlobalComponentType = {
|
||||||
load_from_db: boolean | undefined;
|
load_from_db: boolean | undefined;
|
||||||
password: boolean | undefined;
|
password: boolean | undefined;
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
||||||
"sortableList",
|
"sortableList",
|
||||||
"connect",
|
"connect",
|
||||||
"auth",
|
"auth",
|
||||||
|
"query",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const FLEX_VIEW_TYPES = ["bool"];
|
export const FLEX_VIEW_TYPES = ["bool"];
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ export const switchCaseModalSize = (size: string) => {
|
||||||
minWidth = "min-w-[40vw]";
|
minWidth = "min-w-[40vw]";
|
||||||
height = "h-[40vh]";
|
height = "h-[40vh]";
|
||||||
break;
|
break;
|
||||||
|
case "small-query":
|
||||||
|
minWidth = "min-w-[35vw]";
|
||||||
|
height = "h-fit";
|
||||||
|
break;
|
||||||
case "medium-small-tall":
|
case "medium-small-tall":
|
||||||
minWidth = "min-w-[50vw]";
|
minWidth = "min-w-[50vw]";
|
||||||
height = "h-[70vh]";
|
height = "h-[70vh]";
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ interface BaseModalProps {
|
||||||
| "retangular"
|
| "retangular"
|
||||||
| "smaller"
|
| "smaller"
|
||||||
| "small"
|
| "small"
|
||||||
|
| "small-query"
|
||||||
| "medium"
|
| "medium"
|
||||||
| "medium-tall"
|
| "medium-tall"
|
||||||
| "large"
|
| "large"
|
||||||
|
|
|
||||||
79
src/frontend/src/modals/queryModal/index.tsx
Normal file
79
src/frontend/src/modals/queryModal/index.tsx
Normal file
|
|
@ -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<HTMLTextAreaElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof value === "string") setInputValue(value);
|
||||||
|
}, [value, modalOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal
|
||||||
|
onChangeOpenModal={(open) => {}}
|
||||||
|
open={modalOpen}
|
||||||
|
setOpen={setModalOpen}
|
||||||
|
size="small-query"
|
||||||
|
>
|
||||||
|
<BaseModal.Trigger disable={disabled} asChild>
|
||||||
|
{children}
|
||||||
|
</BaseModal.Trigger>
|
||||||
|
<BaseModal.Header>
|
||||||
|
<div className="flex w-full items-start gap-3">
|
||||||
|
<div className="flex">
|
||||||
|
<span data-testid="modal-title">{title ?? TEXT_DIALOG_TITLE}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseModal.Header>
|
||||||
|
<BaseModal.Content className="flex flex-col gap-2" overflowHidden>
|
||||||
|
<div className={classNames("flex h-full w-full rounded-lg border")}>
|
||||||
|
<Textarea
|
||||||
|
ref={textRef}
|
||||||
|
className="form-input h-full w-full resize-none overflow-auto rounded-lg focus-visible:ring-1"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(event) => {
|
||||||
|
setInputValue(event.target.value);
|
||||||
|
}}
|
||||||
|
placeholder={placeholder ?? EDIT_TEXT_PLACEHOLDER}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
handleKeyDown(e, value, "");
|
||||||
|
}}
|
||||||
|
id={"text-area-modal"}
|
||||||
|
data-testid={"text-area-modal"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
</BaseModal.Content>
|
||||||
|
<BaseModal.Footer
|
||||||
|
submit={{
|
||||||
|
label: "Apply",
|
||||||
|
dataTestId: "genericModalBtnSave",
|
||||||
|
onClick: () => {
|
||||||
|
setValue(inputValue);
|
||||||
|
setModalOpen(false);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -91,6 +91,7 @@ export type InputFieldType = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
temp_file?: boolean;
|
temp_file?: boolean;
|
||||||
|
separator?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OutputFieldProxyType = {
|
export type OutputFieldProxyType = {
|
||||||
|
|
|
||||||
|
|
@ -673,6 +673,17 @@ export type textModalPropsType = {
|
||||||
setOpen?: (open: boolean) => void;
|
setOpen?: (open: boolean) => void;
|
||||||
onCloseModal?: () => void;
|
onCloseModal?: () => void;
|
||||||
};
|
};
|
||||||
|
export type queryModalPropsType = {
|
||||||
|
setValue: (value: string) => void;
|
||||||
|
value: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
children?: ReactNode;
|
||||||
|
open?: boolean;
|
||||||
|
setOpen?: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type newFlowModalPropsType = {
|
export type newFlowModalPropsType = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
|
||||||
141
src/frontend/tests/core/unit/queryInputComponent.spec.ts
Normal file
141
src/frontend/tests/core/unit/queryInputComponent.spec.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { expect, Page, test } from "@playwright/test";
|
||||||
|
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||||
|
|
||||||
|
// TODO: This component doesn't have slider needs updating
|
||||||
|
test(
|
||||||
|
"user should be able to use query input",
|
||||||
|
{
|
||||||
|
tag: ["@release", "@workspace"],
|
||||||
|
},
|
||||||
|
async ({ page }) => {
|
||||||
|
await awaitBootstrapTest(page);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.getByTestId("blank-flow").click();
|
||||||
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
|
await page.getByTestId("sidebar-search-input").fill("openai");
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="modelsOpenAI"]', {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("modelsOpenAI")
|
||||||
|
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.getByTestId("fit_view").click();
|
||||||
|
|
||||||
|
await page.getByTestId("title-OpenAI").click();
|
||||||
|
await page.getByTestId("code-button-modal").click();
|
||||||
|
|
||||||
|
let cleanCode = await extractAndCleanCode(page);
|
||||||
|
|
||||||
|
// Replace the multiline string in the code
|
||||||
|
let newCode = cleanCode.replace(
|
||||||
|
`StrInput(
|
||||||
|
name="openai_api_base",
|
||||||
|
display_name="OpenAI API Base",
|
||||||
|
advanced=True,
|
||||||
|
info="The base URL of the OpenAI API. "
|
||||||
|
"Defaults to https://api.openai.com/v1. "
|
||||||
|
"You can change this to use other APIs like JinaChat, LocalAI and Prem.",
|
||||||
|
),`,
|
||||||
|
`QueryInput(
|
||||||
|
name="openai_api_base",
|
||||||
|
display_name="OpenAI API Base",
|
||||||
|
advanced=False,
|
||||||
|
info="THIS IS A TEST INFORMATION TO DISPLAY",
|
||||||
|
placeholder="THIS IS A TEST PLACEHOLDER",
|
||||||
|
),`,
|
||||||
|
);
|
||||||
|
|
||||||
|
newCode = newCode.replace(
|
||||||
|
`from langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput`,
|
||||||
|
`from langflow.inputs import BoolInput, DictInput, DropdownInput, IntInput, SecretStrInput, SliderInput, StrInput, QueryInput`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// make sure codes are different
|
||||||
|
expect(cleanCode).not.toEqual(newCode);
|
||||||
|
await page.locator("textarea").last().press(`ControlOrMeta+a`);
|
||||||
|
await page.keyboard.press("Backspace");
|
||||||
|
await page.locator("textarea").last().fill(newCode);
|
||||||
|
await page.locator('//*[@id="checkAndSaveBtn"]').click();
|
||||||
|
|
||||||
|
await page.getByTestId("fit_view").click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("query_query_openai_api_base")
|
||||||
|
.fill("THIS IS A TEST VALUE");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("button_open_text_area_modal_query_query_openai_api_base")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
expect(await page.getByTestId("text-area-modal").inputValue()).toEqual(
|
||||||
|
"THIS IS A TEST VALUE",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
await page.getByText("THIS IS A TEST INFORMATION TO DISPLAY").isVisible(),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
await page.getByTestId("text-area-modal").fill("THIS IS A NEW VALUE");
|
||||||
|
|
||||||
|
await page.getByTestId("genericModalBtnSave").click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.getByTestId("query_query_openai_api_base").inputValue(),
|
||||||
|
).toEqual("THIS IS A NEW VALUE");
|
||||||
|
|
||||||
|
await page.getByTestId("edit-button-modal").click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.getByTestId("query_query_edit_openai_api_base").inputValue(),
|
||||||
|
).toEqual("THIS IS A NEW VALUE");
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId(
|
||||||
|
"button_open_text_area_modal_query_query_edit_openai_api_base_advanced",
|
||||||
|
)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("text-area-modal")
|
||||||
|
.fill("THIS IA TEST TEXT INSIDE CONTROLS PANEL");
|
||||||
|
|
||||||
|
await page.getByTestId("genericModalBtnSave").click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.getByTestId("query_query_edit_openai_api_base").inputValue(),
|
||||||
|
).toEqual("THIS IA TEST TEXT INSIDE CONTROLS PANEL");
|
||||||
|
|
||||||
|
await page.getByText("Close").last().click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.getByTestId("query_query_openai_api_base").inputValue(),
|
||||||
|
).toEqual("THIS IA TEST TEXT INSIDE CONTROLS PANEL");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
async function extractAndCleanCode(page: Page): Promise<string> {
|
||||||
|
const outerHTML = await page
|
||||||
|
.locator('//*[@id="codeValue"]')
|
||||||
|
.evaluate((el) => el.outerHTML);
|
||||||
|
|
||||||
|
const valueMatch = outerHTML.match(/value="([\s\S]*?)"/);
|
||||||
|
if (!valueMatch) {
|
||||||
|
throw new Error("Could not find value attribute in the HTML");
|
||||||
|
}
|
||||||
|
|
||||||
|
let codeContent = valueMatch[1]
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(///g, "/");
|
||||||
|
|
||||||
|
return codeContent;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue