Skip to content

Commit

Permalink
feat: Add an option to load N comments when viewing issue (ankitpokhr…
Browse files Browse the repository at this point in the history
  • Loading branch information
ankitpokhrel authored Dec 12, 2021
1 parent 23fb170 commit e229260
Show file tree
Hide file tree
Showing 13 changed files with 238 additions and 64 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ $ jira issue view ISSUE-1
The view screen will display linked issues and the latest comment after description. Note that the displayed comment may
not be the latest one if you for some reason have more than 5k comments in a ticket.

```sh
# Show 5 recent comments when viewing the issue
$ jira issue view ISSUE-1 --comments 5
```

#### Link
The `link` command lets you link two issues.

Expand Down
13 changes: 7 additions & 6 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/spf13/viper"

"github.com/ankitpokhrel/jira-cli/pkg/jira"
"github.com/ankitpokhrel/jira-cli/pkg/jira/filter"
)

const clientTimeout = 15 * time.Second
Expand Down Expand Up @@ -56,21 +57,21 @@ func ProxyCreate(c *jira.Client, cr *jira.CreateRequest) (*jira.CreateResponse,
// ProxyGetIssue uses either a v2 or v3 version of the Jira GET /issue/{key}
// endpoint to fetch the issue details based on configured installation type.
// Defaults to v3 if installation type is not defined in the config.
func ProxyGetIssue(c *jira.Client, key string) (*jira.Issue, error) {
func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Issue, error) {
var (
issue *jira.Issue
err error
iss *jira.Issue
err error
)

it := viper.GetString("installation")

if it == jira.InstallationTypeLocal {
issue, err = c.GetIssueV2(key)
iss, err = c.GetIssueV2(key, opts...)
} else {
issue, err = c.GetIssue(key)
iss, err = c.GetIssue(key, opts...)
}

return issue, err
return iss, err
}

// ProxySearch uses either a v2 or v3 version of the Jira GET /search endpoint
Expand Down
17 changes: 13 additions & 4 deletions internal/cmd/issue/view/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import (
"github.com/ankitpokhrel/jira-cli/internal/cmdutil"
tuiView "github.com/ankitpokhrel/jira-cli/internal/view"
"github.com/ankitpokhrel/jira-cli/pkg/jira"
"github.com/ankitpokhrel/jira-cli/pkg/jira/filter/issue"
)

const (
helpText = `View displays contents of an issue.`
examples = `$ jira issue view ISSUE-1`
examples = `$ jira issue view ISSUE-1
# Show 5 recent comments when viewing the issue
$ jira issue view ISSUE-1 --comments 5`
)

// NewCmdView is a view command.
Expand All @@ -30,6 +34,7 @@ func NewCmdView() *cobra.Command {
Run: view,
}

cmd.Flags().Uint("comments", 1, "Show N comments")
cmd.Flags().Bool("plain", false, "Display output in plain mode")

return &cmd
Expand All @@ -39,13 +44,16 @@ func view(cmd *cobra.Command, args []string) {
debug, err := cmd.Flags().GetBool("debug")
cmdutil.ExitIfError(err)

comments, err := cmd.Flags().GetUint("comments")
cmdutil.ExitIfError(err)

key := cmdutil.GetJiraIssueKey(viper.GetString("project.key"), args[0])
issue, err := func() (*jira.Issue, error) {
iss, err := func() (*jira.Issue, error) {
s := cmdutil.Info("Fetching issue details...")
defer s.Stop()

client := api.Client(jira.Config{Debug: debug})
return api.ProxyGetIssue(client, key)
return api.ProxyGetIssue(client, key, issue.NewNumCommentsFilter(comments))
}()
cmdutil.ExitIfError(err)

Expand All @@ -54,8 +62,9 @@ func view(cmd *cobra.Command, args []string) {

v := tuiView.Issue{
Server: viper.GetString("server"),
Data: issue,
Data: iss,
Display: tuiView.DisplayFormat{Plain: plain},
Options: tuiView.IssueOption{NumComments: comments},
}
cmdutil.ExitIfError(v.Render())
}
10 changes: 6 additions & 4 deletions internal/view/epic.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/ankitpokhrel/jira-cli/api"
"github.com/ankitpokhrel/jira-cli/pkg/jira"
"github.com/ankitpokhrel/jira-cli/pkg/jira/filter/issue"
"github.com/ankitpokhrel/jira-cli/pkg/tui"
)

Expand Down Expand Up @@ -39,13 +40,14 @@ func (el EpicList) Render() error {
dataFn := func() interface{} {
data := d.(tui.TableData)
ci := getKeyColumnIndex(data[0])
issue, _ := api.ProxyGetIssue(api.Client(jira.Config{}), data[r][ci])
return issue
iss, _ := api.ProxyGetIssue(api.Client(jira.Config{}), data[r][ci], issue.NewNumCommentsFilter(1))
return iss
}
renderFn := func(i interface{}) (string, error) {
iss := Issue{
Server: el.Server,
Data: i.(*jira.Issue),
Server: el.Server,
Data: i.(*jira.Issue),
Options: IssueOption{NumComments: 1},
}
return iss.RenderedOut(renderer)
}
Expand Down
111 changes: 81 additions & 30 deletions internal/view/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,22 @@ func newBlankFragment(n int) fragment {
}
}

type issueComment struct {
meta string
body string
}

// IssueOption is filtering options for an issue.
type IssueOption struct {
NumComments uint
}

// Issue is a list view for issues.
type Issue struct {
Server string
Data *jira.Issue
Display DisplayFormat
Options IssueOption
}

// Render renders the view.
Expand Down Expand Up @@ -87,10 +98,13 @@ func (i Issue) String() string {
if len(i.Data.Fields.IssueLinks) > 0 {
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n", i.separator("Linked Issues"), i.linkedIssues()))
}
if i.Data.Fields.Comment.Total > 0 {
author, body := i.comment()
sep := fmt.Sprintf("%d comments", i.Data.Fields.Comment.Total)
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n\n%s\n", i.separator(sep), author, body))
total := i.Data.Fields.Comment.Total
if total > 0 && i.Options.NumComments > 0 {
sep := fmt.Sprintf("%d Comments", total)
s.WriteString(fmt.Sprintf("\n\n%s", i.separator(sep)))
for _, comment := range i.comments() {
s.WriteString(fmt.Sprintf("\n\n%s\n\n%s\n", comment.meta, comment.body))
}
}
s.WriteString(i.footer())

Expand Down Expand Up @@ -124,17 +138,21 @@ func (i Issue) fragments() []fragment {
)
}

if i.Data.Fields.Comment.Total > 0 {
author, body := i.comment()
if i.Data.Fields.Comment.Total > 0 && i.Options.NumComments > 0 {
scraps = append(
scraps,
newBlankFragment(1),
fragment{Body: i.separator(fmt.Sprintf("%d comments", i.Data.Fields.Comment.Total))},
fragment{Body: i.separator(fmt.Sprintf("%d Comments", i.Data.Fields.Comment.Total))},
newBlankFragment(2),
fragment{Body: author},
newBlankFragment(1),
fragment{Body: body, Parse: true},
)
for _, comment := range i.comments() {
scraps = append(
scraps,
fragment{Body: comment.meta},
newBlankFragment(1),
fragment{Body: comment.body, Parse: true},
)
}
}

return append(scraps, newBlankFragment(1), fragment{Body: i.footer()}, newBlankFragment(2))
Expand Down Expand Up @@ -191,7 +209,7 @@ func (i Issue) header() string {
wch = fmt.Sprintf("You + %d watchers", i.Data.Fields.Watches.WatchCount-1)
}
return fmt.Sprintf(
"%s %s %s %s ⌛ %s 👷 %s 🔑️ %s 💭 %d comments \U0001F9F5 %d linked issues\n# %s\n⏱️ %s 🔎 %s 🚀 %s 📦 %s 🏷️ %s 👀 %s",
"%s %s %s %s ⌛ %s 👷 %s 🔑️ %s 💭 %d comments \U0001F9F5 %d linked\n# %s\n⏱️ %s 🔎 %s 🚀 %s 📦 %s 🏷️ %s 👀 %s",
iti, it, sti, st, cmdutil.FormatDateTimeHuman(i.Data.Fields.Updated, jira.RFC3339), as, i.Data.Key,
i.Data.Fields.Comment.Total, len(i.Data.Fields.IssueLinks),
i.Data.Fields.Summary,
Expand Down Expand Up @@ -273,12 +291,12 @@ func (i Issue) linkedIssues() string {

for _, k := range keys {
linked.WriteString(
fmt.Sprintf("\n %s\n\n", coloredOut(strings.ToUpper(k), color.FgWhite, color.Bold)),
fmt.Sprintf("\n %s\n\n", coloredOut(strings.ToUpper(k), color.FgWhite, color.Bold)),
)
for _, iss := range linkMap[k] {
linked.WriteString(
fmt.Sprintf(
" %s %s • %s • %s • %s\n",
" %s %s • %s • %s • %s\n",
coloredOut(pad(iss.Key, maxKeyLen), color.FgGreen, color.Bold),
shortenAndPad(iss.Fields.Summary, summaryLen),
pad(iss.Fields.IssueType.Name, maxTypeLen),
Expand All @@ -292,28 +310,61 @@ func (i Issue) linkedIssues() string {
return linked.String()
}

func (i Issue) comment() (string, string) {
if i.Data.Fields.Comment.Total == 0 {
return "", ""
func (i Issue) comments() []issueComment {
comments := make([]issueComment, 0, i.Options.NumComments)

total := i.Data.Fields.Comment.Total
if total == 0 {
return comments
}
latestComment := i.Data.Fields.Comment.Comments[i.Data.Fields.Comment.Total-1]
var body string
if adfNode, ok := latestComment.Body.(*adf.ADF); ok {
body = adf.NewTranslator(adfNode, adf.NewMarkdownTranslator()).Translate()
} else {
body = latestComment.Body.(string)
body = md.FromJiraMD(body)

limit := int(i.Options.NumComments)
if limit > total {
limit = total
}
return fmt.Sprintf(
"\n %s • %s • %s",
coloredOut(latestComment.Author.Name, color.FgWhite, color.Bold),
coloredOut(cmdutil.FormatDateTimeHuman(latestComment.Created, jira.RFC3339), color.FgWhite, color.Bold),
coloredOut("Latest comment", color.FgCyan, color.Bold),
), body

for idx := total - 1; idx >= total-limit; idx-- {
c := i.Data.Fields.Comment.Comments[idx]
var body string
if adfNode, ok := c.Body.(*adf.ADF); ok {
body = adf.NewTranslator(adfNode, adf.NewMarkdownTranslator()).Translate()
} else {
body = c.Body.(string)
body = md.FromJiraMD(body)
}
meta := fmt.Sprintf(
"\n %s • %s",
coloredOut(c.Author.Name, color.FgWhite, color.Bold),
coloredOut(cmdutil.FormatDateTimeHuman(c.Created, jira.RFC3339), color.FgWhite, color.Bold),
)
if idx == total-1 {
meta += fmt.Sprintf(" • %s", coloredOut("Latest comment", color.FgCyan, color.Bold))
}
comments = append(comments, issueComment{
meta: meta,
body: body,
})
}

return comments
}

func (i Issue) footer() string {
return gray(fmt.Sprintf("View this issue on Jira: %s/browse/%s", i.Server, i.Data.Key))
var out strings.Builder

nc := int(i.Options.NumComments)
if i.Data.Fields.Comment.Total > 0 && nc > 0 && nc < i.Data.Fields.Comment.Total {
if i.Display.Plain {
out.WriteString("\n")
}
out.WriteString(fmt.Sprintf("%s\n", gray("Use --comments <limit> with `jira issue view` to load more comments")))
}
if i.Display.Plain {
out.WriteString("\n")
}
out.WriteString(gray(fmt.Sprintf("View this issue on Jira: %s/browse/%s", i.Server, i.Data.Key)))

return out.String()
}

// renderPlain renders the issue in plain view.
Expand Down
7 changes: 5 additions & 2 deletions internal/view/issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestIssueDetailsRenderInPlainView(t *testing.T) {
Display: DisplayFormat{Plain: true},
}

expected := "🐞 Bug ✅ Done ⌛ Sun, 13 Dec 20 👷 Person A 🔑️ TEST-1 💭 0 comments \U0001F9F5 0 linked issues\n# This is a test\n⏱️ Sun, 13 Dec 20 🔎 Person Z 🚀 High 📦 BE, FE 🏷️ None 👀 You + 3 watchers\n\n------------------------ Description ------------------------\n\nTest description\n\n"
expected := "🐞 Bug ✅ Done ⌛ Sun, 13 Dec 20 👷 Person A 🔑️ TEST-1 💭 0 comments \U0001F9F5 0 linked\n# This is a test\n⏱️ Sun, 13 Dec 20 🔎 Person Z 🚀 High 📦 BE, FE 🏷️ None 👀 You + 3 watchers\n\n------------------------ Description ------------------------\n\nTest description\n\n\n"
if xterm256() {
expected += "\x1b[38;5;242mView this issue on Jira: https://test.local/browse/TEST-1\x1b[m"
} else {
Expand Down Expand Up @@ -198,13 +198,16 @@ func TestIssueDetailsWithV2Description(t *testing.T) {
Server: "https://test.local",
Data: data,
Display: DisplayFormat{Plain: true},
Options: IssueOption{NumComments: 2},
}
assert.NoError(t, issue.renderPlain(&b))

expected := "🐞 Bug ✅ Done ⌛ Sun, 13 Dec 20 👷 Person A 🔑️ TEST-1 💭 3 comments \U0001F9F5 2 linked issues\n# This is a test\n⏱️ Sun, 13 Dec 20 🔎 Person Z 🚀 High 📦 BE, FE 🏷️ None 👀 0 watchers\n\n------------------------ Description ------------------------\n\n# Title\n## Subtitle\nThis is a **bold** and _italic_ text with [a link](https://ankit.pl) in between.\n\n\n------------------------ Linked Issues ------------------------\n\n\n BLOCKS\n\n TEST-2 Something is broken • Bug • High • TO DO\n\n RELATES TO\n\n TEST-3 Everything is on fire • Bug • Urgent • Done \n\n\n\n------------------------ 3 comments ------------------------\n\n\n Person C • Wed, 24 Nov 21 • Latest comment\n\nTest comment C\n"
expected := "🐞 Bug ✅ Done ⌛ Sun, 13 Dec 20 👷 Person A 🔑️ TEST-1 💭 3 comments \U0001F9F5 2 linked\n# This is a test\n⏱️ Sun, 13 Dec 20 🔎 Person Z 🚀 High 📦 BE, FE 🏷️ None 👀 0 watchers\n\n------------------------ Description ------------------------\n\n# Title\n## Subtitle\nThis is a **bold** and _italic_ text with [a link](https://ankit.pl) in between.\n\n\n------------------------ Linked Issues ------------------------\n\n\n BLOCKS\n\n TEST-2 Something is broken • Bug • High • TO DO\n\n RELATES TO\n\n TEST-3 Everything is on fire • Bug • Urgent • Done \n\n\n\n------------------------ 3 Comments ------------------------\n\n\n Person C • Wed, 24 Nov 21 • Latest comment\n\nTest comment C\n\n\n\n Person B • Tue, 23 Nov 21\n\nTest comment B\n\n"
if xterm256() {
expected += "\x1b[38;5;242mUse --comments <limit> with `jira issue view` to load more comments\x1b[m\n\n"
expected += "\x1b[38;5;242mView this issue on Jira: https://test.local/browse/TEST-1\x1b[m"
} else {
expected += "\x1b[0;90mUse --comments <limit> with `jira issue view` to load more comments\x1b[0m\n\n"
expected += "\x1b[0;90mView this issue on Jira: https://test.local/browse/TEST-1\x1b[0m"
}
actual := issue.String()
Expand Down
10 changes: 6 additions & 4 deletions internal/view/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/ankitpokhrel/jira-cli/api"
"github.com/ankitpokhrel/jira-cli/pkg/jira"
"github.com/ankitpokhrel/jira-cli/pkg/jira/filter/issue"
"github.com/ankitpokhrel/jira-cli/pkg/tui"
)

Expand Down Expand Up @@ -60,13 +61,14 @@ func (l IssueList) Render() error {
tui.WithViewModeFunc(func(r, c int, _ interface{}) (func() interface{}, func(interface{}) (string, error)) {
dataFn := func() interface{} {
ci := getKeyColumnIndex(data[0])
issue, _ := api.ProxyGetIssue(api.Client(jira.Config{}), data[r][ci])
return issue
iss, _ := api.ProxyGetIssue(api.Client(jira.Config{}), data[r][ci], issue.NewNumCommentsFilter(1))
return iss
}
renderFn := func(i interface{}) (string, error) {
iss := Issue{
Server: l.Server,
Data: i.(*jira.Issue),
Server: l.Server,
Data: i.(*jira.Issue),
Options: IssueOption{NumComments: 1},
}
return iss.RenderedOut(renderer)
}
Expand Down
10 changes: 6 additions & 4 deletions internal/view/sprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ankitpokhrel/jira-cli/api"
"github.com/ankitpokhrel/jira-cli/internal/cmdutil"
"github.com/ankitpokhrel/jira-cli/pkg/jira"
"github.com/ankitpokhrel/jira-cli/pkg/jira/filter/issue"
"github.com/ankitpokhrel/jira-cli/pkg/tui"
)

Expand Down Expand Up @@ -50,13 +51,14 @@ func (sl SprintList) Render() error {
dataFn := func() interface{} {
data := d.(tui.TableData)
ci := getKeyColumnIndex(data[0])
issue, _ := api.ProxyGetIssue(api.Client(jira.Config{}), data[r][ci])
return issue
iss, _ := api.ProxyGetIssue(api.Client(jira.Config{}), data[r][ci], issue.NewNumCommentsFilter(1))
return iss
}
renderFn := func(i interface{}) (string, error) {
iss := Issue{
Server: sl.Server,
Data: i.(*jira.Issue),
Server: sl.Server,
Data: i.(*jira.Issue),
Options: IssueOption{NumComments: 1},
}
return iss.RenderedOut(renderer)
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/jira/filter/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package filter provides a way to pass optional parameters to client methods.
// To create a new filter, you will need to satisfy `Filter` interface.
package filter
Loading

0 comments on commit e229260

Please sign in to comment.