350 lines
9.1 KiB
Go
350 lines
9.1 KiB
Go
package jira
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ankitpokhrel/jira-cli/pkg/adf"
|
|
"github.com/ankitpokhrel/jira-cli/pkg/md"
|
|
)
|
|
|
|
// CreateResponse struct holds response from POST /issue endpoint.
|
|
type CreateResponse struct {
|
|
ID string `json:"id"`
|
|
Key string `json:"key"`
|
|
}
|
|
|
|
// CreateRequest struct holds request data for create request.
|
|
type CreateRequest struct {
|
|
Project string
|
|
Name string
|
|
IssueType string
|
|
// ParentIssueKey is required when creating a sub-task for classic project.
|
|
// This can also be used to attach epic for next-gen project.
|
|
ParentIssueKey string
|
|
Summary string
|
|
Body interface{} // string in v1/v2 and adf.ADF in v3
|
|
Reporter string
|
|
Assignee string
|
|
Priority string
|
|
Labels []string
|
|
Components []string
|
|
FixVersions []string
|
|
AffectsVersions []string
|
|
OriginalEstimate string
|
|
// EpicField is the dynamic epic field name
|
|
// that changes per jira installation.
|
|
EpicField string
|
|
// SubtaskField is usually called "Sub-task" but is
|
|
// case-sensitive in Jira and can differ slightly
|
|
// in different Jira versions.
|
|
SubtaskField string
|
|
// CustomFields holds all custom fields passed
|
|
// while creating an issue.
|
|
CustomFields map[string]string
|
|
|
|
projectType string
|
|
installationType string
|
|
configuredCustomFields []IssueTypeField
|
|
}
|
|
|
|
// ForProjectType sets jira project type.
|
|
func (cr *CreateRequest) ForProjectType(pt string) {
|
|
cr.projectType = pt
|
|
}
|
|
|
|
// ForInstallationType sets jira project type.
|
|
func (cr *CreateRequest) ForInstallationType(it string) {
|
|
cr.installationType = it
|
|
}
|
|
|
|
// WithCustomFields sets valid custom fields for the issue.
|
|
func (cr *CreateRequest) WithCustomFields(cf []IssueTypeField) {
|
|
cr.configuredCustomFields = cf
|
|
}
|
|
|
|
// Create creates an issue using v3 version of the POST /issue endpoint.
|
|
func (c *Client) Create(req *CreateRequest) (*CreateResponse, error) {
|
|
return c.create(req, apiVersion3)
|
|
}
|
|
|
|
// CreateV2 creates an issue using v2 version of the POST /issue endpoint.
|
|
func (c *Client) CreateV2(req *CreateRequest) (*CreateResponse, error) {
|
|
return c.create(req, apiVersion2)
|
|
}
|
|
|
|
func (c *Client) create(req *CreateRequest, ver string) (*CreateResponse, error) {
|
|
data := c.getRequestData(req)
|
|
|
|
body, err := json.Marshal(&data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
header := Header{
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
var res *http.Response
|
|
|
|
switch ver {
|
|
case apiVersion2:
|
|
res, err = c.PostV2(context.Background(), "/issue", body, header)
|
|
default:
|
|
res, err = c.Post(context.Background(), "/issue", body, header)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if res == nil {
|
|
return nil, ErrEmptyResponse
|
|
}
|
|
defer func() { _ = res.Body.Close() }()
|
|
|
|
if res.StatusCode != http.StatusCreated {
|
|
return nil, formatUnexpectedResponse(res)
|
|
}
|
|
|
|
var out CreateResponse
|
|
|
|
err = json.NewDecoder(res.Body).Decode(&out)
|
|
|
|
return &out, err
|
|
}
|
|
|
|
func (*Client) getRequestData(req *CreateRequest) *createRequest {
|
|
if req.Labels == nil {
|
|
req.Labels = []string{}
|
|
}
|
|
|
|
cf := createFields{
|
|
Project: struct {
|
|
Key string `json:"key"`
|
|
}{Key: req.Project},
|
|
IssueType: struct {
|
|
Name string `json:"name"`
|
|
}{Name: req.IssueType},
|
|
Name: req.Name,
|
|
Summary: req.Summary,
|
|
Labels: req.Labels,
|
|
epicField: req.EpicField,
|
|
}
|
|
|
|
switch v := req.Body.(type) {
|
|
case string:
|
|
cf.Description = md.ToJiraMD(v)
|
|
case *adf.ADF:
|
|
cf.Description = v
|
|
}
|
|
|
|
data := createRequest{
|
|
Update: struct{}{},
|
|
Fields: createFieldsMarshaler{cf},
|
|
}
|
|
|
|
if req.ParentIssueKey != "" {
|
|
subtaskField := IssueTypeSubTask
|
|
if req.SubtaskField != "" {
|
|
subtaskField = req.SubtaskField
|
|
}
|
|
|
|
if req.projectType == ProjectTypeNextGen || data.Fields.M.IssueType.Name == subtaskField {
|
|
data.Fields.M.Parent = &struct {
|
|
Key string `json:"key"`
|
|
}{Key: req.ParentIssueKey}
|
|
} else {
|
|
data.Fields.M.Name = req.ParentIssueKey
|
|
}
|
|
}
|
|
if req.Reporter != "" {
|
|
if req.installationType == InstallationTypeLocal {
|
|
data.Fields.M.Reporter = &nameOrAccountID{Name: &req.Reporter}
|
|
} else {
|
|
data.Fields.M.Reporter = &nameOrAccountID{AccountID: &req.Reporter}
|
|
}
|
|
}
|
|
if req.Assignee != "" {
|
|
if req.installationType == InstallationTypeLocal {
|
|
data.Fields.M.Assignee = &nameOrAccountID{Name: &req.Assignee}
|
|
} else {
|
|
data.Fields.M.Assignee = &nameOrAccountID{AccountID: &req.Assignee}
|
|
}
|
|
}
|
|
if req.Priority != "" {
|
|
data.Fields.M.Priority = &struct {
|
|
Name string `json:"name,omitempty"`
|
|
}{Name: req.Priority}
|
|
}
|
|
if len(req.Components) > 0 {
|
|
comps := make([]struct {
|
|
Name string `json:"name,omitempty"`
|
|
}, 0, len(req.Components))
|
|
|
|
for _, c := range req.Components {
|
|
comps = append(comps, struct {
|
|
Name string `json:"name,omitempty"`
|
|
}{c})
|
|
}
|
|
data.Fields.M.Components = comps
|
|
}
|
|
if len(req.FixVersions) > 0 {
|
|
versions := make([]struct {
|
|
Name string `json:"name,omitempty"`
|
|
}, 0, len(req.FixVersions))
|
|
|
|
for _, v := range req.FixVersions {
|
|
versions = append(versions, struct {
|
|
Name string `json:"name,omitempty"`
|
|
}{v})
|
|
}
|
|
data.Fields.M.FixVersions = versions
|
|
}
|
|
if len(req.AffectsVersions) > 0 {
|
|
versions := make([]struct {
|
|
Name string `json:"name,omitempty"`
|
|
}, 0, len(req.AffectsVersions))
|
|
|
|
for _, v := range req.AffectsVersions {
|
|
versions = append(versions, struct {
|
|
Name string `json:"name,omitempty"`
|
|
}{v})
|
|
}
|
|
data.Fields.M.AffectsVersions = versions
|
|
}
|
|
if req.OriginalEstimate != "" {
|
|
data.Fields.M.TimeTracking = &struct {
|
|
OriginalEstimate string `json:"originalEstimate,omitempty"`
|
|
}{OriginalEstimate: req.OriginalEstimate}
|
|
}
|
|
|
|
constructCustomFields(req.CustomFields, req.configuredCustomFields, &data)
|
|
|
|
return &data
|
|
}
|
|
|
|
func constructCustomFields(fields map[string]string, configuredFields []IssueTypeField, data *createRequest) {
|
|
if len(fields) == 0 || len(configuredFields) == 0 {
|
|
return
|
|
}
|
|
|
|
data.Fields.M.customFields = make(customField)
|
|
|
|
for key, val := range fields {
|
|
for _, configured := range configuredFields {
|
|
identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-")
|
|
if identifier != strings.ToLower(key) {
|
|
continue
|
|
}
|
|
|
|
switch configured.Schema.DataType {
|
|
case customFieldFormatOption:
|
|
data.Fields.M.customFields[configured.Key] = customFieldTypeOption{Value: val}
|
|
case customFieldFormatProject:
|
|
data.Fields.M.customFields[configured.Key] = customFieldTypeProject{Value: val}
|
|
case customFieldFormatArray:
|
|
pieces := strings.Split(strings.TrimSpace(val), ",")
|
|
if configured.Schema.Items == customFieldFormatOption {
|
|
items := make([]customFieldTypeOption, 0)
|
|
for _, p := range pieces {
|
|
items = append(items, customFieldTypeOption{Value: p})
|
|
}
|
|
data.Fields.M.customFields[configured.Key] = items
|
|
} else {
|
|
data.Fields.M.customFields[configured.Key] = pieces
|
|
}
|
|
case customFieldFormatNumber:
|
|
num, err := strconv.ParseFloat(val, 64) //nolint:gomnd
|
|
if err != nil {
|
|
// Let Jira API handle data type error for now.
|
|
data.Fields.M.customFields[configured.Key] = val
|
|
} else {
|
|
data.Fields.M.customFields[configured.Key] = customFieldTypeNumber(num)
|
|
}
|
|
default:
|
|
data.Fields.M.customFields[configured.Key] = val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type createRequest struct {
|
|
Update struct{} `json:"update"`
|
|
Fields createFieldsMarshaler `json:"fields"`
|
|
}
|
|
|
|
type nameOrAccountID struct {
|
|
Name *string `json:"name,omitempty"` // For local installation.
|
|
AccountID *string `json:"accountId,omitempty"` // For cloud installation.
|
|
}
|
|
|
|
type createFields struct {
|
|
Project struct {
|
|
Key string `json:"key"`
|
|
} `json:"project"`
|
|
IssueType struct {
|
|
Name string `json:"name"`
|
|
} `json:"issuetype"`
|
|
Parent *struct {
|
|
Key string `json:"key"`
|
|
} `json:"parent,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Summary string `json:"summary"`
|
|
Description interface{} `json:"description,omitempty"`
|
|
Reporter *nameOrAccountID `json:"reporter,omitempty"`
|
|
Assignee *nameOrAccountID `json:"assignee,omitempty"`
|
|
Priority *struct {
|
|
Name string `json:"name,omitempty"`
|
|
} `json:"priority,omitempty"`
|
|
Labels []string `json:"labels,omitempty"`
|
|
Components []struct {
|
|
Name string `json:"name,omitempty"`
|
|
} `json:"components,omitempty"`
|
|
FixVersions []struct {
|
|
Name string `json:"name,omitempty"`
|
|
} `json:"fixVersions,omitempty"`
|
|
AffectsVersions []struct {
|
|
Name string `json:"name,omitempty"`
|
|
} `json:"versions,omitempty"`
|
|
TimeTracking *struct {
|
|
OriginalEstimate string `json:"originalEstimate,omitempty"`
|
|
} `json:"timetracking,omitempty"`
|
|
epicField string
|
|
customFields customField
|
|
}
|
|
|
|
type createFieldsMarshaler struct {
|
|
M createFields
|
|
}
|
|
|
|
// MarshalJSON is a custom marshaler to handle dynamic field.
|
|
func (cfm *createFieldsMarshaler) MarshalJSON() ([]byte, error) {
|
|
m, err := json.Marshal(cfm.M)
|
|
if err != nil {
|
|
return m, err
|
|
}
|
|
|
|
var temp interface{}
|
|
if err := json.Unmarshal(m, &temp); err != nil {
|
|
return nil, err
|
|
}
|
|
dm := temp.(map[string]interface{})
|
|
|
|
if epic, ok := dm["name"]; ok {
|
|
if cfm.M.epicField != "" {
|
|
dm[cfm.M.epicField] = epic
|
|
}
|
|
delete(dm, "name")
|
|
}
|
|
|
|
for key, val := range cfm.M.customFields {
|
|
dm[key] = val
|
|
}
|
|
|
|
return json.Marshal(dm)
|
|
}
|