feat: add new Tab input (#7032)
* Added TabMixin * Added Tab component on inputs * Added Tab component to initializations * Added tests for tab input * Added Tab Component type * Added options and active tab to input field type * Added tab component on frontend * Instantiate tab component * Update package lock * Refactor input classes and imports for consistency - Reordered imports to maintain consistency across files. - Simplified class definitions by removing unnecessary line breaks. - Updated the `__all__` list in `__init__.py` files to include `TableInput` consistently. - Adjusted test cases for cleaner syntax without altering functionality. * Add constants for tab options limits in input mixin - Introduced `MAX_TAB_OPTIONS` and `MAX_TAB_OPTION_LENGTH` constants for better maintainability. - Updated validation logic in `TabMixin` to use these constants for clearer and more flexible error messages. * Refactor tab input validation tests for improved clarity - Replaced individual test cases for invalid tab inputs with a parameterized test function. - Enhanced test coverage by including cases for too many options, exceeding character limits, and non-string values. - Improved documentation within the test function for better understanding of validation scenarios. * Enhance tab input validation tests with parameterization - Refactored `test_tab_input_valid` to use `pytest.mark.parametrize` for improved test coverage and clarity. - Included multiple scenarios for valid tab inputs, such as standard, fewer options, and empty options. - Updated assertions to reflect the expected outcomes based on parameterized inputs. * Enhance TabInput validation to ensure value is a string and one of the specified options - Updated the `validate_value` method to enforce that the input value is a string. - Added a check to validate that the value is among the allowed options, raising a ValueError with a descriptive message if not. - Improved error handling for better user feedback on invalid inputs. * Fix optional chaining in error handling within CodeAreaModal - Updated the error check in the `delayedFunction` to use optional chaining for safer access to the error detail. - This change ensures that the code handles cases where `detail` may be undefined, improving robustness. * Add 'TAB' field type to schema and update direct types list - Included 'TAB' as a valid field type in the schema conversion dictionary. - Updated the DIRECT_TYPES list to include 'tab', ensuring consistency across type definitions. - These changes enhance the flexibility of the input handling for tab components. * Add unit test * Re-added noqa * fix: unit tests * [autofix.ci] apply automated fixes --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: italojohnny <italojohnnydosanjos@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
7aca264fec
commit
95ceb52fa3
14 changed files with 408 additions and 7 deletions
|
|
@ -22,6 +22,7 @@ from .inputs import (
|
||||||
SecretStrInput,
|
SecretStrInput,
|
||||||
SliderInput,
|
SliderInput,
|
||||||
StrInput,
|
StrInput,
|
||||||
|
TabInput,
|
||||||
TableInput,
|
TableInput,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -52,5 +53,6 @@ __all__ = [
|
||||||
"SecretStrInput",
|
"SecretStrInput",
|
||||||
"SliderInput",
|
"SliderInput",
|
||||||
"StrInput",
|
"StrInput",
|
||||||
|
"TabInput",
|
||||||
"TableInput",
|
"TableInput",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
2
src/backend/base/langflow/inputs/constants.py
Normal file
2
src/backend/base/langflow/inputs/constants.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
MAX_TAB_OPTIONS = 3
|
||||||
|
MAX_TAB_OPTION_LENGTH = 20
|
||||||
|
|
@ -11,6 +11,7 @@ from pydantic import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from langflow.field_typing.range_spec import RangeSpec
|
from langflow.field_typing.range_spec import RangeSpec
|
||||||
|
from langflow.inputs.constants import MAX_TAB_OPTION_LENGTH, MAX_TAB_OPTIONS
|
||||||
from langflow.inputs.validators import CoalesceBool
|
from langflow.inputs.validators import CoalesceBool
|
||||||
from langflow.schema.table import Column, TableOptions, TableSchema
|
from langflow.schema.table import Column, TableOptions, TableSchema
|
||||||
|
|
||||||
|
|
@ -30,6 +31,7 @@ class FieldTypes(str, Enum):
|
||||||
TABLE = "table"
|
TABLE = "table"
|
||||||
LINK = "link"
|
LINK = "link"
|
||||||
SLIDER = "slider"
|
SLIDER = "slider"
|
||||||
|
TAB = "tab"
|
||||||
|
|
||||||
|
|
||||||
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
||||||
|
|
@ -188,6 +190,31 @@ class DropDownMixin(BaseModel):
|
||||||
"""Dictionary of dialog inputs for the field. Default is an empty object."""
|
"""Dictionary of dialog inputs for the field. Default is an empty object."""
|
||||||
|
|
||||||
|
|
||||||
|
class TabMixin(BaseModel):
|
||||||
|
"""Mixin for tab input fields that allows a maximum of 3 values, each with a maximum of 20 characters."""
|
||||||
|
|
||||||
|
options: list[str] = Field(default_factory=list, max_length=3)
|
||||||
|
"""List of tab options. Maximum of 3 values allowed."""
|
||||||
|
|
||||||
|
@field_validator("options")
|
||||||
|
@classmethod
|
||||||
|
def validate_options(cls, v):
|
||||||
|
"""Validate that there are at most 3 tab values and each value has at most 20 characters."""
|
||||||
|
if len(v) > MAX_TAB_OPTIONS:
|
||||||
|
msg = f"Maximum of {MAX_TAB_OPTIONS} tab values allowed. Got {len(v)} values."
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
for i, value in enumerate(v):
|
||||||
|
if len(value) > MAX_TAB_OPTION_LENGTH:
|
||||||
|
msg = (
|
||||||
|
f"Tab value at index {i} exceeds maximum length of {MAX_TAB_OPTION_LENGTH} "
|
||||||
|
f"characters. Got {len(value)} characters."
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class MultilineMixin(BaseModel):
|
class MultilineMixin(BaseModel):
|
||||||
multiline: CoalesceBool = True
|
multiline: CoalesceBool = True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ from collections.abc import AsyncIterator, Iterator
|
||||||
from typing import Any, TypeAlias, get_args
|
from typing import Any, TypeAlias, get_args
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator, model_validator
|
||||||
|
|
||||||
from langflow.inputs.validators import CoalesceBool
|
from langflow.inputs.validators import CoalesceBool
|
||||||
from langflow.schema.data import Data
|
from langflow.schema.data import Data
|
||||||
|
|
@ -26,6 +26,7 @@ from .input_mixin import (
|
||||||
SerializableFieldTypes,
|
SerializableFieldTypes,
|
||||||
SliderMixin,
|
SliderMixin,
|
||||||
TableMixin,
|
TableMixin,
|
||||||
|
TabMixin,
|
||||||
ToolModeMixin,
|
ToolModeMixin,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -54,7 +55,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi
|
||||||
"- A single dictionary (will become a one-row table)\n"
|
"- A single dictionary (will become a one-row table)\n"
|
||||||
"- A Data object (Langflow's internal data structure)\n"
|
"- A Data object (Langflow's internal data structure)\n"
|
||||||
)
|
)
|
||||||
raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError
|
raise ValueError(msg) # noqa: TRY004
|
||||||
# Ensure each item in the list is either a dict or a Data instance.
|
# Ensure each item in the list is either a dict or a Data instance.
|
||||||
for i, item in enumerate(v):
|
for i, item in enumerate(v):
|
||||||
if not isinstance(item, dict | Data):
|
if not isinstance(item, dict | Data):
|
||||||
|
|
@ -64,7 +65,7 @@ class TableInput(BaseInputMixin, MetadataTraceMixin, TableMixin, ListableInputMi
|
||||||
"- A Data object (Langflow's internal data structure for passing data between components)\n"
|
"- A Data object (Langflow's internal data structure for passing data between components)\n"
|
||||||
f"Instead, got a {type(item).__name__}. Please check the format of your input data."
|
f"Instead, got a {type(item).__name__}. Please check the format of your input data."
|
||||||
)
|
)
|
||||||
raise ValueError(msg) # noqa: TRY004 Pydantic only catches ValueError or AssertionError
|
raise ValueError(msg) # noqa: TRY004
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -105,7 +106,13 @@ class CodeInput(BaseInputMixin, ListableInputMixin, InputTraceMixin, ToolModeMix
|
||||||
|
|
||||||
|
|
||||||
# Applying mixins to a specific input type
|
# Applying mixins to a specific input type
|
||||||
class StrInput(BaseInputMixin, ListableInputMixin, DatabaseLoadMixin, MetadataTraceMixin, ToolModeMixin):
|
class StrInput(
|
||||||
|
BaseInputMixin,
|
||||||
|
ListableInputMixin,
|
||||||
|
DatabaseLoadMixin,
|
||||||
|
MetadataTraceMixin,
|
||||||
|
ToolModeMixin,
|
||||||
|
):
|
||||||
field_type: SerializableFieldTypes = FieldTypes.TEXT
|
field_type: SerializableFieldTypes = FieldTypes.TEXT
|
||||||
load_from_db: CoalesceBool = False
|
load_from_db: CoalesceBool = False
|
||||||
"""Defines if the field will allow the user to open a text editor. Default is False."""
|
"""Defines if the field will allow the user to open a text editor. Default is False."""
|
||||||
|
|
@ -399,7 +406,13 @@ class BoolInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolMode
|
||||||
value: CoalesceBool = False
|
value: CoalesceBool = False
|
||||||
|
|
||||||
|
|
||||||
class NestedDictInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, InputTraceMixin, ToolModeMixin):
|
class NestedDictInput(
|
||||||
|
BaseInputMixin,
|
||||||
|
ListableInputMixin,
|
||||||
|
MetadataTraceMixin,
|
||||||
|
InputTraceMixin,
|
||||||
|
ToolModeMixin,
|
||||||
|
):
|
||||||
"""Represents a nested dictionary field.
|
"""Represents a nested dictionary field.
|
||||||
|
|
||||||
This class represents a nested dictionary input and provides functionality for handling dictionary values.
|
This class represents a nested dictionary input and provides functionality for handling dictionary values.
|
||||||
|
|
@ -451,6 +464,39 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeM
|
||||||
dialog_inputs: dict[str, Any] = Field(default_factory=dict)
|
dialog_inputs: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class TabInput(BaseInputMixin, TabMixin, MetadataTraceMixin, ToolModeMixin):
|
||||||
|
"""Represents a tab input field.
|
||||||
|
|
||||||
|
This class represents a tab input field that allows a maximum of 3 values, each with a maximum of 20 characters.
|
||||||
|
It inherits from the `BaseInputMixin` and `TabMixin` classes.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.TAB.
|
||||||
|
options (list[str]): List of tab options. Maximum of 3 values allowed, each with a maximum of 20 characters.
|
||||||
|
active_tab (int): Index of the currently active tab. Defaults to 0.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_type: SerializableFieldTypes = FieldTypes.TAB
|
||||||
|
options: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
@classmethod
|
||||||
|
def validate_value(cls, values):
|
||||||
|
"""Validates the value to ensure it's one of the tab values."""
|
||||||
|
options = values.options # Agora temos certeza de que options está disponível
|
||||||
|
value = values.value
|
||||||
|
|
||||||
|
if not isinstance(value, str):
|
||||||
|
msg = f"TabInput value must be a string. Got {type(value).__name__}."
|
||||||
|
raise TypeError(msg)
|
||||||
|
|
||||||
|
if value not in options and value != "":
|
||||||
|
msg = f"TabInput value must be one of the following: {options}. Got: '{value}'"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeMixin):
|
class MultiselectInput(BaseInputMixin, ListableInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeMixin):
|
||||||
"""Represents a multiselect input field.
|
"""Represents a multiselect input field.
|
||||||
|
|
||||||
|
|
@ -541,6 +587,7 @@ InputTypes: TypeAlias = (
|
||||||
| LinkInput
|
| LinkInput
|
||||||
| SliderInput
|
| SliderInput
|
||||||
| DataFrameInput
|
| DataFrameInput
|
||||||
|
| TabInput
|
||||||
)
|
)
|
||||||
|
|
||||||
InputTypesMap: dict[str, type[InputTypes]] = {t.__name__: t for t in get_args(InputTypes)}
|
InputTypesMap: dict[str, type[InputTypes]] = {t.__name__: t for t in get_args(InputTypes)}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from langflow.inputs import (
|
||||||
SecretStrInput,
|
SecretStrInput,
|
||||||
SliderInput,
|
SliderInput,
|
||||||
StrInput,
|
StrInput,
|
||||||
|
TabInput,
|
||||||
TableInput,
|
TableInput,
|
||||||
)
|
)
|
||||||
from langflow.template import Output
|
from langflow.template import Output
|
||||||
|
|
@ -52,5 +53,6 @@ __all__ = [
|
||||||
"SecretStrInput",
|
"SecretStrInput",
|
||||||
"SliderInput",
|
"SliderInput",
|
||||||
"StrInput",
|
"StrInput",
|
||||||
|
"TabInput",
|
||||||
"TableInput",
|
"TableInput",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ _convert_field_type_to_type: dict[FieldTypes, type] = {
|
||||||
FieldTypes.PROMPT: str,
|
FieldTypes.PROMPT: str,
|
||||||
FieldTypes.CODE: str,
|
FieldTypes.CODE: str,
|
||||||
FieldTypes.OTHER: str,
|
FieldTypes.OTHER: str,
|
||||||
|
FieldTypes.TAB: str,
|
||||||
}
|
}
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ def python_function(text: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set]
|
PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set]
|
||||||
DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider"]
|
DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider", "tab"]
|
||||||
|
|
||||||
|
|
||||||
LOADERS_INFO: list[dict[str, Any]] = [
|
LOADERS_INFO: list[dict[str, Any]] = [
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from langflow.inputs.inputs import (
|
||||||
SecretStrInput,
|
SecretStrInput,
|
||||||
SliderInput,
|
SliderInput,
|
||||||
StrInput,
|
StrInput,
|
||||||
|
TabInput,
|
||||||
TableInput,
|
TableInput,
|
||||||
)
|
)
|
||||||
from langflow.inputs.utils import instantiate_input
|
from langflow.inputs.utils import instantiate_input
|
||||||
|
|
@ -213,6 +214,81 @@ def test_file_input_valid():
|
||||||
assert file_input.value == ["/path/to/file"]
|
assert file_input.value == ["/path/to/file"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_id", "options", "value", "expected_options", "expected_value"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"standard_valid",
|
||||||
|
["Tab1", "Tab2", "Tab3"],
|
||||||
|
"Tab1",
|
||||||
|
["Tab1", "Tab2", "Tab3"],
|
||||||
|
"Tab1",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fewer_options",
|
||||||
|
["Tab1", "Tab2"],
|
||||||
|
"Tab2",
|
||||||
|
["Tab1", "Tab2"],
|
||||||
|
"Tab2",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"empty_options",
|
||||||
|
[],
|
||||||
|
"",
|
||||||
|
[],
|
||||||
|
"",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_tab_input_valid(test_id, options, value, expected_options, expected_value):
|
||||||
|
"""Test TabInput validation with valid inputs."""
|
||||||
|
data = TabInput(
|
||||||
|
name=f"valid_tab_{test_id}",
|
||||||
|
options=options,
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
assert data.options == expected_options
|
||||||
|
assert data.value == expected_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_id", "options", "value", "error_expected"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"too_many_options",
|
||||||
|
["Tab1", "Tab2", "Tab3", "Tab4"],
|
||||||
|
"Tab1",
|
||||||
|
ValidationError,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"option_too_long",
|
||||||
|
[
|
||||||
|
"Tab1",
|
||||||
|
"ThisTabValueIsTooLongAndExceedsTwentyCharacters",
|
||||||
|
"Tab3",
|
||||||
|
],
|
||||||
|
"Tab1",
|
||||||
|
ValidationError,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"non_string_value",
|
||||||
|
["Tab1", "Tab2", "Tab3"],
|
||||||
|
123,
|
||||||
|
TypeError,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_tab_input_invalid(test_id, options, value, error_expected):
|
||||||
|
"""Test TabInput validation with invalid inputs."""
|
||||||
|
if error_expected:
|
||||||
|
with pytest.raises(error_expected):
|
||||||
|
TabInput(
|
||||||
|
name=f"invalid_tab_{test_id}",
|
||||||
|
options=options,
|
||||||
|
value=value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_instantiate_input_comprehensive():
|
def test_instantiate_input_comprehensive():
|
||||||
valid_data = {
|
valid_data = {
|
||||||
"StrInput": {"name": "str_input", "value": "A string"},
|
"StrInput": {"name": "str_input", "value": "A string"},
|
||||||
|
|
@ -224,6 +300,11 @@ def test_instantiate_input_comprehensive():
|
||||||
"name": "multiselect_input",
|
"name": "multiselect_input",
|
||||||
"value": ["option1", "option2"],
|
"value": ["option1", "option2"],
|
||||||
},
|
},
|
||||||
|
"TabInput": {
|
||||||
|
"name": "tab_input",
|
||||||
|
"options": ["Tab1", "Tab2", "Tab3"],
|
||||||
|
"value": "Tab1",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for input_type, data in valid_data.items():
|
for input_type, data in valid_data.items():
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs-button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { InputProps, TabComponentType } from "../../types";
|
||||||
|
|
||||||
|
export default function TabComponent({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
editNode,
|
||||||
|
handleOnNewValue,
|
||||||
|
disabled,
|
||||||
|
options = [],
|
||||||
|
...baseInputProps
|
||||||
|
}: InputProps<string, TabComponentType>) {
|
||||||
|
const [activeTab, setActiveTab] = useState<string>(value || "");
|
||||||
|
|
||||||
|
// Update the active tab when the component props change
|
||||||
|
useEffect(() => {
|
||||||
|
if (options.length > 0) {
|
||||||
|
// If value is one of the options, use it
|
||||||
|
if (value && options.includes(value)) {
|
||||||
|
setActiveTab(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [options, value]);
|
||||||
|
|
||||||
|
// Handle tab change
|
||||||
|
const handleTabChange = (value: string) => {
|
||||||
|
setActiveTab(value);
|
||||||
|
handleOnNewValue({ value }, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate tab values - maximum 3 tabs, each with maximum 20 characters
|
||||||
|
const validOptions = options
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((tab) => (tab.length > 20 ? tab.substring(0, 20) : tab));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Tabs
|
||||||
|
defaultValue={activeTab}
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={handleTabChange}
|
||||||
|
className={`w-full ${disabled ? "pointer-events-none opacity-70" : ""}`}
|
||||||
|
>
|
||||||
|
<TabsList className="w-full">
|
||||||
|
{validOptions.map((tab, index) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={`${id}_tab_${index}`}
|
||||||
|
value={tab}
|
||||||
|
className="flex-1"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
import TableNodeComponent from "@/components/core/parameterRenderComponent/components/TableNodeComponent";
|
import TableNodeComponent from "@/components/core/parameterRenderComponent/components/TableNodeComponent";
|
||||||
import CodeAreaComponent from "@/components/core/parameterRenderComponent/components/codeAreaComponent";
|
import CodeAreaComponent from "@/components/core/parameterRenderComponent/components/codeAreaComponent";
|
||||||
import SliderComponent from "@/components/core/parameterRenderComponent/components/sliderComponent";
|
import SliderComponent from "@/components/core/parameterRenderComponent/components/sliderComponent";
|
||||||
|
import TabComponent from "@/components/core/parameterRenderComponent/components/tabComponent";
|
||||||
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
||||||
import { APIClassType, InputFieldType } from "@/types/api";
|
import { APIClassType, InputFieldType } from "@/types/api";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
@ -208,6 +209,14 @@ export function ParameterRenderComponent({
|
||||||
id={`slider_${id}`}
|
id={`slider_${id}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "tab":
|
||||||
|
return (
|
||||||
|
<TabComponent
|
||||||
|
{...baseInputProps}
|
||||||
|
options={templateData?.options || []}
|
||||||
|
id={`tab_${id}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <EmptyParameterComponent {...baseInputProps} />;
|
return <EmptyParameterComponent {...baseInputProps} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,10 @@ export type MultiselectComponentType = {
|
||||||
combobox?: boolean;
|
combobox?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TabComponentType = {
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type NodeInfoType = {
|
export type NodeInfoType = {
|
||||||
flowId: string;
|
flowId: string;
|
||||||
nodeType: string;
|
nodeType: string;
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ export default function CodeAreaModal({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Function to be executed after the state changes
|
// Function to be executed after the state changes
|
||||||
const delayedFunction = setTimeout(() => {
|
const delayedFunction = setTimeout(() => {
|
||||||
if (error?.detail.error !== undefined) {
|
if (error?.detail?.error !== undefined) {
|
||||||
//trigger to update the height, does not really apply any height
|
//trigger to update the height, does not really apply any height
|
||||||
setHeight("90%");
|
setHeight("90%");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ export type InputFieldType = {
|
||||||
refresh_button_text?: string;
|
refresh_button_text?: string;
|
||||||
combobox?: boolean;
|
combobox?: boolean;
|
||||||
info?: string;
|
info?: string;
|
||||||
|
options?: string[];
|
||||||
|
active_tab?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
|
|
||||||
164
src/frontend/tests/core/unit/tabComponent.spec.ts
Normal file
164
src/frontend/tests/core/unit/tabComponent.spec.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import { expect, Page, test } from "@playwright/test";
|
||||||
|
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||||
|
|
||||||
|
test(
|
||||||
|
"user should interact with tab component",
|
||||||
|
{ tag: ["@release", "@workspace"] },
|
||||||
|
async ({ context, page }) => {
|
||||||
|
await awaitBootstrapTest(page);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.getByTestId("blank-flow").click();
|
||||||
|
|
||||||
|
await page.waitForSelector(
|
||||||
|
'[data-testid="sidebar-custom-component-button"]',
|
||||||
|
{
|
||||||
|
timeout: 3000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByTestId("sidebar-custom-component-button").click();
|
||||||
|
await page.getByTitle("fit view").click();
|
||||||
|
await page.getByTitle("zoom out").click();
|
||||||
|
|
||||||
|
await page.getByTestId("title-Custom Component").first().click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="code-button-modal"]', {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("code-button-modal").click();
|
||||||
|
|
||||||
|
let cleanCode = await extractAndCleanCode(page);
|
||||||
|
|
||||||
|
// Use regex pattern to match the imports section more flexibly
|
||||||
|
cleanCode = updateComponentCode(cleanCode, {
|
||||||
|
imports: ["MessageTextInput", "Output", "TabInput"],
|
||||||
|
inputs: [
|
||||||
|
{
|
||||||
|
name: "MessageTextInput",
|
||||||
|
config: {
|
||||||
|
name: "input_value",
|
||||||
|
display_name: "Input Value",
|
||||||
|
info: "This is a custom component Input",
|
||||||
|
value: "Hello, World!",
|
||||||
|
tool_mode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TabInput",
|
||||||
|
config: {
|
||||||
|
name: "tab_selection",
|
||||||
|
display_name: "Tab Selection",
|
||||||
|
options: ["Tab 1", "Tab 2", "Tab 3"],
|
||||||
|
value: "Tab 1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.locator("textarea").last().press(`ControlOrMeta+a`);
|
||||||
|
await page.keyboard.press("Backspace");
|
||||||
|
await page.locator("textarea").last().fill(cleanCode);
|
||||||
|
await page.locator('//*[@id="checkAndSaveBtn"]').click();
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="fit_view"]', {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("fit_view").click();
|
||||||
|
await page.getByTestId("zoom_out").click();
|
||||||
|
|
||||||
|
// Verify that all tabs are visible
|
||||||
|
expect(await page.getByText("Tab 1").isVisible()).toBeTruthy();
|
||||||
|
expect(await page.getByText("Tab 2").isVisible()).toBeTruthy();
|
||||||
|
expect(await page.getByText("Tab 3").isVisible()).toBeTruthy();
|
||||||
|
|
||||||
|
// Verify that Tab 1 is active by default (as specified in the value)
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.getByRole("tab", { name: "Tab 1", selected: true })
|
||||||
|
.isVisible(),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
// Click on Tab 2 and verify it becomes active
|
||||||
|
await page.getByRole("tab", { name: "Tab 2" }).click();
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.getByRole("tab", { name: "Tab 2", selected: true })
|
||||||
|
.isVisible(),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
// Click on Tab 3 and verify it becomes active
|
||||||
|
await page.getByRole("tab", { name: "Tab 3" }).click();
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.getByRole("tab", { name: "Tab 3", selected: true })
|
||||||
|
.isVisible(),
|
||||||
|
).toBeTruthy();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateComponentCode(
|
||||||
|
code: string,
|
||||||
|
updates: {
|
||||||
|
imports?: string[];
|
||||||
|
inputs?: Array<{ name: string; config: Record<string, any> }>;
|
||||||
|
},
|
||||||
|
): string {
|
||||||
|
let updatedCode = code;
|
||||||
|
|
||||||
|
// Update imports
|
||||||
|
if (updates.imports) {
|
||||||
|
const importPattern = /from\s+langflow\.io\s+import\s+([^;\n]+)/;
|
||||||
|
const newImports = updates.imports.join(", ");
|
||||||
|
updatedCode = updatedCode.replace(
|
||||||
|
importPattern,
|
||||||
|
`from langflow.io import ${newImports}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update inputs
|
||||||
|
if (updates.inputs) {
|
||||||
|
const inputsPattern = /inputs\s*=\s*\[([\s\S]*?)\]/;
|
||||||
|
const newInputs = updates.inputs
|
||||||
|
.map(({ name, config }) => {
|
||||||
|
const params = Object.entries(config)
|
||||||
|
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
||||||
|
.join(",\n ");
|
||||||
|
return ` ${name}(\n ${params}\n )`;
|
||||||
|
})
|
||||||
|
.join(",\n");
|
||||||
|
updatedCode = updatedCode.replace(
|
||||||
|
inputsPattern,
|
||||||
|
`inputs = [\n${newInputs}\n ]`,
|
||||||
|
);
|
||||||
|
updatedCode = updatedCode.replace("true", "True");
|
||||||
|
updatedCode = updatedCode.replace("false", "False");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCode;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue