feat(editable): update assigned actors to use display names

- Refactored AssignedActors to return display names instead of logins.
- Updated related functions to ensure consistency in display names.
- Enhanced comments for clarity on display name logic and actor types.
This commit is contained in:
Kynan Ware 2025-05-14 20:26:53 -06:00
parent d4832ba015
commit eace1e889a
6 changed files with 87 additions and 17 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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