From 60f2a02e31b92965221bdfe44398835fce6fe96e Mon Sep 17 00:00:00 2001 From: Syd7 Date: Sat, 15 Oct 2022 09:59:24 +0100 Subject: [PATCH] feat: Add weblink to issue (#483) Fixes #446 --- README.md | 11 ++ internal/cmd/issue/link/link.go | 4 +- internal/cmd/issue/link/remote/remote.go | 166 +++++++++++++++++++++++ pkg/jira/issue.go | 39 ++++++ pkg/jira/issue_test.go | 28 ++++ 5 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/issue/link/remote/remote.go diff --git a/README.md b/README.md index 8ab1cb32..2c3a962d 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,17 @@ $ jira issue link $ jira issue link ISSUE-1 ISSUE-2 Blocks ``` +##### Remote +The `remote` command lets you add a remote web link to an issue. + +```sh +# Adds a remote web link using an interactive prompt +$ jira issue link remote + +# Pass required parameters to skip prompt +$ jira issue link remote ISSUE-1 https://example.com "Example text" +``` + #### Unlink The `unlink` command lets you unlink two linked issues. diff --git a/internal/cmd/issue/link/link.go b/internal/cmd/issue/link/link.go index 61008d96..32b82c8c 100644 --- a/internal/cmd/issue/link/link.go +++ b/internal/cmd/issue/link/link.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/viper" "github.com/ankitpokhrel/jira-cli/api" + "github.com/ankitpokhrel/jira-cli/internal/cmd/issue/link/remote" "github.com/ankitpokhrel/jira-cli/internal/cmdutil" "github.com/ankitpokhrel/jira-cli/internal/query" "github.com/ankitpokhrel/jira-cli/pkg/jira" @@ -37,7 +38,8 @@ func NewCmdLink() *cobra.Command { Run: link, } - cmd.Flags().Bool("web", false, "Open inward issue in web browser after successful linking") + cmd.AddCommand(remote.NewCmdRemoteLink()) + cmd.PersistentFlags().Bool("web", false, "Open issue in web browser after successful linking") return &cmd } diff --git a/internal/cmd/issue/link/remote/remote.go b/internal/cmd/issue/link/remote/remote.go new file mode 100644 index 00000000..977a02b5 --- /dev/null +++ b/internal/cmd/issue/link/remote/remote.go @@ -0,0 +1,166 @@ +package remote + +import ( + "fmt" + + "github.com/AlecAivazis/survey/v2" + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/ankitpokhrel/jira-cli/api" + "github.com/ankitpokhrel/jira-cli/internal/cmdutil" + "github.com/ankitpokhrel/jira-cli/internal/query" + "github.com/ankitpokhrel/jira-cli/pkg/jira" +) + +const ( + helpText = `Adds a remote web link to an issue` + examples = `$ jira issue link remote ISSUE-1 http://weblink.com weblink-title` +) + +// NewCmdRemoteLink is a link command. +func NewCmdRemoteLink() *cobra.Command { + cmd := cobra.Command{ + Use: "remote ISSUE_KEY WEBLINK_URL WEBLINK_TITLE", + Short: "Adds a remote web link to an issue", + Long: helpText, + Example: examples, + Aliases: []string{"rmln"}, + Annotations: map[string]string{ + "help:args": "ISSUE_KEY\tIssue key, eg: ISSUE-1\n" + + "WEBLINK_URL\tUrl of the weblink\n" + + "WEBLINK_TITLE\tTitle of the weblink", + }, + Run: remotelink, + } + + return &cmd +} + +func remotelink(cmd *cobra.Command, args []string) { + project := viper.GetString("project.key") + params := parseArgsAndFlags(cmd.Flags(), args, project) + client := api.Client(jira.Config{Debug: params.debug}) + lc := linkCmd{ + client: client, + params: params, + } + + cmdutil.ExitIfError(lc.setIssueKey(project)) + cmdutil.ExitIfError(lc.setRemoteLinkURL()) + cmdutil.ExitIfError(lc.setRemoteLinkTitle()) + + err := func() error { + s := cmdutil.Info("Creating remote web link for issue") + defer s.Stop() + + return client.RemoteLinkIssue(lc.params.issueKey, lc.params.title, lc.params.url) + }() + cmdutil.ExitIfError(err) + + server := viper.GetString("server") + + cmdutil.Success("Remote web link created for Issue %s", lc.params.issueKey) + fmt.Printf("%s/browse/%s\n", server, lc.params.issueKey) + + if web, _ := cmd.Flags().GetBool("web"); web { + err := cmdutil.Navigate(server, lc.params.issueKey) + cmdutil.ExitIfError(err) + } +} + +type linkParams struct { + issueKey string + url string + title string + debug bool +} + +func parseArgsAndFlags(flags query.FlagParser, args []string, project string) *linkParams { + var issueKey, url, title string + nargs := len(args) + if nargs >= 1 { + issueKey = cmdutil.GetJiraIssueKey(project, args[0]) + } + if nargs >= 2 { + url = args[1] + } + if nargs >= 3 { + title = args[2] + } + + debug, err := flags.GetBool("debug") + cmdutil.ExitIfError(err) + + return &linkParams{ + issueKey: issueKey, + url: url, + title: title, + debug: debug, + } +} + +type linkCmd struct { + client *jira.Client + params *linkParams +} + +func (lc *linkCmd) setIssueKey(project string) error { + if lc.params.issueKey != "" { + return nil + } + + var ans string + + qs := &survey.Question{ + Name: "issueKey", + Prompt: &survey.Input{Message: "Issue key"}, + Validate: survey.Required, + } + if err := survey.Ask([]*survey.Question{qs}, &ans); err != nil { + return err + } + lc.params.issueKey = cmdutil.GetJiraIssueKey(project, ans) + + return nil +} + +func (lc *linkCmd) setRemoteLinkURL() error { + if lc.params.url != "" { + return nil + } + + var ans string + + qs := &survey.Question{ + Name: "remoteLinkURL", + Prompt: &survey.Input{Message: "Remote link URL"}, + Validate: survey.Required, + } + if err := survey.Ask([]*survey.Question{qs}, &ans); err != nil { + return err + } + lc.params.url = ans + + return nil +} + +func (lc *linkCmd) setRemoteLinkTitle() error { + if lc.params.title != "" { + return nil + } + + var ans string + + qs := &survey.Question{ + Name: "remoteLinkTitle", + Prompt: &survey.Input{Message: "Remote link title"}, + Validate: survey.Required, + } + if err := survey.Ask([]*survey.Question{qs}, &ans); err != nil { + return err + } + lc.params.title = ans + + return nil +} diff --git a/pkg/jira/issue.go b/pkg/jira/issue.go index a702a515..493c9a97 100644 --- a/pkg/jira/issue.go +++ b/pkg/jira/issue.go @@ -380,3 +380,42 @@ func ifaceToADF(v interface{}) *adf.ADF { return doc } + +type remotelinkRequest struct { + RemoteObject struct { + URL string `json:"url"` + Title string `json:"title"` + } `json:"object"` +} + +// RemoteLinkIssue adds a remote link to an issue using POST /issue/{issueId}/remotelink endpoint. +func (c *Client) RemoteLinkIssue(issueID, title, url string) error { + body, err := json.Marshal(remotelinkRequest{ + RemoteObject: struct { + URL string `json:"url"` + Title string `json:"title"` + }{Title: title, URL: url}, + }) + if err != nil { + return err + } + + path := fmt.Sprintf("/issue/%s/remotelink", issueID) + + res, err := c.PostV2(context.Background(), path, body, Header{ + "Accept": "application/json", + "Content-Type": "application/json", + }) + if err != nil { + return err + } + if res == nil { + return ErrEmptyResponse + } + defer func() { _ = res.Body.Close() }() + + if res.StatusCode != http.StatusCreated { + return formatUnexpectedResponse(res) + } + return nil +} diff --git a/pkg/jira/issue_test.go b/pkg/jira/issue_test.go index 609857cd..e885b4c5 100644 --- a/pkg/jira/issue_test.go +++ b/pkg/jira/issue_test.go @@ -526,3 +526,31 @@ func TestGetField(t *testing.T) { _, err = client.GetField() assert.NotNil(t, err) } + +func TestRemoteLinkIssue(t *testing.T) { + var unexpectedStatusCode bool + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/rest/api/2/issue/TEST-1/remotelink", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "application/json", r.Header.Get("Accept")) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + + if unexpectedStatusCode { + w.WriteHeader(400) + } else { + w.WriteHeader(201) + } + })) + defer server.Close() + + client := NewClient(Config{Server: server.URL}, WithTimeout(3*time.Second)) + + err := client.RemoteLinkIssue("TEST-1", "weblink title", "http://weblink.com") + assert.NoError(t, err) + + unexpectedStatusCode = true + + err = client.RemoteLinkIssue("TEST-1", "weblink title", "https://weblink.com") + assert.Error(t, &ErrUnexpectedResponse{}, err) +}