Repository: ankitpokhrel/jira-cli
Pull request #321 (2022-03-06 18:21:52)

fix: Issue with assigning user
Partly relates to #198 


Patch:
diff --git a/internal/cmd/issue/assign/assign.go b/internal/cmd/issue/assign/assign.go
index 5ca4546..352f958 100644
--- a/internal/cmd/issue/assign/assign.go
+++ b/internal/cmd/issue/assign/assign.go
@@ -74,8 +74,6 @@ func assign(cmd *cobra.Command, args []string) {
 	if lu != strings.ToLower(optionNone) && lu != "x" && lu != jira.AssigneeDefault {
 		cmdutil.ExitIfError(ac.setAvailableUsers(project))
 		cmdutil.ExitIfError(ac.setAssignee(project))
-
-		lu = strings.ToLower(ac.params.user)
 	}
 
 	u, err := ac.verifyAssignee()
@@ -88,7 +86,7 @@ func assign(cmd *cobra.Command, args []string) {
 
 	switch {
 	case u != nil:
-		uname = u.Name
+		uname = getQueryableName(u.DisplayName, u.Name)
 	case lu == strings.ToLower(optionNone) || lu == "x":
 		assignee = jira.AssigneeNone
 		uname = "unassigned"
@@ -173,7 +171,7 @@ func (ac *assignCmd) setIssueKey(project string) error {
 
 func (ac *assignCmd) setAssignee(project string) error {
 	if ac.params.user != "" && len(ac.users) == 1 {
-		ac.params.user = ac.users[0].Name
+		ac.params.user = getQueryableName(ac.users[0].Name, ac.users[0].DisplayName)
 		return nil
 	}
 
@@ -241,7 +239,11 @@ func (ac *assignCmd) getOptions(last bool) []string {
 
 	for _, t := range ac.users {
 		if t.Active {
-			validUsers = append(validUsers, t.Name)
+			name := t.DisplayName
+			if t.Name != "" {
+				name += fmt.Sprintf(" (%s)", t.Name)
+			}
+			validUsers = append(validUsers, name)
 		}
 	}
 	always := []string{optionDefault, optionNone, optionCancel}
@@ -285,12 +287,8 @@ func (ac *assignCmd) getSearchKeyword() error {
 }
 
 func (ac *assignCmd) searchAndAssignUser(project string) error {
-	q := ac.params.user
-	if q == "" {
-		q = "*"
-	}
 	u, err := api.ProxyUserSearch(ac.client, &jira.UserSearchOptions{
-		Query:      q,
+		Query:      ac.params.user,
 		Project:    project,
 		MaxResults: maxResults,
 	})
@@ -309,25 +307,38 @@ func (ac *assignCmd) setAvailableUsers(project string) error {
 }
 
 func (ac *assignCmd) verifyAssignee() (*jira.User, error) {
-	u, d, n := strings.ToLower(ac.params.user), strings.ToLower(optionDefault), strings.ToLower(optionNone)
-	if u == d || u == n || u == "x" {
+	assignee := strings.ToLower(ac.params.user)
+	if assignee == strings.ToLower(optionDefault) || assignee == strings.ToLower(optionNone) || assignee == "x" {
 		return nil, nil
 	}
 
 	var user *jira.User
 
-	st := strings.ToLower(ac.params.user)
 	for _, u := range ac.users {
-		if strings.ToLower(u.Name) == st || strings.ToLower(u.Email) == st {
+		name := strings.ToLower(getQueryableName(u.Name, u.DisplayName))
+		if name == assignee || strings.ToLower(u.Email) == assignee {
+			user = u
+		}
+		if strings.ToLower(fmt.Sprintf("%s (%s)", u.DisplayName, u.Name)) == assignee {
 			user = u
 		}
+		if user != nil {
+			break
+		}
 	}
 
 	if user == nil {
 		return nil, fmt.Errorf("invalid assignee \"%s\"", ac.params.user)
 	}
 	if !user.Active {
-		return nil, fmt.Errorf("user \"%s\" is not active", user.Name)
+		return nil, fmt.Errorf("user \"%s\" is not active", getQueryableName(user.Name, user.DisplayName))
 	}
 	return user, nil
 }
+
+func getQueryableName(name, displayName string) string {
+	if name != "" {
+		return name
+	}
+	return displayName
+}
diff --git a/internal/cmd/issue/edit/edit.go b/internal/cmd/issue/edit/edit.go
index 46ff5a1..d8ad3f4 100644
--- a/internal/cmd/issue/edit/edit.go
+++ b/internal/cmd/issue/edit/edit.go
@@ -1,7 +1,6 @@
 package edit
 
 import (
-	"errors"
 	"fmt"
 	"strings"
 
@@ -128,30 +127,6 @@ func edit(cmd *cobra.Command, args []string) {
 		}
 	}
 
-	var userAccountID string
-
-	if params.assignee != "" {
-		err := func() error {
-			s := cmdutil.Info("Looking for assignee...")
-			defer s.Stop()
-
-			user, err := client.UserSearch(&jira.UserSearchOptions{
-				Query: params.assignee,
-			})
-			if err != nil {
-				return err
-			}
-			if len(user) == 0 {
-				return errors.New("unable to find assignee")
-			}
-
-			userAccountID = user[0].AccountID
-
-			return nil
-		}()
-		cmdutil.ExitIfError(err)
-	}
-
 	if params.isEmpty() {
 		fmt.Println()
 		cmdutil.Failed("Nothing to update")
@@ -176,7 +151,6 @@ func edit(cmd *cobra.Command, args []string) {
 		edr := jira.EditRequest{
 			Summary:    params.summary,
 			Body:       body,
-			Assignee:   userAccountID,
 			Priority:   params.priority,
 			Labels:     labels,
 			Components: params.components,
@@ -188,12 +162,36 @@ func edit(cmd *cobra.Command, args []string) {
 
 	cmdutil.Success("Issue updated\n%s/browse/%s", server, params.issueKey)
 
+	handleUserAssign(project, params.issueKey, params.assignee, client)
+
 	if web, _ := cmd.Flags().GetBool("web"); web {
 		err := cmdutil.Navigate(server, params.issueKey)
 		cmdutil.ExitIfError(err)
 	}
 }
 
+func handleUserAssign(project, key, assignee string, client *jira.Client) {
+	if assignee == "" {
+		return
+	}
+	if assignee == "x" {
+		if err := api.ProxyAssignIssue(client, key, nil, jira.AssigneeNone); err != nil {
+			cmdutil.Failed("Unable to unassign user: %s", err.Error())
+		}
+		return
+	}
+	user, err := api.ProxyUserSearch(client, &jira.UserSearchOptions{
+		Query:   assignee,
+		Project: project,
+	})
+	if err != nil || len(user) == 0 {
+		cmdutil.Failed("Unable to find assignee")
+	}
+	if err = api.ProxyAssignIssue(client, key, user[0], assignee); err != nil {
+		cmdutil.Failed("Unable to set assignee: %s", err.Error())
+	}
+}
+
 type editCmd struct {
 	client *jira.Client
 	params *editParams
@@ -260,7 +258,7 @@ type editParams struct {
 	debug      bool
 }
 
-func (ep editParams) isEmpty() bool {
+func (ep *editParams) isEmpty() bool {
 	return ep.summary == "" && ep.body == "" && ep.priority == "" &&
 		ep.assignee == "" && len(ep.labels) == 0 && len(ep.components) == 0
 }
diff --git a/pkg/jira/edit.go b/pkg/jira/edit.go
index 1a76939..7d39b9d 100644
--- a/pkg/jira/edit.go
+++ b/pkg/jira/edit.go
@@ -19,7 +19,6 @@ type EditRequest struct {
 	ParentIssueKey string
 	Summary        string
 	Body           string
-	Assignee       string
 	Priority       string
 	Labels         []string
 	Components     []string
@@ -27,7 +26,7 @@ type EditRequest struct {
 
 // Edit updates an issue using POST /issue endpoint.
 func (c *Client) Edit(key string, req *EditRequest) error {
-	data := c.getRequestDataForEdit(req)
+	data := getRequestDataForEdit(req)
 
 	body, err := json.Marshal(&data)
 	if err != nil {
@@ -60,11 +59,6 @@ type editFields struct {
 	Description []struct {
 		Set string `json:"set,omitempty"`
 	} `json:"description,omitempty"`
-	Assignee []struct {
-		Set struct {
-			AccountID string `json:"accountId,omitempty"`
-		} `json:"set,omitempty"`
-	} `json:"assignee,omitempty"`
 	Priority []struct {
 		Set struct {
 			Name string `json:"name,omitempty"`
@@ -85,16 +79,13 @@ type editFieldsMarshaler struct {
 }
 
 // MarshalJSON is a custom marshaler to handle empty fields.
-func (cfm editFieldsMarshaler) MarshalJSON() ([]byte, error) {
+func (cfm *editFieldsMarshaler) MarshalJSON() ([]byte, error) {
 	if len(cfm.M.Summary) == 0 || cfm.M.Summary[0].Set == "" {
 		cfm.M.Summary = nil
 	}
 	if len(cfm.M.Description) == 0 || cfm.M.Description[0].Set == "" {
 		cfm.M.Description = nil
 	}
-	if len(cfm.M.Assignee) == 0 || cfm.M.Assignee[0].Set.AccountID == "" {
-		cfm.M.Assignee = nil
-	}
 	if len(cfm.M.Priority) == 0 || cfm.M.Priority[0].Set.Name == "" {
 		cfm.M.Priority = nil
 	}
@@ -118,7 +109,7 @@ type editRequest struct {
 	} `json:"fields"`
 }
 
-func (c *Client) getRequestDataForEdit(req *EditRequest) *editRequest {
+func getRequestDataForEdit(req *EditRequest) *editRequest {
 	if req.Labels == nil {
 		req.Labels = []string{}
 	}
@@ -130,13 +121,6 @@ func (c *Client) getRequestDataForEdit(req *EditRequest) *editRequest {
 		Description: []struct {
 			Set string `json:"set,omitempty"`
 		}{{Set: req.Body}},
-		Assignee: []struct {
-			Set struct {
-				AccountID string `json:"accountId,omitempty"`
-			} `json:"set,omitempty"`
-		}{{Set: struct {
-			AccountID string `json:"accountId,omitempty"`
-		}{AccountID: req.Assignee}}},
 		Priority: []struct {
 			Set struct {
 				Name string `json:"name,omitempty"`
diff --git a/pkg/jira/types.go b/pkg/jira/types.go
index 44dc8b0..0d76054 100644
--- a/pkg/jira/types.go
+++ b/pkg/jira/types.go
@@ -150,8 +150,9 @@ type Transition struct {
 
 // User holds user info.
 type User struct {
-	AccountID string `json:"accountId"`
-	Email     string `json:"emailAddress"`
-	Name      string `json:"displayName"`
-	Active    bool   `json:"active"`
+	AccountID   string `json:"accountId,omitempty"`
+	Email       string `json:"emailAddress"`
+	Name        string `json:"name,omitempty"`
+	DisplayName string `json:"displayName"`
+	Active      bool   `json:"active"`
 }


Test patch:
diff --git a/pkg/jira/testdata/users.json b/pkg/jira/testdata/users.json
index 2e8ef01..47eba48 100644
--- a/pkg/jira/testdata/users.json
+++ b/pkg/jira/testdata/users.json
@@ -2,6 +2,7 @@
   {
     "accountId": "5fb82376aca10c006949f35b",
     "emailAddress": "jane@domain.tld",
+    "name": "janedoe",
     "displayName": "Jane Doe",
     "active": true
   },
diff --git a/pkg/jira/user_test.go b/pkg/jira/user_test.go
index 98ae56b..d13107d 100644
--- a/pkg/jira/user_test.go
+++ b/pkg/jira/user_test.go
@@ -56,16 +56,17 @@ func TestUserSearch(t *testing.T) {
 
 	expected := []*User{
 		{
-			AccountID: "5fb82376aca10c006949f35b",
-			Email:     "jane@domain.tld",
-			Name:      "Jane Doe",
-			Active:    true,
+			AccountID:   "5fb82376aca10c006949f35b",
+			Email:       "jane@domain.tld",
+			Name:        "janedoe",
+			DisplayName: "Jane Doe",
+			Active:      true,
 		},
 		{
-			AccountID: "5fb82376aca10c006949f35c",
-			Email:     "jon@domain.tld",
-			Name:      "Jon Doe",
-			Active:    false,
+			AccountID:   "5fb82376aca10c006949f35c",
+			Email:       "jon@domain.tld",
+			DisplayName: "Jon Doe",
+			Active:      false,
 		},
 	}
 	assert.Equal(t, expected, actual)
@@ -80,8 +81,6 @@ func TestUserSearch(t *testing.T) {
 
 	apiVersion2 = true
 
-	_, err = client.UserSearchV2(&UserSearchOptions{
-		Username: "doe",
-	})
+	_, err = client.UserSearchV2(&UserSearchOptions{})
 	assert.Error(t, &ErrUnexpectedResponse{}, err)
 }