Skip to content

Commit efe3f16

Browse files
zwickCopilot
andauthored
Add named field columns to gh project item-list (cli#13823)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent ae66a1c commit efe3f16

6 files changed

Lines changed: 813 additions & 5 deletions

File tree

pkg/cmd/project/item-list/item_list.go

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type listOpts struct {
2121
owner string
2222
number int32
2323
query string
24+
fields []string
25+
fieldIDs []string
2426
exporter cmdutil.Exporter
2527
}
2628

@@ -55,6 +57,9 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
5557
5658
# List items with the "bug" label that are not done
5759
$ gh project item-list 1 --owner "@me" --query "label:bug -status:Done"
60+
61+
# Show the "Status" and "Priority" field values as extra columns
62+
$ gh project item-list 1 --owner "@me" --field "Status" --field "Priority"
5863
`),
5964
Args: cobra.MaximumNArgs(1),
6065
RunE: func(cmd *cobra.Command, args []string) error {
@@ -71,6 +76,22 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
7176
opts.number = int32(num)
7277
}
7378

79+
if err := cmdutil.MutuallyExclusive(
80+
"only one of `--field` or `--field-id` may be used",
81+
len(opts.fields) > 0,
82+
len(opts.fieldIDs) > 0,
83+
); err != nil {
84+
return err
85+
}
86+
87+
if err := cmdutil.MutuallyExclusive(
88+
"cannot use `--format` with `--field` or `--field-id`",
89+
opts.exporter != nil,
90+
len(opts.fields) > 0 || len(opts.fieldIDs) > 0,
91+
); err != nil {
92+
return err
93+
}
94+
7495
config := listConfig{
7596
io: f.IOStreams,
7697
client: client,
@@ -101,6 +122,8 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
101122

102123
listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user")
103124
listCmd.Flags().StringVar(&opts.query, "query", "", `Filter items using the Projects filter syntax, e.g. "assignee:octocat -status:Done"`)
125+
listCmd.Flags().StringArrayVar(&opts.fields, "field", nil, "Name of a field to show as an extra column")
126+
listCmd.Flags().StringArrayVar(&opts.fieldIDs, "field-id", nil, "ID of a field to show as an extra column")
104127
cmdutil.AddFormatFlags(listCmd, &opts.exporter)
105128
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", queries.LimitDefault, "Maximum number of items to fetch")
106129

@@ -142,15 +165,73 @@ func runList(config listConfig) error {
142165
return config.opts.exporter.Write(config.io, project.DetailedItems())
143166
}
144167

145-
return printResults(config, project.Items.Nodes, owner.Login)
168+
// Resolve any requested extra field columns to (header, fieldID) pairs. Name
169+
// columns are resolved against the fields already returned with the items, so
170+
// there is no separate field-ID preflight lookup. When extra columns are
171+
// requested for a project whose field list spans more than one page, fetch the
172+
// complete field list first so resolution does not miss fields beyond the
173+
// first page.
174+
fields := project.Fields.Nodes
175+
if (len(config.opts.fields) > 0 || len(config.opts.fieldIDs) > 0) && project.Fields.PageInfo.HasNextPage {
176+
withFields, err := config.client.ProjectFields(owner, config.opts.number, project.Fields.TotalCount)
177+
if err != nil {
178+
return err
179+
}
180+
fields = withFields.Fields.Nodes
181+
}
182+
183+
extraFields, err := resolveFieldColumns(config.opts, fields)
184+
if err != nil {
185+
return err
186+
}
187+
188+
return printResults(config, project.Items.Nodes, owner.Login, extraFields)
189+
}
190+
191+
// fieldColumn identifies an extra table column to render for each item: Header
192+
// is the column title and FieldID is the field node ID whose value to show.
193+
type fieldColumn struct {
194+
Header string
195+
FieldID string
196+
}
197+
198+
// resolveFieldColumns turns the --field (names) and --field-id (node IDs) flags
199+
// into ordered column descriptors. Names are resolved with ResolveFieldByName so
200+
// unknown or ambiguous names fail with candidate-carrying errors; IDs are
201+
// validated against the project's fields so an unknown ID also fails clearly.
202+
func resolveFieldColumns(opts listOpts, fields []queries.ProjectField) ([]fieldColumn, error) {
203+
columns := make([]fieldColumn, 0, len(opts.fields)+len(opts.fieldIDs))
204+
205+
for _, name := range opts.fields {
206+
field, err := queries.ResolveFieldByName(fields, name)
207+
if err != nil {
208+
return nil, err
209+
}
210+
columns = append(columns, fieldColumn{Header: field.Name(), FieldID: field.ID()})
211+
}
212+
213+
for _, id := range opts.fieldIDs {
214+
field, err := queries.ResolveFieldByID(fields, id)
215+
if err != nil {
216+
return nil, err
217+
}
218+
columns = append(columns, fieldColumn{Header: field.Name(), FieldID: field.ID()})
219+
}
220+
221+
return columns, nil
146222
}
147223

148-
func printResults(config listConfig, items []queries.ProjectItem, login string) error {
224+
func printResults(config listConfig, items []queries.ProjectItem, login string, extraFields []fieldColumn) error {
149225
if len(items) == 0 {
150226
return cmdutil.NewNoResultsError(fmt.Sprintf("Project %d for owner %s has no items", config.opts.number, login))
151227
}
152228

153-
tp := tableprinter.New(config.io, tableprinter.WithHeader("Type", "Title", "Number", "Repository", "ID"))
229+
headers := []string{"Type", "Title", "Number", "Repository", "ID"}
230+
for _, f := range extraFields {
231+
headers = append(headers, f.Header)
232+
}
233+
234+
tp := tableprinter.New(config.io, tableprinter.WithHeader(headers...))
154235

155236
for _, i := range items {
156237
tp.AddField(i.Type())
@@ -162,8 +243,22 @@ func printResults(config listConfig, items []queries.ProjectItem, login string)
162243
}
163244
tp.AddField(i.Repo())
164245
tp.AddField(i.ID(), tableprinter.WithTruncate(nil))
246+
for _, f := range extraFields {
247+
tp.AddField(fieldValueForItem(i, f.FieldID))
248+
}
165249
tp.EndRow()
166250
}
167251

168252
return tp.Render()
169253
}
254+
255+
// fieldValueForItem returns the display value of the field identified by fieldID
256+
// on item i, or an empty string when the item has no value for that field.
257+
func fieldValueForItem(i queries.ProjectItem, fieldID string) string {
258+
for _, v := range i.FieldValues.Nodes {
259+
if v.ID() == fieldID {
260+
return v.DisplayValue()
261+
}
262+
}
263+
return ""
264+
}

0 commit comments

Comments
 (0)