feat: enhance URL component with improved description and render parameters (#5623)
* [LFOSS-74]: new input list UI * [LFOSS-93]: node colors * 📝 (Graph Vector Store RAG.json): Update description of ChatInput class to improve clarity and consistency 📝 (Graph Vector Store RAG.json): Update description of URLComponent class to improve clarity and consistency 📝 (select-items.tsx): Change noteDataType import to NoteDataType for consistency and clarity 📝 (dropdown-menu.tsx): Add RenderIcons component to display keyboard shortcuts for actions 📝 (index.tsx): Change parameter type from React.MouseEvent to KeyboardEvent for removeInput and handleDuplicateInput functions 📝 (use-overlap-shortcuts.tsx): Create custom hook to handle keyboard shortcuts with support for multiple key variations and modifiers * 📝 (backend): Remove unnecessary metadata for URLComponent in multiple files 📝 (frontend): Refactor input components to use listAddLabel instead of metadata in multiple files * ♻️ (NodeInputField/index.tsx): Remove unused 'metadata' variable to clean up the code and improve readability * [autofix.ci] apply automated fixes * merge fix * [autofix.ci] apply automated fixes * fix tests * 🐛 (generalBugs-shard-5.spec.ts): fix incorrect comments numbering connections 💡 (generalBugs-shard-5.spec.ts): add clarifying comments for connection steps in the test case * 🐛 (generalBugs-shard-5.spec.ts): fix filling delimiter in popover-anchor-input to resolve UI bug 📝 (generalBugs-shard-5.spec.ts): update test cases to improve test coverage and accuracy * ✨ (NodeOutputfield/index.tsx): Add top margin to improve spacing of NodeOutputField component 🔧 (generalBugs-shard-5.spec.ts): Remove commented out code related to input filling and waiting for visibility to clean up the test file and improve readability * 🔧 (index.tsx): increase padding-right from 6 to 10 in input-edit-node class to improve spacing for better user experience --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
febae4b589
commit
e2ff7b314b
19 changed files with 529 additions and 201 deletions
|
|
@ -12,7 +12,7 @@ from langflow.schema.message import Message
|
|||
|
||||
class URLComponent(Component):
|
||||
display_name = "URL"
|
||||
description = "Fetch content from one or more URLs."
|
||||
description = "Load and retrive data from specified URLs."
|
||||
icon = "layout-template"
|
||||
name = "URL"
|
||||
|
||||
|
|
@ -23,6 +23,8 @@ class URLComponent(Component):
|
|||
info="Enter one or more URLs, by clicking the '+' button.",
|
||||
is_list=True,
|
||||
tool_mode=True,
|
||||
placeholder="Enter a URL...",
|
||||
list_add_label="Add URL",
|
||||
),
|
||||
DropdownInput(
|
||||
name="format",
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@
|
|||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Load and retrive data from specified URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
},
|
||||
"format": {
|
||||
"_input_type": "DropdownInput",
|
||||
|
|
|
|||
|
|
@ -1668,7 +1668,7 @@
|
|||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Load and retrive data from specified URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
},
|
||||
"format": {
|
||||
"_input_type": "DropdownInput",
|
||||
|
|
@ -1814,7 +1814,7 @@
|
|||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Load and retrive data from specified URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
},
|
||||
"format": {
|
||||
"_input_type": "DropdownInput",
|
||||
|
|
@ -1966,7 +1966,7 @@
|
|||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Load and retrive data from specified URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
},
|
||||
"format": {
|
||||
"_input_type": "DropdownInput",
|
||||
|
|
|
|||
|
|
@ -2638,7 +2638,7 @@
|
|||
"show": true,
|
||||
"title_case": false,
|
||||
"type": "code",
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Fetch content from one or more URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
"value": "import re\n\nfrom langchain_community.document_loaders import AsyncHtmlLoader, WebBaseLoader\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_text\nfrom langflow.io import DropdownInput, MessageTextInput, Output\nfrom langflow.schema import Data\nfrom langflow.schema.dataframe import DataFrame\nfrom langflow.schema.message import Message\n\n\nclass URLComponent(Component):\n display_name = \"URL\"\n description = \"Load and retrive data from specified URLs.\"\n icon = \"layout-template\"\n name = \"URL\"\n\n inputs = [\n MessageTextInput(\n name=\"urls\",\n display_name=\"URLs\",\n info=\"Enter one or more URLs, by clicking the '+' button.\",\n is_list=True,\n tool_mode=True,\n placeholder=\"Enter a URL...\",\n list_add_label=\"Add URL\",\n ),\n DropdownInput(\n name=\"format\",\n display_name=\"Output Format\",\n info=\"Output Format. Use 'Text' to extract the text from the HTML or 'Raw HTML' for the raw HTML content.\",\n options=[\"Text\", \"Raw HTML\"],\n value=\"Text\",\n ),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def ensure_url(self, string: str) -> str:\n \"\"\"Ensures the given string is a URL by adding 'http://' if it doesn't start with 'http://' or 'https://'.\n\n Raises an error if the string is not a valid URL.\n\n Parameters:\n string (str): The string to be checked and possibly modified.\n\n Returns:\n str: The modified string that is ensured to be a URL.\n\n Raises:\n ValueError: If the string is not a valid URL.\n \"\"\"\n if not string.startswith((\"http://\", \"https://\")):\n string = \"http://\" + string\n\n # Basic URL validation regex\n url_regex = re.compile(\n r\"^(https?:\\/\\/)?\" # optional protocol\n r\"(www\\.)?\" # optional www\n r\"([a-zA-Z0-9.-]+)\" # domain\n r\"(\\.[a-zA-Z]{2,})?\" # top-level domain\n r\"(:\\d+)?\" # optional port\n r\"(\\/[^\\s]*)?$\", # optional path\n re.IGNORECASE,\n )\n\n if not url_regex.match(string):\n msg = f\"Invalid URL: {string}\"\n raise ValueError(msg)\n\n return string\n\n def fetch_content(self) -> list[Data]:\n urls = [self.ensure_url(url.strip()) for url in self.urls if url.strip()]\n if self.format == \"Raw HTML\":\n loader = AsyncHtmlLoader(web_path=urls, encoding=\"utf-8\")\n else:\n loader = WebBaseLoader(web_paths=urls, encoding=\"utf-8\")\n docs = loader.load()\n data = [Data(text=doc.page_content, **doc.metadata) for doc in docs]\n self.status = data\n return data\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n\n result_string = data_to_text(\"{text}\", data)\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n return DataFrame(self.fetch_content())\n"
|
||||
},
|
||||
"format": {
|
||||
"_input_type": "DropdownInput",
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ class MetadataTraceMixin(BaseModel):
|
|||
# Mixin for input fields that can be listable
|
||||
class ListableInputMixin(BaseModel):
|
||||
is_list: bool = Field(default=False, alias="list")
|
||||
list_add_label: str | None = Field(default="Add More")
|
||||
|
||||
|
||||
# Specific mixin for fields needing database interaction
|
||||
|
|
|
|||
|
|
@ -187,7 +187,11 @@ export default function NodeInputField({
|
|||
handleNodeClass={handleNodeClass}
|
||||
nodeClass={data.node!}
|
||||
disabled={disabled}
|
||||
placeholder={isToolMode ? DEFAULT_TOOLSET_PLACEHOLDER : undefined}
|
||||
placeholder={
|
||||
isToolMode
|
||||
? DEFAULT_TOOLSET_PLACEHOLDER
|
||||
: data.node?.template[name].placeholder
|
||||
}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { ForwardedIconComponent } from "@/components/common/genericIconComponent
|
|||
import { SelectItem } from "@/components/ui/select";
|
||||
import { SelectContentWithoutPortal } from "@/components/ui/select-custom";
|
||||
import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
|
||||
import { noteDataType } from "@/types/flow";
|
||||
import { NoteDataType } from "@/types/flow";
|
||||
|
||||
import { memo } from "react";
|
||||
|
||||
export const SelectItems = memo(
|
||||
({ shortcuts, data }: { shortcuts: any[]; data: noteDataType }) => (
|
||||
({ shortcuts, data }: { shortcuts: any[]; data: NoteDataType }) => (
|
||||
<SelectContentWithoutPortal>
|
||||
<SelectItem value="duplicate">
|
||||
<ToolbarSelectItem
|
||||
|
|
|
|||
|
|
@ -7,39 +7,26 @@ import { getTestId } from "../helpers/get-test-id";
|
|||
|
||||
export const ButtonInputList = ({
|
||||
index,
|
||||
value,
|
||||
addNewInput,
|
||||
removeInput,
|
||||
disabled,
|
||||
editNode,
|
||||
addIcon,
|
||||
componentName,
|
||||
}: {
|
||||
index: number;
|
||||
value: string[];
|
||||
addNewInput: (e) => void;
|
||||
removeInput: (index: number, e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
disabled: boolean;
|
||||
editNode: boolean;
|
||||
addIcon: boolean;
|
||||
componentName: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={
|
||||
(index === 0 && value.length <= 1) || addIcon
|
||||
? addNewInput
|
||||
: (e) => removeInput(index, e)
|
||||
}
|
||||
onClick={addNewInput}
|
||||
className={cn(
|
||||
"hit-area-icon group flex items-center justify-center text-center",
|
||||
"hit-area-icon group absolute flex -translate-y-8 translate-x-[15.5rem] items-center justify-center bg-background text-center hover:bg-muted",
|
||||
disabled
|
||||
? "pointer-events-none bg-background hover:bg-background"
|
||||
: "",
|
||||
(index === 0 && value.length <= 1) || addIcon
|
||||
? "bg-background hover:bg-muted"
|
||||
: "hover:bg-smooth-red",
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
|
|
@ -49,24 +36,15 @@ export const ButtonInputList = ({
|
|||
"hit-area-icon flex items-center justify-center",
|
||||
getButtonClassName(disabled),
|
||||
)}
|
||||
data-testid={getTestId(
|
||||
(index === 0 && value.length <= 1) || addIcon ? "plus" : "minus",
|
||||
index,
|
||||
editNode,
|
||||
componentName,
|
||||
)}
|
||||
data-testid={getTestId("plus", index, editNode, componentName)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<IconComponent
|
||||
name={
|
||||
(index === 0 && value.length <= 1) || addIcon ? "Plus" : "Trash2"
|
||||
}
|
||||
name="Plus"
|
||||
className={cn(
|
||||
"icon-size justify-self-center text-muted-foreground",
|
||||
!disabled && "hover:cursor-pointer hover:text-foreground",
|
||||
(index === 0 && value.length <= 1) || addIcon
|
||||
? "group-hover:text-foreground"
|
||||
: "group-hover:text-destructive",
|
||||
"group-hover:text-foreground",
|
||||
)}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,147 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import RenderIcons from "@/components/common/renderIconComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useKeyboardShortcut } from "@/hooks/use-overlap-shortcuts";
|
||||
import { useShortcutsStore } from "@/stores/shortcuts";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export const DropdownMenuInputList = ({
|
||||
index,
|
||||
dropdownOpen,
|
||||
setDropdownOpen,
|
||||
editNode,
|
||||
handleDuplicateInput,
|
||||
removeInput,
|
||||
}: {
|
||||
index: number;
|
||||
dropdownOpen: number | null;
|
||||
setDropdownOpen: (open: number) => void;
|
||||
editNode: boolean;
|
||||
handleDuplicateInput: (
|
||||
index: number,
|
||||
e: React.MouseEvent<HTMLDivElement> | KeyboardEvent,
|
||||
) => void;
|
||||
removeInput: (
|
||||
index: number,
|
||||
e: React.MouseEvent<HTMLDivElement> | KeyboardEvent,
|
||||
) => void;
|
||||
}) => {
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
|
||||
const shortcutKeys = {
|
||||
duplicate:
|
||||
shortcuts.find((obj) => obj.name === "Duplicate")?.shortcut || "",
|
||||
delete: shortcuts.find((obj) => obj.name === "Delete")?.shortcut || "",
|
||||
};
|
||||
|
||||
const handleShortcut = useCallback(
|
||||
(shortcutName: string, event: KeyboardEvent) => {
|
||||
if (shortcutName === "duplicate") {
|
||||
handleDuplicateInput(index, event);
|
||||
} else if (shortcutName === "delete") {
|
||||
removeInput(index, event);
|
||||
}
|
||||
setDropdownOpen(-1);
|
||||
},
|
||||
[index, handleDuplicateInput, removeInput, setDropdownOpen],
|
||||
);
|
||||
|
||||
useKeyboardShortcut({
|
||||
shortcutKeys,
|
||||
isEnabled: dropdownOpen === index,
|
||||
onShortcut: handleShortcut,
|
||||
preventDefault: true,
|
||||
stopPropagation: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenu
|
||||
open={dropdownOpen === index}
|
||||
onOpenChange={(open) => setDropdownOpen(open ? index : -1)}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
tabIndex={index}
|
||||
className="absolute translate-x-60 bg-background transition-opacity peer-focus:opacity-0"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
data-testid={`input-list-dropdown-menu-${index}-${editNode ? "edit" : "view"}`}
|
||||
size={editNode ? "iconSm" : "iconMd"}
|
||||
className={cn("group", editNode ? "ml-4" : "")}
|
||||
autoFocus={false}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Ellipsis"
|
||||
aria-hidden="true"
|
||||
className="h-5 w-5 text-muted-foreground group-hover:text-foreground"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[185px] translate-x-20" side="bottom">
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
handleDuplicateInput(index, e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
data-testid={`input-list-dropdown-menu-${index}-duplicate`}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="CopyPlus"
|
||||
aria-hidden="true"
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
<span>Duplicate</span>
|
||||
|
||||
<div className="flex grow content-end justify-end self-center text-[12px]">
|
||||
<span
|
||||
className={`flex content-end items-center rounded-sm bg-muted px-1.5 py-[0.1em] text-muted-foreground`}
|
||||
>
|
||||
<RenderIcons
|
||||
filteredShortcut={shortcuts
|
||||
.find((obj) => obj.name === "Duplicate")
|
||||
?.shortcut!?.split("+")}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
removeInput(index, e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className="cursor-pointer text-destructive"
|
||||
data-testid={`input-list-dropdown-menu-${index}-delete`}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Trash2"
|
||||
aria-hidden="true"
|
||||
className="mr-2 h-4 w-4"
|
||||
/>
|
||||
<span>Delete</span>
|
||||
|
||||
<div className="flex grow content-end justify-end self-center text-[12px]">
|
||||
<span
|
||||
className={`flex content-end items-center rounded-sm px-1.5 py-[0.1em] text-muted-foreground`}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Delete"
|
||||
className="h-4 w-4 stroke-2 text-red-400"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
|
||||
import _ from "lodash";
|
||||
import { classNames, cn } from "../../../../../utils/utils";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "../../../../ui/input";
|
||||
import { ButtonInputList } from "./components/button-input-list";
|
||||
import { DropdownMenuInputList } from "./components/dropdown-menu";
|
||||
|
||||
import { GRADIENT_CLASS } from "@/constants/constants";
|
||||
import { cn } from "../../../../../utils/utils";
|
||||
import { getPlaceholder } from "../../helpers/get-placeholder-disabled";
|
||||
import { InputListComponentType, InputProps } from "../../types";
|
||||
import { ButtonInputList } from "./components/button-input-list";
|
||||
|
||||
export default function InputListComponent({
|
||||
value = [""],
|
||||
|
|
@ -15,89 +19,140 @@ export default function InputListComponent({
|
|||
componentName,
|
||||
id,
|
||||
placeholder,
|
||||
listAddLabel,
|
||||
}: InputProps<string[], InputListComponentType>): JSX.Element {
|
||||
const [dropdownOpen, setDropdownOpen] = useState<number | null>(null);
|
||||
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled && value.length > 0 && value[0] !== "") {
|
||||
handleOnNewValue({ value: [""] }, { skipSnapshot: true });
|
||||
}
|
||||
}, [disabled]);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
}, [disabled, handleOnNewValue, value]);
|
||||
|
||||
// @TODO Recursive Character Text Splitter - the value might be in string format, whereas the InputListComponent specifically requires an array format. To ensure smooth operation and prevent potential errors, it's crucial that we handle the conversion from a string to an array with the string as its element.
|
||||
if (typeof value === "string") {
|
||||
value = [value];
|
||||
}
|
||||
|
||||
if (!value?.length) value = [""];
|
||||
|
||||
const handleInputChange = (index, newValue) => {
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList[index] = newValue;
|
||||
handleOnNewValue({ value: newInputList });
|
||||
};
|
||||
const handleInputChange = useCallback(
|
||||
(index: number, newValue: string) => {
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList[index] = newValue;
|
||||
handleOnNewValue({ value: newInputList });
|
||||
},
|
||||
[value, handleOnNewValue],
|
||||
);
|
||||
|
||||
const addNewInput = (e) => {
|
||||
e.preventDefault();
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList.push("");
|
||||
handleOnNewValue({ value: newInputList });
|
||||
};
|
||||
const addNewInput = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList.push("");
|
||||
handleOnNewValue({ value: newInputList });
|
||||
},
|
||||
[value, handleOnNewValue],
|
||||
);
|
||||
|
||||
const removeInput = (index, e) => {
|
||||
e.preventDefault();
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList.splice(index, 1);
|
||||
handleOnNewValue({ value: newInputList });
|
||||
};
|
||||
const removeInput = useCallback(
|
||||
(index: number, e: React.MouseEvent | KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList.splice(index, 1);
|
||||
handleOnNewValue({ value: newInputList });
|
||||
setDropdownOpen(null);
|
||||
},
|
||||
[value, handleOnNewValue],
|
||||
);
|
||||
|
||||
const handleDuplicateInput = useCallback(
|
||||
(index: number, e: React.MouseEvent | KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const newInputList = _.cloneDeep(value);
|
||||
newInputList.splice(index, 0, newInputList[index]);
|
||||
handleOnNewValue({ value: newInputList });
|
||||
setDropdownOpen(null);
|
||||
},
|
||||
[value, handleOnNewValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
value.length > 1 && editNode ? "my-1" : "",
|
||||
"flex w-full flex-col gap-3",
|
||||
<div className={cn("w-full", editNode && "max-h-52")}>
|
||||
{!editNode && !disabled && (
|
||||
<ButtonInputList
|
||||
index={0}
|
||||
addNewInput={addNewInput}
|
||||
disabled={disabled}
|
||||
editNode={editNode}
|
||||
componentName={componentName || ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex w-full flex-col gap-3">
|
||||
{value.map((singleValue, index) => (
|
||||
<div key={index} className="relative flex w-full items-center gap-2">
|
||||
<div className="group flex flex-1 items-center rounded-md bg-background">
|
||||
<Input
|
||||
ref={index === 0 ? inputRef : null}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
value={singleValue}
|
||||
className={cn(
|
||||
editNode ? "input-edit-node pr-6" : "pr-10",
|
||||
disabled ? "disabled-state" : "",
|
||||
focusedIndex === index
|
||||
? "text-primary"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
placeholder={getPlaceholder(disabled, placeholder)}
|
||||
onChange={(event) =>
|
||||
handleInputChange(index, event.target.value)
|
||||
}
|
||||
data-testid={`${id}_${index}`}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
onBlur={() => setFocusedIndex(null)}
|
||||
/>
|
||||
|
||||
{focusedIndex !== index && !disabled && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-6 w-16",
|
||||
editNode ? "translate-x-[12rem]" : "translate-x-[11.1rem]",
|
||||
)}
|
||||
style={{
|
||||
pointerEvents: "none",
|
||||
background: GRADIENT_CLASS,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!disabled && (
|
||||
<DropdownMenuInputList
|
||||
index={index}
|
||||
dropdownOpen={dropdownOpen!}
|
||||
setDropdownOpen={setDropdownOpen}
|
||||
editNode={editNode}
|
||||
handleDuplicateInput={handleDuplicateInput}
|
||||
removeInput={removeInput}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!disabled && (
|
||||
<Button
|
||||
unstyled
|
||||
onClick={addNewInput}
|
||||
className="btn-add-input-list"
|
||||
data-testid={`input-list-add-more-${editNode ? "edit" : "view"}`}
|
||||
>
|
||||
<span className="mr-2 text-lg">+</span> {listAddLabel ?? "Add More"}
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{value.map((singleValue, index) => (
|
||||
<div key={index} className="flex w-full items-center gap-3">
|
||||
<Input
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
value={singleValue}
|
||||
ref={index === 0 ? inputRef : null}
|
||||
className={cn(
|
||||
editNode ? "input-edit-node" : "",
|
||||
disabled ? "disabled-state" : "",
|
||||
"peer relative",
|
||||
index === 0 && value.length > 1 && "w-3/4 pr-7 focus:pr-3",
|
||||
)}
|
||||
placeholder={getPlaceholder(disabled, placeholder)}
|
||||
onChange={(event) => handleInputChange(index, event.target.value)}
|
||||
data-testid={`${id}_${index}`}
|
||||
/>
|
||||
{index === 0 && value.length > 1 && (
|
||||
<ButtonInputList
|
||||
index={index}
|
||||
value={value}
|
||||
addNewInput={addNewInput}
|
||||
removeInput={removeInput}
|
||||
disabled={disabled}
|
||||
editNode={editNode}
|
||||
addIcon
|
||||
componentName={componentName || ""}
|
||||
/>
|
||||
)}
|
||||
<ButtonInputList
|
||||
index={index}
|
||||
value={value}
|
||||
addNewInput={addNewInput}
|
||||
removeInput={removeInput}
|
||||
disabled={disabled}
|
||||
editNode={editNode}
|
||||
addIcon={false}
|
||||
componentName={componentName || ""}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export function ParameterRenderComponent({
|
|||
placeholder,
|
||||
isToolMode,
|
||||
};
|
||||
|
||||
if (TEXT_FIELD_TYPES.includes(templateData.type ?? "")) {
|
||||
if (templateData.list) {
|
||||
if (!templateData.options) {
|
||||
|
|
@ -73,6 +74,7 @@ export function ParameterRenderComponent({
|
|||
{...baseInputProps}
|
||||
componentName={name}
|
||||
id={`inputlist_${id}`}
|
||||
listAddLabel={templateData?.list_add_label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export type BaseInputProps<valueType = any> = {
|
|||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
isToolMode?: boolean;
|
||||
metadata?: any;
|
||||
};
|
||||
|
||||
// Generic type for composing input props
|
||||
|
|
@ -73,6 +74,7 @@ export type StrRenderComponentType = {
|
|||
export type InputListComponentType = {
|
||||
componentName?: string;
|
||||
id?: string;
|
||||
listAddLabel?: string;
|
||||
};
|
||||
|
||||
export type DropDownComponentType = {
|
||||
|
|
|
|||
120
src/frontend/src/hooks/use-overlap-shortcuts.tsx
Normal file
120
src/frontend/src/hooks/use-overlap-shortcuts.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
const KEY_MAPPINGS: { [key: string]: string[] } = {
|
||||
backspace: ["backspace", "delete"],
|
||||
delete: ["delete", "backspace"],
|
||||
enter: ["enter", "return"],
|
||||
escape: ["escape", "esc"],
|
||||
space: [" ", "spacebar", "space"],
|
||||
arrowup: ["arrowup", "up"],
|
||||
arrowdown: ["arrowdown", "down"],
|
||||
arrowleft: ["arrowleft", "left"],
|
||||
arrowright: ["arrowright", "right"],
|
||||
};
|
||||
|
||||
interface UseKeyboardShortcutProps {
|
||||
shortcutKeys: { [key: string]: string };
|
||||
isEnabled?: boolean;
|
||||
onShortcut: (shortcutName: string, event: KeyboardEvent) => void;
|
||||
preventDefault?: boolean;
|
||||
stopPropagation?: boolean;
|
||||
}
|
||||
|
||||
export const useKeyboardShortcut = ({
|
||||
shortcutKeys,
|
||||
isEnabled = true,
|
||||
onShortcut,
|
||||
preventDefault = true,
|
||||
stopPropagation = true,
|
||||
}: UseKeyboardShortcutProps) => {
|
||||
const propsRef = useRef({ shortcutKeys, isEnabled, onShortcut });
|
||||
|
||||
useEffect(() => {
|
||||
propsRef.current = { shortcutKeys, isEnabled, onShortcut };
|
||||
}, [shortcutKeys, isEnabled, onShortcut]);
|
||||
|
||||
const normalizeKey = useCallback((key: string): string[] => {
|
||||
const lowercaseKey = key.toLowerCase();
|
||||
return KEY_MAPPINGS[lowercaseKey] || [lowercaseKey];
|
||||
}, []);
|
||||
|
||||
const parseShortcut = useCallback((shortcut: string) => {
|
||||
const parts = shortcut
|
||||
.toLowerCase()
|
||||
.split("+")
|
||||
.map((part) => part.trim());
|
||||
|
||||
// Handle 'mod' key (cmd on Mac, ctrl on others)
|
||||
const modIndex = parts.indexOf("mod");
|
||||
if (modIndex !== -1) {
|
||||
parts[modIndex] = navigator.platform.toLowerCase().includes("mac")
|
||||
? "meta"
|
||||
: "ctrl";
|
||||
}
|
||||
|
||||
return parts;
|
||||
}, []);
|
||||
|
||||
const matchesShortcut = useCallback(
|
||||
(event: KeyboardEvent, shortcut: string) => {
|
||||
if (!shortcut) return false;
|
||||
|
||||
const parts = parseShortcut(shortcut);
|
||||
const shortcutKey = parts[parts.length - 1];
|
||||
const possibleKeys = normalizeKey(shortcutKey);
|
||||
const eventKey = event.key.toLowerCase();
|
||||
|
||||
// Check if the pressed key matches any possible variations
|
||||
const keyMatches = possibleKeys.includes(eventKey);
|
||||
|
||||
// Get modifiers state
|
||||
const modifiers = {
|
||||
ctrl: event.ctrlKey,
|
||||
alt: event.altKey,
|
||||
shift: event.shiftKey,
|
||||
meta: event.metaKey,
|
||||
};
|
||||
|
||||
// Check modifiers
|
||||
const modifierParts = parts.slice(0, -1);
|
||||
const modifiersMatch = modifierParts.every(
|
||||
(mod) => modifiers[mod as keyof typeof modifiers],
|
||||
);
|
||||
|
||||
// Check no extra modifiers
|
||||
const hasExtraModifiers = Object.entries(modifiers).some(
|
||||
([mod, pressed]) => pressed && !modifierParts.includes(mod),
|
||||
);
|
||||
|
||||
return keyMatches && modifiersMatch && !hasExtraModifiers;
|
||||
},
|
||||
[normalizeKey, parseShortcut],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const { shortcutKeys, isEnabled, onShortcut } = propsRef.current;
|
||||
|
||||
if (!isEnabled) return;
|
||||
|
||||
for (const [name, shortcut] of Object.entries(shortcutKeys)) {
|
||||
if (matchesShortcut(event, shortcut)) {
|
||||
if (preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (stopPropagation) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
onShortcut(name, event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown, true);
|
||||
};
|
||||
}, [preventDefault, stopPropagation, matchesShortcut]);
|
||||
};
|
||||
|
|
@ -1276,6 +1276,10 @@
|
|||
.input-slider-text {
|
||||
@apply absolute bottom-[4.2rem] right-3 w-14 cursor-text rounded-sm px-2 py-[1px] text-center hover:ring-[1px] hover:ring-slider-input-border;
|
||||
}
|
||||
|
||||
.btn-add-input-list {
|
||||
@apply mt-3 flex h-8 w-full items-center justify-center rounded-md p-2 text-sm hover:bg-muted;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient background */
|
||||
|
|
|
|||
|
|
@ -431,6 +431,7 @@ export const nodeColors: { [char: string]: string } = {
|
|||
};
|
||||
|
||||
export const nodeColorsName: { [char: string]: string } = {
|
||||
// custom_components: "#ab11ab",
|
||||
inputs: "emerald",
|
||||
outputs: "red",
|
||||
data: "sky",
|
||||
|
|
@ -462,7 +463,6 @@ export const nodeColorsName: { [char: string]: string } = {
|
|||
astra_assistants: "indigo",
|
||||
langchain_utilities: "sky",
|
||||
output_parsers: "yellow",
|
||||
// custom_components: "#ab11ab",
|
||||
retrievers: "yellow",
|
||||
str: "indigo",
|
||||
Text: "indigo",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ test(
|
|||
await page
|
||||
.getByTestId("processingParse Data")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 100, y: 400 },
|
||||
targetPosition: { x: 100, y: 500 },
|
||||
});
|
||||
|
||||
//fifth component
|
||||
|
|
|
|||
|
|
@ -26,12 +26,22 @@ test(
|
|||
|
||||
await zoomOut(page, 4);
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page
|
||||
.getByTestId("inputsText Input")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 500, y: 150 },
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page
|
||||
.getByTestId("inputsText Input")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 670, y: 200 },
|
||||
});
|
||||
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("combine text");
|
||||
|
||||
|
|
@ -45,12 +55,20 @@ test(
|
|||
targetPosition: { x: 10, y: 10 },
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByTestId("popover-anchor-input-delimiter").fill("-");
|
||||
|
||||
await page
|
||||
.getByTestId("processingCombine Text")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||
targetPosition: { x: 200, y: 10 },
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByTestId("popover-anchor-input-delimiter").last().fill("-");
|
||||
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("text");
|
||||
|
||||
|
|
@ -161,7 +179,7 @@ test(
|
|||
|
||||
await page.getByTestId("group-node").click();
|
||||
|
||||
//connection 2
|
||||
//connection 1
|
||||
const elementTextOutput0 = page
|
||||
.getByTestId("handle-textinput-shownode-text-right")
|
||||
.nth(0);
|
||||
|
|
@ -171,17 +189,28 @@ test(
|
|||
);
|
||||
await elementGroupInput0.click();
|
||||
|
||||
//connection 3
|
||||
//connection 2
|
||||
const elementTextOutput1 = page
|
||||
.getByTestId("handle-textinput-shownode-text-right")
|
||||
.nth(2);
|
||||
.nth(4);
|
||||
await elementTextOutput1.click();
|
||||
|
||||
const elementGroupInput1 = page
|
||||
.getByTestId("handle-groupnode-shownode-second text-left")
|
||||
.nth(1);
|
||||
.first();
|
||||
await elementGroupInput1.click();
|
||||
|
||||
//connection 3
|
||||
const elementTextOutput2 = page
|
||||
.getByTestId("handle-textinput-shownode-text-right")
|
||||
.nth(2);
|
||||
await elementTextOutput2.click();
|
||||
|
||||
const elementGroupInput2 = page
|
||||
.getByTestId("handle-groupnode-shownode-second text-left")
|
||||
.nth(1)
|
||||
.last();
|
||||
await elementGroupInput2.click();
|
||||
|
||||
//connection 4
|
||||
const elementGroupOutput = page
|
||||
.getByTestId("handle-groupnode-shownode-combined text-right")
|
||||
|
|
@ -200,20 +229,10 @@ test(
|
|||
.nth(1)
|
||||
.fill(secondRandomName);
|
||||
|
||||
await page
|
||||
.getByPlaceholder("Type something...", { exact: true })
|
||||
.nth(4)
|
||||
.fill(thirdRandomName);
|
||||
|
||||
await page
|
||||
.getByPlaceholder("Type something...", { exact: true })
|
||||
.nth(3)
|
||||
.fill("-");
|
||||
|
||||
await page
|
||||
.getByPlaceholder("Type something...", { exact: true })
|
||||
.nth(2)
|
||||
.fill("-");
|
||||
.fill(thirdRandomName);
|
||||
|
||||
await page.getByTestId("button_run_text output").last().click();
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ test("chat_io_teste", { tag: ["@release", "@workspace"] }, async ({ page }) => {
|
|||
await page.getByTestId("input-chat-playground").click();
|
||||
await page.getByTestId("input-chat-playground").fill("teste");
|
||||
await page.getByTestId("button-send").first().click();
|
||||
const chat_input = page.getByTestId("chat-message-User-teste");
|
||||
await expect(chat_input).toHaveText("teste", { timeout: 10000 });
|
||||
const chat_input = await page
|
||||
.getByTestId("chat-message-User-teste")
|
||||
.textContent();
|
||||
|
||||
expect(chat_input).toBe("teste");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ test(
|
|||
});
|
||||
await page
|
||||
.getByTestId("dataURL")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.getByTestId("add-component-button-url").click();
|
||||
});
|
||||
await adjustScreenView(page);
|
||||
|
||||
await page.getByTestId("inputlist_str_urls_0").fill("test test test test");
|
||||
|
|
@ -53,93 +54,83 @@ test(
|
|||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page.getByTestId("input-list-minus-btn-edit_urls-1").click();
|
||||
await page.getByTestId("input-list-dropdown-menu-0-edit").click();
|
||||
|
||||
const plusButtonLocator = page.getByTestId(
|
||||
"input-list-minus-btn-edit_urls-1",
|
||||
);
|
||||
const elementCount = await plusButtonLocator?.count();
|
||||
await page.getByTestId("input-list-dropdown-menu-0-delete").click();
|
||||
|
||||
if (elementCount > 1) {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-2-edit").count(),
|
||||
).toBe(0);
|
||||
|
||||
await page.getByTestId("input-list-dropdown-menu-1-edit").click();
|
||||
|
||||
await page.getByTestId("input-list-dropdown-menu-1-delete").click();
|
||||
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-1-edit").count(),
|
||||
).toBe(0);
|
||||
|
||||
await page.getByText("Close").last().click();
|
||||
|
||||
await page.getByTestId("input-list-minus-btn_urls-2").isHidden();
|
||||
await page.getByTestId("input-list-add-more-view").click();
|
||||
await page.getByTestId("input-list-add-more-view").click();
|
||||
await page.getByTestId("input-list-add-more-view").click();
|
||||
|
||||
await page.getByTestId("input-list-plus-btn_urls-0").click();
|
||||
await page.getByTestId("input-list-plus-btn_urls-0").click();
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-0-view").count(),
|
||||
).toBe(1);
|
||||
|
||||
await page.getByTestId("inputlist_str_urls_0").fill("test test test test");
|
||||
await page
|
||||
.getByTestId("inputlist_str_urls_1")
|
||||
.fill("test1 test1 test1 test1");
|
||||
await page
|
||||
.getByTestId("inputlist_str_urls_2")
|
||||
.fill("test2 test2 test2 test2");
|
||||
await page
|
||||
.getByTestId("inputlist_str_urls_3")
|
||||
.fill("test3 test3 test3 test3");
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-1-view").count(),
|
||||
).toBe(1);
|
||||
|
||||
await page.getByTestId("div-generic-node").click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("advanced-button-modal").click();
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-2-view").count(),
|
||||
).toBe(1);
|
||||
|
||||
const value0Edit = await page
|
||||
.getByTestId("inputlist_str_edit_urls_0")
|
||||
.inputValue();
|
||||
const value1Edit = await page
|
||||
.getByTestId("inputlist_str_edit_urls_1")
|
||||
.inputValue();
|
||||
const value2Edit = await page
|
||||
.getByTestId("inputlist_str_edit_urls_2")
|
||||
.inputValue();
|
||||
const value3Edit = await page
|
||||
.getByTestId("inputlist_str_edit_urls_3")
|
||||
.inputValue();
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-3-view").count(),
|
||||
).toBe(1);
|
||||
|
||||
if (
|
||||
value0Edit !== "test test test test" ||
|
||||
value1Edit !== "test1 test1 test1 test1" ||
|
||||
value2Edit !== "test2 test2 test2 test2" ||
|
||||
value3Edit !== "test3 test3 test3 test3"
|
||||
) {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
expect(
|
||||
await page.getByTestId("input-list-dropdown-menu-4-view").count(),
|
||||
).toBe(0);
|
||||
|
||||
await page.getByTestId("input-list-minus-btn-edit_urls-1").click();
|
||||
await page.getByTestId("input-list-minus-btn-edit_urls-1").click();
|
||||
await page.getByTestId("input-list-minus-btn-edit_urls-1").click();
|
||||
await page.getByTestId("input-list-dropdown-menu-0-view").click();
|
||||
await page.getByTestId("input-list-dropdown-menu-0-duplicate").click();
|
||||
|
||||
const plusButtonLocatorEdit0 = await page.getByTestId(
|
||||
"input-list-plus-btn-edit_urls-0",
|
||||
);
|
||||
const elementCountEdit0 = await plusButtonLocatorEdit0?.count();
|
||||
|
||||
const plusButtonLocatorEdit2 = await page.getByTestId(
|
||||
"input-list-plus-btn-edit_urls-1",
|
||||
);
|
||||
const elementCountEdit2 = await plusButtonLocatorEdit2?.count();
|
||||
|
||||
if (elementCountEdit0 > 1 || elementCountEdit2 > 0) {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
const minusButtonLocatorEdit1 = await page.getByTestId(
|
||||
"input-list-minus-btn-edit_urls-1",
|
||||
expect(await page.getByTestId("inputlist_str_urls_0").inputValue()).toBe(
|
||||
"test1 test1 test1 test1",
|
||||
);
|
||||
|
||||
const elementCountMinusEdit1 = await minusButtonLocatorEdit1?.count();
|
||||
|
||||
const minusButtonLocatorEdit2 = await page.getByTestId(
|
||||
"input-list-minus-btn-edit_urls-2",
|
||||
expect(await page.getByTestId("inputlist_str_urls_1").inputValue()).toBe(
|
||||
"test1 test1 test1 test1",
|
||||
);
|
||||
|
||||
const elementCountMinusEdit2 = await minusButtonLocatorEdit2?.count();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
if (elementCountMinusEdit1 > 1 || elementCountMinusEdit2 > 0) {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
expect(
|
||||
await page.getByTestId("inputlist_str_edit_urls_0").inputValue(),
|
||||
).toBe("test1 test1 test1 test1");
|
||||
|
||||
expect(
|
||||
await page.getByTestId("inputlist_str_edit_urls_1").inputValue(),
|
||||
).toBe("test1 test1 test1 test1");
|
||||
|
||||
await page.getByTestId("input-list-dropdown-menu-1-edit").click();
|
||||
|
||||
await page.getByTestId("input-list-dropdown-menu-1-duplicate").click();
|
||||
|
||||
expect(
|
||||
await page.getByTestId("inputlist_str_edit_urls_0").inputValue(),
|
||||
).toBe("test1 test1 test1 test1");
|
||||
|
||||
expect(
|
||||
await page.getByTestId("inputlist_str_edit_urls_1").inputValue(),
|
||||
).toBe("test1 test1 test1 test1");
|
||||
|
||||
expect(
|
||||
await page.getByTestId("inputlist_str_edit_urls_2").inputValue(),
|
||||
).toBe("test1 test1 test1 test1");
|
||||
},
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue