mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-04 21:32:39 +02:00
fix: resolve graphql complexity, json extraction, kb sync, and ui prefill for linear HIL
This commit is contained in:
parent
d4e2ebb99f
commit
c8413ee2bf
12 changed files with 269 additions and 213 deletions
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.connectors.linear_connector import LinearConnector
|
from app.connectors.linear_connector import LinearAPIError, LinearConnector
|
||||||
from app.services.linear import LinearToolMetadataService
|
from app.services.linear import LinearToolMetadataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -230,11 +230,10 @@ def create_create_linear_issue_tool(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.error(f"Error creating Linear issue: {e}", exc_info=True)
|
logger.error(f"Error creating Linear issue: {e}", exc_info=True)
|
||||||
return {
|
if isinstance(e, (ValueError, LinearAPIError)):
|
||||||
"status": "error",
|
message = str(e)
|
||||||
"message": str(e)
|
else:
|
||||||
if isinstance(e, ValueError)
|
message = "Something went wrong while creating the issue. Please try again."
|
||||||
else f"Unexpected error: {e!s}",
|
return {"status": "error", "message": message}
|
||||||
}
|
|
||||||
|
|
||||||
return create_linear_issue
|
return create_linear_issue
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.connectors.linear_connector import LinearConnector
|
from app.connectors.linear_connector import LinearAPIError, LinearConnector
|
||||||
from app.services.linear import LinearToolMetadataService
|
from app.services.linear import LinearToolMetadataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -253,11 +253,10 @@ def create_delete_linear_issue_tool(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.error(f"Error deleting Linear issue: {e}", exc_info=True)
|
logger.error(f"Error deleting Linear issue: {e}", exc_info=True)
|
||||||
return {
|
if isinstance(e, (ValueError, LinearAPIError)):
|
||||||
"status": "error",
|
message = str(e)
|
||||||
"message": str(e)
|
else:
|
||||||
if isinstance(e, ValueError)
|
message = "Something went wrong while deleting the issue. Please try again."
|
||||||
else f"Unexpected error: {e!s}",
|
return {"status": "error", "message": message}
|
||||||
}
|
|
||||||
|
|
||||||
return delete_linear_issue
|
return delete_linear_issue
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.connectors.linear_connector import LinearConnector
|
from app.connectors.linear_connector import LinearAPIError, LinearConnector
|
||||||
from app.services.linear import LinearKBSyncService, LinearToolMetadataService
|
from app.services.linear import LinearKBSyncService, LinearToolMetadataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -290,12 +290,11 @@ def create_update_linear_issue_tool(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.error(f"Error updating Linear issue: {e}", exc_info=True)
|
logger.error(f"Error updating Linear issue: {e}", exc_info=True)
|
||||||
return {
|
if isinstance(e, (ValueError, LinearAPIError)):
|
||||||
"status": "error",
|
message = str(e)
|
||||||
"message": str(e)
|
else:
|
||||||
if isinstance(e, ValueError)
|
message = "Something went wrong while updating the issue. Please try again."
|
||||||
else f"Unexpected error: {e!s}",
|
return {"status": "error", "message": message}
|
||||||
}
|
|
||||||
|
|
||||||
return update_linear_issue
|
return update_linear_issue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.connectors.notion_history import NotionHistoryConnector
|
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||||
from app.services.notion import NotionToolMetadataService
|
from app.services.notion import NotionToolMetadataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -224,11 +224,10 @@ def create_create_notion_page_tool(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.error(f"Error creating Notion page: {e}", exc_info=True)
|
logger.error(f"Error creating Notion page: {e}", exc_info=True)
|
||||||
return {
|
if isinstance(e, (ValueError, NotionAPIError)):
|
||||||
"status": "error",
|
message = str(e)
|
||||||
"message": str(e)
|
else:
|
||||||
if isinstance(e, ValueError)
|
message = "Something went wrong while creating the page. Please try again."
|
||||||
else f"Unexpected error: {e!s}",
|
return {"status": "error", "message": message}
|
||||||
}
|
|
||||||
|
|
||||||
return create_notion_page
|
return create_notion_page
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.connectors.notion_history import NotionHistoryConnector
|
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||||
from app.services.notion.tool_metadata_service import NotionToolMetadataService
|
from app.services.notion.tool_metadata_service import NotionToolMetadataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -262,11 +262,10 @@ def create_delete_notion_page_tool(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.error(f"Error deleting Notion page: {e}", exc_info=True)
|
logger.error(f"Error deleting Notion page: {e}", exc_info=True)
|
||||||
return {
|
if isinstance(e, (ValueError, NotionAPIError)):
|
||||||
"status": "error",
|
message = str(e)
|
||||||
"message": str(e)
|
else:
|
||||||
if isinstance(e, ValueError)
|
message = "Something went wrong while deleting the page. Please try again."
|
||||||
else f"Unexpected error: {e!s}",
|
return {"status": "error", "message": message}
|
||||||
}
|
|
||||||
|
|
||||||
return delete_notion_page
|
return delete_notion_page
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from langchain_core.tools import tool
|
||||||
from langgraph.types import interrupt
|
from langgraph.types import interrupt
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.connectors.notion_history import NotionHistoryConnector
|
from app.connectors.notion_history import NotionAPIError, NotionHistoryConnector
|
||||||
from app.services.notion import NotionToolMetadataService
|
from app.services.notion import NotionToolMetadataService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -261,11 +261,10 @@ def create_update_notion_page_tool(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
logger.error(f"Error updating Notion page: {e}", exc_info=True)
|
logger.error(f"Error updating Notion page: {e}", exc_info=True)
|
||||||
return {
|
if isinstance(e, (ValueError, NotionAPIError)):
|
||||||
"status": "error",
|
message = str(e)
|
||||||
"message": str(e)
|
else:
|
||||||
if isinstance(e, ValueError)
|
message = "Something went wrong while updating the page. Please try again."
|
||||||
else f"Unexpected error: {e!s}",
|
return {"status": "error", "message": message}
|
||||||
}
|
|
||||||
|
|
||||||
return update_notion_page
|
return update_notion_page
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,14 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql"
|
LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql"
|
||||||
|
|
||||||
|
|
||||||
|
class LinearAPIError(Exception):
|
||||||
|
"""Raised when the Linear API returns a non-200 response.
|
||||||
|
|
||||||
|
The message is always user-presentable; callers should surface it directly
|
||||||
|
without any additional prefix or wrapping.
|
||||||
|
"""
|
||||||
|
|
||||||
ORGANIZATION_QUERY = """
|
ORGANIZATION_QUERY = """
|
||||||
query {
|
query {
|
||||||
organization {
|
organization {
|
||||||
|
|
@ -243,6 +251,37 @@ class LinearConnector:
|
||||||
"Authorization": f"Bearer {self._credentials.access_token}",
|
"Authorization": f"Bearer {self._credentials.access_token}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _raise_api_error(status_code: int, body: str) -> None:
|
||||||
|
"""Parse a non-200 Linear API response and raise a clean exception.
|
||||||
|
|
||||||
|
Translates known Linear error codes into user-readable messages so that
|
||||||
|
raw GraphQL payloads never reach the end user.
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
friendly = None
|
||||||
|
try:
|
||||||
|
payload = _json.loads(body)
|
||||||
|
errors = payload.get("errors", [])
|
||||||
|
if errors:
|
||||||
|
ext = errors[0].get("extensions", {})
|
||||||
|
code = ext.get("code", "")
|
||||||
|
if code == "INPUT_ERROR" and "too complex" in errors[0].get("message", "").lower():
|
||||||
|
friendly = (
|
||||||
|
"Linear rejected the request because the workspace is too large "
|
||||||
|
"to fetch in one query. Please try again — if the problem persists, "
|
||||||
|
"contact support."
|
||||||
|
)
|
||||||
|
elif ext.get("userPresentableMessage"):
|
||||||
|
friendly = ext["userPresentableMessage"]
|
||||||
|
elif errors[0].get("message"):
|
||||||
|
friendly = errors[0]["message"]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise LinearAPIError(friendly or f"Linear API error (HTTP {status_code})")
|
||||||
|
|
||||||
async def execute_graphql_query(
|
async def execute_graphql_query(
|
||||||
self, query: str, variables: dict[str, Any] | None = None
|
self, query: str, variables: dict[str, Any] | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
@ -281,9 +320,7 @@ class LinearConnector:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json()
|
return response.json()
|
||||||
else:
|
else:
|
||||||
raise Exception(
|
self._raise_api_error(response.status_code, response.text)
|
||||||
f"Query failed with status code {response.status_code}: {response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get_all_issues(
|
async def get_all_issues(
|
||||||
self, include_comments: bool = True
|
self, include_comments: bool = True
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@ from app.utils.oauth_security import TokenEncryption
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NotionAPIError(Exception):
|
||||||
|
"""Raised when the Notion API returns a non-200 response.
|
||||||
|
|
||||||
|
The message is always user-presentable; callers should surface it directly
|
||||||
|
without any additional prefix or wrapping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Type variable for generic return type
|
# Type variable for generic return type
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
@ -250,8 +259,9 @@ class NotionHistoryConnector:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Failed to refresh Notion token for connector {self._connector_id}: {e!s}"
|
f"Failed to refresh Notion token for connector {self._connector_id}: {e!s}"
|
||||||
)
|
)
|
||||||
raise Exception(
|
raise NotionAPIError(
|
||||||
f"Failed to refresh Notion OAuth credentials: {e!s}"
|
"Failed to refresh your Notion connection. "
|
||||||
|
"Please try again or reconnect your Notion account."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
return self._credentials.access_token
|
return self._credentials.access_token
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ class LinearKBSyncService:
|
||||||
issue_identifier = formatted_issue.get("identifier", "")
|
issue_identifier = formatted_issue.get("identifier", "")
|
||||||
issue_title = formatted_issue.get("title", "")
|
issue_title = formatted_issue.get("title", "")
|
||||||
state = formatted_issue.get("state", "Unknown")
|
state = formatted_issue.get("state", "Unknown")
|
||||||
priority = issue_raw.get("priority", 0)
|
priority = issue_raw.get("priorityLabel", "Unknown")
|
||||||
comment_count = len(formatted_issue.get("comments", []))
|
comment_count = len(formatted_issue.get("comments", []))
|
||||||
description = formatted_issue.get("description", "")
|
description = formatted_issue.get("description", "")
|
||||||
|
|
||||||
|
|
@ -125,16 +125,20 @@ class LinearKBSyncService:
|
||||||
issue_content, search_space_id
|
issue_content, search_space_id
|
||||||
)
|
)
|
||||||
document.embedding = summary_embedding
|
document.embedding = summary_embedding
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
|
|
||||||
document.document_metadata = {
|
document.document_metadata = {
|
||||||
**document.document_metadata,
|
**(document.document_metadata or {}),
|
||||||
"issue_id": issue_id,
|
"issue_id": issue_id,
|
||||||
"issue_identifier": issue_identifier,
|
"issue_identifier": issue_identifier,
|
||||||
"issue_title": issue_title,
|
"issue_title": issue_title,
|
||||||
"state": state,
|
"state": state,
|
||||||
|
"priority": priority,
|
||||||
"comment_count": comment_count,
|
"comment_count": comment_count,
|
||||||
"indexed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
"indexed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"connector_id": connector_id,
|
"connector_id": connector_id,
|
||||||
}
|
}
|
||||||
|
flag_modified(document, "document_metadata")
|
||||||
safe_set_chunks(document, chunks)
|
safe_set_chunks(document, chunks)
|
||||||
document.updated_at = get_current_timestamp()
|
document.updated_at = get_current_timestamp()
|
||||||
|
|
||||||
|
|
@ -159,7 +163,7 @@ class LinearKBSyncService:
|
||||||
query = """
|
query = """
|
||||||
query LinearIssueSync($id: String!) {
|
query LinearIssueSync($id: String!) {
|
||||||
issue(id: $id) {
|
issue(id: $id) {
|
||||||
id identifier title description priority
|
id identifier title description priority priorityLabel
|
||||||
createdAt updatedAt url
|
createdAt updatedAt url
|
||||||
state { id name type color }
|
state { id name type color }
|
||||||
creator { id name email }
|
creator { id name email }
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,7 @@ class LinearToolMetadataService:
|
||||||
"""Fetch all teams with their states, members, and labels."""
|
"""Fetch all teams with their states, members, and labels."""
|
||||||
query = """
|
query = """
|
||||||
query {
|
query {
|
||||||
teams {
|
teams(first: 25) {
|
||||||
nodes {
|
nodes {
|
||||||
id name key
|
id name key
|
||||||
states { nodes { id name type color position } }
|
states { nodes { id name type color position } }
|
||||||
|
|
@ -248,7 +248,19 @@ class LinearToolMetadataService:
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
result = await client.execute_graphql_query(query)
|
result = await client.execute_graphql_query(query)
|
||||||
return result.get("data", {}).get("teams", {}).get("nodes", [])
|
raw_teams = result.get("data", {}).get("teams", {}).get("nodes", [])
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": t.get("id"),
|
||||||
|
"name": t.get("name"),
|
||||||
|
"key": t.get("key"),
|
||||||
|
"states": (t.get("states") or {}).get("nodes", []),
|
||||||
|
"members": (t.get("members") or {}).get("nodes", []),
|
||||||
|
"labels": (t.get("labels") or {}).get("nodes", []),
|
||||||
|
}
|
||||||
|
for t in raw_teams
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def _fetch_issue_context(
|
async def _fetch_issue_context(
|
||||||
|
|
@ -296,11 +308,15 @@ class LinearToolMetadataService:
|
||||||
SearchSourceConnector.user_id == user_id,
|
SearchSourceConnector.user_id == user_id,
|
||||||
or_(
|
or_(
|
||||||
func.lower(
|
func.lower(
|
||||||
Document.document_metadata["issue_title"].astext
|
Document.document_metadata.op("->>")(
|
||||||
|
"issue_title"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
== ref_lower,
|
== ref_lower,
|
||||||
func.lower(
|
func.lower(
|
||||||
Document.document_metadata["issue_identifier"].astext
|
Document.document_metadata.op("->>")(
|
||||||
|
"issue_identifier"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
== ref_lower,
|
== ref_lower,
|
||||||
func.lower(Document.title) == ref_lower,
|
func.lower(Document.title) == ref_lower,
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ function ApprovalCard({
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{workspaces.map((w) => (
|
{workspaces.map((w) => (
|
||||||
<SelectItem key={w.id} value={String(w.id)}>
|
<SelectItem key={w.id} value={String(w.id)}>
|
||||||
{w.organization_name}
|
{w.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -150,28 +150,31 @@ function ApprovalCard({
|
||||||
const priorities = context?.priorities ?? [];
|
const priorities = context?.priorities ?? [];
|
||||||
const issue = context?.issue;
|
const issue = context?.issue;
|
||||||
|
|
||||||
|
const initialEditState = {
|
||||||
|
title: actionArgs.new_title ? String(actionArgs.new_title) : (issue?.title ?? ""),
|
||||||
|
description: actionArgs.new_description
|
||||||
|
? String(actionArgs.new_description)
|
||||||
|
: (issue?.description ?? ""),
|
||||||
|
stateId: actionArgs.new_state_id
|
||||||
|
? String(actionArgs.new_state_id)
|
||||||
|
: (issue?.current_state?.id ?? "__none__"),
|
||||||
|
assigneeId: actionArgs.new_assignee_id
|
||||||
|
? String(actionArgs.new_assignee_id)
|
||||||
|
: (issue?.current_assignee?.id ?? "__none__"),
|
||||||
|
priority:
|
||||||
|
actionArgs.new_priority != null
|
||||||
|
? String(actionArgs.new_priority)
|
||||||
|
: String(issue?.priority ?? 0),
|
||||||
|
labelIds: Array.isArray(actionArgs.new_label_ids)
|
||||||
|
? (actionArgs.new_label_ids as string[])
|
||||||
|
: (issue?.current_labels?.map((l) => l.id) ?? []),
|
||||||
|
};
|
||||||
|
|
||||||
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
|
const [decided, setDecided] = useState<"approve" | "reject" | "edit" | null>(
|
||||||
interruptData.__decided__ ?? null
|
interruptData.__decided__ ?? null
|
||||||
);
|
);
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editedTitle, setEditedTitle] = useState(
|
const [editedArgs, setEditedArgs] = useState(initialEditState);
|
||||||
actionArgs.new_title ? String(actionArgs.new_title) : ""
|
|
||||||
);
|
|
||||||
const [editedDescription, setEditedDescription] = useState(
|
|
||||||
actionArgs.new_description ? String(actionArgs.new_description) : ""
|
|
||||||
);
|
|
||||||
const [selectedStateId, setSelectedStateId] = useState(
|
|
||||||
actionArgs.new_state_id ? String(actionArgs.new_state_id) : "__none__"
|
|
||||||
);
|
|
||||||
const [selectedAssigneeId, setSelectedAssigneeId] = useState(
|
|
||||||
actionArgs.new_assignee_id ? String(actionArgs.new_assignee_id) : "__none__"
|
|
||||||
);
|
|
||||||
const [selectedPriority, setSelectedPriority] = useState(
|
|
||||||
actionArgs.new_priority != null ? String(actionArgs.new_priority) : "__none__"
|
|
||||||
);
|
|
||||||
const [selectedLabelIds, setSelectedLabelIds] = useState<string[]>(
|
|
||||||
Array.isArray(actionArgs.new_label_ids) ? (actionArgs.new_label_ids as string[]) : []
|
|
||||||
);
|
|
||||||
|
|
||||||
const reviewConfig = interruptData.review_configs[0];
|
const reviewConfig = interruptData.review_configs[0];
|
||||||
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
const allowedDecisions = reviewConfig?.allowed_decisions ?? ["approve", "reject"];
|
||||||
|
|
@ -203,12 +206,13 @@ function ApprovalCard({
|
||||||
issue_id: issue?.id,
|
issue_id: issue?.id,
|
||||||
document_id: issue?.document_id,
|
document_id: issue?.document_id,
|
||||||
connector_id: context?.workspace?.id,
|
connector_id: context?.workspace?.id,
|
||||||
new_title: editedTitle || null,
|
new_title: editedArgs.title || null,
|
||||||
new_description: editedDescription || null,
|
new_description: editedArgs.description || null,
|
||||||
new_state_id: selectedStateId === "__none__" ? null : selectedStateId,
|
new_state_id: editedArgs.stateId === "__none__" ? null : editedArgs.stateId,
|
||||||
new_assignee_id: selectedAssigneeId === "__none__" ? null : selectedAssigneeId,
|
new_assignee_id: editedArgs.assigneeId === "__none__" ? null : editedArgs.assigneeId,
|
||||||
new_priority: selectedPriority === "__none__" ? null : Number(selectedPriority),
|
new_priority: Number(editedArgs.priority),
|
||||||
new_label_ids: labelsWereProposed || selectedLabelIds.length > 0 ? selectedLabelIds : null,
|
new_label_ids:
|
||||||
|
labelsWereProposed || editedArgs.labelIds.length > 0 ? editedArgs.labelIds : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,9 +417,9 @@ function ApprovalCard({
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
id="linear-update-title"
|
id="linear-update-title"
|
||||||
value={editedTitle}
|
value={editedArgs.title}
|
||||||
onChange={(e) => setEditedTitle(e.target.value)}
|
onChange={(e) => setEditedArgs({ ...editedArgs, title: e.target.value })}
|
||||||
placeholder="Leave empty to keep current title"
|
placeholder="Issue title"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -424,13 +428,13 @@ function ApprovalCard({
|
||||||
htmlFor="linear-update-description"
|
htmlFor="linear-update-description"
|
||||||
className="text-xs font-medium text-muted-foreground mb-1.5 block"
|
className="text-xs font-medium text-muted-foreground mb-1.5 block"
|
||||||
>
|
>
|
||||||
New Description
|
Description
|
||||||
</label>
|
</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="linear-update-description"
|
id="linear-update-description"
|
||||||
value={editedDescription}
|
value={editedArgs.description}
|
||||||
onChange={(e) => setEditedDescription(e.target.value)}
|
onChange={(e) => setEditedArgs({ ...editedArgs, description: e.target.value })}
|
||||||
placeholder="Leave empty to keep current description"
|
placeholder="Issue description"
|
||||||
rows={5}
|
rows={5}
|
||||||
className="resize-none"
|
className="resize-none"
|
||||||
/>
|
/>
|
||||||
|
|
@ -440,12 +444,14 @@ function ApprovalCard({
|
||||||
<>
|
<>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-xs font-medium text-muted-foreground">State</div>
|
<div className="text-xs font-medium text-muted-foreground">State</div>
|
||||||
<Select value={selectedStateId} onValueChange={setSelectedStateId}>
|
<Select
|
||||||
|
value={editedArgs.stateId}
|
||||||
|
onValueChange={(v) => setEditedArgs({ ...editedArgs, stateId: v })}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Unchanged" />
|
<SelectValue placeholder="Select state" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__none__">Unchanged</SelectItem>
|
|
||||||
{team.states.map((s) => (
|
{team.states.map((s) => (
|
||||||
<SelectItem key={s.id} value={s.id}>
|
<SelectItem key={s.id} value={s.id}>
|
||||||
{s.name}
|
{s.name}
|
||||||
|
|
@ -457,12 +463,15 @@ function ApprovalCard({
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Assignee</div>
|
<div className="text-xs font-medium text-muted-foreground">Assignee</div>
|
||||||
<Select value={selectedAssigneeId} onValueChange={setSelectedAssigneeId}>
|
<Select
|
||||||
|
value={editedArgs.assigneeId}
|
||||||
|
onValueChange={(v) => setEditedArgs({ ...editedArgs, assigneeId: v })}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Unchanged" />
|
<SelectValue placeholder="Select assignee" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__none__">Unchanged</SelectItem>
|
<SelectItem value="__none__">Unassigned</SelectItem>
|
||||||
{team.members
|
{team.members
|
||||||
.filter((m) => m.active)
|
.filter((m) => m.active)
|
||||||
.map((m) => (
|
.map((m) => (
|
||||||
|
|
@ -476,12 +485,14 @@ function ApprovalCard({
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="text-xs font-medium text-muted-foreground">Priority</div>
|
<div className="text-xs font-medium text-muted-foreground">Priority</div>
|
||||||
<Select value={selectedPriority} onValueChange={setSelectedPriority}>
|
<Select
|
||||||
|
value={editedArgs.priority}
|
||||||
|
onValueChange={(v) => setEditedArgs({ ...editedArgs, priority: v })}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Unchanged" />
|
<SelectValue placeholder="Select priority" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="__none__">Unchanged</SelectItem>
|
|
||||||
{priorities.map((p) => (
|
{priorities.map((p) => (
|
||||||
<SelectItem key={p.priority} value={String(p.priority)}>
|
<SelectItem key={p.priority} value={String(p.priority)}>
|
||||||
{p.label}
|
{p.label}
|
||||||
|
|
@ -496,17 +507,18 @@ function ApprovalCard({
|
||||||
<div className="text-xs font-medium text-muted-foreground">Labels</div>
|
<div className="text-xs font-medium text-muted-foreground">Labels</div>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{team.labels.map((label) => {
|
{team.labels.map((label) => {
|
||||||
const isSelected = selectedLabelIds.includes(label.id);
|
const isSelected = editedArgs.labelIds.includes(label.id);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={label.id}
|
key={label.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSelectedLabelIds((prev) =>
|
setEditedArgs({
|
||||||
isSelected
|
...editedArgs,
|
||||||
? prev.filter((id) => id !== label.id)
|
labelIds: isSelected
|
||||||
: [...prev, label.id]
|
? editedArgs.labelIds.filter((id) => id !== label.id)
|
||||||
)
|
: [...editedArgs.labelIds, label.id],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-opacity ${
|
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium transition-opacity ${
|
||||||
isSelected
|
isSelected
|
||||||
|
|
@ -578,24 +590,7 @@ function ApprovalCard({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setEditedTitle(actionArgs.new_title ? String(actionArgs.new_title) : "");
|
setEditedArgs(initialEditState); // Reset to original args
|
||||||
setEditedDescription(
|
|
||||||
actionArgs.new_description ? String(actionArgs.new_description) : ""
|
|
||||||
);
|
|
||||||
setSelectedStateId(
|
|
||||||
actionArgs.new_state_id ? String(actionArgs.new_state_id) : "__none__"
|
|
||||||
);
|
|
||||||
setSelectedAssigneeId(
|
|
||||||
actionArgs.new_assignee_id ? String(actionArgs.new_assignee_id) : "__none__"
|
|
||||||
);
|
|
||||||
setSelectedPriority(
|
|
||||||
actionArgs.new_priority != null ? String(actionArgs.new_priority) : "__none__"
|
|
||||||
);
|
|
||||||
setSelectedLabelIds(
|
|
||||||
Array.isArray(actionArgs.new_label_ids)
|
|
||||||
? (actionArgs.new_label_ids as string[])
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue