Skip to content

Commit a0bcccc

Browse files
committed
idea: make filtering api-level; support raw too
1 parent 67b6775 commit a0bcccc

File tree

12 files changed

+481
-69
lines changed

12 files changed

+481
-69
lines changed

api/client.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,13 @@ func ProxyCreate(c *jira.Client, cr *jira.CreateRequest) (*jira.CreateResponse,
9696
}
9797

9898
// ProxyGetIssueRaw executes the same request as ProxyGetIssue but returns raw API response body string.
99-
func ProxyGetIssueRaw(c *jira.Client, key string) (string, error) {
99+
// The fields parameter restricts which fields to fetch from Jira. Empty string returns all fields.
100+
func ProxyGetIssueRaw(c *jira.Client, key string, fields string) (string, error) {
100101
it := viper.GetString("installation")
101102
if it == jira.InstallationTypeLocal {
102-
return c.GetIssueV2Raw(key)
103+
return c.GetIssueV2Raw(key, fields)
103104
}
104-
return c.GetIssueRaw(key)
105+
return c.GetIssueRaw(key, fields)
105106
}
106107

107108
// ProxyGetIssue uses either a v2 or v3 version of the Jira GET /issue/{key}
@@ -127,7 +128,8 @@ func ProxyGetIssue(c *jira.Client, key string, opts ...filter.Filter) (*jira.Iss
127128
// ProxySearch uses either a v2 or v3 version of the Jira GET /search endpoint
128129
// to search for the relevant issues based on configured installation type.
129130
// Defaults to v3 if installation type is not defined in the config.
130-
func ProxySearch(c *jira.Client, jql string, from, limit uint) (*jira.SearchResult, error) {
131+
// The fields parameter controls which fields to fetch from Jira. Empty string uses defaults.
132+
func ProxySearch(c *jira.Client, jql string, from, limit uint, fields string) (*jira.SearchResult, error) {
131133
var (
132134
issues *jira.SearchResult
133135
err error
@@ -136,9 +138,9 @@ func ProxySearch(c *jira.Client, jql string, from, limit uint) (*jira.SearchResu
136138
it := viper.GetString("installation")
137139

138140
if it == jira.InstallationTypeLocal {
139-
issues, err = c.SearchV2(jql, from, limit)
141+
issues, err = c.SearchV2(jql, from, limit, fields)
140142
} else {
141-
issues, err = c.Search(jql, limit)
143+
issues, err = c.Search(jql, limit, fields)
142144
}
143145

144146
return issues, err

internal/cmd/epic/list/list.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func singleEpicView(flags query.FlagParser, key, project, projectType, server st
106106
q.Params().Parent = key
107107
q.Params().IssueType = ""
108108

109-
resp, err = client.Search(q.Get(), q.Params().Limit)
109+
resp, err = client.Search(q.Get(), q.Params().Limit, "")
110110
} else {
111111
resp, err = client.EpicIssues(key, q.Get(), q.Params().From, q.Params().Limit)
112112
}
@@ -181,7 +181,7 @@ func epicExplorerView(cmd *cobra.Command, flags query.FlagParser, project, proje
181181
s := cmdutil.Info("Fetching epics...")
182182
defer s.Stop()
183183

184-
resp, err := api.ProxySearch(client, q.Get(), q.Params().From, q.Params().Limit)
184+
resp, err := api.ProxySearch(client, q.Get(), q.Params().From, q.Params().Limit, "")
185185
if err != nil {
186186
return nil, err
187187
}
@@ -209,7 +209,7 @@ func epicExplorerView(cmd *cobra.Command, flags query.FlagParser, project, proje
209209
q.Params().Parent = key
210210
q.Params().IssueType = ""
211211

212-
resp, err = client.Search(q.Get(), q.Params().Limit)
212+
resp, err = client.Search(q.Get(), q.Params().Limit, "")
213213
} else {
214214
resp, err = client.EpicIssues(key, "", q.Params().From, q.Params().Limit)
215215
}

internal/cmd/issue/list/list.go

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ $ jira issue list --raw
6060
# List issues in JSON with human-readable custom field names
6161
$ jira issue list --json
6262
63+
# List issues in JSON with only specific fields (efficient!)
64+
$ jira issue list --json --fields "key,summary,status,assignee"
65+
66+
# List issues in JSON with custom fields by name
67+
$ jira issue list --json --fields "key,summary,Epic Name,Story Points"
68+
6369
# List issues of type "Epic" in status "Done"
6470
$ jira issue list -tEpic -sDone
6571
@@ -113,6 +119,25 @@ func loadList(cmd *cobra.Command, args []string) {
113119
cmdutil.ExitIfError(cmd.Flags().Set("jql", searchQuery))
114120
}
115121

122+
// Check for --json and --raw flags to determine API field filtering
123+
jsonOutput, err := cmd.Flags().GetBool("json")
124+
cmdutil.ExitIfError(err)
125+
126+
rawOutput, err := cmd.Flags().GetBool("raw")
127+
cmdutil.ExitIfError(err)
128+
129+
var apiFields string
130+
if jsonOutput || rawOutput {
131+
// For --json or --raw output, use --fields for API-level filtering (optional)
132+
fieldsStr, err := cmd.Flags().GetString("fields")
133+
cmdutil.ExitIfError(err)
134+
135+
// Translate human-readable field names to field IDs
136+
if fieldsStr != "" {
137+
apiFields = cmdcommon.TranslateFieldNames(fieldsStr)
138+
}
139+
}
140+
116141
issues, err := func() ([]*jira.Issue, error) {
117142
s := cmdutil.Info("Fetching issues...")
118143
defer s.Stop()
@@ -122,7 +147,7 @@ func loadList(cmd *cobra.Command, args []string) {
122147
return nil, err
123148
}
124149

125-
resp, err := api.ProxySearch(api.DefaultClient(debug), q.Get(), q.Params().From, q.Params().Limit)
150+
resp, err := api.ProxySearch(api.DefaultClient(debug), q.Get(), q.Params().From, q.Params().Limit, apiFields)
126151
if err != nil {
127152
return nil, err
128153
}
@@ -137,34 +162,27 @@ func loadList(cmd *cobra.Command, args []string) {
137162
return
138163
}
139164

140-
jsonOutput, err := cmd.Flags().GetBool("json")
141-
cmdutil.ExitIfError(err)
142-
143-
raw, err := cmd.Flags().GetBool("raw")
144-
cmdutil.ExitIfError(err)
145-
146165
if jsonOutput {
147-
// Get filter fields
148-
filterStr, err := cmd.Flags().GetString("filter")
166+
noWarnings, err := cmd.Flags().GetBool("no-warnings")
149167
cmdutil.ExitIfError(err)
150168

151-
var filterFields []string
152-
if filterStr != "" {
153-
filterFields = strings.Split(filterStr, ",")
154-
// Trim whitespace from each field
155-
for i := range filterFields {
156-
filterFields[i] = strings.TrimSpace(filterFields[i])
157-
}
158-
}
159-
160-
noWarnings, err := cmd.Flags().GetBool("no-warnings")
169+
// Get fields for output filtering
170+
// Note: For issue list, the search API doesn't properly restrict fields,
171+
// so we need to do output filtering even though we set API fields parameter.
172+
fieldsStr, err := cmd.Flags().GetString("fields")
161173
cmdutil.ExitIfError(err)
162174

175+
fieldMappings, err := cmdcommon.GetConfiguredCustomFields()
176+
if err != nil {
177+
fieldMappings = []jira.IssueTypeField{}
178+
}
179+
180+
filterFields := cmdcommon.BuildFilterPaths(fieldsStr, fieldMappings)
163181
outputJSON(issues, filterFields, noWarnings)
164182
return
165183
}
166184

167-
if raw {
185+
if rawOutput {
168186
outputRawJSON(issues)
169187
return
170188
}
@@ -251,7 +269,7 @@ func outputJSON(issues []*jira.Issue, filter []string, noWarnings bool) {
251269
fieldMappings = []jira.IssueTypeField{}
252270
}
253271

254-
// Convert custom field IDs to readable names and apply filter
272+
// Convert custom field IDs to readable names and apply output filter
255273
result, err := jira.TransformIssueFields(rawJSON, fieldMappings, filter)
256274
if err != nil {
257275
cmdutil.Failed("Failed to format JSON output: %s", err)
@@ -306,7 +324,10 @@ func SetFlags(cmd *cobra.Command) {
306324
cmd.Flags().Uint("comments", 1, "Show N comments when viewing the issue")
307325
cmd.Flags().Bool("raw", false, "Print raw JSON output")
308326
cmd.Flags().Bool("json", false, "Print JSON output with human-readable custom field names")
309-
cmd.Flags().String("filter", "", "Comma-separated list of fields to include in JSON output (e.g., 'key,fields.summary,fields.status.name'). Only works with --json")
327+
cmd.Flags().String("fields", "", "Comma-separated list of fields to fetch from Jira API (e.g., 'key,summary,status,assignee'). "+
328+
"Use Jira field names, human-readable names from your config (e.g., 'Story Points', 'Sprint'), "+
329+
"custom field IDs (e.g., 'customfield_10001'), or special values like '*navigable' (common fields), '*all' (most fields). "+
330+
"Only works with --json or --raw. If not specified, uses Jira's default field selection.")
310331
cmd.Flags().Bool("no-warnings", false, "Suppress warnings about field name collisions. Only works with --json")
311332
cmd.Flags().Bool("csv", false, "Print output in CSV format")
312333

internal/cmd/issue/view/view.go

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package view
22

33
import (
44
"fmt"
5-
"strings"
65

76
"github.com/spf13/cobra"
87
"github.com/spf13/viper"
@@ -26,7 +25,10 @@ $ jira issue view ISSUE-1 --comments 5
2625
$ jira issue view ISSUE-1 --raw
2726
2827
# Get JSON output with human-readable custom field names
29-
$ jira issue view ISSUE-1 --json`
28+
$ jira issue view ISSUE-1 --json
29+
30+
# Get JSON with only specific fields (efficient!)
31+
$ jira issue view ISSUE-1 --json --fields "key,summary,status,Story Points"`
3032

3133
flagRaw = "raw"
3234
flagJSON = "json"
@@ -60,7 +62,10 @@ func NewCmdView() *cobra.Command {
6062
cmd.Flags().Bool(flagPlain, false, "Display output in plain mode")
6163
cmd.Flags().Bool(flagRaw, false, "Print raw Jira API response")
6264
cmd.Flags().Bool(flagJSON, false, "Print JSON output with human-readable custom field names")
63-
cmd.Flags().String("filter", "", "Comma-separated list of fields to include in JSON output (e.g., 'key,fields.summary,fields.status.name'). Only works with --json")
65+
cmd.Flags().String("fields", "", "Comma-separated list of fields to fetch from Jira API (e.g., 'key,summary,status,assignee'). "+
66+
"Use Jira field names, human-readable names from your config (e.g., 'Story Points', 'Sprint'), "+
67+
"custom field IDs (e.g., 'customfield_10001'), or special values like '*navigable' (common fields), '*all' (most fields). "+
68+
"Only works with --json or --raw. If not specified, returns all fields.")
6469
cmd.Flags().Bool(flagNoWarnings, false, "Suppress warnings about field name collisions. Only works with --json")
6570

6671
return &cmd
@@ -90,12 +95,18 @@ func viewRaw(cmd *cobra.Command, args []string) {
9095

9196
key := cmdutil.GetJiraIssueKey(viper.GetString(configProject), args[0])
9297

98+
// Get fields for API-level filtering and translate names to IDs
99+
fieldsStr, err := cmd.Flags().GetString("fields")
100+
cmdutil.ExitIfError(err)
101+
102+
fields := cmdcommon.TranslateFieldNames(fieldsStr)
103+
93104
apiResp, err := func() (string, error) {
94105
s := cmdutil.Info(messageFetchingData)
95106
defer s.Stop()
96107

97108
client := api.DefaultClient(debug)
98-
return api.ProxyGetIssueRaw(client, key)
109+
return api.ProxyGetIssueRaw(client, key, fields)
99110
}()
100111
cmdutil.ExitIfError(err)
101112

@@ -108,13 +119,19 @@ func viewJSON(cmd *cobra.Command, args []string) {
108119

109120
key := cmdutil.GetJiraIssueKey(viper.GetString(configProject), args[0])
110121

111-
// Get raw JSON
122+
// Get fields for API-level filtering and translate names to IDs
123+
fieldsStr, err := cmd.Flags().GetString("fields")
124+
cmdutil.ExitIfError(err)
125+
126+
fields := cmdcommon.TranslateFieldNames(fieldsStr)
127+
128+
// Get raw JSON with API field filtering
112129
apiResp, err := func() (string, error) {
113130
s := cmdutil.Info(messageFetchingData)
114131
defer s.Stop()
115132

116133
client := api.DefaultClient(debug)
117-
return api.ProxyGetIssueRaw(client, key)
134+
return api.ProxyGetIssueRaw(client, key, fields)
118135
}()
119136
cmdutil.ExitIfError(err)
120137

@@ -125,20 +142,9 @@ func viewJSON(cmd *cobra.Command, args []string) {
125142
fieldMappings = []jira.IssueTypeField{}
126143
}
127144

128-
// Get filter fields
129-
filterStr, err := cmd.Flags().GetString("filter")
130-
cmdutil.ExitIfError(err)
145+
// Build filter paths for output filtering when --fields is specified
146+
filterFields := cmdcommon.BuildFilterPaths(fieldsStr, fieldMappings)
131147

132-
var filterFields []string
133-
if filterStr != "" {
134-
filterFields = strings.Split(filterStr, ",")
135-
// Trim whitespace from each field
136-
for i := range filterFields {
137-
filterFields[i] = strings.TrimSpace(filterFields[i])
138-
}
139-
}
140-
141-
// Convert custom field IDs to readable names and apply filter
142148
result, err := jira.TransformIssueFields([]byte(apiResp), fieldMappings, filterFields)
143149
if err != nil {
144150
cmdutil.Failed("Failed to format JSON output: %s", err)
@@ -192,3 +198,5 @@ func viewPretty(cmd *cobra.Command, args []string) {
192198
}
193199
cmdutil.ExitIfError(v.Render())
194200
}
201+
202+

internal/cmdcommon/fields.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package cmdcommon
2+
3+
import (
4+
"strings"
5+
6+
"github.com/ankitpokhrel/jira-cli/pkg/jira"
7+
)
8+
9+
// TranslateFieldNames converts human-readable field names to field IDs.
10+
// For example: "Story Points,summary" -> "customfield_10001,summary"
11+
// Leaves unknown names and field IDs unchanged.
12+
// Also normalizes the input by trimming whitespace and removing empty fields.
13+
func TranslateFieldNames(fieldsStr string) string {
14+
if fieldsStr == "" {
15+
return ""
16+
}
17+
18+
// Get field mappings from config
19+
fieldMappings, err := GetConfiguredCustomFields()
20+
21+
// Build name -> ID map (case-insensitive for user convenience)
22+
nameToID := make(map[string]string)
23+
if err == nil {
24+
for _, field := range fieldMappings {
25+
nameToID[strings.ToLower(field.Name)] = field.Key
26+
}
27+
}
28+
29+
// Process each field in the comma-separated list
30+
fields := strings.Split(fieldsStr, ",")
31+
translatedFields := make([]string, 0, len(fields))
32+
33+
for _, field := range fields {
34+
field = strings.TrimSpace(field)
35+
36+
// Skip empty fields (important for cases like "key,,summary")
37+
if field == "" {
38+
continue
39+
}
40+
41+
// If it's already a customfield ID or special value, keep as-is
42+
if strings.HasPrefix(field, "customfield_") || strings.HasPrefix(field, "*") {
43+
translatedFields = append(translatedFields, field)
44+
continue
45+
}
46+
47+
// Try to translate from config (case-insensitive)
48+
if fieldID, ok := nameToID[strings.ToLower(field)]; ok {
49+
translatedFields = append(translatedFields, fieldID)
50+
} else {
51+
// Unknown field name, keep as-is (might be a standard field like "key", "summary")
52+
translatedFields = append(translatedFields, field)
53+
}
54+
}
55+
56+
return strings.Join(translatedFields, ",")
57+
}
58+
59+
// BuildFilterPaths converts field names to JSON paths for output filtering.
60+
// For example: "key,summary,Rank" -> ["key", "fields.summary", "fields.rank"]
61+
// This accounts for custom field name transformations (e.g., "Rank" -> "rank" in camelCase).
62+
func BuildFilterPaths(fieldsStr string, fieldMappings []jira.IssueTypeField) []string {
63+
if fieldsStr == "" {
64+
return nil
65+
}
66+
67+
// Build mappings for field name translation
68+
nameToID := make(map[string]string)
69+
idToName := make(map[string]string)
70+
for _, field := range fieldMappings {
71+
nameToID[strings.ToLower(field.Name)] = field.Key
72+
idToName[field.Key] = field.Name
73+
}
74+
75+
// Convert field names to JSON paths for filtering
76+
var filterFields []string
77+
jiraFields := strings.Split(fieldsStr, ",")
78+
79+
for _, field := range jiraFields {
80+
field = strings.TrimSpace(field)
81+
82+
// Skip wildcards and empty
83+
if strings.HasPrefix(field, "*") || field == "" {
84+
continue
85+
}
86+
87+
// Top-level fields like "key", "id", "self"
88+
if field == "key" || field == "id" || field == "self" {
89+
filterFields = append(filterFields, field)
90+
continue
91+
}
92+
93+
// Check if it's a custom field name that will be transformed
94+
if _, ok := nameToID[strings.ToLower(field)]; ok {
95+
// It's a custom field - use the transformed camelCase name
96+
filterFields = append(filterFields, "fields."+jira.ToFieldName(field))
97+
} else if strings.HasPrefix(field, "customfield_") {
98+
// It's a field ID - check if it will be transformed
99+
if name, ok := idToName[field]; ok {
100+
filterFields = append(filterFields, "fields."+jira.ToFieldName(name))
101+
} else {
102+
// Keep field ID as-is (not in config)
103+
filterFields = append(filterFields, "fields."+field)
104+
}
105+
} else {
106+
// Standard field like "summary", "status"
107+
filterFields = append(filterFields, "fields."+field)
108+
}
109+
}
110+
111+
return filterFields
112+
}

0 commit comments

Comments
 (0)