diff --git a/github/github-accessors.go b/github/github-accessors.go index 0a6cc1e6b53..99c123a1ea3 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -590,6 +590,46 @@ func (a *AddProjectItemOptions) GetType() *ProjectV2ItemContentType { return a.Type } +// GetDataType returns the DataType field if it's non-nil, zero value otherwise. +func (a *AddProjectV2FieldRequest) GetDataType() string { + if a == nil || a.DataType == nil { + return "" + } + return *a.DataType +} + +// GetIssueFieldID returns the IssueFieldID field if it's non-nil, zero value otherwise. +func (a *AddProjectV2FieldRequest) GetIssueFieldID() int64 { + if a == nil || a.IssueFieldID == nil { + return 0 + } + return *a.IssueFieldID +} + +// GetIterationConfiguration returns the IterationConfiguration field. +func (a *AddProjectV2FieldRequest) GetIterationConfiguration() *ProjectV2FieldIterationConfiguration { + if a == nil { + return nil + } + return a.IterationConfiguration +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (a *AddProjectV2FieldRequest) GetName() string { + if a == nil || a.Name == nil { + return "" + } + return *a.Name +} + +// GetSingleSelectOptions returns the SingleSelectOptions slice if it's non-nil, nil otherwise. +func (a *AddProjectV2FieldRequest) GetSingleSelectOptions() []*ProjectV2FieldSingleSelectOption { + if a == nil || a.SingleSelectOptions == nil { + return nil + } + return a.SingleSelectOptions +} + // GetMessage returns the Message field if it's non-nil, zero value otherwise. func (a *AddResourcesToCostCenterResponse) GetMessage() string { if a == nil || a.Message == nil { @@ -11078,6 +11118,54 @@ func (c *CreateOrUpdateIssueTypesOptions) GetName() string { return c.Name } +// GetBody returns the Body field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2DraftItemRequest) GetBody() string { + if c == nil || c.Body == nil { + return "" + } + return *c.Body +} + +// GetTitle returns the Title field. +func (c *CreateProjectV2DraftItemRequest) GetTitle() string { + if c == nil { + return "" + } + return c.Title +} + +// GetFilter returns the Filter field if it's non-nil, zero value otherwise. +func (c *CreateProjectV2ViewRequest) GetFilter() string { + if c == nil || c.Filter == nil { + return "" + } + return *c.Filter +} + +// GetLayout returns the Layout field. +func (c *CreateProjectV2ViewRequest) GetLayout() string { + if c == nil { + return "" + } + return c.Layout +} + +// GetName returns the Name field. +func (c *CreateProjectV2ViewRequest) GetName() string { + if c == nil { + return "" + } + return c.Name +} + +// GetVisibleFields returns the VisibleFields slice if it's non-nil, nil otherwise. +func (c *CreateProjectV2ViewRequest) GetVisibleFields() []int64 { + if c == nil || c.VisibleFields == nil { + return nil + } + return c.VisibleFields +} + // GetFrom returns the From field if it's non-nil, zero value otherwise. func (c *CreateProtectedChanges) GetFrom() bool { if c == nil || c.From == nil { @@ -28870,6 +28958,54 @@ func (p *ProjectV2FieldIteration) GetTitle() *ProjectV2TextContent { return p.Title } +// GetDuration returns the Duration field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfiguration) GetDuration() int { + if p == nil || p.Duration == nil { + return 0 + } + return *p.Duration +} + +// GetIterations returns the Iterations slice if it's non-nil, nil otherwise. +func (p *ProjectV2FieldIterationConfiguration) GetIterations() []*ProjectV2FieldIterationConfigurationIteration { + if p == nil || p.Iterations == nil { + return nil + } + return p.Iterations +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfiguration) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetDuration returns the Duration field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfigurationIteration) GetDuration() int { + if p == nil || p.Duration == nil { + return 0 + } + return *p.Duration +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfigurationIteration) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIterationConfigurationIteration) GetTitle() string { + if p == nil || p.Title == nil { + return "" + } + return *p.Title +} + // GetColor returns the Color field if it's non-nil, zero value otherwise. func (p *ProjectV2FieldOption) GetColor() string { if p == nil || p.Color == nil { @@ -28902,6 +29038,30 @@ func (p *ProjectV2FieldOption) GetName() *ProjectV2TextContent { return p.Name } +// GetColor returns the Color field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldSingleSelectOption) GetColor() string { + if p == nil || p.Color == nil { + return "" + } + return *p.Color +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldSingleSelectOption) GetDescription() string { + if p == nil || p.Description == nil { + return "" + } + return *p.Description +} + +// GetName returns the Name field. +func (p *ProjectV2FieldSingleSelectOption) GetName() string { + if p == nil { + return "" + } + return p.Name +} + // GetArchivedAt returns the ArchivedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetArchivedAt() Timestamp { if p == nil || p.ArchivedAt == nil { @@ -29222,6 +29382,142 @@ func (p *ProjectV2TextContent) GetRaw() string { return *p.Raw } +// GetCreatedAt returns the CreatedAt field. +func (p *ProjectV2View) GetCreatedAt() Timestamp { + if p == nil { + return Timestamp{} + } + return p.CreatedAt +} + +// GetCreator returns the Creator field. +func (p *ProjectV2View) GetCreator() User { + if p == nil { + return User{} + } + return p.Creator +} + +// GetFilter returns the Filter field if it's non-nil, zero value otherwise. +func (p *ProjectV2View) GetFilter() string { + if p == nil || p.Filter == nil { + return "" + } + return *p.Filter +} + +// GetGroupBy returns the GroupBy slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetGroupBy() []int64 { + if p == nil || p.GroupBy == nil { + return nil + } + return p.GroupBy +} + +// GetHTMLURL returns the HTMLURL field. +func (p *ProjectV2View) GetHTMLURL() string { + if p == nil { + return "" + } + return p.HTMLURL +} + +// GetID returns the ID field. +func (p *ProjectV2View) GetID() int64 { + if p == nil { + return 0 + } + return p.ID +} + +// GetLayout returns the Layout field. +func (p *ProjectV2View) GetLayout() string { + if p == nil { + return "" + } + return p.Layout +} + +// GetName returns the Name field. +func (p *ProjectV2View) GetName() string { + if p == nil { + return "" + } + return p.Name +} + +// GetNodeID returns the NodeID field. +func (p *ProjectV2View) GetNodeID() string { + if p == nil { + return "" + } + return p.NodeID +} + +// GetNumber returns the Number field. +func (p *ProjectV2View) GetNumber() int { + if p == nil { + return 0 + } + return p.Number +} + +// GetProjectURL returns the ProjectURL field. +func (p *ProjectV2View) GetProjectURL() string { + if p == nil { + return "" + } + return p.ProjectURL +} + +// GetSortBy returns the SortBy slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetSortBy() []*ProjectV2ViewSortBy { + if p == nil || p.SortBy == nil { + return nil + } + return p.SortBy +} + +// GetUpdatedAt returns the UpdatedAt field. +func (p *ProjectV2View) GetUpdatedAt() Timestamp { + if p == nil { + return Timestamp{} + } + return p.UpdatedAt +} + +// GetVerticalGroupBy returns the VerticalGroupBy slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetVerticalGroupBy() []int64 { + if p == nil || p.VerticalGroupBy == nil { + return nil + } + return p.VerticalGroupBy +} + +// GetVisibleFields returns the VisibleFields slice if it's non-nil, nil otherwise. +func (p *ProjectV2View) GetVisibleFields() []int64 { + if p == nil || p.VisibleFields == nil { + return nil + } + return p.VisibleFields +} + +// GetDirection returns the Direction field if it's non-nil, zero value otherwise. +func (p *ProjectV2ViewSortBy) GetDirection() string { + if p == nil || p.Direction == nil { + return "" + } + return *p.Direction +} + +// GetFieldID returns the FieldID field if it's non-nil, zero value otherwise. +func (p *ProjectV2ViewSortBy) GetFieldID() int64 { + if p == nil || p.FieldID == nil { + return 0 + } + return *p.FieldID +} + // GetAllowDeletions returns the AllowDeletions field. func (p *Protection) GetAllowDeletions() *AllowDeletions { if p == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 7433e4499e8..9392ba021ac 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -747,6 +747,58 @@ func TestAddProjectItemOptions_GetType(tt *testing.T) { a.GetType() } +func TestAddProjectV2FieldRequest_GetDataType(tt *testing.T) { + tt.Parallel() + var zeroValue string + a := &AddProjectV2FieldRequest{DataType: &zeroValue} + a.GetDataType() + a = &AddProjectV2FieldRequest{} + a.GetDataType() + a = nil + a.GetDataType() +} + +func TestAddProjectV2FieldRequest_GetIssueFieldID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + a := &AddProjectV2FieldRequest{IssueFieldID: &zeroValue} + a.GetIssueFieldID() + a = &AddProjectV2FieldRequest{} + a.GetIssueFieldID() + a = nil + a.GetIssueFieldID() +} + +func TestAddProjectV2FieldRequest_GetIterationConfiguration(tt *testing.T) { + tt.Parallel() + a := &AddProjectV2FieldRequest{} + a.GetIterationConfiguration() + a = nil + a.GetIterationConfiguration() +} + +func TestAddProjectV2FieldRequest_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + a := &AddProjectV2FieldRequest{Name: &zeroValue} + a.GetName() + a = &AddProjectV2FieldRequest{} + a.GetName() + a = nil + a.GetName() +} + +func TestAddProjectV2FieldRequest_GetSingleSelectOptions(tt *testing.T) { + tt.Parallel() + zeroValue := []*ProjectV2FieldSingleSelectOption{} + a := &AddProjectV2FieldRequest{SingleSelectOptions: zeroValue} + a.GetSingleSelectOptions() + a = &AddProjectV2FieldRequest{} + a.GetSingleSelectOptions() + a = nil + a.GetSingleSelectOptions() +} + func TestAddResourcesToCostCenterResponse_GetMessage(tt *testing.T) { tt.Parallel() var zeroValue string @@ -14091,6 +14143,63 @@ func TestCreateOrUpdateIssueTypesOptions_GetName(tt *testing.T) { c.GetName() } +func TestCreateProjectV2DraftItemRequest_GetBody(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2DraftItemRequest{Body: &zeroValue} + c.GetBody() + c = &CreateProjectV2DraftItemRequest{} + c.GetBody() + c = nil + c.GetBody() +} + +func TestCreateProjectV2DraftItemRequest_GetTitle(tt *testing.T) { + tt.Parallel() + c := &CreateProjectV2DraftItemRequest{} + c.GetTitle() + c = nil + c.GetTitle() +} + +func TestCreateProjectV2ViewRequest_GetFilter(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CreateProjectV2ViewRequest{Filter: &zeroValue} + c.GetFilter() + c = &CreateProjectV2ViewRequest{} + c.GetFilter() + c = nil + c.GetFilter() +} + +func TestCreateProjectV2ViewRequest_GetLayout(tt *testing.T) { + tt.Parallel() + c := &CreateProjectV2ViewRequest{} + c.GetLayout() + c = nil + c.GetLayout() +} + +func TestCreateProjectV2ViewRequest_GetName(tt *testing.T) { + tt.Parallel() + c := &CreateProjectV2ViewRequest{} + c.GetName() + c = nil + c.GetName() +} + +func TestCreateProjectV2ViewRequest_GetVisibleFields(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + c := &CreateProjectV2ViewRequest{VisibleFields: zeroValue} + c.GetVisibleFields() + c = &CreateProjectV2ViewRequest{} + c.GetVisibleFields() + c = nil + c.GetVisibleFields() +} + func TestCreateProtectedChanges_GetFrom(tt *testing.T) { tt.Parallel() var zeroValue bool @@ -36401,6 +36510,72 @@ func TestProjectV2FieldIteration_GetTitle(tt *testing.T) { p.GetTitle() } +func TestProjectV2FieldIterationConfiguration_GetDuration(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldIterationConfiguration{Duration: &zeroValue} + p.GetDuration() + p = &ProjectV2FieldIterationConfiguration{} + p.GetDuration() + p = nil + p.GetDuration() +} + +func TestProjectV2FieldIterationConfiguration_GetIterations(tt *testing.T) { + tt.Parallel() + zeroValue := []*ProjectV2FieldIterationConfigurationIteration{} + p := &ProjectV2FieldIterationConfiguration{Iterations: zeroValue} + p.GetIterations() + p = &ProjectV2FieldIterationConfiguration{} + p.GetIterations() + p = nil + p.GetIterations() +} + +func TestProjectV2FieldIterationConfiguration_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIterationConfiguration{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2FieldIterationConfiguration{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2FieldIterationConfigurationIteration_GetDuration(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldIterationConfigurationIteration{Duration: &zeroValue} + p.GetDuration() + p = &ProjectV2FieldIterationConfigurationIteration{} + p.GetDuration() + p = nil + p.GetDuration() +} + +func TestProjectV2FieldIterationConfigurationIteration_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIterationConfigurationIteration{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2FieldIterationConfigurationIteration{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2FieldIterationConfigurationIteration_GetTitle(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIterationConfigurationIteration{Title: &zeroValue} + p.GetTitle() + p = &ProjectV2FieldIterationConfigurationIteration{} + p.GetTitle() + p = nil + p.GetTitle() +} + func TestProjectV2FieldOption_GetColor(tt *testing.T) { tt.Parallel() var zeroValue string @@ -36439,6 +36614,36 @@ func TestProjectV2FieldOption_GetName(tt *testing.T) { p.GetName() } +func TestProjectV2FieldSingleSelectOption_GetColor(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldSingleSelectOption{Color: &zeroValue} + p.GetColor() + p = &ProjectV2FieldSingleSelectOption{} + p.GetColor() + p = nil + p.GetColor() +} + +func TestProjectV2FieldSingleSelectOption_GetDescription(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldSingleSelectOption{Description: &zeroValue} + p.GetDescription() + p = &ProjectV2FieldSingleSelectOption{} + p.GetDescription() + p = nil + p.GetDescription() +} + +func TestProjectV2FieldSingleSelectOption_GetName(tt *testing.T) { + tt.Parallel() + p := &ProjectV2FieldSingleSelectOption{} + p.GetName() + p = nil + p.GetName() +} + func TestProjectV2Item_GetArchivedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -36834,6 +37039,163 @@ func TestProjectV2TextContent_GetRaw(tt *testing.T) { p.GetRaw() } +func TestProjectV2View_GetCreatedAt(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetCreatedAt() + p = nil + p.GetCreatedAt() +} + +func TestProjectV2View_GetCreator(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetCreator() + p = nil + p.GetCreator() +} + +func TestProjectV2View_GetFilter(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2View{Filter: &zeroValue} + p.GetFilter() + p = &ProjectV2View{} + p.GetFilter() + p = nil + p.GetFilter() +} + +func TestProjectV2View_GetGroupBy(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + p := &ProjectV2View{GroupBy: zeroValue} + p.GetGroupBy() + p = &ProjectV2View{} + p.GetGroupBy() + p = nil + p.GetGroupBy() +} + +func TestProjectV2View_GetHTMLURL(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetHTMLURL() + p = nil + p.GetHTMLURL() +} + +func TestProjectV2View_GetID(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2View_GetLayout(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetLayout() + p = nil + p.GetLayout() +} + +func TestProjectV2View_GetName(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetName() + p = nil + p.GetName() +} + +func TestProjectV2View_GetNodeID(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2View_GetNumber(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetNumber() + p = nil + p.GetNumber() +} + +func TestProjectV2View_GetProjectURL(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetProjectURL() + p = nil + p.GetProjectURL() +} + +func TestProjectV2View_GetSortBy(tt *testing.T) { + tt.Parallel() + zeroValue := []*ProjectV2ViewSortBy{} + p := &ProjectV2View{SortBy: zeroValue} + p.GetSortBy() + p = &ProjectV2View{} + p.GetSortBy() + p = nil + p.GetSortBy() +} + +func TestProjectV2View_GetUpdatedAt(tt *testing.T) { + tt.Parallel() + p := &ProjectV2View{} + p.GetUpdatedAt() + p = nil + p.GetUpdatedAt() +} + +func TestProjectV2View_GetVerticalGroupBy(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + p := &ProjectV2View{VerticalGroupBy: zeroValue} + p.GetVerticalGroupBy() + p = &ProjectV2View{} + p.GetVerticalGroupBy() + p = nil + p.GetVerticalGroupBy() +} + +func TestProjectV2View_GetVisibleFields(tt *testing.T) { + tt.Parallel() + zeroValue := []int64{} + p := &ProjectV2View{VisibleFields: zeroValue} + p.GetVisibleFields() + p = &ProjectV2View{} + p.GetVisibleFields() + p = nil + p.GetVisibleFields() +} + +func TestProjectV2ViewSortBy_GetDirection(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2ViewSortBy{Direction: &zeroValue} + p.GetDirection() + p = &ProjectV2ViewSortBy{} + p.GetDirection() + p = nil + p.GetDirection() +} + +func TestProjectV2ViewSortBy_GetFieldID(tt *testing.T) { + tt.Parallel() + var zeroValue int64 + p := &ProjectV2ViewSortBy{FieldID: &zeroValue} + p.GetFieldID() + p = &ProjectV2ViewSortBy{} + p.GetFieldID() + p = nil + p.GetFieldID() +} + func TestProtection_GetAllowDeletions(tt *testing.T) { tt.Parallel() p := &Protection{} diff --git a/github/github-iterators.go b/github/github-iterators.go index 7d0a13f0919..e59597afd90 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -4921,6 +4921,37 @@ func (s *ProjectsService) ListOrganizationProjectItemsIter(ctx context.Context, } } +// ListOrganizationProjectViewItemsIter returns an iterator that paginates through all results of ListOrganizationProjectViewItems. +func (s *ProjectsService) ListOrganizationProjectViewItemsIter(ctx context.Context, org string, projectNumber int, viewNumber int, opts *ListProjectItemsOptions) iter.Seq2[*ProjectV2Item, error] { + return func(yield func(*ProjectV2Item, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListProjectItemsOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListOrganizationProjectViewItems(ctx, org, projectNumber, viewNumber, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.After == "" { + break + } + opts.After = resp.After + } + } +} + // ListOrganizationProjectsIter returns an iterator that paginates through all results of ListOrganizationProjects. func (s *ProjectsService) ListOrganizationProjectsIter(ctx context.Context, org string, opts *ListProjectsOptions) iter.Seq2[*ProjectV2, error] { return func(yield func(*ProjectV2, error) bool) { @@ -5014,6 +5045,37 @@ func (s *ProjectsService) ListUserProjectItemsIter(ctx context.Context, username } } +// ListUserProjectViewItemsIter returns an iterator that paginates through all results of ListUserProjectViewItems. +func (s *ProjectsService) ListUserProjectViewItemsIter(ctx context.Context, username string, projectNumber int, viewNumber int, opts *ListProjectItemsOptions) iter.Seq2[*ProjectV2Item, error] { + return func(yield func(*ProjectV2Item, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListProjectItemsOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListUserProjectViewItems(ctx, username, projectNumber, viewNumber, opts) + if err != nil { + yield(nil, err) + return + } + + for _, item := range results { + if !yield(item, nil) { + return + } + } + + if resp.After == "" { + break + } + opts.After = resp.After + } + } +} + // ListUserProjectsIter returns an iterator that paginates through all results of ListUserProjects. func (s *ProjectsService) ListUserProjectsIter(ctx context.Context, username string, opts *ListProjectsOptions) iter.Seq2[*ProjectV2, error] { return func(yield func(*ProjectV2, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index 071516b9264..abee7d43d42 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -10815,6 +10815,78 @@ func TestProjectsService_ListOrganizationProjectItemsIter(t *testing.T) { } } +func TestProjectsService_ListOrganizationProjectViewItemsIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListProjectItemsOptions{} + iter = client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Projects.ListOrganizationProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + iter(func(item *ProjectV2Item, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Projects.ListOrganizationProjectViewItemsIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestProjectsService_ListOrganizationProjectsIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -11031,6 +11103,78 @@ func TestProjectsService_ListUserProjectItemsIter(t *testing.T) { } } +func TestProjectsService_ListUserProjectViewItemsIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `[{},{},{}]`) + case 2: + fmt.Fprint(w, `[{},{},{},{}]`) + case 3: + fmt.Fprint(w, `[{},{}]`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `[{},{}]`) + } + }) + + iter := client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListProjectItemsOptions{} + iter = client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Projects.ListUserProjectViewItemsIter(t.Context(), "", 0, 0, nil) + gotItems = 0 + iter(func(item *ProjectV2Item, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Projects.ListUserProjectViewItemsIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestProjectsService_ListUserProjectsIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t) diff --git a/github/projects.go b/github/projects.go index 79d6262db2d..53c999121ee 100644 --- a/github/projects.go +++ b/github/projects.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" ) // ProjectsService handles communication with the project V2 @@ -728,3 +729,327 @@ func (s *ProjectsService) DeleteUserProjectItem(ctx context.Context, username st return s.client.Do(req, nil) } + +// CreateProjectV2DraftItemRequest specifies the parameters to create a draft item in a project. +type CreateProjectV2DraftItemRequest struct { + // Title is the title of the draft issue item to create. + Title string `json:"title"` + // Body is the body content of the draft issue item to create. + Body *string `json:"body,omitempty"` +} + +// CreateOrganizationProjectDraftItem creates a draft item in an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/drafts?apiVersion=2022-11-28#create-draft-item-for-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/drafts +func (s *ProjectsService) CreateOrganizationProjectDraftItem(ctx context.Context, org string, projectNumber int, body CreateProjectV2DraftItemRequest) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/drafts", org, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var item *ProjectV2Item + resp, err := s.client.Do(req, &item) + if err != nil { + return nil, resp, err + } + + return item, resp, nil +} + +// CreateUserProjectDraftItem creates a draft item in a user owned project. +// +// Note: this endpoint identifies the user by their unique numeric ID (user_id) +// in the path, not by username, mirroring the GitHub REST API. +// +// GitHub API docs: https://docs.github.com/rest/projects/drafts?apiVersion=2022-11-28#create-draft-item-for-user-owned-project +// +//meta:operation POST /user/{user_id}/projectsV2/{project_number}/drafts +func (s *ProjectsService) CreateUserProjectDraftItem(ctx context.Context, userID int64, projectNumber int, body CreateProjectV2DraftItemRequest) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("user/%v/projectsV2/%v/drafts", userID, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var item *ProjectV2Item + resp, err := s.client.Do(req, &item) + if err != nil { + return nil, resp, err + } + + return item, resp, nil +} + +// ProjectV2FieldSingleSelectOption represents an option to create for a single_select project field. +type ProjectV2FieldSingleSelectOption struct { + // Name is the display name of the option. + Name string `json:"name"` + // Color is the color associated with the option. + // One of: BLUE, GRAY, GREEN, ORANGE, PINK, PURPLE, RED, YELLOW. + Color *string `json:"color,omitempty"` + // Description is an optional description of the option. + Description *string `json:"description,omitempty"` +} + +// ProjectV2FieldIterationConfiguration represents the configuration to create an iteration project field. +type ProjectV2FieldIterationConfiguration struct { + // StartDate is the start date of the first iteration in ISO 8601 (YYYY-MM-DD) format. + StartDate *string `json:"start_date,omitempty"` + // Duration is the default duration for iterations in days. + Duration *int `json:"duration,omitempty"` + // Iterations holds zero or more iterations for the field. + Iterations []*ProjectV2FieldIterationConfigurationIteration `json:"iterations,omitempty"` +} + +// ProjectV2FieldIterationConfigurationIteration represents a single iteration within a ProjectV2FieldIterationConfiguration. +type ProjectV2FieldIterationConfigurationIteration struct { + // Title is the title of the iteration. + Title *string `json:"title,omitempty"` + // StartDate is the start date of the iteration in ISO 8601 (YYYY-MM-DD) format. + StartDate *string `json:"start_date,omitempty"` + // Duration is the duration of the iteration in days. + Duration *int `json:"duration,omitempty"` +} + +// AddProjectV2FieldRequest specifies the parameters to add a field to a project. +// +// The GitHub API models the body as a one-of: set IssueFieldID to link a +// built-in issue field, or set Name and DataType to create a new field. When +// DataType is "single_select", SingleSelectOptions is required; when DataType +// is "iteration", IterationConfiguration is required. +type AddProjectV2FieldRequest struct { + // Name is the name of the field. + Name *string `json:"name,omitempty"` + // DataType is the field's data type. + // One of: text, number, date, single_select, iteration. + DataType *string `json:"data_type,omitempty"` + // SingleSelectOptions holds the options for a single_select field. (Required for single_select.) + SingleSelectOptions []*ProjectV2FieldSingleSelectOption `json:"single_select_options,omitempty"` + // IterationConfiguration holds the configuration for an iteration field. (Required for iteration.) + IterationConfiguration *ProjectV2FieldIterationConfiguration `json:"iteration_configuration,omitempty"` + // IssueFieldID links an existing built-in issue field to the project. (Organization owned projects only.) + IssueFieldID *int64 `json:"issue_field_id,omitempty"` +} + +// AddOrganizationProjectField adds a field to an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields?apiVersion=2022-11-28#add-a-field-to-an-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/fields +func (s *ProjectsService) AddOrganizationProjectField(ctx context.Context, org string, projectNumber int, body AddProjectV2FieldRequest) (*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/fields", org, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var field *ProjectV2Field + resp, err := s.client.Do(req, &field) + if err != nil { + return nil, resp, err + } + + return field, resp, nil +} + +// AddUserProjectField adds a field to a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields?apiVersion=2022-11-28#add-field-to-user-owned-project +// +//meta:operation POST /users/{username}/projectsV2/{project_number}/fields +func (s *ProjectsService) AddUserProjectField(ctx context.Context, username string, projectNumber int, body AddProjectV2FieldRequest) (*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/fields", username, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var field *ProjectV2Field + resp, err := s.client.Do(req, &field) + if err != nil { + return nil, resp, err + } + + return field, resp, nil +} + +// ProjectV2View represents a view in a project. +type ProjectV2View struct { + ID int64 `json:"id"` + Number int `json:"number"` + Name string `json:"name"` + Layout string `json:"layout"` + NodeID string `json:"node_id"` + ProjectURL string `json:"project_url"` + HTMLURL string `json:"html_url"` + Creator User `json:"creator"` + Filter *string `json:"filter,omitempty"` + VisibleFields []int64 `json:"visible_fields"` + SortBy []*ProjectV2ViewSortBy `json:"sort_by"` + GroupBy []int64 `json:"group_by"` + VerticalGroupBy []int64 `json:"vertical_group_by"` + CreatedAt Timestamp `json:"created_at"` + UpdatedAt Timestamp `json:"updated_at"` +} + +// ProjectV2ViewSortBy represents a single sort criterion of a project view. +// +// On the wire it is encoded as a [field_id, direction] tuple rather than an +// object, so it has custom JSON (un)marshaling. +type ProjectV2ViewSortBy struct { + // FieldID is the ID of the field to sort by. + FieldID *int64 `json:"-"` + // Direction is the sort direction, one of "asc" or "desc". + Direction *string `json:"-"` +} + +// UnmarshalJSON implements custom unmarshaling for the [field_id, direction] +// tuple. The field_id may be encoded as either a number or a string. +func (s *ProjectV2ViewSortBy) UnmarshalJSON(data []byte) error { + var tuple []json.RawMessage + if err := json.Unmarshal(data, &tuple); err != nil { + return err + } + if len(tuple) != 2 { + return fmt.Errorf("expected a [field_id, direction] tuple, got %v elements", len(tuple)) + } + + var fieldID int64 + if err := json.Unmarshal(tuple[0], &fieldID); err != nil { + // The OpenAPI schema allows the field_id to be a string as well. + var str string + if err2 := json.Unmarshal(tuple[0], &str); err2 != nil { + return fmt.Errorf("invalid field_id: %w", err2) + } + fieldID, err = strconv.ParseInt(str, 10, 64) + if err != nil { + return fmt.Errorf("invalid field_id %q: %w", str, err) + } + } + + var direction string + if err := json.Unmarshal(tuple[1], &direction); err != nil { + return fmt.Errorf("invalid direction: %w", err) + } + + s.FieldID = &fieldID + s.Direction = &direction + return nil +} + +// MarshalJSON implements custom marshaling to the [field_id, direction] tuple. +func (s ProjectV2ViewSortBy) MarshalJSON() ([]byte, error) { + return json.Marshal([]any{s.FieldID, s.Direction}) +} + +// CreateProjectV2ViewRequest specifies the parameters to create a project view. +type CreateProjectV2ViewRequest struct { + // Name is the view's display name. + Name string `json:"name"` + // Layout is the view's layout. One of: table, board, roadmap. + Layout string `json:"layout"` + // Filter is an optional query string to filter the items shown in the view. + Filter *string `json:"filter,omitempty"` + // VisibleFields lists the field IDs to display. Not applicable to the roadmap layout. + VisibleFields []int64 `json:"visible_fields,omitempty"` +} + +// CreateOrganizationProjectView creates a view for an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/views?apiVersion=2022-11-28#create-a-view-for-an-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/views +func (s *ProjectsService) CreateOrganizationProjectView(ctx context.Context, org string, projectNumber int, body CreateProjectV2ViewRequest) (*ProjectV2View, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/views", org, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var view *ProjectV2View + resp, err := s.client.Do(req, &view) + if err != nil { + return nil, resp, err + } + + return view, resp, nil +} + +// CreateUserProjectView creates a view for a user owned project. +// +// Note: this endpoint identifies the user by their unique numeric ID (user_id) +// in the path, not by username, mirroring the GitHub REST API. +// +// GitHub API docs: https://docs.github.com/rest/projects/views?apiVersion=2022-11-28#create-a-view-for-a-user-owned-project +// +//meta:operation POST /users/{user_id}/projectsV2/{project_number}/views +func (s *ProjectsService) CreateUserProjectView(ctx context.Context, userID int64, projectNumber int, body CreateProjectV2ViewRequest) (*ProjectV2View, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/views", userID, projectNumber) + req, err := s.client.NewRequest(ctx, "POST", u, body) + if err != nil { + return nil, nil, err + } + + var view *ProjectV2View + resp, err := s.client.Do(req, &view) + if err != nil { + return nil, resp, err + } + + return view, resp, nil +} + +// ListOrganizationProjectViewItems lists the items of a view in an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items?apiVersion=2022-11-28#list-items-for-an-organization-project-view +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/views/{view_number}/items +func (s *ProjectsService) ListOrganizationProjectViewItems(ctx context.Context, org string, projectNumber, viewNumber int, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/views/%v/items", org, projectNumber, viewNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var items []*ProjectV2Item + resp, err := s.client.Do(req, &items) + if err != nil { + return nil, resp, err + } + + return items, resp, nil +} + +// ListUserProjectViewItems lists the items of a view in a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items?apiVersion=2022-11-28#list-items-for-a-user-project-view +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/views/{view_number}/items +func (s *ProjectsService) ListUserProjectViewItems(ctx context.Context, username string, projectNumber, viewNumber int, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/views/%v/items", username, projectNumber, viewNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var items []*ProjectV2Item + resp, err := s.client.Do(req, &items) + if err != nil { + return nil, resp, err + } + + return items, resp, nil +} diff --git a/github/projects_test.go b/github/projects_test.go index 296b4bf8dba..4206a44c15c 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -12,6 +12,8 @@ import ( "net/http" "testing" "time" + + "github.com/google/go-cmp/cmp" ) func TestProjectsService_ListOrganizationProjects(t *testing.T) { @@ -1432,3 +1434,388 @@ func TestProjectV2ItemContent_Marshal(t *testing.T) { testJSONMarshalOnly(t, &content, want) }) } + +func TestProjectsService_CreateOrganizationProjectDraftItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2DraftItemRequest{Title: "My draft", Body: Ptr("Draft body")} + + mux.HandleFunc("/orgs/o/projectsV2/1/drafts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":42,"node_id":"PVTI_draft","content_type":"DraftIssue"}`) + }) + + ctx := t.Context() + item, _, err := client.Projects.CreateOrganizationProjectDraftItem(ctx, "o", 1, input) + if err != nil { + t.Fatalf("Projects.CreateOrganizationProjectDraftItem returned error: %v", err) + } + want := &ProjectV2Item{ID: Ptr(int64(42)), NodeID: Ptr("PVTI_draft"), ContentType: Ptr(ProjectV2ItemContentTypeDraftIssue)} + if diff := cmp.Diff(want, item); diff != "" { + t.Errorf("Projects.CreateOrganizationProjectDraftItem mismatch (-want +got):\n%v", diff) + } + + const methodName = "CreateOrganizationProjectDraftItem" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.CreateOrganizationProjectDraftItem(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateOrganizationProjectDraftItem(ctx, "o", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_CreateUserProjectDraftItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2DraftItemRequest{Title: "My draft"} + + mux.HandleFunc("/user/12345/projectsV2/1/drafts", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":43,"node_id":"PVTI_draft_u","content_type":"DraftIssue"}`) + }) + + ctx := t.Context() + item, _, err := client.Projects.CreateUserProjectDraftItem(ctx, 12345, 1, input) + if err != nil { + t.Fatalf("Projects.CreateUserProjectDraftItem returned error: %v", err) + } + want := &ProjectV2Item{ID: Ptr(int64(43)), NodeID: Ptr("PVTI_draft_u"), ContentType: Ptr(ProjectV2ItemContentTypeDraftIssue)} + if diff := cmp.Diff(want, item); diff != "" { + t.Errorf("Projects.CreateUserProjectDraftItem mismatch (-want +got):\n%v", diff) + } + + const methodName = "CreateUserProjectDraftItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateUserProjectDraftItem(ctx, 12345, 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_AddOrganizationProjectField(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := AddProjectV2FieldRequest{ + Name: Ptr("Priority"), + DataType: Ptr("single_select"), + SingleSelectOptions: []*ProjectV2FieldSingleSelectOption{ + {Name: "High", Color: Ptr("RED"), Description: Ptr("Urgent")}, + {Name: "Low", Color: Ptr("GRAY")}, + }, + } + + mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":7,"name":"Priority","data_type":"single_select"}`) + }) + + ctx := t.Context() + field, _, err := client.Projects.AddOrganizationProjectField(ctx, "o", 1, input) + if err != nil { + t.Fatalf("Projects.AddOrganizationProjectField returned error: %v", err) + } + want := &ProjectV2Field{ID: Ptr(int64(7)), Name: Ptr("Priority"), DataType: Ptr("single_select")} + if diff := cmp.Diff(want, field); diff != "" { + t.Errorf("Projects.AddOrganizationProjectField mismatch (-want +got):\n%v", diff) + } + + const methodName = "AddOrganizationProjectField" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.AddOrganizationProjectField(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddOrganizationProjectField(ctx, "o", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_AddUserProjectField(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := AddProjectV2FieldRequest{ + Name: Ptr("Sprint"), + DataType: Ptr("iteration"), + IterationConfiguration: &ProjectV2FieldIterationConfiguration{ + StartDate: Ptr("2026-01-01"), + Duration: Ptr(14), + Iterations: []*ProjectV2FieldIterationConfigurationIteration{ + {Title: Ptr("Sprint 1"), StartDate: Ptr("2026-01-01"), Duration: Ptr(14)}, + }, + }, + } + + mux.HandleFunc("/users/u/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":8,"name":"Sprint","data_type":"iteration"}`) + }) + + ctx := t.Context() + field, _, err := client.Projects.AddUserProjectField(ctx, "u", 1, input) + if err != nil { + t.Fatalf("Projects.AddUserProjectField returned error: %v", err) + } + want := &ProjectV2Field{ID: Ptr(int64(8)), Name: Ptr("Sprint"), DataType: Ptr("iteration")} + if diff := cmp.Diff(want, field); diff != "" { + t.Errorf("Projects.AddUserProjectField mismatch (-want +got):\n%v", diff) + } + + const methodName = "AddUserProjectField" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.AddUserProjectField(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddUserProjectField(ctx, "u", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_CreateOrganizationProjectView(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2ViewRequest{ + Name: "My board", + Layout: "board", + Filter: Ptr("is:open"), + VisibleFields: []int64{1, 2, 3}, + } + + mux.HandleFunc("/orgs/o/projectsV2/1/views", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + // The first field_id is larger than 2^53 to ensure it is decoded as an + // int64 without the precision loss that a float64 would introduce. + fmt.Fprint(w, `{"id":5,"number":2,"name":"My board","layout":"board","sort_by":[[9007199254740993,"asc"],[456,"desc"]]}`) + }) + + ctx := t.Context() + view, _, err := client.Projects.CreateOrganizationProjectView(ctx, "o", 1, input) + if err != nil { + t.Fatalf("Projects.CreateOrganizationProjectView returned error: %v", err) + } + want := &ProjectV2View{ + ID: 5, + Number: 2, + Name: "My board", + Layout: "board", + SortBy: []*ProjectV2ViewSortBy{ + {FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")}, + {FieldID: Ptr(int64(456)), Direction: Ptr("desc")}, + }, + } + if diff := cmp.Diff(want, view); diff != "" { + t.Errorf("Projects.CreateOrganizationProjectView mismatch (-want +got):\n%v", diff) + } + + const methodName = "CreateOrganizationProjectView" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.CreateOrganizationProjectView(ctx, "\n", 1, input) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateOrganizationProjectView(ctx, "o", 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_CreateUserProjectView(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := CreateProjectV2ViewRequest{Name: "My table", Layout: "table"} + + mux.HandleFunc("/users/12345/projectsV2/1/views", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testJSONBody(t, r, input) + fmt.Fprint(w, `{"id":6,"number":3,"name":"My table","layout":"table"}`) + }) + + ctx := t.Context() + view, _, err := client.Projects.CreateUserProjectView(ctx, 12345, 1, input) + if err != nil { + t.Fatalf("Projects.CreateUserProjectView returned error: %v", err) + } + want := &ProjectV2View{ID: 6, Number: 3, Name: "My table", Layout: "table"} + if diff := cmp.Diff(want, view); diff != "" { + t.Errorf("Projects.CreateUserProjectView mismatch (-want +got):\n%v", diff) + } + + const methodName = "CreateUserProjectView" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.CreateUserProjectView(ctx, 12345, 1, input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_ListOrganizationProjectViewItems(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/views/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"after": "2", "before": "1", "per_page": "50", "fields": "10,11", "q": "status:open"}) + fmt.Fprint(w, `[{"id":21,"node_id":"PVTI_view_item"}]`) + }) + + opts := &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: "2", Before: "1", PerPage: 50}, Query: "status:open"}, Fields: []int64{10, 11}} + ctx := t.Context() + items, _, err := client.Projects.ListOrganizationProjectViewItems(ctx, "o", 1, 2, opts) + if err != nil { + t.Fatalf("Projects.ListOrganizationProjectViewItems returned error: %v", err) + } + want := []*ProjectV2Item{{ID: Ptr(int64(21)), NodeID: Ptr("PVTI_view_item")}} + if diff := cmp.Diff(want, items); diff != "" { + t.Errorf("Projects.ListOrganizationProjectViewItems mismatch (-want +got):\n%v", diff) + } + + const methodName = "ListOrganizationProjectViewItems" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListOrganizationProjectViewItems(ctx, "\n", 1, 2, opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListOrganizationProjectViewItems(ctx, "o", 1, 2, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_ListUserProjectViewItems(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2/1/views/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "30"}) + fmt.Fprint(w, `[{"id":22,"node_id":"PVTI_view_item_u"}]`) + }) + + opts := &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{PerPage: 30}}} + ctx := t.Context() + items, _, err := client.Projects.ListUserProjectViewItems(ctx, "u", 1, 2, opts) + if err != nil { + t.Fatalf("Projects.ListUserProjectViewItems returned error: %v", err) + } + want := []*ProjectV2Item{{ID: Ptr(int64(22)), NodeID: Ptr("PVTI_view_item_u")}} + if diff := cmp.Diff(want, items); diff != "" { + t.Errorf("Projects.ListUserProjectViewItems mismatch (-want +got):\n%v", diff) + } + + const methodName = "ListUserProjectViewItems" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListUserProjectViewItems(ctx, "\n", 1, 2, opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListUserProjectViewItems(ctx, "u", 1, 2, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectV2ViewSortBy_UnmarshalJSON(t *testing.T) { + t.Parallel() + + t.Run("numeric and string field_id", func(t *testing.T) { + t.Parallel() + // The second element uses a string field_id, which the OpenAPI schema + // permits. The first exceeds 2^53 to confirm int64 (not float64) decoding. + want := []*ProjectV2ViewSortBy{ + {FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")}, + {FieldID: Ptr(int64(456)), Direction: Ptr("desc")}, + } + testJSONUnmarshalOnly(t, want, `[[9007199254740993,"asc"],["456","desc"]]`) + }) + + t.Run("wrong tuple length is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[1]`), &got); err == nil { + t.Error("Unmarshal of a 1-element tuple = nil error, want error") + } + }) + + t.Run("non-array is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`{"a":1}`), &got); err == nil { + t.Error("Unmarshal of a non-array = nil error, want error") + } + }) + + t.Run("field_id of an unsupported type is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[true,"asc"]`), &got); err == nil { + t.Error("Unmarshal of a boolean field_id = nil error, want error") + } + }) + + t.Run("non-numeric string field_id is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`["abc","asc"]`), &got); err == nil { + t.Error(`Unmarshal of field_id "abc" = nil error, want error`) + } + }) + + t.Run("non-string direction is an error", func(t *testing.T) { + t.Parallel() + var got ProjectV2ViewSortBy + if err := json.Unmarshal([]byte(`[123,456]`), &got); err == nil { + t.Error("Unmarshal of a numeric direction = nil error, want error") + } + }) + + t.Run("round trips through MarshalJSON", func(t *testing.T) { + t.Parallel() + in := &ProjectV2ViewSortBy{FieldID: Ptr(int64(9007199254740993)), Direction: Ptr("asc")} + b, err := json.Marshal(in) + if err != nil { + t.Fatalf("Marshal returned error: %v", err) + } + if got, want := string(b), `[9007199254740993,"asc"]`; got != want { + t.Errorf("Marshal = %v, want %v", got, want) + } + var out ProjectV2ViewSortBy + if err := json.Unmarshal(b, &out); err != nil { + t.Fatalf("Unmarshal returned error: %v", err) + } + if out.GetFieldID() != 9007199254740993 || out.GetDirection() != "asc" { + t.Errorf("round trip = %+v, want {9007199254740993 asc}", out) + } + }) +}