Skip to content

Commit 4d19d2b

Browse files
committed
feat: Add remote links to issue view
Adds a list of remote links to the issue view, if they exist, right below the issue links.
1 parent adab79f commit 4d19d2b

File tree

6 files changed

+122
-4
lines changed

6 files changed

+122
-4
lines changed

api/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ func ProxyGetIssueRaw(c *jira.Client, key string) (string, error) {
107107
// ProxyGetIssue uses either a v2 or v3 version of the Jira GET /issue/{key}
108108
// endpoint to fetch the issue details based on configured installation type.
109109
// Defaults to v3 if installation type is not defined in the config.
110+
// Also fetches remote links for the issue.
110111
func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Issue, error) {
111112
var (
112113
iss *jira.Issue
@@ -121,6 +122,19 @@ func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Iss
121122
iss, err = c.GetIssue(key, opts...)
122123
}
123124

125+
if err != nil {
126+
return iss, err
127+
}
128+
129+
// Fetch remote links for the issue
130+
remoteLinks, err := c.GetIssueRemoteLinks(key)
131+
if err != nil {
132+
// Don't fail the entire request if remote links can't be fetched
133+
// Just log and continue without remote links
134+
remoteLinks = []jira.RemoteLink{}
135+
}
136+
iss.Fields.RemoteLinks = remoteLinks
137+
124138
return iss, err
125139
}
126140

go.sum

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2
2222
github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM=
2323
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
2424
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
25-
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
25+
github.com/charmbracelet/glamour v0.9.1 h1:Q7PdJLOx8EoepsXUvW6Puz5WQ3YUElIGQdYKrIpiGLA=
2626
github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
2727
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
2828
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=

internal/cmd/issue/view/view.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package view
22

33
import (
4+
"encoding/json"
45
"fmt"
56

67
"github.com/spf13/cobra"
@@ -78,7 +79,20 @@ func viewRaw(cmd *cobra.Command, args []string) {
7879
defer s.Stop()
7980

8081
client := api.DefaultClient(debug)
81-
return api.ProxyGetIssueRaw(client, key)
82+
83+
// Fetch the issue with remote links included (same as structured view)
84+
issue, err := api.ProxyGetIssue(client, key)
85+
if err != nil {
86+
return "", err
87+
}
88+
89+
// Convert back to JSON for raw output
90+
rawJSON, err := json.MarshalIndent(issue, "", " ")
91+
if err != nil {
92+
return "", err
93+
}
94+
95+
return string(rawJSON), nil
8296
}()
8397
cmdutil.ExitIfError(err)
8498

internal/view/issue.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ func (i Issue) String() string {
109109
if len(i.Data.Fields.IssueLinks) > 0 {
110110
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n", i.separator("Linked Issues"), i.linkedIssues()))
111111
}
112+
if len(i.Data.Fields.RemoteLinks) > 0 {
113+
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n", i.separator("External Links"), i.remoteLinks()))
114+
}
112115
total := i.Data.Fields.Comment.Total
113116
if total > 0 && i.Options.NumComments > 0 {
114117
sep := fmt.Sprintf("%d Comments", total)
@@ -160,6 +163,17 @@ func (i Issue) fragments() []fragment {
160163
)
161164
}
162165

166+
if len(i.Data.Fields.RemoteLinks) > 0 {
167+
scraps = append(
168+
scraps,
169+
newBlankFragment(1),
170+
fragment{Body: i.separator("External Links")},
171+
newBlankFragment(2),
172+
fragment{Body: i.remoteLinks()},
173+
newBlankFragment(1),
174+
)
175+
}
176+
163177
if i.Data.Fields.Comment.Total > 0 && i.Options.NumComments > 0 {
164178
scraps = append(
165179
scraps,
@@ -378,6 +392,40 @@ func (i Issue) linkedIssues() string {
378392
return linked.String()
379393
}
380394

395+
func (i Issue) remoteLinks() string {
396+
if len(i.Data.Fields.RemoteLinks) == 0 {
397+
return ""
398+
}
399+
400+
var (
401+
remote strings.Builder
402+
maxTitleLen int
403+
summaryLen = defaultSummaryLength
404+
)
405+
406+
// Calculate max lengths for formatting
407+
for _, link := range i.Data.Fields.RemoteLinks {
408+
maxTitleLen = max(len(link.Object.Title), maxTitleLen)
409+
}
410+
411+
if maxTitleLen < summaryLen {
412+
summaryLen = maxTitleLen
413+
}
414+
415+
remote.WriteString("\n")
416+
for _, link := range i.Data.Fields.RemoteLinks {
417+
remote.WriteString(
418+
fmt.Sprintf(
419+
" %s\n %s\n\n",
420+
coloredOut(shortenAndPad(link.Object.Title, summaryLen), color.FgCyan, color.Bold),
421+
coloredOut(link.Object.URL, color.FgBlue, color.Underline),
422+
),
423+
)
424+
}
425+
426+
return remote.String()
427+
}
428+
381429
func (i Issue) comments() []issueComment {
382430
total := i.Data.Fields.Comment.Total
383431
comments := make([]issueComment, 0, total)

pkg/jira/issue.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,37 @@ func (c *Client) RemoteLinkIssue(issueID, title, url string) error {
460460
return nil
461461
}
462462

463+
// GetIssueRemoteLinks fetches remote links for an issue using GET /issue/{issueId}/remotelink endpoint.
464+
func (c *Client) GetIssueRemoteLinks(issueID string) ([]RemoteLink, error) {
465+
path := fmt.Sprintf("/issue/%s/remotelink", issueID)
466+
467+
res, err := c.GetV2(context.Background(), path, nil)
468+
if err != nil {
469+
return nil, err
470+
}
471+
if res == nil {
472+
return nil, ErrEmptyResponse
473+
}
474+
defer func() { _ = res.Body.Close() }()
475+
476+
if res.StatusCode != http.StatusOK {
477+
return nil, formatUnexpectedResponse(res)
478+
}
479+
480+
body, err := io.ReadAll(res.Body)
481+
if err != nil {
482+
return nil, err
483+
}
484+
485+
var remoteLinks []RemoteLink
486+
err = json.Unmarshal(body, &remoteLinks)
487+
if err != nil {
488+
return nil, err
489+
}
490+
491+
return remoteLinks, nil
492+
}
493+
463494
// WatchIssue adds user as a watcher using v2 version of the POST /issue/{key}/watchers endpoint.
464495
func (c *Client) WatchIssue(key, watcher string) error {
465496
return c.watchIssue(key, watcher, apiVersion3)

pkg/jira/types.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ type IssueFields struct {
122122
InwardIssue *Issue `json:"inwardIssue,omitempty"`
123123
OutwardIssue *Issue `json:"outwardIssue,omitempty"`
124124
} `json:"issueLinks"`
125-
Created string `json:"created"`
126-
Updated string `json:"updated"`
125+
RemoteLinks []RemoteLink `json:"remoteLinks,omitempty"`
126+
Created string `json:"created"`
127+
Updated string `json:"updated"`
127128
}
128129

129130
// Field holds field info.
@@ -165,6 +166,16 @@ type IssueLinkType struct {
165166
Outward string `json:"outward"`
166167
}
167168

169+
// RemoteLink holds remote link info.
170+
type RemoteLink struct {
171+
ID int `json:"id"`
172+
Self string `json:"self"`
173+
Object struct {
174+
URL string `json:"url"`
175+
Title string `json:"title"`
176+
} `json:"object"`
177+
}
178+
168179
// Sprint holds sprint info.
169180
type Sprint struct {
170181
ID int `json:"id"`

0 commit comments

Comments
 (0)