feat: new tool mode dialog and UI (#7647)
* Add tools input to the backend * Add tools type * Add tools component to parameter render * Added tools to langflow supported types * Add tools modal * Instantiate tools component in parameter render * Removed div when there is not any visible actions * Added margin * Add ComboBoxItem and ListItem components for enhanced selection UI * Update ComboBoxItem to display item description and enhance ToolsModal layout * Refactor ToolsModal header styling for improved layout and icon padding * Enhance ComboBoxItem layout and styling; update ToolsModal size and class for better responsiveness * Changed display name * Adds truncate for badges * Adds custom styling for table used for Tools modal * Added Tools modal with AgGrid * Changed button * made name and description editable * Parse values for saving * Add focused row without triggering checkbox click * [autofix.ci] apply automated fixes * Added types for tools modal * added toolsTable with sidebar on toolsModal * Added changes to work with MCP * update component.py to add display name and display description in tool mode * removed editing directly * Fixed editing * removed to upper case * Make editing apply filters * Adds design changes for MCP * Adds new design and null check * 📝 (frontend): add data-testid attribute to elements for testing purposes 🔧 (frontend): update data-testid attribute values for consistency and clarity in testing ✅ (frontend): update tests to use correct selectors and improve test coverage for editing tools functionality * ✅ (edit-tools.spec.ts): update test to use a more reliable method for checking visibility of an element * ✅ (edit-tools.spec.ts): add "@components" tag to the test to categorize it under components for better organization and filtering in test suites. * Updated design with new design * update padding * send args to tools data * Implemented showing arguments passed to LLM in tool mode * add componentg name to description * update package lock * fixed tests * fixed backend test * fixed backend test * fixed formatting * Fixed frontend tests * updated font sizes for badges on actions and styling on sortable list * Update tool mode design * added tooltips for info * tool name update * Update component_tool.py * styling utils * default values change * fixed tools test * fix format issues --------- Co-authored-by: deon-sanchez <deon.sanchez@datastax.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com>
This commit is contained in:
parent
6019b3ee06
commit
b1217f96fe
30 changed files with 1045 additions and 188 deletions
|
|
@ -273,12 +273,14 @@ class ComposioBaseComponent(Component):
|
||||||
|
|
||||||
def configure_tools(self, toolset: ComposioToolSet) -> list[Tool]:
|
def configure_tools(self, toolset: ComposioToolSet) -> list[Tool]:
|
||||||
tools = toolset.get_tools(actions=self._actions_data.keys())
|
tools = toolset.get_tools(actions=self._actions_data.keys())
|
||||||
|
logger.info(f"Tools: {tools}")
|
||||||
configured_tools = []
|
configured_tools = []
|
||||||
for tool in tools:
|
for tool in tools:
|
||||||
# Set the sanitized name
|
# Set the sanitized name
|
||||||
tool.name = self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
|
display_name = self._sanitized_names.get(tool.name, self._name_sanitizer.sub("-", tool.name))
|
||||||
# Set the tags
|
# Set the tags
|
||||||
tool.tags = [tool.name]
|
tool.tags = [tool.name]
|
||||||
|
tool.metadata = {"display_name": display_name, "display_description": tool.description}
|
||||||
configured_tools.append(tool)
|
configured_tools.append(tool)
|
||||||
return configured_tools
|
return configured_tools
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,19 +42,19 @@ def _get_input_type(input_: InputTypes):
|
||||||
def build_description(component: Component, output: Output) -> str:
|
def build_description(component: Component, output: Output) -> str:
|
||||||
if not output.required_inputs:
|
if not output.required_inputs:
|
||||||
logger.warning(f"Output {output.name} does not have required inputs defined")
|
logger.warning(f"Output {output.name} does not have required inputs defined")
|
||||||
|
name = component.name or component.__class__.__name__
|
||||||
if output.required_inputs:
|
if output.required_inputs:
|
||||||
args = ", ".join(
|
args = ", ".join(
|
||||||
sorted(
|
sorted(
|
||||||
[
|
[
|
||||||
f"{input_name}: {_get_input_type(component._inputs[input_name])}"
|
f"{name}. {input_name}: {_get_input_type(component._inputs[input_name])}"
|
||||||
for input_name in output.required_inputs
|
for input_name in output.required_inputs
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
args = ""
|
args = ""
|
||||||
return f"{output.method}({args}) - {component.description}"
|
return f"{name}. {output.method}({args}) - {component.description}"
|
||||||
|
|
||||||
|
|
||||||
async def send_message_noop(
|
async def send_message_noop(
|
||||||
|
|
@ -239,7 +239,7 @@ class ComponentToolkit:
|
||||||
else:
|
else:
|
||||||
args_schema = create_input_schema(self.component.inputs)
|
args_schema = create_input_schema(self.component.inputs)
|
||||||
|
|
||||||
name = f"{self.component.name or self.component.__class__.__name__ or ''}.{output.method}".strip(".")
|
name = f"{output.method}".strip(".")
|
||||||
formatted_name = _format_tool_name(name)
|
formatted_name = _format_tool_name(name)
|
||||||
event_manager = self.component._event_manager
|
event_manager = self.component._event_manager
|
||||||
if asyncio.iscoroutinefunction(output_method):
|
if asyncio.iscoroutinefunction(output_method):
|
||||||
|
|
@ -252,6 +252,10 @@ class ComponentToolkit:
|
||||||
handle_tool_error=True,
|
handle_tool_error=True,
|
||||||
callbacks=callbacks,
|
callbacks=callbacks,
|
||||||
tags=[formatted_name],
|
tags=[formatted_name],
|
||||||
|
metadata={
|
||||||
|
"display_name": formatted_name,
|
||||||
|
"display_description": build_description(self.component, output),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|
@ -264,6 +268,10 @@ class ComponentToolkit:
|
||||||
handle_tool_error=True,
|
handle_tool_error=True,
|
||||||
callbacks=callbacks,
|
callbacks=callbacks,
|
||||||
tags=[formatted_name],
|
tags=[formatted_name],
|
||||||
|
metadata={
|
||||||
|
"display_name": formatted_name,
|
||||||
|
"display_description": build_description(self.component, output),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if len(tools) == 1 and (tool_name or tool_description):
|
if len(tools) == 1 and (tool_name or tool_description):
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,6 @@ class ComposioGmailAPIComponent(ComposioBaseComponent):
|
||||||
|
|
||||||
def set_default_tools(self):
|
def set_default_tools(self):
|
||||||
self._default_tools = {
|
self._default_tools = {
|
||||||
self.sanitize_action_name("GMAIL_SEND_EMAIL").replace(" ", "-"),
|
"GMAIL_SEND_EMAIL",
|
||||||
self.sanitize_action_name("GMAIL_FETCH_EMAILS").replace(" ", "-"),
|
"GMAIL_FETCH_EMAILS",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -581,6 +581,6 @@ class ComposioSlackAPIComponent(ComposioBaseComponent):
|
||||||
|
|
||||||
def set_default_tools(self):
|
def set_default_tools(self):
|
||||||
self._default_tools = {
|
self._default_tools = {
|
||||||
self.sanitize_action_name("SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL").replace(" ", "-"),
|
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
|
||||||
self.sanitize_action_name("SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY").replace(" ", "-"),
|
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ from pydantic import BaseModel, ValidationError
|
||||||
from langflow.base.tools.constants import (
|
from langflow.base.tools.constants import (
|
||||||
TOOL_OUTPUT_DISPLAY_NAME,
|
TOOL_OUTPUT_DISPLAY_NAME,
|
||||||
TOOL_OUTPUT_NAME,
|
TOOL_OUTPUT_NAME,
|
||||||
TOOL_TABLE_SCHEMA,
|
|
||||||
TOOLS_METADATA_INFO,
|
TOOLS_METADATA_INFO,
|
||||||
TOOLS_METADATA_INPUT_NAME,
|
TOOLS_METADATA_INPUT_NAME,
|
||||||
)
|
)
|
||||||
|
|
@ -33,7 +32,6 @@ from langflow.schema.artifact import get_artifact_type, post_process_raw
|
||||||
from langflow.schema.data import Data
|
from langflow.schema.data import Data
|
||||||
from langflow.schema.message import ErrorMessage, Message
|
from langflow.schema.message import ErrorMessage, Message
|
||||||
from langflow.schema.properties import Source
|
from langflow.schema.properties import Source
|
||||||
from langflow.schema.table import FieldParserType, TableOptions
|
|
||||||
from langflow.services.tracing.schema import Log
|
from langflow.services.tracing.schema import Log
|
||||||
from langflow.template.field.base import UNDEFINED, Input, Output
|
from langflow.template.field.base import UNDEFINED, Input, Output
|
||||||
from langflow.template.frontend_node.custom_components import ComponentFrontendNode
|
from langflow.template.frontend_node.custom_components import ComponentFrontendNode
|
||||||
|
|
@ -1174,10 +1172,14 @@ class Component(CustomComponent):
|
||||||
"description": tool.description,
|
"description": tool.description,
|
||||||
"tags": tool.tags if hasattr(tool, "tags") and tool.tags else [tool.name],
|
"tags": tool.tags if hasattr(tool, "tags") and tool.tags else [tool.name],
|
||||||
"status": True, # Initialize all tools with status True
|
"status": True, # Initialize all tools with status True
|
||||||
|
"display_name": tool.metadata.get("display_name", tool.name),
|
||||||
|
"display_description": tool.metadata.get("display_description", tool.description),
|
||||||
|
"args": tool.args,
|
||||||
|
# "args_schema": tool.args_schema,
|
||||||
}
|
}
|
||||||
for tool in tools
|
for tool in tools
|
||||||
]
|
]
|
||||||
|
# print(tool_data)
|
||||||
if hasattr(self, TOOLS_METADATA_INPUT_NAME):
|
if hasattr(self, TOOLS_METADATA_INPUT_NAME):
|
||||||
old_tags = self._extract_tools_tags(self.tools_metadata)
|
old_tags = self._extract_tools_tags(self.tools_metadata)
|
||||||
new_tags = self._extract_tools_tags(tool_data)
|
new_tags = self._extract_tools_tags(tool_data)
|
||||||
|
|
@ -1203,35 +1205,16 @@ class Component(CustomComponent):
|
||||||
self.tools_metadata = tool_data
|
self.tools_metadata = tool_data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from langflow.io import TableInput
|
from langflow.io import ToolsInput
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
msg = "Failed to import TableInput from langflow.io"
|
msg = "Failed to import ToolsInput from langflow.io"
|
||||||
raise ImportError(msg) from e
|
raise ImportError(msg) from e
|
||||||
|
|
||||||
return TableInput(
|
return ToolsInput(
|
||||||
name=TOOLS_METADATA_INPUT_NAME,
|
name=TOOLS_METADATA_INPUT_NAME,
|
||||||
display_name="Edit tools",
|
display_name="Actions",
|
||||||
real_time_refresh=True,
|
info=TOOLS_METADATA_INFO,
|
||||||
table_schema=TOOL_TABLE_SCHEMA,
|
|
||||||
value=tool_data,
|
value=tool_data,
|
||||||
table_icon="Hammer",
|
|
||||||
trigger_icon="Hammer",
|
|
||||||
trigger_text="",
|
|
||||||
table_options=TableOptions(
|
|
||||||
block_add=True,
|
|
||||||
block_delete=True,
|
|
||||||
block_edit=True, # Allow editing for status toggle
|
|
||||||
block_sort=True,
|
|
||||||
block_filter=True,
|
|
||||||
block_hide=True,
|
|
||||||
block_select=True,
|
|
||||||
hide_options=True,
|
|
||||||
field_parsers={
|
|
||||||
"name": [FieldParserType.SNAKE_CASE, FieldParserType.NO_BLANK],
|
|
||||||
"commands": FieldParserType.COMMANDS,
|
|
||||||
},
|
|
||||||
description=TOOLS_METADATA_INFO,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_project_name(self):
|
def get_project_name(self):
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ class ParameterHandler:
|
||||||
params[field_name] = val
|
params[field_name] = val
|
||||||
case str():
|
case str():
|
||||||
params[field_name] = bool(val)
|
params[field_name] = bool(val)
|
||||||
case "table":
|
case "table" | "tools":
|
||||||
if isinstance(val, list) and all(isinstance(item, dict) for item in val):
|
if isinstance(val, list) and all(isinstance(item, dict) for item in val):
|
||||||
params[field_name] = pd.DataFrame(val)
|
params[field_name] = pd.DataFrame(val)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from .inputs import (
|
||||||
StrInput,
|
StrInput,
|
||||||
TabInput,
|
TabInput,
|
||||||
TableInput,
|
TableInput,
|
||||||
|
ToolsInput,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
@ -62,4 +63,5 @@ __all__ = [
|
||||||
"StrInput",
|
"StrInput",
|
||||||
"TabInput",
|
"TabInput",
|
||||||
"TableInput",
|
"TableInput",
|
||||||
|
"ToolsInput",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class FieldTypes(str, Enum):
|
||||||
SLIDER = "slider"
|
SLIDER = "slider"
|
||||||
TAB = "tab"
|
TAB = "tab"
|
||||||
QUERY = "query"
|
QUERY = "query"
|
||||||
|
TOOLS = "tools"
|
||||||
|
|
||||||
|
|
||||||
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
SerializableFieldTypes = Annotated[FieldTypes, PlainSerializer(lambda v: v.value, return_type=str)]
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,21 @@ class HandleInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin):
|
||||||
field_type: SerializableFieldTypes = FieldTypes.OTHER
|
field_type: SerializableFieldTypes = FieldTypes.OTHER
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsInput(BaseInputMixin, ListableInputMixin, MetadataTraceMixin, ToolModeMixin):
|
||||||
|
"""Represents an Input that contains a list of tools to activate, deactivate, or edit.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
field_type (SerializableFieldTypes): The field type of the input.
|
||||||
|
value (list[dict]): The value of the input.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
field_type: SerializableFieldTypes = FieldTypes.TOOLS
|
||||||
|
value: list[dict] = Field(default_factory=list)
|
||||||
|
is_list: bool = True
|
||||||
|
real_time_refresh: bool = True
|
||||||
|
|
||||||
|
|
||||||
class DataInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin):
|
class DataInput(HandleInput, InputTraceMixin, ListableInputMixin, ToolModeMixin):
|
||||||
"""Represents an Input that has a Handle that receives a Data object.
|
"""Represents an Input that has a Handle that receives a Data object.
|
||||||
|
|
||||||
|
|
@ -644,6 +659,7 @@ InputTypes: TypeAlias = (
|
||||||
| MultilineInput
|
| MultilineInput
|
||||||
| MultilineSecretInput
|
| MultilineSecretInput
|
||||||
| NestedDictInput
|
| NestedDictInput
|
||||||
|
| ToolsInput
|
||||||
| PromptInput
|
| PromptInput
|
||||||
| CodeInput
|
| CodeInput
|
||||||
| SecretStrInput
|
| SecretStrInput
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ from langflow.inputs import (
|
||||||
StrInput,
|
StrInput,
|
||||||
TabInput,
|
TabInput,
|
||||||
TableInput,
|
TableInput,
|
||||||
|
ToolsInput,
|
||||||
)
|
)
|
||||||
from langflow.template import Output
|
from langflow.template import Output
|
||||||
|
|
||||||
|
|
@ -57,4 +58,5 @@ __all__ = [
|
||||||
"StrInput",
|
"StrInput",
|
||||||
"TabInput",
|
"TabInput",
|
||||||
"TableInput",
|
"TableInput",
|
||||||
|
"ToolsInput",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ DIRECT_TYPES = [
|
||||||
"auth",
|
"auth",
|
||||||
"connect",
|
"connect",
|
||||||
"query",
|
"query",
|
||||||
|
"tools",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ def test_component_tool():
|
||||||
calculator_component = CalculatorToolComponent()
|
calculator_component = CalculatorToolComponent()
|
||||||
component_toolkit = ComponentToolkit(component=calculator_component)
|
component_toolkit = ComponentToolkit(component=calculator_component)
|
||||||
component_tool = component_toolkit.get_tools()[0]
|
component_tool = component_toolkit.get_tools()[0]
|
||||||
assert component_tool.name == "CalculatorTool-run_model"
|
assert component_tool.name == "run_model"
|
||||||
assert issubclass(component_tool.args_schema, BaseModel)
|
assert issubclass(component_tool.args_schema, BaseModel)
|
||||||
# TODO: fix this
|
# TODO: fix this
|
||||||
# assert component_tool.args_schema.model_json_schema()["properties"] == {
|
# assert component_tool.args_schema.model_json_schema()["properties"] == {
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ def test_component_inputs_toolkit():
|
||||||
component = AllInputsComponent()
|
component = AllInputsComponent()
|
||||||
component_toolkit = ComponentToolkit(component=component)
|
component_toolkit = ComponentToolkit(component=component)
|
||||||
component_tool = component_toolkit.get_tools()[0]
|
component_tool = component_toolkit.get_tools()[0]
|
||||||
assert component_tool.name == "AllInputsComponent-build_output"
|
assert component_tool.name == "build_output"
|
||||||
assert issubclass(component_tool.args_schema, BaseModel)
|
assert issubclass(component_tool.args_schema, BaseModel)
|
||||||
properties = component_tool.args_schema.model_json_schema()["properties"]
|
properties = component_tool.args_schema.model_json_schema()["properties"]
|
||||||
|
|
||||||
|
|
|
||||||
4
src/frontend/package-lock.json
generated
4
src/frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "langflow",
|
"name": "langflow",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "langflow",
|
"name": "langflow",
|
||||||
"version": "1.3.3",
|
"version": "1.3.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/number-input": "^2.1.2",
|
"@chakra-ui/number-input": "^2.1.2",
|
||||||
"@headlessui/react": "^2.0.4",
|
"@headlessui/react": "^2.0.4",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface ComboBoxItemProps {
|
||||||
|
item: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ComboBoxItem = ({ item }: ComboBoxItemProps) => {
|
||||||
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row pb-1">
|
||||||
|
<div className="inline-flex justify-start">
|
||||||
|
<label
|
||||||
|
className="relative flex cursor-pointer"
|
||||||
|
htmlFor={`check-${item?.name}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={() => setIsChecked(!isChecked)}
|
||||||
|
className="peer h-5 w-5 cursor-pointer appearance-none rounded border border-border border-muted-foreground shadow transition-all hover:shadow-md"
|
||||||
|
id={`check-${item?.name}`}
|
||||||
|
/>
|
||||||
|
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform text-black opacity-0 peer-checked:bg-primary peer-checked:opacity-100">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Button
|
||||||
|
unstyled
|
||||||
|
className="mx-2 flex w-full justify-start rounded-md hover:bg-accent"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="ml-2 flex w-72 cursor-pointer text-sm font-bold text-primary"
|
||||||
|
htmlFor={`check-${item?.name}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{item?.name}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="flex w-72 cursor-pointer text-sm text-muted-foreground"
|
||||||
|
htmlFor={`check-${item?.name}`}
|
||||||
|
>
|
||||||
|
<span className="truncate">{item?.description}</span>
|
||||||
|
</label>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ComboBoxItem;
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/utils/utils";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const ListItem = ({
|
||||||
|
item,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
onMouseEnter,
|
||||||
|
onMouseLeave,
|
||||||
|
isFocused,
|
||||||
|
isKeyboardNavActive,
|
||||||
|
dataTestId,
|
||||||
|
}: {
|
||||||
|
item: any;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
className?: string;
|
||||||
|
onMouseEnter: () => void;
|
||||||
|
onMouseLeave: () => void;
|
||||||
|
isFocused: boolean;
|
||||||
|
isKeyboardNavActive: boolean;
|
||||||
|
dataTestId: string;
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const itemRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
// Clear hover state when keyboard navigation is active
|
||||||
|
useEffect(() => {
|
||||||
|
if (isKeyboardNavActive) {
|
||||||
|
setIsHovered(false);
|
||||||
|
}
|
||||||
|
}, [isKeyboardNavActive]);
|
||||||
|
|
||||||
|
// Scroll into view when focused by keyboard
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFocused && itemRef.current) {
|
||||||
|
itemRef.current.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
}, [isFocused]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={itemRef}
|
||||||
|
key={item.id}
|
||||||
|
data-testid={dataTestId}
|
||||||
|
unstyled
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
"group flex w-full rounded-md px-2 py-0.5",
|
||||||
|
!isKeyboardNavActive && "hover:bg-muted", // Only apply hover styles when not in keyboard nav
|
||||||
|
!item.metaData && "py-2.5",
|
||||||
|
isFocused && "bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!isKeyboardNavActive) {
|
||||||
|
setIsHovered(true);
|
||||||
|
onMouseEnter();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setIsHovered(false);
|
||||||
|
onMouseLeave();
|
||||||
|
}}
|
||||||
|
// Disable pointer events during keyboard navigation
|
||||||
|
style={{ pointerEvents: isKeyboardNavActive ? "none" : "auto" }}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
|
{item.icon && (
|
||||||
|
<div>
|
||||||
|
<ForwardedIconComponent name={item.icon} className="mr-2 h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex w-full flex-col truncate">
|
||||||
|
<div className="flex w-full truncate text-mmd font-semibold">
|
||||||
|
<span className="truncate">{item.name}</span>
|
||||||
|
</div>
|
||||||
|
{"metaData" in item && item.metaData && (
|
||||||
|
<div className="flex w-full truncate text-mmd text-gray-500">
|
||||||
|
<span className="truncate">{item.metaData}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isHovered || isFocused ? (
|
||||||
|
<div className="ml-auto flex items-center justify-start rounded-md">
|
||||||
|
<div className="flex items-center pr-1.5 text-mmd font-semibold text-muted-foreground">
|
||||||
|
Select
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center rounded-md">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="corner-down-left"
|
||||||
|
className="h-3.5 w-3.5 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Always show the check icon when selected, regardless of hover/focus state
|
||||||
|
isSelected && (
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="check"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto flex h-4 w-4",
|
||||||
|
item.link === "validated" && "text-green-500",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListItem;
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
|
||||||
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
|
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
|
||||||
import { InputProps } from "@/components/core/parameterRenderComponent/types";
|
import { InputProps } from "@/components/core/parameterRenderComponent/types";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { DialogHeader } from "@/components/ui/dialog";
|
import { DialogHeader } from "@/components/ui/dialog";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close";
|
import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close";
|
||||||
import { cn, testIdCase } from "@/utils/utils";
|
import { testIdCase } from "@/utils/utils";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import ListItem from "./ListItem";
|
||||||
|
|
||||||
// Update interface with better types
|
// Update interface with better types
|
||||||
interface ListSelectionComponentProps {
|
interface ListSelectionComponentProps {
|
||||||
|
|
@ -20,118 +19,6 @@ interface ListSelectionComponentProps {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem = ({
|
|
||||||
item,
|
|
||||||
isSelected,
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
onMouseEnter,
|
|
||||||
onMouseLeave,
|
|
||||||
isFocused,
|
|
||||||
isKeyboardNavActive,
|
|
||||||
dataTestId,
|
|
||||||
}: {
|
|
||||||
item: any;
|
|
||||||
isSelected: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
className?: string;
|
|
||||||
onMouseEnter: () => void;
|
|
||||||
onMouseLeave: () => void;
|
|
||||||
isFocused: boolean;
|
|
||||||
isKeyboardNavActive: boolean;
|
|
||||||
dataTestId: string;
|
|
||||||
}) => {
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
const itemRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
// Clear hover state when keyboard navigation is active
|
|
||||||
useEffect(() => {
|
|
||||||
if (isKeyboardNavActive) {
|
|
||||||
setIsHovered(false);
|
|
||||||
}
|
|
||||||
}, [isKeyboardNavActive]);
|
|
||||||
|
|
||||||
// Scroll into view when focused by keyboard
|
|
||||||
useEffect(() => {
|
|
||||||
if (isFocused && itemRef.current) {
|
|
||||||
itemRef.current.scrollIntoView({ block: "nearest" });
|
|
||||||
}
|
|
||||||
}, [isFocused]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
ref={itemRef}
|
|
||||||
key={item.id}
|
|
||||||
data-testid={dataTestId}
|
|
||||||
unstyled
|
|
||||||
size="sm"
|
|
||||||
className={cn(
|
|
||||||
"group flex w-full rounded-md px-2 py-0.5",
|
|
||||||
!isKeyboardNavActive && "hover:bg-muted", // Only apply hover styles when not in keyboard nav
|
|
||||||
!item.metaData && "py-2.5",
|
|
||||||
isFocused && "bg-muted",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
onClick={onClick}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
if (!isKeyboardNavActive) {
|
|
||||||
setIsHovered(true);
|
|
||||||
onMouseEnter();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setIsHovered(false);
|
|
||||||
onMouseLeave();
|
|
||||||
}}
|
|
||||||
// Disable pointer events during keyboard navigation
|
|
||||||
style={{ pointerEvents: isKeyboardNavActive ? "none" : "auto" }}
|
|
||||||
>
|
|
||||||
<div className="flex w-full items-center gap-2">
|
|
||||||
{item.icon && (
|
|
||||||
<div>
|
|
||||||
<ForwardedIconComponent name={item.icon} className="mr-2 h-4 w-4" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex w-full flex-col truncate">
|
|
||||||
<div className="flex w-full truncate text-mmd font-semibold">
|
|
||||||
<span className="truncate">{item.name}</span>
|
|
||||||
</div>
|
|
||||||
{"metaData" in item && item.metaData && (
|
|
||||||
<div className="flex w-full truncate text-mmd text-gray-500">
|
|
||||||
<span className="truncate">{item.metaData}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isHovered || isFocused ? (
|
|
||||||
<div className="ml-auto flex items-center justify-start rounded-md">
|
|
||||||
<div className="flex items-center pr-1.5 text-mmd font-semibold text-muted-foreground">
|
|
||||||
Select
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center rounded-md">
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="corner-down-left"
|
|
||||||
className="h-3.5 w-3.5 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Always show the check icon when selected, regardless of hover/focus state
|
|
||||||
isSelected && (
|
|
||||||
<ForwardedIconComponent
|
|
||||||
name="check"
|
|
||||||
className={cn(
|
|
||||||
"ml-auto flex h-4 w-4",
|
|
||||||
item.link === "validated" && "text-green-500",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ListSelectionComponent = ({
|
const ListSelectionComponent = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
@ -286,7 +173,7 @@ const ListSelectionComponent = ({
|
||||||
name={nodeClass?.icon || "unknown"}
|
name={nodeClass?.icon || "unknown"}
|
||||||
className="h-[18px] w-[18px] text-muted-foreground"
|
className="h-[18px] w-[18px] text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<div className="text-mmd font-semibold">
|
<div className="text-[13px] font-semibold">
|
||||||
{nodeClass?.display_name}
|
{nodeClass?.display_name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,11 @@ const RenderInputParameters = ({
|
||||||
(templateField: string, idx) => {
|
(templateField: string, idx) => {
|
||||||
const template = data.node?.template[templateField];
|
const template = data.node?.template[templateField];
|
||||||
|
|
||||||
if (!template?.show || template?.advanced) {
|
if (
|
||||||
|
!template?.show ||
|
||||||
|
template?.advanced ||
|
||||||
|
(template?.tool_mode && isToolMode)
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import { ICON_STROKE_WIDTH } from "@/constants/constants";
|
||||||
|
import ToolsModal from "@/modals/toolsModal";
|
||||||
|
import { cn, testIdCase } from "@/utils/utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ForwardedIconComponent } from "../../../../common/genericIconComponent";
|
||||||
|
import { Badge } from "../../../../ui/badge";
|
||||||
|
import { Button } from "../../../../ui/button";
|
||||||
|
import { Skeleton } from "../../../../ui/skeleton";
|
||||||
|
import { InputProps, ToolsComponentType } from "../../types";
|
||||||
|
|
||||||
|
export default function ToolsComponent({
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
editNode = false,
|
||||||
|
id = "",
|
||||||
|
handleOnNewValue,
|
||||||
|
isAction = false,
|
||||||
|
button_description,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
disabled = false,
|
||||||
|
template,
|
||||||
|
}: InputProps<any[] | undefined, ToolsComponentType>): JSX.Element {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const actions = value
|
||||||
|
?.filter((action) => action.status === true)
|
||||||
|
.map((action) => {
|
||||||
|
return {
|
||||||
|
name: action.name,
|
||||||
|
tags: action.tags,
|
||||||
|
description: action.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleActionsQt = isAction ? 20 : 4;
|
||||||
|
|
||||||
|
const visibleActions = actions?.slice(0, visibleActionsQt) || [];
|
||||||
|
const remainingCount = actions
|
||||||
|
? Math.max(0, actions.length - visibleActionsQt)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center",
|
||||||
|
disabled && "cursor-not-allowed",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value && (
|
||||||
|
<ToolsModal
|
||||||
|
open={isModalOpen}
|
||||||
|
setOpen={setIsModalOpen}
|
||||||
|
isAction={isAction}
|
||||||
|
description={description}
|
||||||
|
template={template}
|
||||||
|
rows={value}
|
||||||
|
handleOnNewValue={handleOnNewValue}
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="relative flex w-full items-center gap-3"
|
||||||
|
data-testid={"div-" + id}
|
||||||
|
>
|
||||||
|
{(visibleActions.length > 0 || isAction) && (
|
||||||
|
<Button
|
||||||
|
variant={"ghost"}
|
||||||
|
disabled={!value || disabled}
|
||||||
|
size={"iconMd"}
|
||||||
|
className={cn(
|
||||||
|
"absolute -top-8 right-0 font-semibold text-muted-foreground group-hover:text-primary",
|
||||||
|
)}
|
||||||
|
data-testid="button_open_actions"
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="Settings2"
|
||||||
|
className="icon-size"
|
||||||
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
|
/>
|
||||||
|
{button_description}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!value ? (
|
||||||
|
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
|
||||||
|
{[...Array(4)].map((_, index) => (
|
||||||
|
<Skeleton key={index} className="h-6 w-20 rounded-full" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : visibleActions.length > 0 ? (
|
||||||
|
<div className="flex w-full flex-wrap gap-1 overflow-hidden py-1.5">
|
||||||
|
{visibleActions.map((action, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant="secondaryStatic"
|
||||||
|
size="sq"
|
||||||
|
className="truncate font-normal"
|
||||||
|
data-testid={testIdCase(`tool_${action.name}`)}
|
||||||
|
>
|
||||||
|
<span className="truncate text-xxs font-medium">
|
||||||
|
{action.name.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{remainingCount > 0 && (
|
||||||
|
<span className="ml-1 self-center text-xs font-normal text-muted-foreground">
|
||||||
|
+{remainingCount} more
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
visibleActions.length === 0 &&
|
||||||
|
isAction && (
|
||||||
|
<div className="mt-2 flex w-full flex-col items-center gap-2 rounded-md border border-dashed p-8">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
No actions added to this server
|
||||||
|
</span>
|
||||||
|
<Button size={"sm"} onClick={() => setIsModalOpen(true)}>
|
||||||
|
<span>Add actions</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{visibleActions.length === 0 && !isAction && value && (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
size={editNode ? "xs" : "default"}
|
||||||
|
className={
|
||||||
|
"w-full " +
|
||||||
|
(disabled ? "pointer-events-none cursor-not-allowed" : "")
|
||||||
|
}
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
>
|
||||||
|
<span>Select actions</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -32,27 +32,27 @@ const SortableListItem = memo(
|
||||||
}) => (
|
}) => (
|
||||||
<li
|
<li
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium text-gray-800",
|
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium",
|
||||||
limit === 1 ? "h-6 rounded-md bg-muted" : "group cursor-grab",
|
limit === 1 ? "h-6 rounded-md bg-muted" : "group cursor-grab",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{limit !== 1 && (
|
{limit !== 1 && (
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name="grid-horizontal"
|
name="GridHorizontal"
|
||||||
className="h-5 w-5 fill-gray-300 text-gray-300"
|
className="h-5 w-5 text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex w-full items-center gap-x-2">
|
<div className="flex w-full items-center gap-x-2">
|
||||||
{limit !== 1 && (
|
{limit !== 1 && (
|
||||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-400 text-center text-white">
|
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-border text-center text-mmd text-primary">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"truncate text-muted-foreground",
|
"truncate text-xxs font-medium text-muted-foreground",
|
||||||
limit === 1 ? "max-w-56 pl-2" : "max-w-48",
|
limit === 1 ? "max-w-56 pl-2" : "max-w-48",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -61,22 +61,16 @@ const SortableListItem = memo(
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant={limit !== 1 ? "outline" : "ghost"}
|
variant={"ghost"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto h-6 w-6 opacity-0 transition-opacity duration-200",
|
"ml-auto h-6 w-6 text-muted-foreground opacity-0 transition-opacity duration-200",
|
||||||
limit === 1
|
limit === 1
|
||||||
? "group pr-1 opacity-100"
|
? "group pr-1 opacity-100 hover:text-foreground"
|
||||||
: "hover:border hover:border-destructive hover:bg-transparent hover:opacity-100",
|
: "hover:text-destructive group-hover:opacity-100",
|
||||||
)}
|
)}
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent name="x" className={cn("h-6 w-6")} />
|
||||||
name="x"
|
|
||||||
className={cn(
|
|
||||||
"h-6 w-6 text-red-500",
|
|
||||||
limit === 1 && "text-gray-500 group-hover:text-input",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/
|
||||||
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";
|
||||||
|
import ToolsComponent from "./components/ToolsComponent";
|
||||||
import ConnectionComponent from "./components/connectionComponent";
|
import ConnectionComponent from "./components/connectionComponent";
|
||||||
import DictComponent from "./components/dictComponent";
|
import DictComponent from "./components/dictComponent";
|
||||||
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||||
|
|
@ -203,6 +204,16 @@ export function ParameterRenderComponent({
|
||||||
table_icon={templateData?.table_icon}
|
table_icon={templateData?.table_icon}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "tools":
|
||||||
|
return (
|
||||||
|
<ToolsComponent
|
||||||
|
{...baseInputProps}
|
||||||
|
description={templateData.info || "Add or edit data"}
|
||||||
|
title={nodeClass?.display_name ?? "Tools"}
|
||||||
|
icon={nodeClass?.icon ?? ""}
|
||||||
|
template={nodeClass?.template}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case "slider":
|
case "slider":
|
||||||
return (
|
return (
|
||||||
<SliderComponent
|
<SliderComponent
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
import { APIClassType, InputFieldType, TableOptionsTypeAPI } from "@/types/api";
|
import {
|
||||||
|
APIClassType,
|
||||||
|
APITemplateType,
|
||||||
|
InputFieldType,
|
||||||
|
TableOptionsTypeAPI,
|
||||||
|
} from "@/types/api";
|
||||||
import { RangeSpecType } from "@/types/components";
|
import { RangeSpecType } from "@/types/components";
|
||||||
import { ColumnField } from "@/types/utils/functions";
|
import { ColumnField } from "@/types/utils/functions";
|
||||||
|
|
||||||
|
|
@ -40,6 +45,15 @@ export type TableComponentType = {
|
||||||
table_icon?: string;
|
table_icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ToolsComponentType = {
|
||||||
|
description: string;
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
button_description?: string;
|
||||||
|
isAction?: boolean;
|
||||||
|
template?: APITemplateType;
|
||||||
|
};
|
||||||
|
|
||||||
export type FloatComponentType = {
|
export type FloatComponentType = {
|
||||||
rangeSpec: RangeSpecType;
|
rangeSpec: RangeSpecType;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -659,6 +659,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
||||||
"connect",
|
"connect",
|
||||||
"auth",
|
"auth",
|
||||||
"query",
|
"query",
|
||||||
|
"tools",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const FLEX_VIEW_TYPES = ["bool"];
|
export const FLEX_VIEW_TYPES = ["bool"];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,386 @@
|
||||||
|
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
|
import TableComponent from "@/components/core/parameterRenderComponent/components/tableComponent";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarContent,
|
||||||
|
SidebarGroup,
|
||||||
|
SidebarGroupContent,
|
||||||
|
SidebarHeader,
|
||||||
|
useSidebar,
|
||||||
|
} from "@/components/ui/sidebar";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
|
import { APITemplateType } from "@/types/api";
|
||||||
|
import { parseString } from "@/utils/stringManipulation";
|
||||||
|
import { ColDef } from "ag-grid-community";
|
||||||
|
import { AgGridReact } from "ag-grid-react";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export default function ToolsTable({
|
||||||
|
rows,
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
isAction,
|
||||||
|
open,
|
||||||
|
handleOnNewValue,
|
||||||
|
template,
|
||||||
|
}: {
|
||||||
|
rows: any[];
|
||||||
|
data: any[];
|
||||||
|
template?: APITemplateType;
|
||||||
|
setData: (data: any[]) => void;
|
||||||
|
open: boolean;
|
||||||
|
handleOnNewValue: handleOnNewValueType;
|
||||||
|
isAction: boolean;
|
||||||
|
}) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [selectedRows, setSelectedRows] = useState<any[] | null>(null);
|
||||||
|
const agGrid = useRef<AgGridReact>(null);
|
||||||
|
|
||||||
|
const [focusedRow, setFocusedRow] = useState<any | null>(null);
|
||||||
|
const [sidebarName, setSidebarName] = useState<string>("");
|
||||||
|
const [sidebarDescription, setSidebarDescription] = useState<string>("");
|
||||||
|
|
||||||
|
const { setOpen: setSidebarOpen } = useSidebar();
|
||||||
|
|
||||||
|
const getRowId = useMemo(() => {
|
||||||
|
return (params: any) => params.data.display_name ?? params.data.name;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialData = cloneDeep(rows);
|
||||||
|
setData(initialData);
|
||||||
|
const filter = initialData.filter((row) => row.status === true);
|
||||||
|
setSelectedRows(filter);
|
||||||
|
}, [rows, open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialData = cloneDeep(rows);
|
||||||
|
const filter = initialData.filter((row) => row.status === true);
|
||||||
|
if (agGrid.current) {
|
||||||
|
agGrid.current?.api?.forEachNode((node) => {
|
||||||
|
if (
|
||||||
|
filter.some(
|
||||||
|
(row) =>
|
||||||
|
(row.display_name ?? row.name) ===
|
||||||
|
(node.data.display_name ?? node.data.name),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
node.setSelected(true);
|
||||||
|
} else {
|
||||||
|
node.setSelected(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [agGrid.current]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open && selectedRows) {
|
||||||
|
handleOnNewValue({
|
||||||
|
value: data.map((row) => {
|
||||||
|
const processedValue = (
|
||||||
|
row.name !== ""
|
||||||
|
? parseString(row.name, ["snake_case", "no_blank", "lowercase"])
|
||||||
|
: parseString(row.display_name, [
|
||||||
|
"snake_case",
|
||||||
|
"no_blank",
|
||||||
|
"lowercase",
|
||||||
|
])
|
||||||
|
).slice(0, 46);
|
||||||
|
|
||||||
|
const processedDescription =
|
||||||
|
row.description !== "" ? row.description : row.display_description;
|
||||||
|
|
||||||
|
return selectedRows?.some(
|
||||||
|
(selected) =>
|
||||||
|
(selected.display_name ?? selected.name) ===
|
||||||
|
(row.display_name ?? row.name),
|
||||||
|
)
|
||||||
|
? {
|
||||||
|
...row,
|
||||||
|
status: true,
|
||||||
|
name: processedValue,
|
||||||
|
description: processedDescription,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...row,
|
||||||
|
status: false,
|
||||||
|
name: processedValue,
|
||||||
|
description: processedDescription,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focusedRow) {
|
||||||
|
setSidebarName(focusedRow.name);
|
||||||
|
setSidebarDescription(focusedRow.description);
|
||||||
|
} else {
|
||||||
|
setSidebarName("");
|
||||||
|
setSidebarDescription("");
|
||||||
|
}
|
||||||
|
}, [focusedRow]);
|
||||||
|
|
||||||
|
const columnDefs: ColDef[] = [
|
||||||
|
{
|
||||||
|
field: isAction ? "display_name" : "name",
|
||||||
|
headerName: isAction ? "Flow Name" : "Name",
|
||||||
|
flex: 1,
|
||||||
|
valueGetter: (params) =>
|
||||||
|
!isAction
|
||||||
|
? parseString(
|
||||||
|
params.data.display_name !== ""
|
||||||
|
? params.data.display_name
|
||||||
|
: params.data.name,
|
||||||
|
["space_case"],
|
||||||
|
)
|
||||||
|
: params.data.display_name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "description",
|
||||||
|
headerName: "Description",
|
||||||
|
flex: 2,
|
||||||
|
cellClass: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: isAction ? "name" : "tags",
|
||||||
|
headerName: isAction ? "Action" : "Slug",
|
||||||
|
flex: 1,
|
||||||
|
resizable: false,
|
||||||
|
valueGetter: (params) =>
|
||||||
|
isAction
|
||||||
|
? params.data.name !== ""
|
||||||
|
? parseString(params.data.name, [
|
||||||
|
"snake_case",
|
||||||
|
"no_blank",
|
||||||
|
"uppercase",
|
||||||
|
])
|
||||||
|
: parseString(params.data.display_name, [
|
||||||
|
"snake_case",
|
||||||
|
"no_blank",
|
||||||
|
"uppercase",
|
||||||
|
])
|
||||||
|
: parseString(params.data.tags.join(", "), [
|
||||||
|
"snake_case",
|
||||||
|
"uppercase",
|
||||||
|
]),
|
||||||
|
cellClass: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: "tags",
|
||||||
|
headerName: "Tags",
|
||||||
|
flex: 1,
|
||||||
|
hide: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const handleSelectionChanged = (event) => {
|
||||||
|
if (open) {
|
||||||
|
const selectedData = event.api.getSelectedRows();
|
||||||
|
setSelectedRows(selectedData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSidebarInputChange = (
|
||||||
|
field: "name" | "description",
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
if (!focusedRow) return;
|
||||||
|
|
||||||
|
const originalName = focusedRow.display_name;
|
||||||
|
|
||||||
|
setFocusedRow((prev) => (prev ? { ...prev, [field]: value } : null));
|
||||||
|
|
||||||
|
if (agGrid.current) {
|
||||||
|
const updatedRow = { ...focusedRow, [field]: value };
|
||||||
|
|
||||||
|
agGrid.current.api.applyTransaction({
|
||||||
|
update: [updatedRow],
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedData = data.map((row) =>
|
||||||
|
(row.display_name ?? row.name) === originalName ? updatedRow : row,
|
||||||
|
);
|
||||||
|
setData(updatedData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionArgs = useMemo(() => {
|
||||||
|
return Object.entries(focusedRow?.args ?? {}).map(
|
||||||
|
([key, value]: [string, any]) => ({
|
||||||
|
display_name: value.title,
|
||||||
|
name: key,
|
||||||
|
description: value.description ?? null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}, [focusedRow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<main className="flex h-full w-full flex-1 flex-col gap-2 overflow-hidden py-4">
|
||||||
|
<div className="flex-none px-4">
|
||||||
|
<Input
|
||||||
|
icon="Search"
|
||||||
|
placeholder="Search actions..."
|
||||||
|
inputClassName="h-8"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<TableComponent
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
rowData={data}
|
||||||
|
quickFilterText={searchQuery}
|
||||||
|
ref={agGrid}
|
||||||
|
rowSelection="multiple"
|
||||||
|
suppressRowClickSelection={true}
|
||||||
|
className="ag-tool-mode h-full w-full overflow-visible"
|
||||||
|
headerHeight={32}
|
||||||
|
rowHeight={32}
|
||||||
|
onSelectionChanged={handleSelectionChanged}
|
||||||
|
tableOptions={{
|
||||||
|
block_hide: true,
|
||||||
|
}}
|
||||||
|
onRowClicked={(event) => {
|
||||||
|
setFocusedRow(event.data);
|
||||||
|
setSidebarOpen(true);
|
||||||
|
}}
|
||||||
|
getRowId={getRowId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Sidebar
|
||||||
|
side="right"
|
||||||
|
className="flex h-full flex-col overflow-auto border-l border-border"
|
||||||
|
>
|
||||||
|
<SidebarHeader className="flex-none px-4 py-4">
|
||||||
|
{focusedRow &&
|
||||||
|
(isAction ? (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
className="text-mmd font-medium"
|
||||||
|
htmlFor="sidebar-name-input"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
id="sidebar-name-input"
|
||||||
|
value={sidebarName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSidebarName(e.target.value);
|
||||||
|
handleSidebarInputChange("name", e.target.value);
|
||||||
|
}}
|
||||||
|
maxLength={46}
|
||||||
|
placeholder="Edit name..."
|
||||||
|
data-testid="input_update_name"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{isAction
|
||||||
|
? "Used as the function name when this flow is exposed to clients."
|
||||||
|
: "Used as the function name when this tool is exposed to the agent."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
className="text-mmd font-medium"
|
||||||
|
htmlFor="sidebar-desc-input"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Textarea
|
||||||
|
id="sidebar-desc-input"
|
||||||
|
value={sidebarDescription}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSidebarDescription(e.target.value);
|
||||||
|
handleSidebarInputChange("description", e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Edit description..."
|
||||||
|
className="h-24"
|
||||||
|
data-testid="input_update_description"
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{isAction
|
||||||
|
? "This is the description for the action exposed to the clients."
|
||||||
|
: "This is the description for the tool exposed to the agents."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1" data-testid="sidebar_header">
|
||||||
|
<h3
|
||||||
|
className="text-base font-medium"
|
||||||
|
data-testid="sidebar_header_name"
|
||||||
|
>
|
||||||
|
{parseString(focusedRow?.display_name || focusedRow?.name, [
|
||||||
|
"space_case",
|
||||||
|
])}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className="text-mmd text-muted-foreground"
|
||||||
|
data-testid="sidebar_header_description"
|
||||||
|
>
|
||||||
|
{focusedRow?.display_description ?? focusedRow?.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</SidebarHeader>
|
||||||
|
{!isAction && <Separator />}
|
||||||
|
<SidebarContent className="flex flex-1 flex-col gap-0 overflow-visible px-2">
|
||||||
|
{focusedRow && (
|
||||||
|
<div className="flex h-full flex-col gap-4">
|
||||||
|
<SidebarGroup className="flex-1">
|
||||||
|
<SidebarGroupContent className="h-full pb-4">
|
||||||
|
<div className="flex h-full flex-col gap-4">
|
||||||
|
{actionArgs.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<h3 className="mt-2 text-base font-medium">
|
||||||
|
Parameters
|
||||||
|
</h3>
|
||||||
|
<p className="text-mmd text-muted-foreground">
|
||||||
|
Manage inputs for this action
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{actionArgs.map((field) => (
|
||||||
|
<div key={field.name} className="flex flex-col gap-2">
|
||||||
|
<label className="flex text-sm font-medium">
|
||||||
|
{field.display_name}
|
||||||
|
{field.description && (
|
||||||
|
<ShadTooltip content={field.description}>
|
||||||
|
<div className="flex items-center text-sm font-medium hover:cursor-help">
|
||||||
|
<ForwardedIconComponent
|
||||||
|
name="info"
|
||||||
|
className="ml-1.5 h-4 w-4 text-muted-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="sidebar-desc-input"
|
||||||
|
disabled
|
||||||
|
placeholder="Input controlled by the agent"
|
||||||
|
onChange={(e) => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SidebarContent>
|
||||||
|
</Sidebar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
src/frontend/src/modals/toolsModal/index.tsx
Normal file
89
src/frontend/src/modals/toolsModal/index.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||||
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
|
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
|
import { APITemplateType } from "@/types/api";
|
||||||
|
import { AgGridReact } from "ag-grid-react";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { ForwardedRef, forwardRef, useState } from "react";
|
||||||
|
import BaseModal from "../baseModal";
|
||||||
|
import ToolsTable from "./components/toolsTable";
|
||||||
|
|
||||||
|
interface ToolsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
description: string;
|
||||||
|
rows: {
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
description: string;
|
||||||
|
status: boolean;
|
||||||
|
}[];
|
||||||
|
handleOnNewValue: handleOnNewValueType;
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
isAction?: boolean;
|
||||||
|
template?: APITemplateType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolsModal = forwardRef<AgGridReact, ToolsModalProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
description,
|
||||||
|
rows,
|
||||||
|
handleOnNewValue,
|
||||||
|
template,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
open,
|
||||||
|
isAction = false,
|
||||||
|
setOpen,
|
||||||
|
...props
|
||||||
|
}: ToolsModalProps,
|
||||||
|
ref: ForwardedRef<AgGridReact>,
|
||||||
|
) => {
|
||||||
|
const handleSetOpen = (newOpen: boolean) => {
|
||||||
|
if (setOpen) {
|
||||||
|
setOpen(newOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [data, setData] = useState<any[]>(cloneDeep(rows));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseModal
|
||||||
|
open={open}
|
||||||
|
size="templates"
|
||||||
|
className="flex max-h-[50vh] gap-0 p-0"
|
||||||
|
setOpen={(newOpen) => {
|
||||||
|
handleSetOpen(newOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BaseModal.Header>
|
||||||
|
<div className="flex w-full flex-row items-center border-b border-border px-4 py-3">
|
||||||
|
{icon && (
|
||||||
|
<ForwardedIconComponent name={icon} className="mr-2 h-6 w-6" />
|
||||||
|
)}
|
||||||
|
<div>{title}</div>
|
||||||
|
</div>
|
||||||
|
</BaseModal.Header>
|
||||||
|
<BaseModal.Content overflowHidden className="flex flex-col p-0">
|
||||||
|
<div className="flex h-full">
|
||||||
|
<SidebarProvider width="20rem" defaultOpen={false}>
|
||||||
|
<ToolsTable
|
||||||
|
rows={rows}
|
||||||
|
isAction={isAction}
|
||||||
|
template={template}
|
||||||
|
data={data}
|
||||||
|
setData={setData}
|
||||||
|
open={open}
|
||||||
|
handleOnNewValue={handleOnNewValue}
|
||||||
|
/>
|
||||||
|
</SidebarProvider>
|
||||||
|
</div>
|
||||||
|
</BaseModal.Content>
|
||||||
|
</BaseModal>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ToolsModal;
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
--ag-panel-background-color: hsl(var(--accent)) !important;
|
--ag-panel-background-color: hsl(var(--accent)) !important;
|
||||||
--ag-row-hover-color: hsl(var(--accent)) !important;
|
--ag-row-hover-color: hsl(var(--accent)) !important;
|
||||||
--ag-header-height: 2.5rem !important;
|
--ag-header-height: 2.5rem !important;
|
||||||
|
--ag-checkbox-checked-color: hsl(var(--primary)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-theme-shadcn .ag-paging-panel {
|
.ag-theme-shadcn .ag-paging-panel {
|
||||||
|
|
@ -33,7 +34,6 @@
|
||||||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||||
align-items: self-start;
|
align-items: self-start;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||||
|
|
@ -99,3 +99,46 @@
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-root-wrapper {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.ag-tool-mode .ag-row {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-header {
|
||||||
|
margin-bottom: 0rem !important;
|
||||||
|
border-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-header-cell {
|
||||||
|
padding: 0rem 1rem !important;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-ltr .ag-header-select-all {
|
||||||
|
margin-right: var(--ag-cell-widget-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-paging-panel {
|
||||||
|
border-top: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-cell-focus:not(.ag-cell-inline-editing) {
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-cell {
|
||||||
|
padding: 0rem 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-row-focus {
|
||||||
|
background-color: hsl(var(--accent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-tool-mode .ag-row-selected:not(.ag-row-focus)::before {
|
||||||
|
background-color: hsl(var(--background)) !important;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,7 @@ export type FieldParserType =
|
||||||
| "uppercase"
|
| "uppercase"
|
||||||
| "no_blank"
|
| "no_blank"
|
||||||
| "valid_csv"
|
| "valid_csv"
|
||||||
|
| "space_case"
|
||||||
| "commands";
|
| "commands";
|
||||||
|
|
||||||
export type TableOptionsTypeAPI = {
|
export type TableOptionsTypeAPI = {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
import { ReactFlowJsonObject } from "@xyflow/react";
|
import { ReactFlowJsonObject } from "@xyflow/react";
|
||||||
import { ReactElement, ReactNode } from "react";
|
import { ReactElement, ReactNode } from "react";
|
||||||
import { InputOutput } from "../../constants/enums";
|
import { InputOutput } from "../../constants/enums";
|
||||||
|
|
@ -676,6 +677,21 @@ export type textModalPropsType = {
|
||||||
setOpen?: (open: boolean) => void;
|
setOpen?: (open: boolean) => void;
|
||||||
onCloseModal?: () => void;
|
onCloseModal?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ToolsModalProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
description: string;
|
||||||
|
rows: {
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
description: string;
|
||||||
|
status: boolean;
|
||||||
|
}[];
|
||||||
|
handleOnNewValue: handleOnNewValueType;
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
export type queryModalPropsType = {
|
export type queryModalPropsType = {
|
||||||
setValue: (value: string) => void;
|
setValue: (value: string) => void;
|
||||||
value: string;
|
value: string;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
||||||
import { FieldParserType } from "../types/api";
|
import { FieldParserType } from "../types/api";
|
||||||
|
|
||||||
function toSnakeCase(str: string): string {
|
function toSnakeCase(str: string): string {
|
||||||
return str.trim().replace(/\s+/g, "_");
|
return str.trim().replace(/[-\s]+/g, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toCamelCase(str: string): string {
|
function toCamelCase(str: string): string {
|
||||||
|
|
@ -35,6 +35,15 @@ function toUpperCase(str: string): string {
|
||||||
return str?.toUpperCase();
|
return str?.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toSpaceCase(str: string): string {
|
||||||
|
return str
|
||||||
|
.trim()
|
||||||
|
.replace(/[_\s-]+/g, " ")
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
function noBlank(str: string): string {
|
function noBlank(str: string): string {
|
||||||
const trim = str.trim();
|
const trim = str.trim();
|
||||||
if (trim === "") {
|
if (trim === "") {
|
||||||
|
|
@ -101,6 +110,9 @@ export function parseString(
|
||||||
case "no_blank":
|
case "no_blank":
|
||||||
result = noBlank(result);
|
result = noBlank(result);
|
||||||
break;
|
break;
|
||||||
|
case "space_case":
|
||||||
|
result = toSpaceCase(result);
|
||||||
|
break;
|
||||||
case "valid_csv":
|
case "valid_csv":
|
||||||
result = validCsv(result);
|
result = validCsv(result);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -35,26 +35,46 @@ test(
|
||||||
|
|
||||||
await page.getByTestId("tool-mode-button").click();
|
await page.getByTestId("tool-mode-button").click();
|
||||||
|
|
||||||
await page.locator('[data-testid="icon-Hammer"]').nth(1).waitFor({
|
await page.locator('[data-testid="icon-Hammer"]').nth(0).waitFor({
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
state: "visible",
|
state: "visible",
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId("icon-Hammer").nth(1).click();
|
await page.waitForSelector("text=actions", { timeout: 30000 });
|
||||||
|
|
||||||
await page.waitForSelector("text=edit tools", { timeout: 30000 });
|
await page.getByTestId("button_open_actions").click();
|
||||||
|
|
||||||
|
await page.waitForSelector("text=API Request", { timeout: 30000 });
|
||||||
|
|
||||||
const rowsCount = await page.getByRole("gridcell").count();
|
const rowsCount = await page.getByRole("gridcell").count();
|
||||||
|
|
||||||
expect(rowsCount).toBeGreaterThan(3);
|
expect(rowsCount).toBeGreaterThan(3);
|
||||||
|
|
||||||
expect(await page.getByRole("switch").nth(0).isChecked()).toBe(true);
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(0).isChecked(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
await page.getByRole("switch").nth(0).click();
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
expect(await page.getByRole("switch").nth(0).isChecked()).toBe(false);
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
await page.getByText("Save").last().click();
|
await page.locator('input[data-ref="eInput"]').nth(0).click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
await page.getByText("Close").last().click();
|
||||||
|
|
||||||
await page.waitForSelector(
|
await page.waitForSelector(
|
||||||
'[data-testid="generic-node-title-arrangement"]',
|
'[data-testid="generic-node-title-arrangement"]',
|
||||||
|
|
@ -63,14 +83,54 @@ test(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
await page.waitForSelector('[data-testid="div-tools_tools_metadata"]', {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
await page.getByTestId("icon-Hammer").nth(1).click();
|
expect(
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="div-tools_tools_metadata"]')
|
||||||
|
.isVisible(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
await page.waitForSelector("text=edit tools", { timeout: 30000 });
|
await page.getByTestId("div-tools_tools_metadata").click();
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
expect(await page.getByRole("switch").nth(0).isChecked()).toBe(false);
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(4).isChecked(),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(3).click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.locator('input[data-ref="eInput"]').nth(3).isChecked(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
await page.getByRole("gridcell").nth(0).click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.locator('[data-testid="sidebar_header_name"]').isVisible(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="sidebar_header_description"]')
|
||||||
|
.isVisible(),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await page.getByText("Close").last().click();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await page.locator('[data-testid="tool_make_requests"]').isVisible(),
|
||||||
|
).toBe(true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue