Remove project JSON formatting objects (#8541)

This commit is contained in:
Heath Stewart 2024-01-17 09:56:58 -08:00 committed by GitHub
parent 57f6787c15
commit cf483770c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 417 additions and 625 deletions

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -104,7 +103,7 @@ func runClose(config closeConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.UpdateProjectV2.ProjectV2)
return config.opts.exporter.Write(config.io, query.UpdateProjectV2.ProjectV2)
}
return printResults(config, query.UpdateProjectV2.ProjectV2)
@ -132,8 +131,3 @@ func printResults(config closeConfig, project queries.Project) error {
_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
return err
}
func printJSON(config closeConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -114,7 +113,7 @@ func runCopy(config copyConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.CopyProjectV2.ProjectV2)
return config.opts.exporter.Write(config.io, query.CopyProjectV2.ProjectV2)
}
return printResults(config, query.CopyProjectV2.ProjectV2)
@ -142,8 +141,3 @@ func printResults(config copyConfig, project queries.Project) error {
_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
return err
}
func printJSON(config copyConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}

View file

@ -5,7 +5,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -86,7 +85,7 @@ func runCreate(config createConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.CreateProjectV2.ProjectV2)
return config.opts.exporter.Write(config.io, query.CreateProjectV2.ProjectV2)
}
return printResults(config, query.CreateProjectV2.ProjectV2)
@ -113,8 +112,3 @@ func printResults(config createConfig, project queries.Project) error {
_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
return err
}
func printJSON(config createConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -97,7 +96,7 @@ func runDelete(config deleteConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.DeleteProject.Project)
return config.opts.exporter.Write(config.io, query.DeleteProject.Project)
}
return printResults(config, query.DeleteProject.Project)
@ -124,8 +123,3 @@ func printResults(config deleteConfig, project queries.Project) error {
_, err := fmt.Fprintf(config.io.Out, "Deleted project %d\n", project.Number)
return err
}
func printJSON(config deleteConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -111,7 +110,7 @@ func runEdit(config editConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.UpdateProjectV2.ProjectV2)
return config.opts.exporter.Write(config.io, query.UpdateProjectV2.ProjectV2)
}
return printResults(config, query.UpdateProjectV2.ProjectV2)
@ -153,8 +152,3 @@ func printResults(config editConfig, project queries.Project) error {
_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
return err
}
func printJSON(config editConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -114,7 +113,7 @@ func runCreateField(config createFieldConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.CreateProjectV2Field.Field)
return config.opts.exporter.Write(config.io, query.CreateProjectV2Field.Field)
}
return printResults(config, query.CreateProjectV2Field.Field)
@ -151,8 +150,3 @@ func printResults(config createFieldConfig, field queries.ProjectField) error {
_, err := fmt.Fprintf(config.io.Out, "Created field\n")
return err
}
func printJSON(config createFieldConfig, field queries.ProjectField) error {
projectFieldJSON := format.JSONProjectField(field)
return config.opts.exporter.Write(config.io, projectFieldJSON)
}

View file

@ -4,7 +4,6 @@ import (
"fmt"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -71,7 +70,7 @@ func runDeleteField(config deleteFieldConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.DeleteProjectV2Field.Field)
return config.opts.exporter.Write(config.io, query.DeleteProjectV2Field.Field)
}
return printResults(config, query.DeleteProjectV2Field.Field)
@ -93,8 +92,3 @@ func printResults(config deleteFieldConfig, field queries.ProjectField) error {
_, err := fmt.Fprintf(config.io.Out, "Deleted field\n")
return err
}
func printJSON(config deleteFieldConfig, field queries.ProjectField) error {
projectFieldJSON := format.JSONProjectField(field)
return config.opts.exporter.Write(config.io, projectFieldJSON)
}

View file

@ -7,7 +7,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -95,7 +94,7 @@ func runList(config listConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, project)
return config.opts.exporter.Write(config.io, project.Fields)
}
return printResults(config, project.Fields.Nodes, owner.Login)
@ -117,8 +116,3 @@ func printResults(config listConfig, fields []queries.ProjectField, login string
return tp.Render()
}
func printJSON(config listConfig, project *queries.Project) error {
projectsJSON := format.JSONProjectFields(project)
return config.opts.exporter.Write(config.io, projectsJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -108,7 +107,7 @@ func runAddItem(config addItemConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.CreateProjectItem.ProjectV2Item)
return config.opts.exporter.Write(config.io, query.CreateProjectItem.ProjectV2Item)
}
return printResults(config, query.CreateProjectItem.ProjectV2Item)
@ -132,8 +131,3 @@ func printResults(config addItemConfig, item queries.ProjectItem) error {
_, err := fmt.Fprintf(config.io.Out, "Added item\n")
return err
}
func printJSON(config addItemConfig, item queries.ProjectItem) error {
projectItemJSON := format.JSONProjectItem(item)
return config.opts.exporter.Write(config.io, projectItemJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -110,7 +109,7 @@ func runArchiveItem(config archiveItemConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.UnarchiveProjectItem.ProjectV2Item)
return config.opts.exporter.Write(config.io, query.UnarchiveProjectItem.ProjectV2Item)
}
return printResults(config, query.UnarchiveProjectItem.ProjectV2Item)
@ -122,7 +121,7 @@ func runArchiveItem(config archiveItemConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.ArchiveProjectItem.ProjectV2Item)
return config.opts.exporter.Write(config.io, query.ArchiveProjectItem.ProjectV2Item)
}
return printResults(config, query.ArchiveProjectItem.ProjectV2Item)
@ -159,8 +158,3 @@ func printResults(config archiveItemConfig, item queries.ProjectItem) error {
_, err := fmt.Fprintf(config.io.Out, "Archived item\n")
return err
}
func printJSON(config archiveItemConfig, item queries.ProjectItem) error {
projectItemJSON := format.JSONProjectItem(item)
return config.opts.exporter.Write(config.io, projectItemJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -104,7 +103,7 @@ func runCreateItem(config createItemConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.CreateProjectDraftItem.ProjectV2Item)
return config.opts.exporter.Write(config.io, query.CreateProjectDraftItem.ProjectV2Item)
}
return printResults(config, query.CreateProjectDraftItem.ProjectV2Item)
@ -128,8 +127,3 @@ func printResults(config createItemConfig, item queries.ProjectItem) error {
_, err := fmt.Fprintf(config.io.Out, "Created item\n")
return err
}
func printJSON(config createItemConfig, item queries.ProjectItem) error {
projectItemJSON := format.JSONProjectItem(item)
return config.opts.exporter.Write(config.io, projectItemJSON)
}

View file

@ -7,7 +7,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -219,11 +218,6 @@ func printDraftIssueResults(config editItemConfig, item queries.DraftIssue) erro
return err
}
func printDraftIssueJSON(config editItemConfig, item queries.DraftIssue) error {
projectDraftItemJSON := format.JSONProjectDraftIssue(item)
return config.opts.exporter.Write(config.io, projectDraftItemJSON)
}
func printItemResults(config editItemConfig, item *queries.ProjectItem) error {
if !config.io.IsStdoutTTY() {
return nil
@ -232,11 +226,6 @@ func printItemResults(config editItemConfig, item *queries.ProjectItem) error {
return err
}
func printItemJSON(config editItemConfig, item *queries.ProjectItem) error {
projectItemJSON := format.JSONProjectItem(*item)
return config.opts.exporter.Write(config.io, projectItemJSON)
}
func clearItemFieldValue(config editItemConfig) error {
if err := fieldIdAndProjectIdPresence(config); err != nil {
return err
@ -248,7 +237,7 @@ func clearItemFieldValue(config editItemConfig) error {
}
if config.opts.exporter != nil {
return printItemJSON(config, &query.Clear.Item)
return config.opts.exporter.Write(config.io, &query.Clear.Item)
}
return printItemResults(config, &query.Clear.Item)
@ -267,7 +256,7 @@ func updateDraftIssue(config editItemConfig) error {
}
if config.opts.exporter != nil {
return printDraftIssueJSON(config, query.UpdateProjectV2DraftIssue.DraftIssue)
return config.opts.exporter.Write(config.io, query.UpdateProjectV2DraftIssue.DraftIssue)
}
return printDraftIssueResults(config, query.UpdateProjectV2DraftIssue.DraftIssue)
@ -294,7 +283,7 @@ func updateItemValues(config editItemConfig) error {
}
if config.opts.exporter != nil {
return printItemJSON(config, &query.Update.Item)
return config.opts.exporter.Write(config.io, &query.Update.Item)
}
return printItemResults(config, &query.Update.Item)

View file

@ -546,6 +546,7 @@ func TestRunItemEdit_JSON(t *testing.T) {
"data": map[string]interface{}{
"updateProjectV2DraftIssue": map[string]interface{}{
"draftIssue": map[string]interface{}{
"id": "DI_item_id",
"title": "a title",
"body": "a new body",
},
@ -571,6 +572,6 @@ func TestRunItemEdit_JSON(t *testing.T) {
assert.NoError(t, err)
assert.JSONEq(
t,
`{"id":"","title":"a title","body":"a new body","type":"DraftIssue"}`,
`{"id":"DI_item_id","title":"a title","body":"a new body","type":"DraftIssue"}`,
stdout.String())
}

View file

@ -7,7 +7,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/tableprinter"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -94,7 +93,7 @@ func runList(config listConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, project)
return config.opts.exporter.Write(config.io, project.DetailedItems())
}
return printResults(config, project.Items.Nodes, owner.Login)
@ -122,8 +121,3 @@ func printResults(config listConfig, items []queries.ProjectItem, login string)
return tp.Render()
}
func printJSON(config listConfig, project *queries.Project) error {
projectDetailedItemsJSON := format.JSONProjectDetailedItems(project)
return config.opts.exporter.Write(config.io, projectDetailedItemsJSON)
}

View file

@ -97,14 +97,14 @@ func runList(config listConfig) error {
return err
}
projects, totalCount, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false)
projects, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false)
if err != nil {
return err
}
projects = filterProjects(projects, config)
if config.opts.exporter != nil {
return printJSON(config, projects, totalCount)
return config.opts.exporter.Write(config.io, projects)
}
return printResults(config, projects, owner.Login)
@ -138,26 +138,29 @@ func buildURL(config listConfig) (string, error) {
return url, nil
}
func filterProjects(nodes []queries.Project, config listConfig) []queries.Project {
projects := make([]queries.Project, 0, len(nodes))
for _, p := range nodes {
if !config.opts.closed && p.Closed {
func filterProjects(nodes queries.Projects, config listConfig) queries.Projects {
filtered := queries.Projects{
Nodes: make([]queries.Project, 0, len(nodes.Nodes)),
TotalCount: nodes.TotalCount,
}
for _, project := range nodes.Nodes {
if !config.opts.closed && project.Closed {
continue
}
projects = append(projects, p)
filtered.Nodes = append(filtered.Nodes, project)
}
return projects
return filtered
}
func printResults(config listConfig, projects []queries.Project, owner string) error {
if len(projects) == 0 {
func printResults(config listConfig, projects queries.Projects, owner string) error {
if len(projects.Nodes) == 0 {
return cmdutil.NewNoResultsError(fmt.Sprintf("No projects found for %s", owner))
}
tp := tableprinter.New(config.io, tableprinter.WithHeader("Number", "Title", "State", "ID"))
cs := config.io.ColorScheme()
for _, p := range projects {
for _, p := range projects.Nodes {
tp.AddField(
strconv.Itoa(int(p.Number)),
tableprinter.WithTruncate(nil),
@ -173,8 +176,3 @@ func printResults(config listConfig, projects []queries.Project, owner string) e
return tp.Render()
}
func printJSON(config listConfig, projects []queries.Project, totalCount int) error {
projectsJSON := format.JSONProjects(projects, totalCount)
return config.opts.exporter.Write(config.io, projectsJSON)
}

View file

@ -6,7 +6,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -108,7 +107,7 @@ func runMarkTemplate(config markTemplateConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, *project)
return config.opts.exporter.Write(config.io, *project)
}
return printResults(config, query.TemplateProject.Project)
@ -121,7 +120,7 @@ func runMarkTemplate(config markTemplateConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, query.TemplateProject.Project)
return config.opts.exporter.Write(config.io, query.TemplateProject.Project)
}
return printResults(config, query.TemplateProject.Project)
@ -164,8 +163,3 @@ func printResults(config markTemplateConfig, project queries.Project) error {
_, err := fmt.Fprintf(config.io.Out, "Marked project %d as a template.\n", project.Number)
return err
}
func printJSON(config markTemplateConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}

View file

@ -1,386 +0,0 @@
package format
import (
"strings"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
)
// JSONProject serializes a Project to JSON.
func JSONProject(project queries.Project) ProjectJSON {
return ProjectJSON{
Number: project.Number,
URL: project.URL,
ShortDescription: project.ShortDescription,
Public: project.Public,
Closed: project.Closed,
// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
// deprecated on 2024-03-07 we can start populating this field again.
// Template: project.Template,
Title: project.Title,
ID: project.ID,
Readme: project.Readme,
Items: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: project.Items.TotalCount,
},
Fields: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: project.Fields.TotalCount,
},
Owner: struct {
Type string `json:"type"`
Login string `json:"login"`
}{
Type: project.OwnerType(),
Login: project.OwnerLogin(),
},
}
}
// JSONProjects serializes a slice of Projects to JSON.
// JSON fields are `totalCount` and `projects`.
func JSONProjects(projects []queries.Project, totalCount int) ProjectsJSON {
var result []ProjectJSON
for _, p := range projects {
result = append(result, ProjectJSON{
Number: p.Number,
URL: p.URL,
ShortDescription: p.ShortDescription,
Public: p.Public,
Closed: p.Closed,
// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
// deprecated on 2024-03-07 we can start populating this field again.
// Template: p.Template,
Title: p.Title,
ID: p.ID,
Readme: p.Readme,
Items: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: p.Items.TotalCount,
},
Fields: struct {
TotalCount int `json:"totalCount"`
}{
TotalCount: p.Fields.TotalCount,
},
Owner: struct {
Type string `json:"type"`
Login string `json:"login"`
}{
Type: p.OwnerType(),
Login: p.OwnerLogin(),
},
})
}
return ProjectsJSON{
Projects: result,
TotalCount: totalCount,
}
}
type ProjectJSON struct {
Number int32 `json:"number"`
URL string `json:"url"`
ShortDescription string `json:"shortDescription"`
Public bool `json:"public"`
Closed bool `json:"closed"`
// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
// deprecated on 2024-03-07 we can start populating this field again.
// Template bool `json:"template"`
Title string `json:"title"`
ID string `json:"id"`
Readme string `json:"readme"`
Items struct {
TotalCount int `json:"totalCount"`
} `graphql:"items(first: 100)" json:"items"`
Fields struct {
TotalCount int `json:"totalCount"`
} `graphql:"fields(first:100)" json:"fields"`
Owner struct {
Type string `json:"type"`
Login string `json:"login"`
} `json:"owner"`
}
type ProjectsJSON struct {
Projects []ProjectJSON `json:"projects"`
TotalCount int `json:"totalCount"`
}
// JSONProjectField serializes a ProjectField to JSON.
func JSONProjectField(field queries.ProjectField) ProjectFieldJSON {
val := ProjectFieldJSON{
ID: field.ID(),
Name: field.Name(),
Type: field.Type(),
}
for _, o := range field.Options() {
val.Options = append(val.Options, SingleSelectOptionJSON{
Name: o.Name,
ID: o.ID,
})
}
return val
}
// JSONProjectFields serializes a slice of ProjectFields to JSON.
// JSON fields are `totalCount` and `fields`.
func JSONProjectFields(project *queries.Project) ProjectFieldsJSON {
var result []ProjectFieldJSON
for _, f := range project.Fields.Nodes {
val := ProjectFieldJSON{
ID: f.ID(),
Name: f.Name(),
Type: f.Type(),
}
for _, o := range f.Options() {
val.Options = append(val.Options, SingleSelectOptionJSON{
Name: o.Name,
ID: o.ID,
})
}
result = append(result, val)
}
return ProjectFieldsJSON{
Fields: result,
TotalCount: project.Fields.TotalCount,
}
}
type ProjectFieldJSON struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Options []SingleSelectOptionJSON `json:"options,omitempty"`
}
type ProjectFieldsJSON struct {
Fields []ProjectFieldJSON `json:"fields"`
TotalCount int `json:"totalCount"`
}
type SingleSelectOptionJSON struct {
ID string `json:"id"`
Name string `json:"name"`
}
// JSONProjectItem serializes a ProjectItem to JSON.
func JSONProjectItem(item queries.ProjectItem) ProjectItemJSON {
return ProjectItemJSON{
ID: item.ID(),
Title: item.Title(),
Body: item.Body(),
Type: item.Type(),
URL: item.URL(),
}
}
type ProjectItemJSON struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Type string `json:"type"`
URL string `json:"url,omitempty"`
}
// JSONProjectDraftIssue serializes a DraftIssue to JSON.
// This is needed because the field for
// https://docs.github.com/en/graphql/reference/mutations#updateprojectv2draftissue
// is a DraftIssue https://docs.github.com/en/graphql/reference/objects#draftissue
// and not a ProjectV2Item https://docs.github.com/en/graphql/reference/objects#projectv2item
func JSONProjectDraftIssue(item queries.DraftIssue) DraftIssueJSON {
return DraftIssueJSON{
ID: item.ID,
Title: item.Title,
Body: item.Body,
Type: "DraftIssue",
}
}
type DraftIssueJSON struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
Type string `json:"type"`
}
func projectItemContent(p queries.ProjectItem) any {
switch p.Content.TypeName {
case "DraftIssue":
return struct {
Type string `json:"type"`
Body string `json:"body"`
Title string `json:"title"`
}{
Type: p.Type(),
Body: p.Body(),
Title: p.Title(),
}
case "Issue":
return struct {
Type string `json:"type"`
Body string `json:"body"`
Title string `json:"title"`
Number int `json:"number"`
Repository string `json:"repository"`
URL string `json:"url"`
}{
Type: p.Type(),
Body: p.Body(),
Title: p.Title(),
Number: p.Number(),
Repository: p.Repo(),
URL: p.URL(),
}
case "PullRequest":
return struct {
Type string `json:"type"`
Body string `json:"body"`
Title string `json:"title"`
Number int `json:"number"`
Repository string `json:"repository"`
URL string `json:"url"`
}{
Type: p.Type(),
Body: p.Body(),
Title: p.Title(),
Number: p.Number(),
Repository: p.Repo(),
URL: p.URL(),
}
}
return nil
}
func projectFieldValueData(v queries.FieldValueNodes) any {
switch v.Type {
case "ProjectV2ItemFieldDateValue":
return v.ProjectV2ItemFieldDateValue.Date
case "ProjectV2ItemFieldIterationValue":
return struct {
Title string `json:"title"`
StartDate string `json:"startDate"`
Duration int `json:"duration"`
}{
Title: v.ProjectV2ItemFieldIterationValue.Title,
StartDate: v.ProjectV2ItemFieldIterationValue.StartDate,
Duration: v.ProjectV2ItemFieldIterationValue.Duration,
}
case "ProjectV2ItemFieldNumberValue":
return v.ProjectV2ItemFieldNumberValue.Number
case "ProjectV2ItemFieldSingleSelectValue":
return v.ProjectV2ItemFieldSingleSelectValue.Name
case "ProjectV2ItemFieldTextValue":
return v.ProjectV2ItemFieldTextValue.Text
case "ProjectV2ItemFieldMilestoneValue":
return struct {
Title string `json:"title"`
Description string `json:"description"`
DueOn string `json:"dueOn"`
}{
Title: v.ProjectV2ItemFieldMilestoneValue.Milestone.Title,
Description: v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
DueOn: v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
}
case "ProjectV2ItemFieldLabelValue":
names := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
names = append(names, p.Name)
}
return names
case "ProjectV2ItemFieldPullRequestValue":
urls := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
urls = append(urls, p.Url)
}
return urls
case "ProjectV2ItemFieldRepositoryValue":
return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
case "ProjectV2ItemFieldUserValue":
logins := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
logins = append(logins, p.Login)
}
return logins
case "ProjectV2ItemFieldReviewerValue":
names := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
if p.Type == "Team" {
names = append(names, p.Team.Name)
} else if p.Type == "User" {
names = append(names, p.User.Login)
}
}
return names
}
return nil
}
// serialize creates a map from field to field values
func serializeProjectWithItems(project *queries.Project) []map[string]any {
fields := make(map[string]string)
// make a map of fields by ID
for _, f := range project.Fields.Nodes {
fields[f.ID()] = camelCase(f.Name())
}
itemsSlice := make([]map[string]any, 0)
// for each value, look up the name by ID
// and set the value to the field value
for _, i := range project.Items.Nodes {
o := make(map[string]any)
o["id"] = i.Id
o["content"] = projectItemContent(i)
for _, v := range i.FieldValues.Nodes {
id := v.ID()
value := projectFieldValueData(v)
o[fields[id]] = value
}
itemsSlice = append(itemsSlice, o)
}
return itemsSlice
}
// JSONProjectWithItems returns a detailed JSON representation of project items.
// JSON fields are `totalCount` and `items`.
func JSONProjectDetailedItems(project *queries.Project) ProjectDetailedItems {
items := serializeProjectWithItems(project)
return ProjectDetailedItems{
Items: items,
TotalCount: project.Items.TotalCount,
}
}
type ProjectDetailedItems struct {
Items []map[string]any `json:"items"`
TotalCount int `json:"totalCount"`
}
// camelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
func camelCase(s string) string {
if len(s) == 0 {
return ""
}
if len(s) == 1 {
return strings.ToLower(s)
}
return strings.ToLower(s[0:1]) + s[1:]
}

View file

@ -1,16 +1,15 @@
package format
package queries
import (
"encoding/json"
"testing"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/stretchr/testify/assert"
)
// Regression test from before ExportData was implemented.
func TestJSONProject_User(t *testing.T) {
project := queries.Project{
project := Project{
ID: "123",
Number: 2,
URL: "a url",
@ -23,14 +22,15 @@ func TestJSONProject_User(t *testing.T) {
project.Fields.TotalCount = 2
project.Owner.TypeName = "User"
project.Owner.User.Login = "monalisa"
b, err := json.Marshal(JSONProject(project))
b, err := json.Marshal(project.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b))
assert.JSONEq(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b))
}
// Regression test from before ExportData was implemented.
func TestJSONProject_Org(t *testing.T) {
project := queries.Project{
project := Project{
ID: "123",
Number: 2,
URL: "a url",
@ -43,14 +43,15 @@ func TestJSONProject_Org(t *testing.T) {
project.Fields.TotalCount = 2
project.Owner.TypeName = "Organization"
project.Owner.Organization.Login = "github"
b, err := json.Marshal(JSONProject(project))
b, err := json.Marshal(project.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b))
assert.JSONEq(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b))
}
// Regression test from before ExportData was implemented.
func TestJSONProjects(t *testing.T) {
userProject := queries.Project{
userProject := Project{
ID: "123",
Number: 2,
URL: "a url",
@ -64,7 +65,7 @@ func TestJSONProjects(t *testing.T) {
userProject.Owner.TypeName = "User"
userProject.Owner.User.Login = "monalisa"
orgProject := queries.Project{
orgProject := Project{
ID: "123",
Number: 2,
URL: "a url",
@ -77,34 +78,38 @@ func TestJSONProjects(t *testing.T) {
orgProject.Fields.TotalCount = 2
orgProject.Owner.TypeName = "Organization"
orgProject.Owner.Organization.Login = "github"
projectsJSON := JSONProjects([]queries.Project{userProject, orgProject}, 2)
b, err := json.Marshal(projectsJSON)
projects := Projects{
Nodes: []Project{userProject, orgProject},
TotalCount: 2,
}
b, err := json.Marshal(projects.ExportData(nil))
assert.NoError(t, err)
assert.Equal(
assert.JSONEq(
t,
`{"projects":[{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}},{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}],"totalCount":2}`,
string(b))
}
func TestJSONProjectField_FieldType(t *testing.T) {
field := queries.ProjectField{}
field := ProjectField{}
field.TypeName = "ProjectV2Field"
field.Field.ID = "123"
field.Field.Name = "name"
b, err := json.Marshal(JSONProjectField(field))
b, err := json.Marshal(field.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2Field"}`, string(b))
}
func TestJSONProjectField_SingleSelectType(t *testing.T) {
field := queries.ProjectField{}
field := ProjectField{}
field.TypeName = "ProjectV2SingleSelectField"
field.SingleSelectField.ID = "123"
field.SingleSelectField.Name = "name"
field.SingleSelectField.Options = []queries.SingleSelectFieldOptions{
field.SingleSelectField.Options = []SingleSelectFieldOptions{
{
ID: "123",
Name: "name",
@ -115,35 +120,35 @@ func TestJSONProjectField_SingleSelectType(t *testing.T) {
},
}
b, err := json.Marshal(JSONProjectField(field))
b, err := json.Marshal(field.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}`, string(b))
assert.JSONEq(t, `{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}`, string(b))
}
func TestJSONProjectField_ProjectV2IterationField(t *testing.T) {
field := queries.ProjectField{}
field := ProjectField{}
field.TypeName = "ProjectV2IterationField"
field.IterationField.ID = "123"
field.IterationField.Name = "name"
b, err := json.Marshal(JSONProjectField(field))
b, err := json.Marshal(field.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2IterationField"}`, string(b))
}
func TestJSONProjectFields(t *testing.T) {
field := queries.ProjectField{}
field := ProjectField{}
field.TypeName = "ProjectV2Field"
field.Field.ID = "123"
field.Field.Name = "name"
field2 := queries.ProjectField{}
field2 := ProjectField{}
field2.TypeName = "ProjectV2SingleSelectField"
field2.SingleSelectField.ID = "123"
field2.SingleSelectField.Name = "name"
field2.SingleSelectField.Options = []queries.SingleSelectFieldOptions{
field2.SingleSelectField.Options = []SingleSelectFieldOptions{
{
ID: "123",
Name: "name",
@ -154,73 +159,72 @@ func TestJSONProjectFields(t *testing.T) {
},
}
p := &queries.Project{
p := &Project{
Fields: struct {
TotalCount int
Nodes []queries.ProjectField
PageInfo queries.PageInfo
Nodes []ProjectField
PageInfo PageInfo
}{
Nodes: []queries.ProjectField{field, field2},
Nodes: []ProjectField{field, field2},
TotalCount: 5,
},
}
projectFieldsJSON := JSONProjectFields(p)
b, err := json.Marshal(projectFieldsJSON)
b, err := json.Marshal(p.Fields.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"fields":[{"id":"123","name":"name","type":"ProjectV2Field"},{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}],"totalCount":5}`, string(b))
assert.JSONEq(t, `{"fields":[{"id":"123","name":"name","type":"ProjectV2Field"},{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}],"totalCount":5}`, string(b))
}
func TestJSONProjectItem_DraftIssue(t *testing.T) {
item := queries.ProjectItem{}
item := ProjectItem{}
item.Content.TypeName = "DraftIssue"
item.Id = "123"
item.Content.DraftIssue.Title = "title"
item.Content.DraftIssue.Body = "a body"
b, err := json.Marshal(JSONProjectItem(item))
b, err := json.Marshal(item.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
}
func TestJSONProjectItem_Issue(t *testing.T) {
item := queries.ProjectItem{}
item := ProjectItem{}
item.Content.TypeName = "Issue"
item.Id = "123"
item.Content.Issue.Title = "title"
item.Content.Issue.Body = "a body"
item.Content.Issue.URL = "a-url"
b, err := json.Marshal(JSONProjectItem(item))
b, err := json.Marshal(item.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"Issue","url":"a-url"}`, string(b))
assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"Issue","url":"a-url"}`, string(b))
}
func TestJSONProjectItem_PullRequest(t *testing.T) {
item := queries.ProjectItem{}
item := ProjectItem{}
item.Content.TypeName = "PullRequest"
item.Id = "123"
item.Content.PullRequest.Title = "title"
item.Content.PullRequest.Body = "a body"
item.Content.PullRequest.URL = "a-url"
b, err := json.Marshal(JSONProjectItem(item))
b, err := json.Marshal(item.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"PullRequest","url":"a-url"}`, string(b))
assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"PullRequest","url":"a-url"}`, string(b))
}
func TestJSONProjectDetailedItems(t *testing.T) {
p := &queries.Project{}
p := &Project{}
p.Items.TotalCount = 5
p.Items.Nodes = []queries.ProjectItem{
p.Items.Nodes = []ProjectItem{
{
Id: "issueId",
Content: queries.ProjectItemContent{
Content: ProjectItemContent{
TypeName: "Issue",
Issue: queries.Issue{
Issue: Issue{
Title: "Issue title",
Body: "a body",
Number: 1,
@ -235,9 +239,9 @@ func TestJSONProjectDetailedItems(t *testing.T) {
},
{
Id: "pullRequestId",
Content: queries.ProjectItemContent{
Content: ProjectItemContent{
TypeName: "PullRequest",
PullRequest: queries.PullRequest{
PullRequest: PullRequest{
Title: "Pull Request title",
Body: "a body",
Number: 2,
@ -252,9 +256,9 @@ func TestJSONProjectDetailedItems(t *testing.T) {
},
{
Id: "draftIssueId",
Content: queries.ProjectItemContent{
Content: ProjectItemContent{
TypeName: "DraftIssue",
DraftIssue: queries.DraftIssue{
DraftIssue: DraftIssue{
Title: "Pull Request title",
Body: "a body",
},
@ -262,56 +266,56 @@ func TestJSONProjectDetailedItems(t *testing.T) {
},
}
out, err := json.Marshal(JSONProjectDetailedItems(p))
out, err := json.Marshal(p.DetailedItems())
assert.NoError(t, err)
assert.Equal(
assert.JSONEq(
t,
`{"items":[{"content":{"type":"Issue","body":"a body","title":"Issue title","number":1,"repository":"cli/go-gh","url":"issue-url"},"id":"issueId"},{"content":{"type":"PullRequest","body":"a body","title":"Pull Request title","number":2,"repository":"cli/go-gh","url":"pr-url"},"id":"pullRequestId"},{"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title"},"id":"draftIssueId"}],"totalCount":5}`,
string(out))
}
func TestJSONProjectDraftIssue(t *testing.T) {
item := queries.DraftIssue{}
item := DraftIssue{}
item.ID = "123"
item.Title = "title"
item.Body = "a body"
b, err := json.Marshal(JSONProjectDraftIssue(item))
b, err := json.Marshal(item.ExportData(nil))
assert.NoError(t, err)
assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
}
func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.T) {
iterationField := queries.ProjectField{TypeName: "ProjectV2IterationField"}
iterationField := ProjectField{TypeName: "ProjectV2IterationField"}
iterationField.IterationField.ID = "sprint"
iterationField.IterationField.Name = "Sprint"
iterationFieldValue := queries.FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"}
iterationFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"}
iterationFieldValue.ProjectV2ItemFieldIterationValue.Title = "Iteration Title"
iterationFieldValue.ProjectV2ItemFieldIterationValue.Field = iterationField
draftIssue := queries.ProjectItem{
draftIssue := ProjectItem{
Id: "draftIssueId",
Content: queries.ProjectItemContent{
Content: ProjectItemContent{
TypeName: "DraftIssue",
DraftIssue: queries.DraftIssue{
DraftIssue: DraftIssue{
Title: "Pull Request title",
Body: "a body",
},
},
}
draftIssue.FieldValues.Nodes = []queries.FieldValueNodes{
draftIssue.FieldValues.Nodes = []FieldValueNodes{
iterationFieldValue,
}
p := &queries.Project{}
p.Fields.Nodes = []queries.ProjectField{iterationField}
p := &Project{}
p.Fields.Nodes = []ProjectField{iterationField}
p.Items.TotalCount = 5
p.Items.Nodes = []queries.ProjectItem{
p.Items.Nodes = []ProjectItem{
draftIssue,
}
out, err := json.Marshal(JSONProjectDetailedItems(p))
out, err := json.Marshal(p.DetailedItems())
assert.NoError(t, err)
assert.JSONEq(
t,
@ -321,35 +325,35 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.
}
func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing.T) {
milestoneField := queries.ProjectField{TypeName: "ProjectV2IterationField"}
milestoneField := ProjectField{TypeName: "ProjectV2IterationField"}
milestoneField.IterationField.ID = "milestone"
milestoneField.IterationField.Name = "Milestone"
milestoneFieldValue := queries.FieldValueNodes{Type: "ProjectV2ItemFieldMilestoneValue"}
milestoneFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldMilestoneValue"}
milestoneFieldValue.ProjectV2ItemFieldMilestoneValue.Milestone.Title = "Milestone Title"
milestoneFieldValue.ProjectV2ItemFieldMilestoneValue.Field = milestoneField
draftIssue := queries.ProjectItem{
draftIssue := ProjectItem{
Id: "draftIssueId",
Content: queries.ProjectItemContent{
Content: ProjectItemContent{
TypeName: "DraftIssue",
DraftIssue: queries.DraftIssue{
DraftIssue: DraftIssue{
Title: "Pull Request title",
Body: "a body",
},
},
}
draftIssue.FieldValues.Nodes = []queries.FieldValueNodes{
draftIssue.FieldValues.Nodes = []FieldValueNodes{
milestoneFieldValue,
}
p := &queries.Project{}
p.Fields.Nodes = []queries.ProjectField{milestoneField}
p := &Project{}
p.Fields.Nodes = []ProjectField{milestoneField}
p.Items.TotalCount = 5
p.Items.Nodes = []queries.ProjectItem{
p.Items.Nodes = []ProjectItem{
draftIssue,
}
out, err := json.Marshal(JSONProjectDetailedItems(p))
out, err := json.Marshal(p.DetailedItems())
assert.NoError(t, err)
assert.JSONEq(
t,
@ -357,10 +361,3 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing.
string(out))
}
func TestCamelCase(t *testing.T) {
assert.Equal(t, "camelCase", camelCase("camelCase"))
assert.Equal(t, "camelCase", camelCase("CamelCase"))
assert.Equal(t, "c", camelCase("C"))
assert.Equal(t, "", camelCase(""))
}

View file

@ -128,12 +128,8 @@ type Project struct {
TotalCount int
Nodes []ProjectItem
} `graphql:"items(first: $firstItems, after: $afterItems)"`
Fields struct {
TotalCount int
Nodes []ProjectField
PageInfo PageInfo
} `graphql:"fields(first: $firstFields, after: $afterFields)"`
Owner struct {
Fields ProjectFields `graphql:"fields(first: $firstFields, after: $afterFields)"`
Owner struct {
TypeName string `graphql:"__typename"`
User struct {
Login string
@ -144,6 +140,36 @@ type Project struct {
}
}
func (p Project) DetailedItems() map[string]interface{} {
return map[string]interface{}{
"items": serializeProjectWithItems(&p),
"totalCount": p.Items.TotalCount,
}
}
func (p Project) ExportData(_ []string) map[string]interface{} {
return map[string]interface{}{
"number": p.Number,
"url": p.URL,
"shortDescription": p.ShortDescription,
"public": p.Public,
"closed": p.Closed,
"title": p.Title,
"id": p.ID,
"readme": p.Readme,
"items": map[string]interface{}{
"totalCount": p.Items.TotalCount,
},
"fields": map[string]interface{}{
"totalCount": p.Fields.TotalCount,
},
"owner": map[string]interface{}{
"type": p.OwnerType(),
"login": p.OwnerLogin(),
},
}
}
func (p Project) OwnerType() string {
return p.Owner.TypeName
}
@ -155,6 +181,22 @@ func (p Project) OwnerLogin() string {
return p.Owner.Organization.Login
}
type Projects struct {
Nodes []Project
TotalCount int
}
func (p Projects) ExportData(_ []string) map[string]interface{} {
v := make([]map[string]interface{}, len(p.Nodes))
for i := range p.Nodes {
v[i] = p.Nodes[i].ExportData(nil)
}
return map[string]interface{}{
"projects": v,
"totalCount": p.TotalCount,
}
}
// ProjectItem is a ProjectV2Item GraphQL object https://docs.github.com/en/graphql/reference/objects#projectv2item.
type ProjectItem struct {
Content ProjectItemContent
@ -284,6 +326,19 @@ type DraftIssue struct {
Title string
}
func (i DraftIssue) ExportData(_ []string) map[string]interface{} {
v := map[string]interface{}{
"title": i.Title,
"body": i.Body,
"type": "DraftIssue",
}
// Emulate omitempty.
if i.ID != "" {
v["id"] = i.ID
}
return v
}
type PullRequest struct {
Body string
Title string
@ -294,6 +349,17 @@ type PullRequest struct {
}
}
func (pr PullRequest) ExportData(_ []string) map[string]interface{} {
return map[string]interface{}{
"type": "PullRequest",
"body": pr.Body,
"title": pr.Title,
"number": pr.Number,
"repository": pr.Repository.NameWithOwner,
"url": pr.URL,
}
}
type Issue struct {
Body string
Title string
@ -304,6 +370,50 @@ type Issue struct {
}
}
func (i Issue) ExportData(_ []string) map[string]interface{} {
return map[string]interface{}{
"type": "Issue",
"body": i.Body,
"title": i.Title,
"number": i.Number,
"repository": i.Repository.NameWithOwner,
"url": i.URL,
}
}
func (p ProjectItem) DetailedItem() exportable {
switch p.Type() {
case "DraftIssue":
return DraftIssue{
Body: p.Body(),
Title: p.Title(),
}
case "Issue":
return Issue{
Body: p.Body(),
Title: p.Title(),
Number: p.Number(),
Repository: struct{ NameWithOwner string }{
NameWithOwner: p.Repo(),
},
URL: p.URL(),
}
case "PullRequest":
return PullRequest{
Body: p.Body(),
Title: p.Title(),
Number: p.Number(),
Repository: struct{ NameWithOwner string }{
NameWithOwner: p.Repo(),
},
URL: p.URL(),
}
}
return nil
}
// Type is the underlying type of the project item.
func (p ProjectItem) Type() string {
return p.Content.TypeName
@ -374,6 +484,20 @@ func (p ProjectItem) URL() string {
return ""
}
func (p ProjectItem) ExportData(_ []string) map[string]interface{} {
v := map[string]interface{}{
"id": p.ID(),
"title": p.Title(),
"body": p.Body(),
"type": p.Type(),
}
// Emulate omitempty.
if url := p.URL(); url != "" {
v["url"] = url
}
return v
}
// ProjectItems returns the items of a project. If the OwnerType is VIEWER, no login is required.
// If limit is 0, the default limit is used.
func (c *Client) ProjectItems(o *Owner, number int32, limit int) (*Project, error) {
@ -631,6 +755,13 @@ type SingleSelectFieldOptions struct {
Name string
}
func (f SingleSelectFieldOptions) ExportData(_ []string) map[string]interface{} {
return map[string]interface{}{
"id": f.ID,
"name": f.Name,
}
}
func (p ProjectField) Options() []SingleSelectFieldOptions {
if p.TypeName == "ProjectV2SingleSelectField" {
var options []SingleSelectFieldOptions
@ -645,6 +776,40 @@ func (p ProjectField) Options() []SingleSelectFieldOptions {
return nil
}
func (p ProjectField) ExportData(_ []string) map[string]interface{} {
v := map[string]interface{}{
"id": p.ID(),
"name": p.Name(),
"type": p.Type(),
}
// Emulate omitempty
if opts := p.Options(); len(opts) != 0 {
options := make([]map[string]interface{}, len(opts))
for i, opt := range opts {
options[i] = opt.ExportData(nil)
}
v["options"] = options
}
return v
}
type ProjectFields struct {
TotalCount int
Nodes []ProjectField
PageInfo PageInfo
}
func (p ProjectFields) ExportData(_ []string) map[string]interface{} {
fields := make([]map[string]interface{}, len(p.Nodes))
for i := range p.Nodes {
fields[i] = p.Nodes[i].ExportData(nil)
}
return map[string]interface{}{
"fields": fields,
"totalCount": p.TotalCount,
}
}
// ProjectFields returns a project with fields. If the OwnerType is VIEWER, no login is required.
// If limit is 0, the default limit is used.
func (c *Client) ProjectFields(o *Owner, number int32, limit int) (*Project, error) {
@ -1084,17 +1249,17 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool)
return nil, fmt.Errorf("project number is required when not running interactively")
}
projects, _, err := c.Projects(o.Login, o.Type, 0, fields)
projects, err := c.Projects(o.Login, o.Type, 0, fields)
if err != nil {
return nil, err
}
if len(projects) == 0 {
if len(projects.Nodes) == 0 {
return nil, fmt.Errorf("no projects found for %s", o.Login)
}
options := make([]string, 0, len(projects))
for _, p := range projects {
options := make([]string, 0, len(projects.Nodes))
for _, p := range projects.Nodes {
title := fmt.Sprintf("%s (#%d)", p.Title, p.Number)
options = append(options, title)
}
@ -1104,16 +1269,17 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool)
return nil, err
}
return &projects[answerIndex], nil
return &projects.Nodes[answerIndex], nil
}
// Projects returns all the projects for an Owner. If the OwnerType is VIEWER, no login is required.
// If limit is 0, the default limit is used.
func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) ([]Project, int, error) {
projects := make([]Project, 0)
func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Projects, error) {
projects := Projects{
Nodes: make([]Project, 0),
}
cursor := (*githubv4.String)(nil)
hasNextPage := false
totalCount := 0
if limit == 0 {
limit = LimitDefault
@ -1149,38 +1315,38 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) ([]
if t == UserOwner {
var query userProjects
if err := c.doQuery("UserProjects", &query, variables); err != nil {
return projects, 0, err
return projects, err
}
projects = append(projects, query.Owner.Projects.Nodes...)
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
cursor = &query.Owner.Projects.PageInfo.EndCursor
totalCount = query.Owner.Projects.TotalCount
projects.TotalCount = query.Owner.Projects.TotalCount
} else if t == OrgOwner {
var query orgProjects
if err := c.doQuery("OrgProjects", &query, variables); err != nil {
return projects, 0, err
return projects, err
}
projects = append(projects, query.Owner.Projects.Nodes...)
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
cursor = &query.Owner.Projects.PageInfo.EndCursor
totalCount = query.Owner.Projects.TotalCount
projects.TotalCount = query.Owner.Projects.TotalCount
} else if t == ViewerOwner {
var query viewerProjects
if err := c.doQuery("ViewerProjects", &query, variables); err != nil {
return projects, 0, err
return projects, err
}
projects = append(projects, query.Owner.Projects.Nodes...)
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
cursor = &query.Owner.Projects.PageInfo.EndCursor
totalCount = query.Owner.Projects.TotalCount
projects.TotalCount = query.Owner.Projects.TotalCount
}
if !hasNextPage || len(projects) >= limit {
return projects, totalCount, nil
if !hasNextPage || len(projects.Nodes) >= limit {
return projects, nil
}
if len(projects)+LimitMax > limit {
first := limit - len(projects)
if len(projects.Nodes)+LimitMax > limit {
first := limit - len(projects.Nodes)
variables["first"] = githubv4.Int(first)
}
variables["after"] = cursor
@ -1223,3 +1389,107 @@ func requiredScopesFromServerMessage(msg string) []string {
}
return scopes
}
func projectFieldValueData(v FieldValueNodes) interface{} {
switch v.Type {
case "ProjectV2ItemFieldDateValue":
return v.ProjectV2ItemFieldDateValue.Date
case "ProjectV2ItemFieldIterationValue":
return map[string]interface{}{
"title": v.ProjectV2ItemFieldIterationValue.Title,
"startDate": v.ProjectV2ItemFieldIterationValue.StartDate,
"duration": v.ProjectV2ItemFieldIterationValue.Duration,
}
case "ProjectV2ItemFieldNumberValue":
return v.ProjectV2ItemFieldNumberValue.Number
case "ProjectV2ItemFieldSingleSelectValue":
return v.ProjectV2ItemFieldSingleSelectValue.Name
case "ProjectV2ItemFieldTextValue":
return v.ProjectV2ItemFieldTextValue.Text
case "ProjectV2ItemFieldMilestoneValue":
return map[string]interface{}{
"title": v.ProjectV2ItemFieldMilestoneValue.Milestone.Title,
"description": v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
"dueOn": v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
}
case "ProjectV2ItemFieldLabelValue":
names := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
names = append(names, p.Name)
}
return names
case "ProjectV2ItemFieldPullRequestValue":
urls := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
urls = append(urls, p.Url)
}
return urls
case "ProjectV2ItemFieldRepositoryValue":
return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
case "ProjectV2ItemFieldUserValue":
logins := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
logins = append(logins, p.Login)
}
return logins
case "ProjectV2ItemFieldReviewerValue":
names := make([]string, 0)
for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
if p.Type == "Team" {
names = append(names, p.Team.Name)
} else if p.Type == "User" {
names = append(names, p.User.Login)
}
}
return names
}
return nil
}
// serialize creates a map from field to field values
func serializeProjectWithItems(project *Project) []map[string]interface{} {
fields := make(map[string]string)
// make a map of fields by ID
for _, f := range project.Fields.Nodes {
fields[f.ID()] = camelCase(f.Name())
}
itemsSlice := make([]map[string]interface{}, 0)
// for each value, look up the name by ID
// and set the value to the field value
for _, i := range project.Items.Nodes {
o := make(map[string]interface{})
o["id"] = i.Id
if projectItem := i.DetailedItem(); projectItem != nil {
o["content"] = projectItem.ExportData(nil)
} else {
o["content"] = nil
}
for _, v := range i.FieldValues.Nodes {
id := v.ID()
value := projectFieldValueData(v)
o[fields[id]] = value
}
itemsSlice = append(itemsSlice, o)
}
return itemsSlice
}
// camelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
func camelCase(s string) string {
if len(s) == 0 {
return ""
}
if len(s) == 1 {
return strings.ToLower(s)
}
return strings.ToLower(s[0:1]) + s[1:]
}
type exportable interface {
ExportData([]string) map[string]interface{}
}

View file

@ -428,3 +428,10 @@ func TestProjectItems_FieldTitle(t *testing.T) {
assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.Title, "Iteration Title 1")
assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[1].ProjectV2ItemFieldMilestoneValue.Milestone.Title, "Milestone Title 1")
}
func TestCamelCase(t *testing.T) {
assert.Equal(t, "camelCase", camelCase("camelCase"))
assert.Equal(t, "camelCase", camelCase("CamelCase"))
assert.Equal(t, "c", camelCase("C"))
assert.Equal(t, "", camelCase(""))
}

View file

@ -7,7 +7,6 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -107,7 +106,7 @@ func runView(config viewConfig) error {
}
if config.opts.exporter != nil {
return printJSON(config, *project)
return config.opts.exporter.Write(config.io, *project)
}
return printResults(config, project)
@ -191,8 +190,3 @@ func printResults(config viewConfig, project *queries.Project) error {
_, err = fmt.Fprint(config.io.Out, out)
return err
}
func printJSON(config viewConfig, project queries.Project) error {
projectJSON := format.JSONProject(project)
return config.opts.exporter.Write(config.io, projectJSON)
}