Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/jtk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `fields` command group for custom field management: `create`, `delete` (trash), `restore`, `contexts` (list/create/delete), and `options` (list/add/update/delete) ([#155](https://github.com/open-cli-collective/atlassian-cli/issues/155))
- `projects create`, `update`, `delete`, `restore`, `types` commands for full project management ([#106](https://github.com/open-cli-collective/atlassian-cli/pull/106))
- `automation create` command to create rules from JSON files ([#79](https://github.com/open-cli-collective/atlassian-cli/pull/79))
- `automation enable`, `disable`, `update`, `export` commands for full automation rule management ([#76](https://github.com/open-cli-collective/atlassian-cli/pull/76))
Expand Down
1 change: 1 addition & 0 deletions tools/jtk/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jira-ticket-cli/
│ ├── cmd/ # Cobra commands (one package per resource)
│ │ ├── root/ # Root command, Options struct, global flags
│ │ ├── issues/ # issues list, get, create, update, delete, search, assign, fields, field-options, types, move
│ │ ├── fields/ # fields list, create, delete, restore, contexts (list/create/delete), options (list/add/update/delete)
│ │ ├── projects/ # projects list, get, create, update, delete, restore, types
│ │ ├── transitions/ # transitions list, do
│ │ ├── comments/ # comments list, add, delete
Expand Down
1 change: 1 addition & 0 deletions tools/jtk/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
var (
ErrIssueKeyRequired = errors.New("issue key is required")
ErrProjectKeyRequired = errors.New("project key is required")
ErrFieldIDRequired = errors.New("field ID is required")
)

// APIError is an alias for the shared APIError type
Expand Down
255 changes: 255 additions & 0 deletions tools/jtk/api/field_management.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package api

import (
"encoding/json"
"fmt"
"net/url"
)

// CreateFieldRequest represents a request to create a custom field
type CreateFieldRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Description string `json:"description,omitempty"`
SearcherKey string `json:"searcherKey,omitempty"`
}

// FieldContext represents a custom field context
type FieldContext struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
IsGlobalContext bool `json:"isGlobalContext"`
IsAnyIssueType bool `json:"isAnyIssueType"`
}

// FieldContextsResponse represents the paginated response from listing contexts
type FieldContextsResponse struct {
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
IsLast bool `json:"isLast"`
Values []FieldContext `json:"values"`
}

// CreateFieldContextRequest represents a request to create a field context
type CreateFieldContextRequest struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
ProjectIDs []string `json:"projectIds,omitempty"`
IssueTypeIDs []string `json:"issueTypeIds,omitempty"`
}

// FieldContextOption represents a single option in a context
type FieldContextOption struct {
ID string `json:"id"`
Value string `json:"value"`
Disabled bool `json:"disabled"`
}

// FieldContextOptionsResponse represents the paginated response from listing context options
type FieldContextOptionsResponse struct {
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
IsLast bool `json:"isLast"`
Values []FieldContextOption `json:"values"`
}

// CreateFieldContextOptionsRequest represents a request to create options
type CreateFieldContextOptionsRequest struct {
Options []CreateFieldContextOptionEntry `json:"options"`
}

// CreateFieldContextOptionEntry represents a single option to create
type CreateFieldContextOptionEntry struct {
Value string `json:"value"`
Disabled bool `json:"disabled,omitempty"`
}

// UpdateFieldContextOptionsRequest represents a request to update options
type UpdateFieldContextOptionsRequest struct {
Options []UpdateFieldContextOptionEntry `json:"options"`
}

// UpdateFieldContextOptionEntry represents a single option to update
type UpdateFieldContextOptionEntry struct {
ID string `json:"id"`
Value string `json:"value,omitempty"`
Disabled bool `json:"disabled,omitempty"`
}

// CreateField creates a new custom field
func (c *Client) CreateField(req *CreateFieldRequest) (*Field, error) {
urlStr := fmt.Sprintf("%s/field", c.BaseURL)
body, err := c.post(urlStr, req)
if err != nil {
return nil, err
}

var field Field
if err := json.Unmarshal(body, &field); err != nil {
return nil, fmt.Errorf("failed to parse created field: %w", err)
}

return &field, nil
}

// TrashField moves a custom field to the trash (soft delete)
func (c *Client) TrashField(fieldID string) error {
if fieldID == "" {
return ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/trash", c.BaseURL, url.PathEscape(fieldID))
_, err := c.post(urlStr, nil)
return err
}

// RestoreField restores a custom field from the trash
func (c *Client) RestoreField(fieldID string) error {
if fieldID == "" {
return ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/restore", c.BaseURL, url.PathEscape(fieldID))
_, err := c.post(urlStr, nil)
return err
}

// GetFieldContexts returns the contexts for a custom field
func (c *Client) GetFieldContexts(fieldID string) (*FieldContextsResponse, error) {
if fieldID == "" {
return nil, ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context", c.BaseURL, url.PathEscape(fieldID))
body, err := c.get(urlStr)
if err != nil {
return nil, err
}

var result FieldContextsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse field contexts: %w", err)
}

return &result, nil
}

// GetDefaultFieldContext returns the first context for a field.
// Used when --context is omitted to auto-detect the default context.
func (c *Client) GetDefaultFieldContext(fieldID string) (*FieldContext, error) {
result, err := c.GetFieldContexts(fieldID)
if err != nil {
return nil, err
}

if len(result.Values) == 0 {
return nil, fmt.Errorf("no contexts found for field %s", fieldID)
}

return &result.Values[0], nil
}

// CreateFieldContext creates a new context for a custom field
func (c *Client) CreateFieldContext(fieldID string, req *CreateFieldContextRequest) (*FieldContext, error) {
if fieldID == "" {
return nil, ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context", c.BaseURL, url.PathEscape(fieldID))
body, err := c.post(urlStr, req)
if err != nil {
return nil, err
}

var result FieldContext
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse created field context: %w", err)
}

return &result, nil
}

// DeleteFieldContext deletes a field context
func (c *Client) DeleteFieldContext(fieldID, contextID string) error {
if fieldID == "" {
return ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context/%s", c.BaseURL, url.PathEscape(fieldID), url.PathEscape(contextID))
_, err := c.delete(urlStr)
return err
}

// GetFieldContextOptions returns the options for a field context
func (c *Client) GetFieldContextOptions(fieldID, contextID string) (*FieldContextOptionsResponse, error) {
if fieldID == "" {
return nil, ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context/%s/option", c.BaseURL, url.PathEscape(fieldID), url.PathEscape(contextID))
body, err := c.get(urlStr)
if err != nil {
return nil, err
}

var result FieldContextOptionsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse field context options: %w", err)
}

return &result, nil
}

// CreateFieldContextOptions creates new options in a field context
func (c *Client) CreateFieldContextOptions(fieldID, contextID string, req *CreateFieldContextOptionsRequest) ([]FieldContextOption, error) {
if fieldID == "" {
return nil, ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context/%s/option", c.BaseURL, url.PathEscape(fieldID), url.PathEscape(contextID))
body, err := c.post(urlStr, req)
if err != nil {
return nil, err
}

var result FieldContextOptionsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse created field context options: %w", err)
}

return result.Values, nil
}

// UpdateFieldContextOptions updates existing options in a field context
func (c *Client) UpdateFieldContextOptions(fieldID, contextID string, req *UpdateFieldContextOptionsRequest) ([]FieldContextOption, error) {
if fieldID == "" {
return nil, ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context/%s/option", c.BaseURL, url.PathEscape(fieldID), url.PathEscape(contextID))
body, err := c.put(urlStr, req)
if err != nil {
return nil, err
}

var result FieldContextOptionsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse updated field context options: %w", err)
}

return result.Values, nil
}

// DeleteFieldContextOption deletes an option from a field context
func (c *Client) DeleteFieldContextOption(fieldID, contextID, optionID string) error {
if fieldID == "" {
return ErrFieldIDRequired
}

urlStr := fmt.Sprintf("%s/field/%s/context/%s/option/%s", c.BaseURL, url.PathEscape(fieldID), url.PathEscape(contextID), url.PathEscape(optionID))
_, err := c.delete(urlStr)
return err
}
Loading
Loading