From 38e52db3779e9a6a199b6799283a452bcbe0802b Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Thu, 8 May 2025 11:06:34 -0600 Subject: [PATCH] feat(issue edit): fetch currently assigned actors --- api/queries_issue.go | 16 +++++++ api/queries_repo.go | 7 +++ pkg/cmd/issue/edit/edit.go | 41 +++++++++++++++--- pkg/cmd/issue/edit/edit_test.go | 75 +++++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/api/queries_issue.go b/api/queries_issue.go index f09360152..2b15a41b9 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -38,6 +38,7 @@ type Issue struct { Comments Comments Author Author Assignees Assignees + AssignedActors ActorAssignees Labels Labels ProjectCards ProjectCards ProjectItems ProjectItems @@ -91,6 +92,21 @@ func (a Assignees) Logins() []string { return logins } +type ActorAssignees struct { + Edges []struct { + Node Actor + } + TotalCount int +} + +func (a ActorAssignees) Logins() []string { + logins := make([]string, len(a.Edges)) + for i, a := range a.Edges { + logins[i] = a.Node.Login + } + return logins +} + type Labels struct { Nodes []IssueLabel TotalCount int diff --git a/api/queries_repo.go b/api/queries_repo.go index 93a32d80c..06ae48b12 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -146,6 +146,13 @@ type GitHubUser struct { Name string `json:"name"` } +// Actor is a superset of User and Bot +// At the time of writing, it does not support Name. +type Actor struct { + ID string `json:"id"` + Login string `json:"login"` +} + // BranchRef is the branch name in a GitHub repository type BranchRef struct { Name string `json:"name"` diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 8386cbcfa..a5694e6c9 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -197,9 +197,35 @@ func editRun(opts *EditOptions) error { } } + if opts.Detector == nil { + cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) + opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) + } + + issueFeatures, err := opts.Detector.IssueFeatures() + if err != nil { + return err + } + lookupFields := []string{"id", "number", "title", "body", "url"} if editable.Assignees.Edited { - lookupFields = append(lookupFields, "assignees") + if issueFeatures.ActorIsAssignable { + // At the time of writing, only 10 Actors can be assigned to an issue. + assignedActors := heredoc.Doc(` + assignedActors(first: 10) { + edges { + node { + ... on Actor { + login + } + } + } + } + `) + lookupFields = append(lookupFields, assignedActors) + } else { + lookupFields = append(lookupFields, "assignees") + } } if editable.Labels.Edited { lookupFields = append(lookupFields, "labels") @@ -207,11 +233,6 @@ func editRun(opts *EditOptions) error { if editable.Projects.Edited { // TODO projectsV1Deprecation // Remove this section as we should no longer add projectCards - if opts.Detector == nil { - cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) - opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) - } - projectsV1Support := opts.Detector.ProjectsV1() if projectsV1Support == gh.ProjectsV1Supported { lookupFields = append(lookupFields, "projectCards") @@ -254,7 +275,13 @@ func editRun(opts *EditOptions) error { editable.Title.Default = issue.Title editable.Body.Default = issue.Body - editable.Assignees.Default = issue.Assignees.Logins() + // We use Actors as the default assignees if Actors are assignable + // on this GitHub host. + if issueFeatures.ActorIsAssignable { + editable.Assignees.Default = issue.AssignedActors.Logins() + } else { + editable.Assignees.Default = issue.Assignees.Logins() + } editable.Labels.Default = issue.Labels.Names() editable.Projects.Default = append(issue.ProjectCards.ProjectNames(), issue.ProjectItems.ProjectTitles()...) projectItems := map[string]string{} diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go index c9aa4c409..d0115ab4e 100644 --- a/pkg/cmd/issue/edit/edit_test.go +++ b/pkg/cmd/issue/edit/edit_test.go @@ -875,3 +875,78 @@ func TestProjectsV1Deprecation(t *testing.T) { reg.Verify(t) }) } + +func TestActorIsAssignable(t *testing.T) { + t.Run("when actors are assignable, query includes assignedActors", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + reg.Register( + httpmock.GraphQL(`assignedActors`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + _, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + // Ignore the error because we don't care. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Detector: &fd.EnabledDetectorMock{}, + IssueNumbers: []int{123}, + Editable: prShared.Editable{ + Assignees: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, + }, + }) + + reg.Verify(t) + }) + + t.Run("when actors are not assignable, query includes assignees instead", func(t *testing.T) { + ios, _, _, _ := iostreams.Test() + + reg := &httpmock.Registry{} + // This test should NOT include assignedActors in the query + reg.Exclude(t, httpmock.GraphQL(`assignedActors`)) + // It should include the regular assignees field + reg.Register( + httpmock.GraphQL(`assignees`), + // Simulate a GraphQL error to early exit the test. + httpmock.StatusStringResponse(500, ""), + ) + + _, cmdTeardown := run.Stub() + defer cmdTeardown(t) + + // Ignore the error because we're not really interested in it. + _ = editRun(&EditOptions{ + IO: ios, + HttpClient: func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + }, + BaseRepo: func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + }, + Detector: &fd.DisabledDetectorMock{}, + IssueNumbers: []int{123}, + Editable: prShared.Editable{ + Assignees: prShared.EditableSlice{ + Add: []string{"monalisa", "octocat"}, + Edited: true, + }, + }, + }) + + reg.Verify(t) + }) +}