feat(issue edit): fetch currently assigned actors

This commit is contained in:
Kynan Ware 2025-05-08 11:06:34 -06:00
parent 8ebbd1d4bf
commit 38e52db377
4 changed files with 132 additions and 7 deletions

View file

@ -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

View file

@ -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"`

View file

@ -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{}

View file

@ -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)
})
}