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
4
src/frontend/package-lock.json
generated
4
src/frontend/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "langflow",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "langflow",
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"dependencies": {
|
||||
"@chakra-ui/number-input": "^2.1.2",
|
||||
"@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 ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent";
|
||||
import { InputProps } from "@/components/core/parameterRenderComponent/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DialogHeader } from "@/components/ui/dialog";
|
||||
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 ListItem from "./ListItem";
|
||||
|
||||
// Update interface with better types
|
||||
interface ListSelectionComponentProps {
|
||||
|
|
@ -20,118 +19,6 @@ interface ListSelectionComponentProps {
|
|||
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 = ({
|
||||
open,
|
||||
onClose,
|
||||
|
|
@ -286,7 +173,7 @@ const ListSelectionComponent = ({
|
|||
name={nodeClass?.icon || "unknown"}
|
||||
className="h-[18px] w-[18px] text-muted-foreground"
|
||||
/>
|
||||
<div className="text-mmd font-semibold">
|
||||
<div className="text-[13px] font-semibold">
|
||||
{nodeClass?.display_name}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ const RenderInputParameters = ({
|
|||
(templateField: string, idx) => {
|
||||
const template = data.node?.template[templateField];
|
||||
|
||||
if (!template?.show || template?.advanced) {
|
||||
if (
|
||||
!template?.show ||
|
||||
template?.advanced ||
|
||||
(template?.tool_mode && isToolMode)
|
||||
) {
|
||||
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
|
||||
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 && (
|
||||
<ForwardedIconComponent
|
||||
name="grid-horizontal"
|
||||
className="h-5 w-5 fill-gray-300 text-gray-300"
|
||||
name="GridHorizontal"
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex w-full items-center gap-x-2">
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"truncate text-muted-foreground",
|
||||
"truncate text-xxs font-medium text-muted-foreground",
|
||||
limit === 1 ? "max-w-56 pl-2" : "max-w-48",
|
||||
)}
|
||||
>
|
||||
|
|
@ -61,22 +61,16 @@ const SortableListItem = memo(
|
|||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant={limit !== 1 ? "outline" : "ghost"}
|
||||
variant={"ghost"}
|
||||
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
|
||||
? "group pr-1 opacity-100"
|
||||
: "hover:border hover:border-destructive hover:bg-transparent hover:opacity-100",
|
||||
? "group pr-1 opacity-100 hover:text-foreground"
|
||||
: "hover:text-destructive group-hover:opacity-100",
|
||||
)}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="x"
|
||||
className={cn(
|
||||
"h-6 w-6 text-red-500",
|
||||
limit === 1 && "text-gray-500 group-hover:text-input",
|
||||
)}
|
||||
/>
|
||||
<ForwardedIconComponent name="x" className={cn("h-6 w-6")} />
|
||||
</Button>
|
||||
</li>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/
|
|||
import { TEXT_FIELD_TYPES } from "@/constants/constants";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { useMemo } from "react";
|
||||
import ToolsComponent from "./components/ToolsComponent";
|
||||
import ConnectionComponent from "./components/connectionComponent";
|
||||
import DictComponent from "./components/dictComponent";
|
||||
import { EmptyParameterComponent } from "./components/emptyParameterComponent";
|
||||
|
|
@ -203,6 +204,16 @@ export function ParameterRenderComponent({
|
|||
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":
|
||||
return (
|
||||
<SliderComponent
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
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 { ColumnField } from "@/types/utils/functions";
|
||||
|
||||
|
|
@ -40,6 +45,15 @@ export type TableComponentType = {
|
|||
table_icon?: string;
|
||||
};
|
||||
|
||||
export type ToolsComponentType = {
|
||||
description: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
button_description?: string;
|
||||
isAction?: boolean;
|
||||
template?: APITemplateType;
|
||||
};
|
||||
|
||||
export type FloatComponentType = {
|
||||
rangeSpec: RangeSpecType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -659,6 +659,7 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
"connect",
|
||||
"auth",
|
||||
"query",
|
||||
"tools",
|
||||
]);
|
||||
|
||||
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-row-hover-color: hsl(var(--accent)) !important;
|
||||
--ag-header-height: 2.5rem !important;
|
||||
--ag-checkbox-checked-color: hsl(var(--primary)) !important;
|
||||
}
|
||||
|
||||
.ag-theme-shadcn .ag-paging-panel {
|
||||
|
|
@ -33,7 +34,6 @@
|
|||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||
align-items: self-start;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.ag-cell-wrapper > *:not(.ag-cell-value):not(.ag-group-value) {
|
||||
|
|
@ -99,3 +99,46 @@
|
|||
box-shadow: 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"
|
||||
| "no_blank"
|
||||
| "valid_csv"
|
||||
| "space_case"
|
||||
| "commands";
|
||||
|
||||
export type TableOptionsTypeAPI = {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { ReactFlowJsonObject } from "@xyflow/react";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { InputOutput } from "../../constants/enums";
|
||||
|
|
@ -676,6 +677,21 @@ export type textModalPropsType = {
|
|||
setOpen?: (open: boolean) => 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 = {
|
||||
setValue: (value: string) => void;
|
||||
value: string;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import React from "react";
|
|||
import { FieldParserType } from "../types/api";
|
||||
|
||||
function toSnakeCase(str: string): string {
|
||||
return str.trim().replace(/\s+/g, "_");
|
||||
return str.trim().replace(/[-\s]+/g, "_");
|
||||
}
|
||||
|
||||
function toCamelCase(str: string): string {
|
||||
|
|
@ -35,6 +35,15 @@ function toUpperCase(str: string): string {
|
|||
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 {
|
||||
const trim = str.trim();
|
||||
if (trim === "") {
|
||||
|
|
@ -101,6 +110,9 @@ export function parseString(
|
|||
case "no_blank":
|
||||
result = noBlank(result);
|
||||
break;
|
||||
case "space_case":
|
||||
result = toSpaceCase(result);
|
||||
break;
|
||||
case "valid_csv":
|
||||
result = validCsv(result);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -35,26 +35,46 @@ test(
|
|||
|
||||
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,
|
||||
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();
|
||||
|
||||
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(
|
||||
'[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);
|
||||
|
||||
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