diff --git a/src/backend/base/langflow/components/composio/__init__.py b/src/backend/base/langflow/components/composio/__init__.py
index 0b6e6bc3ee3d..3afaa8093101 100644
--- a/src/backend/base/langflow/components/composio/__init__.py
+++ b/src/backend/base/langflow/components/composio/__init__.py
@@ -2,6 +2,7 @@
from .github_composio import ComposioGitHubAPIComponent
from .gmail_composio import ComposioGmailAPIComponent
from .googlecalendar_composio import ComposioGoogleCalendarAPIComponent
+from .linear_composio import ComposioLinearAPIComponent
from .outlook_composio import ComposioOutlookAPIComponent
from .slack_composio import ComposioSlackAPIComponent
@@ -10,6 +11,7 @@
"ComposioGitHubAPIComponent",
"ComposioGmailAPIComponent",
"ComposioGoogleCalendarAPIComponent",
+ "ComposioLinearAPIComponent",
"ComposioOutlookAPIComponent",
"ComposioSlackAPIComponent",
]
diff --git a/src/backend/base/langflow/components/composio/linear_composio.py b/src/backend/base/langflow/components/composio/linear_composio.py
new file mode 100644
index 000000000000..c4c767a86941
--- /dev/null
+++ b/src/backend/base/langflow/components/composio/linear_composio.py
@@ -0,0 +1,495 @@
+from typing import Any
+
+from composio import Action
+
+from langflow.base.composio.composio_base import ComposioBaseComponent
+from langflow.inputs import (
+ IntInput,
+ MessageTextInput,
+)
+from langflow.logging import logger
+
+
+class ComposioLinearAPIComponent(ComposioBaseComponent):
+ display_name: str = "Linear"
+ description: str = "Linear API"
+ icon = "Linear"
+ documentation: str = "https://docs.composio.dev"
+ app_name = "linear"
+
+ _actions_data: dict = {
+ "LINEAR_CREATE_LINEAR_COMMENT": {
+ "display_name": "Create Linear Comment",
+ "action_fields": ["LINEAR_CREATE_LINEAR_COMMENT_body", "LINEAR_CREATE_LINEAR_COMMENT_issue_id"],
+ },
+ "LINEAR_CREATE_LINEAR_ISSUE": {
+ "display_name": "Create Linear Issue",
+ "action_fields": [
+ "LINEAR_CREATE_LINEAR_ISSUE_assignee_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_description",
+ "LINEAR_CREATE_LINEAR_ISSUE_due_date",
+ "LINEAR_CREATE_LINEAR_ISSUE_estimate",
+ "LINEAR_CREATE_LINEAR_ISSUE_label_ids",
+ "LINEAR_CREATE_LINEAR_ISSUE_parent_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_priority",
+ "LINEAR_CREATE_LINEAR_ISSUE_project_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_state_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_team_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_title",
+ ],
+ },
+ "LINEAR_CREATE_LINEAR_ISSUE_DETAILS": {
+ "display_name": "Get Create Issue Default Params",
+ "action_fields": ["LINEAR_CREATE_LINEAR_ISSUE_DETAILS_team_id"],
+ "get_result_field": True,
+ "result_field": "team",
+ },
+ "LINEAR_CREATE_LINEAR_LABEL": {
+ "display_name": "Create Label",
+ "action_fields": [
+ "LINEAR_CREATE_LINEAR_LABEL_color",
+ "LINEAR_CREATE_LINEAR_LABEL_description",
+ "LINEAR_CREATE_LINEAR_LABEL_name",
+ "LINEAR_CREATE_LINEAR_LABEL_team_id",
+ ],
+ },
+ "LINEAR_DELETE_LINEAR_ISSUE": {
+ "display_name": "Delete Issue",
+ "action_fields": ["LINEAR_DELETE_LINEAR_ISSUE_issue_id"],
+ },
+ "LINEAR_GET_CYCLES_BY_TEAM_ID": {
+ "display_name": "Get Cycles By Team",
+ "action_fields": ["LINEAR_GET_CYCLES_BY_TEAM_ID_team_id"],
+ "get_result_field": True,
+ "result_field": "cycles",
+ },
+ "LINEAR_GET_LINEAR_ISSUE": {
+ "display_name": "Get Linear Issue",
+ "action_fields": ["LINEAR_GET_LINEAR_ISSUE_issue_id"],
+ "get_result_field": True,
+ "result_field": "issue",
+ },
+ "LINEAR_LIST_LINEAR_CYCLES": {
+ "display_name": "Get All Cycles",
+ "action_fields": [],
+ "get_result_field": True,
+ "result_field": "cycles",
+ },
+ "LINEAR_LIST_LINEAR_ISSUES": {
+ "display_name": "Get Issues By Project",
+ "action_fields": [
+ "LINEAR_LIST_LINEAR_ISSUES_after",
+ "LINEAR_LIST_LINEAR_ISSUES_first",
+ "LINEAR_LIST_LINEAR_ISSUES_project_id",
+ ],
+ },
+ "LINEAR_LIST_LINEAR_LABELS": {
+ "display_name": "Get Labels By Team",
+ "action_fields": ["LINEAR_LIST_LINEAR_LABELS_team_id"],
+ "get_result_field": True,
+ "result_field": "labels",
+ },
+ "LINEAR_LIST_LINEAR_PROJECTS": {
+ "display_name": "List Linear Projects",
+ "action_fields": [],
+ "get_result_field": True,
+ "result_field": "projects",
+ },
+ "LINEAR_LIST_LINEAR_STATES": {
+ "display_name": "Get States By Team",
+ "action_fields": ["LINEAR_LIST_LINEAR_STATES_team_id"],
+ "get_result_field": True,
+ "result_field": "states",
+ },
+ "LINEAR_LIST_LINEAR_TEAMS": {
+ "display_name": "Get Teams By Project",
+ "action_fields": ["LINEAR_LIST_LINEAR_TEAMS_project_id"],
+ "get_result_field": True,
+ "result_field": "teams",
+ },
+ "LINEAR_REMOVE_ISSUE_LABEL": {
+ "display_name": "Remove Label From Linear Issue",
+ "action_fields": ["LINEAR_REMOVE_ISSUE_LABEL_issue_id", "LINEAR_REMOVE_ISSUE_LABEL_label_id"],
+ },
+ "LINEAR_UPDATE_ISSUE": {
+ "display_name": "Update Issue",
+ "action_fields": [
+ "LINEAR_UPDATE_ISSUE_assignee_id",
+ "LINEAR_UPDATE_ISSUE_description",
+ "LINEAR_UPDATE_ISSUE_due_date",
+ "LINEAR_UPDATE_ISSUE_estimate",
+ "LINEAR_UPDATE_ISSUE_issue_id",
+ "LINEAR_UPDATE_ISSUE_label_ids",
+ "LINEAR_UPDATE_ISSUE_parent_id",
+ "LINEAR_UPDATE_ISSUE_priority",
+ "LINEAR_UPDATE_ISSUE_project_id",
+ "LINEAR_UPDATE_ISSUE_state_id",
+ "LINEAR_UPDATE_ISSUE_team_id",
+ "LINEAR_UPDATE_ISSUE_title",
+ ],
+ },
+ }
+
+ _all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
+
+ inputs = [
+ *ComposioBaseComponent._base_inputs,
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_COMMENT_body",
+ display_name="Body",
+ info="Content of the comment",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_COMMENT_issue_id",
+ display_name="Issue Id",
+ info="ID of the issue to comment on",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_assignee_id",
+ display_name="Assignee Id",
+ info="ID of the assignee",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_description",
+ display_name="Description",
+ info="Description of the issue",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_due_date",
+ display_name="Due Date",
+ info="Due date of the issue in the comma separated format YYYY,MM,DD,hh,mm,ss. For example, 2024,10,27,12,58,00.", # noqa: E501
+ show=False,
+ advanced=True,
+ ),
+ IntInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_estimate",
+ display_name="Estimate",
+ info="The Int scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.", # noqa: E501
+ show=False,
+ value=0,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_label_ids",
+ display_name="Label Ids",
+ info="List of label IDs",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_parent_id",
+ display_name="Parent Id",
+ info="ID of the parent issue",
+ show=False,
+ advanced=True,
+ ),
+ IntInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_priority",
+ display_name="Priority",
+ info="The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.",
+ show=False,
+ value=0,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_project_id",
+ display_name="Project Id",
+ info="ID of the project",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_state_id",
+ display_name="State Id",
+ info="ID of the issue state",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_team_id",
+ display_name="Team Id",
+ info="ID of the team",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_title",
+ display_name="Title",
+ info="Title of the issue",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_ISSUE_DETAILS_team_id",
+ display_name="Team Id",
+ info="ID of the team for which to fetch details",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_LABEL_color",
+ display_name="Color",
+ info="Color of the label (hex code)",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_LABEL_description",
+ display_name="Description",
+ info="Description of the label",
+ show=False,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_LABEL_name",
+ display_name="Name",
+ info="Name of the label",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_CREATE_LINEAR_LABEL_team_id",
+ display_name="Team Id",
+ info="ID of the team to create the label for",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_DELETE_LINEAR_ISSUE_issue_id",
+ display_name="Issue Id",
+ info="The ID of the issue to delete (can be UUID or shorthand ID like 'LIN-123')",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_GET_CYCLES_BY_TEAM_ID_team_id",
+ display_name="Team Id",
+ info="ID of the team for which to list cycles",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_GET_LINEAR_ISSUE_issue_id",
+ display_name="Issue Id",
+ info="ID of the issue for which to fetch details",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_LIST_LINEAR_ISSUES_after",
+ display_name="After",
+ info="Cursor to start from",
+ show=False,
+ ),
+ IntInput(
+ name="LINEAR_LIST_LINEAR_ISSUES_first",
+ display_name="First",
+ info="Number of issues to return",
+ show=False,
+ value=10,
+ ),
+ MessageTextInput(
+ name="LINEAR_LIST_LINEAR_ISSUES_project_id",
+ display_name="Project Id",
+ info="ID of the project for which to list issues. If this is provided the issues returned will be filtered by the given project ID.", # noqa: E501
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_LIST_LINEAR_LABELS_team_id",
+ display_name="Team Id",
+ info="ID of the team for which to list labels",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_LIST_LINEAR_STATES_team_id",
+ display_name="Team Id",
+ info="ID of the team for which to list states",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_LIST_LINEAR_TEAMS_project_id",
+ display_name="Project Id",
+ info="ID of the project for which to list teams",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_REMOVE_ISSUE_LABEL_issue_id",
+ display_name="Issue Id",
+ info="The ID of the Linear issue from which to remove the label",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_REMOVE_ISSUE_LABEL_label_id",
+ display_name="Label Id",
+ info="The ID of the label to remove from the issue",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_RUN_QUERY_OR_MUTATION_query_or_mutation",
+ display_name="Query Or Mutation",
+ info="Query or mutation to run",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_RUN_QUERY_OR_MUTATION_variables",
+ display_name="Variables",
+ info="Variables to pass to the query or mutation",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_assignee_id",
+ display_name="Assignee Id",
+ info="ID of the assignee",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_description",
+ display_name="Description",
+ info="New description for the issue",
+ show=False,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_due_date",
+ display_name="Due Date",
+ info="Due date of the issue in the comma separated format YYYY,MM,DD,hh,mm,ss. For example, 2024,10,27,12,58,00.", # noqa: E501
+ show=False,
+ advanced=True,
+ ),
+ IntInput(
+ name="LINEAR_UPDATE_ISSUE_estimate",
+ display_name="Estimate",
+ info="Time estimate for the issue in minutes",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_issue_id",
+ display_name="Issue Id",
+ info="ID of the issue to update",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_label_ids",
+ display_name="Label Ids",
+ info="List of label IDs to assign to the issue",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_parent_id",
+ display_name="Parent Id",
+ info="ID of the parent issue",
+ show=False,
+ advanced=True,
+ ),
+ IntInput(
+ name="LINEAR_UPDATE_ISSUE_priority",
+ display_name="Priority",
+ info="The priority of the issue. 0 = No priority, 1 = Urgent, 2 = High, 3 = Normal, 4 = Low.",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_project_id",
+ display_name="Project Id",
+ info="ID of the project",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_state_id",
+ display_name="State Id",
+ info="ID of the issue state",
+ show=False,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_team_id",
+ display_name="Team Id",
+ info="ID of the team",
+ show=False,
+ advanced=True,
+ ),
+ MessageTextInput(
+ name="LINEAR_UPDATE_ISSUE_title",
+ display_name="Title",
+ info="New title for the issue",
+ show=False,
+ ),
+ ]
+
+ def execute_action(self):
+ """Execute action and return response as Message."""
+ toolset = self._build_wrapper()
+
+ try:
+ self._build_action_maps()
+ display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else self.action
+ action_key = self._display_to_key_map.get(display_name)
+ if not action_key:
+ msg = f"Invalid action: {display_name}"
+ raise ValueError(msg)
+
+ enum_name = getattr(Action, action_key)
+ params = {}
+ if action_key in self._actions_data:
+ for field in self._actions_data[action_key]["action_fields"]:
+ value = getattr(self, field)
+
+ if value is None or value == "":
+ continue
+
+ if field in ["LINEAR_CREATE_LINEAR_ISSUE_label_ids", "LINEAR_UPDATE_ISSUE_label_ids"] and value:
+ value = [item.strip() for item in value.split(",")]
+
+ param_name = field.replace(action_key + "_", "")
+
+ params[param_name] = value
+
+ result = toolset.execute_action(
+ action=enum_name,
+ params=params,
+ )
+ if not result.get("successful"):
+ return {"error": result.get("error", "No response")}
+
+ result_data = result.get("data", [])
+ actions_data = self._actions_data.get(action_key, {})
+ # If 'get_result_field' is True and 'result_field' is specified, extract the data
+ # using 'result_field'. Otherwise, fall back to the entire 'data' field in the response.
+ if actions_data.get("get_result_field") and actions_data.get("result_field"):
+ result_data = result_data.get(actions_data.get("result_field"), result.get("data", []))
+ if len(result_data) != 1 and not actions_data.get("result_field") and actions_data.get("get_result_field"):
+ msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}"
+ raise ValueError(msg)
+ return result_data # noqa: TRY300
+ except Exception as e:
+ logger.error(f"Error executing action: {e}")
+ display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)
+ msg = f"Failed to execute {display_name}: {e!s}"
+ raise ValueError(msg) from e
+
+ def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
+ return super().update_build_config(build_config, field_value, field_name)
+
+ def set_default_tools(self):
+ self._default_tools = {
+ self.sanitize_action_name("LINEAR_CREATE_LINEAR_ISSUE").replace(" ", "-"),
+ self.sanitize_action_name("LINEAR_GET_LINEAR_ISSUE").replace(" ", "-"),
+ }
diff --git a/src/backend/tests/unit/components/bundles/composio/test_linear.py b/src/backend/tests/unit/components/bundles/composio/test_linear.py
new file mode 100644
index 000000000000..b52f29c19919
--- /dev/null
+++ b/src/backend/tests/unit/components/bundles/composio/test_linear.py
@@ -0,0 +1,170 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+from composio import Action
+from langflow.components.composio.linear_composio import ComposioLinearAPIComponent
+from langflow.schema.dataframe import DataFrame
+
+from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
+
+from .test_base import MockComposioToolSet
+
+
+class MockAction:
+ LINEAR_CREATE_LINEAR_ISSUE = "LINEAR_CREATE_LINEAR_ISSUE"
+ LINEAR_GET_LINEAR_ISSUE = "LINEAR_GET_LINEAR_ISSUE"
+ LINEAR_LIST_LINEAR_ISSUES = "LINEAR_LIST_LINEAR_ISSUES"
+
+
+class TestLinearComponent(ComponentTestBaseWithoutClient):
+ @pytest.fixture(autouse=True)
+ def mock_composio_toolset(self):
+ with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
+ yield
+
+ @pytest.fixture
+ def component_class(self):
+ return ComposioLinearAPIComponent
+
+ @pytest.fixture
+ def default_kwargs(self):
+ return {"api_key": "", "entity_id": "default", "action": None}
+
+ @pytest.fixture
+ def file_names_mapping(self):
+ return [
+ {"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
+ {"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
+ {"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
+ {"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
+ {"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
+ ]
+
+ def test_init(self, component_class, default_kwargs):
+ component = component_class(**default_kwargs)
+ assert component.display_name == "Linear"
+ assert component.app_name == "linear"
+ assert "LINEAR_CREATE_LINEAR_ISSUE" in component._actions_data
+ assert "LINEAR_GET_LINEAR_ISSUE" in component._actions_data
+
+ def test_execute_action_create_issue(self, component_class, default_kwargs, monkeypatch):
+ monkeypatch.setattr(Action, "LINEAR_CREATE_LINEAR_ISSUE", MockAction.LINEAR_CREATE_LINEAR_ISSUE)
+
+ component = component_class(**default_kwargs)
+ component.api_key = "test_key"
+ component.action = [{"name": "Create Linear Issue"}]
+ component.LINEAR_CREATE_LINEAR_ISSUE_description = "Test Description"
+ component.LINEAR_CREATE_LINEAR_ISSUE_title = "Test Title"
+ component.LINEAR_CREATE_LINEAR_ISSUE_team_id = "123"
+ component.LINEAR_CREATE_LINEAR_ISSUE_project_id = "123"
+
+ component._actions_data = {
+ "LINEAR_CREATE_LINEAR_ISSUE": {
+ "display_name": "Create Linear Issue",
+ "action_fields": [
+ "LINEAR_CREATE_LINEAR_ISSUE_assignee_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_description",
+ "LINEAR_CREATE_LINEAR_ISSUE_due_date",
+ "LINEAR_CREATE_LINEAR_ISSUE_estimate",
+ "LINEAR_CREATE_LINEAR_ISSUE_label_ids",
+ "LINEAR_CREATE_LINEAR_ISSUE_parent_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_priority",
+ "LINEAR_CREATE_LINEAR_ISSUE_project_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_state_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_team_id",
+ "LINEAR_CREATE_LINEAR_ISSUE_title",
+ ],
+ },
+ }
+
+ result = component.execute_action()
+ assert result == {"result": "mocked response"}
+
+ def test_execute_action_get_issue(self, component_class, default_kwargs, monkeypatch):
+ monkeypatch.setattr(Action, "LINEAR_GET_LINEAR_ISSUE", MockAction.LINEAR_GET_LINEAR_ISSUE)
+
+ component = component_class(**default_kwargs)
+ component.api_key = "test_key"
+ component.action = [{"name": "Get Linear Issue"}]
+ component.issue_id = "123"
+
+ component._actions_data = {
+ "LINEAR_GET_LINEAR_ISSUE": {
+ "display_name": "Get Linear Issue",
+ "action_fields": ["LINEAR_GET_LINEAR_ISSUE_issue_id"],
+ },
+ }
+
+ mock_toolset = MagicMock()
+ mock_toolset.execute_action.return_value = {"successful": True, "data": {"messages": "mocked response"}}
+
+ with patch.object(component, "_build_wrapper", return_value=mock_toolset):
+ result = component.execute_action()
+ assert result == {"messages": "mocked response"}
+
+ def test_execute_action_invalid_action(self, component_class, default_kwargs):
+ component = component_class(**default_kwargs)
+ component.api_key = "test_key"
+ component.action = [{"name": "Invalid Action"}]
+
+ with pytest.raises(ValueError, match="Invalid action: Invalid Action"):
+ component.execute_action()
+
+ def test_as_dataframe(self, component_class, default_kwargs, monkeypatch):
+ monkeypatch.setattr(Action, "LINEAR_LIST_LINEAR_ISSUES", MockAction.LINEAR_LIST_LINEAR_ISSUES)
+
+ component = component_class(**default_kwargs)
+ component.api_key = "test_key"
+ component.action = [{"name": "Get Issues By Project"}]
+
+ mock_issues = [
+ {
+ "id": "a1b7b19d-41d5-2972-b102-faa0b65d2b47",
+ "title": "Fix frontend",
+ "description": "Fix error logs on frontend",
+ "assignee": {"email": "test@gmail.com", "id": "271027-3d3f-415e-a784-6d2810351b", "name": "Test User"},
+ "priority": 0,
+ "project": None,
+ "state": {"name": "Todo"},
+ },
+ {
+ "id": "346745c-00a-4a15-ab55-89e329b556",
+ "title": "Fix backend",
+ "description": "Fix error handling on backend",
+ "assignee": None,
+ "priority": 0,
+ "project": None,
+ "state": {"name": "Backlog"},
+ },
+ ]
+
+ with patch.object(component, "execute_action", return_value=mock_issues):
+ result = component.as_dataframe()
+
+ assert isinstance(result, DataFrame)
+
+ assert not result.empty
+
+ data_str = result.to_string()
+ assert "Fix frontend" in data_str
+ assert "Fix backend" in data_str
+
+ def test_update_build_config(self, component_class, default_kwargs):
+ component = component_class(**default_kwargs)
+ build_config = {
+ "auth_link": {"value": "", "auth_tooltip": ""},
+ "action": {
+ "options": [],
+ "helper_text": "",
+ "helper_text_metadata": {},
+ },
+ }
+
+ result = component.update_build_config(build_config, "", "api_key")
+ assert result["auth_link"]["value"] == ""
+ assert "Please provide a valid Composio API Key" in result["auth_link"]["auth_tooltip"]
+ assert result["action"]["options"] == []
+
+ component.api_key = "test_key"
+ result = component.update_build_config(build_config, "test_key", "api_key")
+ assert len(result["action"]["options"]) > 0
diff --git a/src/frontend/src/icons/lazyIconImports.ts b/src/frontend/src/icons/lazyIconImports.ts
index 72947c66afba..aa76c493a0b4 100644
--- a/src/frontend/src/icons/lazyIconImports.ts
+++ b/src/frontend/src/icons/lazyIconImports.ts
@@ -111,6 +111,8 @@ export const lazyIconsMapping = {
})),
Gmail: () =>
import("@/icons/gmail").then((mod) => ({ default: mod.GmailIcon })),
+ Linear: () =>
+ import("@/icons/linear").then((mod) => ({ default: mod.LinearIcon })),
Outlook: () =>
import("@/icons/outlook").then((mod) => ({ default: mod.OutlookIcon })),
Googlecalendar: () =>
diff --git a/src/frontend/src/icons/linear/index.tsx b/src/frontend/src/icons/linear/index.tsx
new file mode 100644
index 000000000000..9ffa622ce199
--- /dev/null
+++ b/src/frontend/src/icons/linear/index.tsx
@@ -0,0 +1,9 @@
+import React, { forwardRef } from "react";
+import LinearIconSVG from "./linear";
+
+export const LinearIcon = forwardRef<
+ SVGSVGElement,
+ React.PropsWithChildren<{}>
+>((props, ref) => {
+ return ;
+});
diff --git a/src/frontend/src/icons/linear/linear.jsx b/src/frontend/src/icons/linear/linear.jsx
new file mode 100644
index 000000000000..7745ad83735a
--- /dev/null
+++ b/src/frontend/src/icons/linear/linear.jsx
@@ -0,0 +1,20 @@
+const Icon = (props) => (
+
+);
+export default Icon;
diff --git a/src/frontend/src/icons/linear/linear.svg b/src/frontend/src/icons/linear/linear.svg
new file mode 100644
index 000000000000..b0df72009709
--- /dev/null
+++ b/src/frontend/src/icons/linear/linear.svg
@@ -0,0 +1,8 @@
+
diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts
index 5fabf052818a..a7d84078a95d 100644
--- a/src/frontend/src/utils/styleUtils.ts
+++ b/src/frontend/src/utils/styleUtils.ts
@@ -229,6 +229,7 @@ export const SIDEBAR_CATEGORIES = [
];
export const SIDEBAR_BUNDLES = [
+ { display_name: "Linear", name: "linear", icon: "Linear" },
{ display_name: "Outlook", name: "outlook", icon: "Outlook" },
{
display_name: "Language Models",
@@ -316,6 +317,7 @@ export const categoryIcons: Record = {
export const nodeIconToDisplayIconMap: Record = {
//Category Icons
+ Linear: "Linear",
input_output: "Cable",
inputs: "Download",
outputs: "Upload",