From bbcdf0213be1456dc44eb712bf426e9a540177ec Mon Sep 17 00:00:00 2001 From: YCK1130 Date: Wed, 24 Jul 2024 11:55:54 +0100 Subject: [PATCH] chore: improve list issue UX --- application/jira/v0/boards.go | 38 +++++++++++----------- application/jira/v0/component_test.go | 46 ++++++++++++++++++++------ application/jira/v0/config/tasks.json | 20 ++++++------ application/jira/v0/issues.go | 47 +++++++++++++++++++-------- 4 files changed, 99 insertions(+), 52 deletions(-) diff --git a/application/jira/v0/boards.go b/application/jira/v0/boards.go index f1b363c9..812e8895 100644 --- a/application/jira/v0/boards.go +++ b/application/jira/v0/boards.go @@ -63,14 +63,9 @@ func (jiraClient *Client) listBoardsTask(ctx context.Context, props *structpb.St } func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (*ListBoardsResp, error) { - var debug DebugSession - debug.SessionStart("listBoards", StaticVerboseLevel) - defer debug.SessionEnd() - apiEndpoint := "rest/agile/1.0/board" req := jiraClient.Client.R().SetResult(&ListBoardsResp{}) - debug.AddMapMessage("opt", *opt) err := addQueryOptions(req, *opt) if err != nil { return nil, err @@ -80,30 +75,35 @@ func (jiraClient *Client) listBoards(_ context.Context, opt *ListBoardsInput) (* if err != nil { return nil, err } - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) - debug.AddMessage("Status", resp.Status()) boards := resp.Result().(*ListBoardsResp) return boards, err } -func (jiraClient *Client) getBoard(_ context.Context, boardID int) (*Board, error) { - var debug DebugSession - debug.SessionStart("getBoard", StaticVerboseLevel) - defer debug.SessionEnd() +type GetBoardResp struct { + Location struct { + DisplayName string `json:"displayName"` + Name string `json:"name"` + ProjectKey string `json:"projectKey"` + ProjectID int `json:"projectId"` + ProjectName string `json:"projectName"` + ProjectTypeKey string `json:"projectTypeKey"` + UserAccountID string `json:"userAccountId"` + UserID string `json:"userId"` + } `json:"location"` + Board +} +func (jiraClient *Client) getBoard(_ context.Context, boardID int) (*GetBoardResp, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req := jiraClient.Client.R().SetResult(&Board{}) - resp, err := req.Get(apiEndpoint) + req := jiraClient.Client.R().SetResult(&GetBoardResp{}) + resp, err := req.Get(apiEndpoint) if err != nil { return nil, fmt.Errorf( err.Error(), errmsg.Message(err), ) } - debug.AddMessage("GET", apiEndpoint) - debug.AddMapMessage("QueryParam", resp.Request.QueryParam) - debug.AddMessage("Status", resp.Status()) - board := resp.Result().(*Board) - return board, err + result := resp.Result().(*GetBoardResp) + + return result, err } diff --git a/application/jira/v0/component_test.go b/application/jira/v0/component_test.go index c18d7664..4b1a97c1 100644 --- a/application/jira/v0/component_test.go +++ b/application/jira/v0/component_test.go @@ -199,7 +199,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "All", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -235,7 +235,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Epics only", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -271,7 +271,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "In backlog only", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -307,7 +307,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues without epic assigned", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -343,7 +343,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues of an epic", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -362,7 +362,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues of an epic(long query)", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -381,7 +381,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Issues of a sprint", input: ListIssuesInput{ - BoardID: 1, + BoardName: "KAN", MaxResults: 10, StartAt: 0, Range: Range{ @@ -400,7 +400,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "Standard Issues", input: ListIssuesInput{ - BoardID: 1, + BoardName: "TST", MaxResults: 10, StartAt: 0, Range: Range{ @@ -418,7 +418,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "ok", name: "JQL", input: ListIssuesInput{ - BoardID: 1, + BoardName: "TST", MaxResults: 10, StartAt: 0, Range: Range{ @@ -437,7 +437,7 @@ func TestComponent_ListIssuesTask(t *testing.T) { _type: "nok", name: "invalid range", input: ListIssuesInput{ - BoardID: 1, + BoardName: "TST", MaxResults: 10, StartAt: 0, Range: Range{ @@ -493,6 +493,32 @@ func TestComponent_ListSprintsTask(t *testing.T) { taskTesting(testcases, taskListSprints, t) } +func TestAuth_nok(t *testing.T) { + c := qt.New(t) + bc := base.Component{Logger: zap.NewNop()} + connector := Init(bc) + c.Run("nok-empty token", func(c *qt.C) { + setup, err := structpb.NewStruct(map[string]any{ + "token": "", + "email": email, + "base-url": "url", + }) + c.Assert(err, qt.IsNil) + _, err = connector.CreateExecution(nil, setup, "invalid") + c.Assert(err, qt.ErrorMatches, "token not provided") + }) + c.Run("nok-empty email", func(c *qt.C) { + setup, err := structpb.NewStruct(map[string]any{ + "token": token, + "email": "", + "base-url": "url", + }) + c.Assert(err, qt.IsNil) + _, err = connector.CreateExecution(nil, setup, "invalid") + c.Assert(err, qt.ErrorMatches, "email not provided") + }) +} + func taskTesting[inType any, outType any](testcases []TaskCase[inType, outType], task string, t *testing.T) { c := qt.New(t) ctx := context.Background() diff --git a/application/jira/v0/config/tasks.json b/application/jira/v0/config/tasks.json index eb6a183e..9683c58c 100644 --- a/application/jira/v0/config/tasks.json +++ b/application/jira/v0/config/tasks.json @@ -359,25 +359,25 @@ "description": "List issues in Jira", "instillUIOrder": 0, "instillEditOnNodeFields": [ - "board-id", + "board-name", "range" ], "properties": { - "board-id": { - "title": "Board ID", - "description": "The ID of the board", - "instillShortDescription": "The ID of the board", + "board-name": { + "title": "Board Name", + "description": "The name of the board", + "instillShortDescription": "The name of the board", "instillUIOrder": 0, - "instillFormat": "integer", + "instillFormat": "string", "instillAcceptFormats": [ - "integer" + "string" ], "instillUpstreamTypes": [ "value", "reference", "template" ], - "type": "integer" + "type": "string" }, "range": { "title": "Range", @@ -546,7 +546,7 @@ }, "jql": { "title": "JQL", - "description": "The JQL query. For example, `project = JRA AND status = Done`. For more information, see [Advanced searching reference](https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/)", + "description": "The JQL query. For example, `type = \"Task\" AND status = \"Done\"`. For more information, see [Advanced searching](https://support.atlassian.com/jira-software-cloud/docs/what-is-advanced-search-in-jira-cloud/)", "instillShortDescription": "The JQL query", "instillUIOrder": 10, "instillFormat": "string", @@ -584,7 +584,7 @@ } }, "required": [ - "board-id" + "board-name" ], "title": "Input", "type": "object" diff --git a/application/jira/v0/issues.go b/application/jira/v0/issues.go index 3341373b..05cf1010 100644 --- a/application/jira/v0/issues.go +++ b/application/jira/v0/issues.go @@ -115,11 +115,12 @@ type Range struct { SprintName string `json:"sprint-name,omitempty"` JQL string `json:"jql,omitempty"` } + type ListIssuesInput struct { - BoardID int `json:"board-id,omitempty" api:"boardId"` - MaxResults int `json:"max-results,omitempty" api:"maxResults"` - StartAt int `json:"start-at,omitempty" api:"startAt"` - Range Range `json:"range,omitempty"` + BoardName string `json:"board-name,omitempty" api:"boardName"` + MaxResults int `json:"max-results,omitempty" api:"maxResults"` + StartAt int `json:"start-at,omitempty" api:"startAt"` + Range Range `json:"range,omitempty"` } type ListIssuesResp struct { @@ -137,7 +138,7 @@ type ListIssuesOutput struct { func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.Struct) (*structpb.Struct, error) { var debug DebugSession - debug.SessionStart("listIssuesTask", StaticVerboseLevel) + debug.SessionStart("listIssuesTask", DevelopVerboseLevel) defer debug.SessionEnd() debug.AddMapMessage("props", props) @@ -149,14 +150,34 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St if err := base.ConvertFromStructpb(props, &opt); err != nil { return nil, err } - debug.AddMapMessage("ListIssuesInput", opt) - board, err := jiraClient.getBoard(ctx, opt.BoardID) + + boards, err := jiraClient.listBoards(ctx, &ListBoardsInput{Name: opt.BoardName}) + if err != nil { + return nil, err + } + if len(boards.Values) == 0 { + return nil, errmsg.AddMessage( + fmt.Errorf("board not found"), + fmt.Sprintf("board with name %s not found", opt.BoardName), + ) + } else if len(boards.Values) > 1 { + return nil, errmsg.AddMessage( + fmt.Errorf("multiple boards found"), + fmt.Sprintf("multiple boards are found with the partial name \"%s\". Please provide a more specific name", opt.BoardName), + ) + } + debug.AddMapMessage("boards", boards) + board := boards.Values[0] + + boardDetails, err := jiraClient.getBoard(ctx, board.ID) if err != nil { return nil, err } - debug.AddMapMessage("board", *board) - boardKey := strings.Split(board.Name, " ")[0] - apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d", opt.BoardID) + projectKey := boardDetails.Location.ProjectKey + if projectKey == "" { + projectKey = strings.Split(board.Name, "-")[0] + } + apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d", board.ID) switch opt.Range.Range { case "All": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-issue-get @@ -167,11 +188,11 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St case "Issues of an epic": // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-epic-epicid-issue-get // use JQL instead - jql = fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", boardKey, opt.Range.EpicKey) + jql = fmt.Sprintf("project=\"%s\" AND parent=\"%s\"", projectKey, opt.Range.EpicKey) case "Issues of a sprint": // API not working: https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-sprint-sprintid-issue-get // use JQL instead - jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", boardKey, opt.Range.SprintName) + jql = fmt.Sprintf("project=\"%s\" AND sprint=\"%s\"", projectKey, opt.Range.SprintName) case "In backlog only": // https://developer.atlassian.com/cloud/jira/software/rest/api-group-board/#api-rest-agile-1-0-board-boardid-backlog-get apiEndpoint = apiEndpoint + "/backlog" @@ -180,7 +201,7 @@ func (jiraClient *Client) listIssuesTask(ctx context.Context, props *structpb.St apiEndpoint = apiEndpoint + "/epic/none/issue" case "Standard Issues": // https://support.atlassian.com/jira-cloud-administration/docs/what-are-issue-types/ - jql = fmt.Sprintf("project=\"%s\" AND issuetype not in (Epic, subtask)", boardKey) + jql = fmt.Sprintf("project=\"%s\" AND issuetype not in (Epic, subtask)", projectKey) case "JQL query": jql = opt.Range.JQL default: