Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements the ability to create, view, and delete issue links between tickets in both Linear and Jira platforms. Users can now search for issues and establish relationships like "blocks," "relates to," or "duplicates" directly from the ticket panel.
Key changes:
- Added UI components for managing issue relationships with search, create, and delete functionality
- Implemented backend API methods for creating and deleting issue links in both Linear and Jira clients
- Extended type definitions to support issue relations and link types across platforms
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| webview-ui/src/shared/types/messages.ts | Added type definitions for issue relations, search results, and new message commands |
| webview-ui/src/linear/ticket-panel/components/IssueRelationsSection.tsx | New component for Linear issue relations with search, create, and delete UI |
| webview-ui/src/linear/ticket-panel/components/IssueRelationsSection.module.css | Styles for Linear issue relations component |
| webview-ui/src/linear/ticket-panel/App.tsx | Integrated IssueRelationsSection into Linear ticket panel |
| webview-ui/src/jira/ticket-panel/components/IssueLinksSection.tsx | Enhanced existing Jira links section with create/delete functionality |
| webview-ui/src/jira/ticket-panel/components/IssueLinksSection.module.css | Added styles for Jira link creation and deletion UI |
| webview-ui/src/jira/ticket-panel/App.tsx | Added link types, search results state, and new message handlers |
| src/shared/base/BaseTicketProvider.ts | Added base interfaces for issue links and link types |
| src/providers/linear/types.ts | Added Linear-specific relation types and interfaces |
| src/providers/linear/LinearTicketPanel.ts | Added handlers for searching, creating, and deleting issue relations |
| src/providers/linear/LinearClient.ts | Implemented GraphQL mutations and queries for issue relations |
| src/providers/jira/server/JiraServerClient.ts | Added REST API methods for managing Jira issue links |
| src/providers/jira/common/BaseJiraClient.ts | Added abstract methods for issue link operations |
| src/providers/jira/cloud/JiraTicketPanel.ts | Added message handlers for link operations |
| src/providers/jira/cloud/JiraCloudClient.ts | Implemented Jira Cloud API methods for issue links |
| docs/planning/ROADMAP_1.0.0.md | Updated roadmap to mark issue links feature as complete |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| <div className={styles.linksList}> | ||
| {links.map((link) => ( | ||
| <div key={link.id} className={styles.linkItemWrapper}> |
There was a problem hiding this comment.
The key attribute was moved from the <a> element to the parent <div>. While this works, it would be more maintainable to keep the key on the direct child of the map function. Consider keeping the key on the <a> element and using a different approach for the wrapper, or ensure this change is intentional and documented.
| input: { | ||
| issueId: "${issueId}", | ||
| relatedIssueId: "${relatedIssueId}", | ||
| type: ${type} |
There was a problem hiding this comment.
The GraphQL mutation is vulnerable to injection attacks because the type parameter is directly interpolated into the query string without proper escaping. Although the type is constrained to specific values in the function signature, this should use GraphQL variables for safer query construction.
src/providers/linear/LinearClient.ts
Outdated
| { identifier: { containsIgnoreCase: "${searchTerm}" } }, | ||
| { title: { containsIgnoreCase: "${searchTerm}" } } |
There was a problem hiding this comment.
The search term is directly interpolated into the GraphQL query without sanitization, which could allow GraphQL injection attacks. Use parameterized queries or properly escape the search term to prevent potential exploits.
|
|
||
| try { | ||
| // Search using JQL: key or summary contains the search term | ||
| const jql = `(key ~ "${searchTerm}" OR summary ~ "${searchTerm}") ORDER BY updated DESC`; |
There was a problem hiding this comment.
The search term is directly interpolated into the JQL query without escaping, creating a JQL injection vulnerability. Escape special characters in the search term or use Jira's query builder methods to safely construct the JQL.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 21 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
webview-ui/src/jira/ticket-panel/components/IssueLinksSection.tsx:1
- Using an anchor tag with href='#' is not ideal for interactive elements that don't navigate. Consider using a button element instead for better semantics and accessibility.
import React, { useState, useEffect, useCallback } from "react";
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| issueId: "${issueId}", | ||
| relatedIssueId: "${relatedIssueId}", |
There was a problem hiding this comment.
String interpolation in GraphQL mutations creates SQL-injection-like vulnerabilities. The IDs should be passed as GraphQL variables instead of being directly interpolated into the query string, even though validation exists.
| mutation { | ||
| issueRelationDelete(id: "${relationId}") { | ||
| success | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| try { | ||
| const response = await this.executeQuery(mutation, { skipCache: true }); |
There was a problem hiding this comment.
Direct string interpolation of relationId in GraphQL mutation is a security risk. Use GraphQL variables to pass parameters safely.
| mutation { | |
| issueRelationDelete(id: "${relationId}") { | |
| success | |
| } | |
| } | |
| `; | |
| try { | |
| const response = await this.executeQuery(mutation, { skipCache: true }); | |
| mutation DeleteIssueRelation($relationId: String!) { | |
| issueRelationDelete(id: $relationId) { | |
| success | |
| } | |
| } | |
| `; | |
| try { | |
| const response = await this.executeQuery(mutation, { | |
| variables: { relationId }, | |
| skipCache: true, | |
| }); |
| // JQL requires escaping of: \ " ' space | ||
| const escapeJQL = (term: string): string => { | ||
| return term | ||
| .replace(/\\/g, '\\\\') // Escape backslashes first | ||
| .replace(/"/g, '\\"') // Escape double quotes | ||
| .replace(/'/g, "\\'") // Escape single quotes | ||
| .replace(/\s/g, '\\ '); // Escape spaces |
There was a problem hiding this comment.
The JQL escaping function does not escape all special JQL characters. Characters like [, ], (, ), and : can still be used to inject JQL syntax. Consider using a more comprehensive escaping approach or parameterized queries if available.
| // JQL requires escaping of: \ " ' space | |
| const escapeJQL = (term: string): string => { | |
| return term | |
| .replace(/\\/g, '\\\\') // Escape backslashes first | |
| .replace(/"/g, '\\"') // Escape double quotes | |
| .replace(/'/g, "\\'") // Escape single quotes | |
| .replace(/\s/g, '\\ '); // Escape spaces | |
| // JQL requires escaping of: + - & | ! ( ) { } [ ] ^ ~ * ? : \ / " ' and space | |
| const escapeJQL = (term: string): string => { | |
| return term | |
| // Escape all JQL special characters by prefixing them with a backslash | |
| .replace(/([+\-&|!(){}\[\]^~*?:\\\/"])/g, "\\$1") | |
| .replace(/'/g, "\\'") // Escape single quotes | |
| .replace(/\s/g, "\\ "); // Escape spaces |
| const selectedLinkType = userSelectedLinkTypeId | ||
| ? linkTypes?.find(t => t.id === userSelectedLinkTypeId) ?? linkTypes?.[0] ?? null | ||
| : linkTypes?.[0] ?? null; |
There was a problem hiding this comment.
The fallback logic for selectedLinkType is complex and could lead to unexpected behavior if userSelectedLinkTypeId points to a non-existent type. Consider simplifying by validating that userSelectedLinkTypeId exists in linkTypes before using it.
src/providers/linear/LinearClient.ts
Outdated
| const escapedSearchTerm = searchTerm | ||
| .replace(/\\/g, '\\\\') // Escape backslashes | ||
| .replace(/"/g, '\\"'); // Escape quotes |
There was a problem hiding this comment.
The GraphQL string escaping is duplicated from the searchUsers function and only handles backslashes and quotes. Additional control characters should be escaped. Consider extracting this into a reusable utility function with comprehensive escaping.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 22 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| issueRelationCreate( | ||
| input: { | ||
| issueId: "${issueId}", | ||
| relatedIssueId: "${relatedIssueId}", |
There was a problem hiding this comment.
The type parameter is directly interpolated into the GraphQL query without proper escaping. While there is validation that checks against allowed values, this should use GraphQL variables instead to prevent any potential injection. Replace the string interpolation with proper GraphQL variable syntax.
| issueRelationCreate( | ||
| input: { | ||
| issueId: "${issueId}", | ||
| relatedIssueId: "${relatedIssueId}", |
There was a problem hiding this comment.
The issueId and relatedIssueId parameters are interpolated directly into the GraphQL query string without using GraphQL variables. This creates a potential GraphQL injection vulnerability. Use GraphQL variables with proper type definitions instead of string interpolation.
| <a | ||
| href="#" | ||
| onClick={(e) => handleClick(e, link.linkedIssue.key)} |
There was a problem hiding this comment.
The link wrapper has been changed to not be the key element. The anchor tag should have a more semantic approach or be replaced with a button element since it's handling click events rather than actual navigation. Buttons are more appropriate for actions and provide better accessibility semantics.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 22 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // eslint-disable-next-line no-useless-escape | ||
| .replace(/([+\-&|!(){}\[\]^~*?:\\/"])/g, '\\$1') // Escape JQL operators |
There was a problem hiding this comment.
The regex pattern escapes the backslash in \\/ which eslint flags as unnecessary since forward slashes don't need escaping in character classes. Consider removing the backslash before the forward slash: .replace(/([+\-&|!(){}\[\]^~*?:/"])/g, '\\$1') to eliminate the need for the eslint-disable comment.
| const selectedLinkType = userSelectedLinkTypeId | ||
| ? linkTypes?.find(t => t.id === userSelectedLinkTypeId) ?? linkTypes?.[0] ?? null | ||
| : linkTypes?.[0] ?? null; | ||
|
|
There was a problem hiding this comment.
The fallback chain linkTypes?.find(...) ?? linkTypes?.[0] ?? null could result in unexpected behavior if the user-selected ID doesn't exist but linkTypes has items. In this case, it correctly falls back to the first link type, but this logic could be clearer by explicitly checking if the found type exists before falling back.
| const selectedLinkType = userSelectedLinkTypeId | |
| ? linkTypes?.find(t => t.id === userSelectedLinkTypeId) ?? linkTypes?.[0] ?? null | |
| : linkTypes?.[0] ?? null; | |
| const selectedLinkType = (() => { | |
| // If no link types are available, there is nothing to select | |
| if (!linkTypes || linkTypes.length === 0) { | |
| return null; | |
| } | |
| // If the user has not explicitly selected a type, default to the first | |
| if (!userSelectedLinkTypeId) { | |
| return linkTypes[0]; | |
| } | |
| // Try to find the user-selected type; if not found, explicitly fall back to the first | |
| const foundType = linkTypes.find(t => t.id === userSelectedLinkTypeId); | |
| return foundType ?? linkTypes[0]; | |
| })(); |
No description provided.