diff --git a/api/queries_issue.go b/api/queries_issue.go index 701b92039..bfdd223e5 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -93,19 +93,50 @@ func (a Assignees) Logins() []string { } type AssignedActors struct { - Edges []struct { - Node Actor - } + Nodes []Actor TotalCount int } -// TODO kw: Display names for actors with special display names. -func (a AssignedActors) Logins() []string { - logins := make([]string, len(a.Edges)) - for i, a := range a.Edges { - logins[i] = a.Node.Login +// DisplayNames returns a list of display names for the assigned actors. +func (a AssignedActors) DisplayNames() []string { + // These display names are used for populating the "default" assigned actors + // from the AssignedActors type. But, this is only one piece of the puzzle + // as later, other queries will fetch the full list of possible assignable + // actors from the repository, and the two lists will be reconciled. + // + // It's important that the display names are the same between the defaults + // (the values returned here) and the full list (the values returned by + // other repository queries). Any discrepancy would result in an + // "invalid default", which means an assigned actor will not be matched + // to an assignable actor and not presented as a "default" selection. + // Not being presented as a default would cause the actor to be potentially + // unassigned if the edits were submitted. + // + // To prevent this, we need shared logic to look up an actor's display name. + // However, our API types between assignedActors and the full list of + // assignableActors are different. So, as an attempt to maintain + // consistency we convert the assignedActors to the same types as the + // repository's assignableActors, treating the assignableActors DisplayName + // methods as the sources of truth. + // TODO KW: make this comment less of a wall of text if needed. + displayNames := make([]string, len(a.Nodes)) + for _, a := range a.Nodes { + if a.TypeName == "User" { + u := NewAssignableUser( + a.ID, + a.Login, + a.Name, + ) + displayNames = append(displayNames, u.DisplayName()) + } else if a.TypeName == "Bot" { + b := NewAssignableBot( + a.ID, + a.Login, + ) + displayNames = append(displayNames, b.DisplayName()) + } } - return logins + return displayNames } type Labels struct { diff --git a/api/queries_repo.go b/api/queries_repo.go index 8f2646159..dd9027ad4 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -146,11 +146,16 @@ 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. +// Actor is a superset of User and Bot, among others. +// At the time of writing, some of these fields +// are not directly supported by the Actor type and +// instead are only available on the User or Bot types +// directly. type Actor struct { - ID string `json:"id"` - Login string `json:"login"` + ID string `json:"id"` + Login string `json:"login"` + Name string `json:"name"` + TypeName string `json:"__typename"` } // BranchRef is the branch name in a GitHub repository @@ -710,6 +715,11 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { found = true break } + if strings.EqualFold(assigneeLogin, a.DisplayName()) { + ids = append(ids, a.ID()) + found = true + break + } } if !found { @@ -1227,7 +1237,17 @@ type AssignableBot struct { login string } +func NewAssignableBot(id, login string) AssignableBot { + return AssignableBot{ + id: id, + login: login, + } +} + func (b AssignableBot) DisplayName() string { + if b.login == "copilot-swe-agent" { + return "Copilot (AI)" + } return b.Login() } diff --git a/api/query_builder.go b/api/query_builder.go index 0ef44a347..a2432673b 100644 --- a/api/query_builder.go +++ b/api/query_builder.go @@ -20,6 +20,25 @@ func shortenQuery(q string) string { return strings.Map(squeeze, q) } +var assignedActors = shortenQuery(` + assignedActors(first: 10) { + nodes { + ...on User { + id, + login, + name, + __typename + } + ...on Bot { + id, + login, + __typename + } + }, + totalCount + } +`) + var issueComments = shortenQuery(` comments(first: 100) { nodes { @@ -367,7 +386,7 @@ func IssueGraphQL(fields []string) string { case "assignees": q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`) case "assignedActors": - q = append(q, `assignedActors(first: 10){edges{node{...on Actor{login}}},totalCount}`) + q = append(q, assignedActors) case "labels": q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`) case "projectCards": diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go index 5cb789543..f3c4e46ad 100644 --- a/pkg/cmd/issue/edit/edit.go +++ b/pkg/cmd/issue/edit/edit.go @@ -267,7 +267,7 @@ func editRun(opts *EditOptions) error { // We use Actors as the default assignees if Actors are assignable // on this GitHub host. if editable.Assignees.ActorAssignees { - editable.Assignees.Default = issue.AssignedActors.Logins() + editable.Assignees.Default = issue.AssignedActors.DisplayNames() } else { editable.Assignees.Default = issue.Assignees.Logins() } diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go index d484f74ed..3002c8dbe 100644 --- a/pkg/cmd/pr/edit/edit.go +++ b/pkg/cmd/pr/edit/edit.go @@ -239,7 +239,7 @@ func editRun(opts *EditOptions) error { editable.Reviewers.Default = pr.ReviewRequests.Logins() if issueFeatures.ActorIsAssignable { editable.Assignees.ActorAssignees = true - editable.Assignees.Default = pr.AssignedActors.Logins() + editable.Assignees.Default = pr.AssignedActors.DisplayNames() } else { editable.Assignees.Default = pr.Assignees.Logins() } diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go index d51405acd..cc11812ae 100644 --- a/pkg/cmd/pr/shared/editable.go +++ b/pkg/cmd/pr/shared/editable.go @@ -411,7 +411,7 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) } var actors []string for _, a := range metadata.AssignableActors { - actors = append(actors, a.Login()) + actors = append(actors, a.DisplayName()) } var teams []string for _, t := range metadata.Teams {