-
Notifications
You must be signed in to change notification settings - Fork 8.3k
issue edit, pr edit: handle display names in interactive assignee editing
#10990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
71f22d8
d4832ba
eace1e8
9a5ea87
52b6ebf
51b1e6c
a22a1bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -93,21 +93,60 @@ 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 | ||
| logins := make([]string, len(a.Nodes)) | ||
| for i, a := range a.Nodes { | ||
| logins[i] = a.Login | ||
| } | ||
| return logins | ||
| } | ||
|
|
||
| // 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. | ||
| var displayNames []string | ||
| 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()) | ||
| } | ||
|
Comment on lines
+132
to
+145
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To future-proof this against new subtypes, do we need an } else {
displayNames = append(displayNames, a.Login)
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally I wouldn't want to do that because any other sub-types that get returned are unexpected behavior in my mind - we don't know what properties a future sub-type might come with - maybe there's a new sub-type that doesn't have an ID or a login. That's probably unlikely, but I'd still like to be intentional about when we decide to start supporting other actors and what unique behavior we may need to add to support them, just like Bot has.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, hmm. I guess in this case the actor is already assigned 🤔 I don't know then. Maybe we could do something here. I'm unsure. |
||
| } | ||
|
andyfeller marked this conversation as resolved.
|
||
| return displayNames | ||
| } | ||
|
|
||
| type Labels struct { | ||
| Nodes []IssueLabel | ||
| TotalCount int | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't we use the same const for this and the one in your other PR (in I mean, it makes sense to define it here in the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, good call 👍
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to do this in the final PR into trunk to avoid merge conflicts since this value is used in a few places. |
||
| return "Copilot (AI)" | ||
| } | ||
| return b.Login() | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -620,6 +620,123 @@ func Test_editRun(t *testing.T) { | |
| }, | ||
| stdout: "https://github.com/OWNER/REPO/issue/123\n", | ||
| }, | ||
| { | ||
| name: "interactive prompts with actor assignee display names when actors available", | ||
| input: &EditOptions{ | ||
| IssueNumbers: []int{123}, | ||
| Interactive: true, | ||
| FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error { | ||
| eo.Assignees.Edited = true | ||
| return nil | ||
| }, | ||
| EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error { | ||
| // Checking that the display name is being used in the prompt. | ||
| require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa (Mona Display Name)"}) | ||
|
|
||
| // Mocking a selection of only MonaLisa in the prompt. | ||
| eo.Assignees.Value = []string{"MonaLisa (Mona Display Name)"} | ||
| return nil | ||
| }, | ||
| FetchOptions: prShared.FetchOptions, | ||
| DetermineEditor: func() (string, error) { return "vim", nil }, | ||
| }, | ||
| httpStubs: func(t *testing.T, reg *httpmock.Registry) { | ||
| mockIsssueNumberGetWithAssignedActors(t, reg, 123) | ||
| reg.Register( | ||
| httpmock.GraphQL(`query RepositoryAssignableActors\b`), | ||
| httpmock.StringResponse(` | ||
| { "data": { "repository": { "suggestedActors": { | ||
| "nodes": [ | ||
| { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, | ||
| { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" } | ||
| ], | ||
| "pageInfo": { "hasNextPage": false } | ||
| } } } } | ||
| `)) | ||
| mockIssueUpdate(t, reg) | ||
| reg.Register( | ||
| httpmock.GraphQL(`mutation ReplaceActorsForAssignable\b`), | ||
| httpmock.GraphQLMutation(` | ||
| { "data": { "replaceActorsForAssignable": { "__typename": "" } } }`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: what is the importance of the empty
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the big reason was just to maintain consistency with the other mocks in this test suite.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For example: func mockIssueUpdateLabels(t *testing.T, reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`mutation LabelAdd\b`),
httpmock.GraphQLMutation(`
{ "data": { "addLabelsToLabelable": { "__typename": "" } } }`,
func(inputs map[string]interface{}) {}),
)
reg.Register(
httpmock.GraphQL(`mutation LabelRemove\b`),
httpmock.GraphQLMutation(`
{ "data": { "removeLabelsFromLabelable": { "__typename": "" } } }`,
func(inputs map[string]interface{}) {}),
)
} |
||
| func(inputs map[string]interface{}) { | ||
| // Checking that despite the display name being returned | ||
| // from the EditFieldsSurvey, the ID is still | ||
| // used in the mutation. | ||
| require.Contains(t, inputs["actorIds"], "MONAID") | ||
| }), | ||
| ) | ||
| }, | ||
| stdout: "https://github.com/OWNER/REPO/issue/123\n", | ||
| }, | ||
| { | ||
| name: "interactive prompts with user assignee logins when actors unavailable", | ||
| input: &EditOptions{ | ||
| IssueNumbers: []int{123}, | ||
| Interactive: true, | ||
| FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error { | ||
| eo.Assignees.Edited = true | ||
| return nil | ||
| }, | ||
| EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error { | ||
| // Checking that only the login is used in the prompt (no display name) | ||
| require.Equal(t, eo.Assignees.Default, []string{"hubot", "MonaLisa"}) | ||
|
|
||
| // Mocking a selection of only MonaLisa in the prompt. | ||
| eo.Assignees.Value = []string{"MonaLisa"} | ||
| return nil | ||
| }, | ||
| FetchOptions: prShared.FetchOptions, | ||
| DetermineEditor: func() (string, error) { return "vim", nil }, | ||
| Detector: &fd.DisabledDetectorMock{}, | ||
| }, | ||
| httpStubs: func(t *testing.T, reg *httpmock.Registry) { | ||
| reg.Register( | ||
| httpmock.GraphQL(`query IssueByNumber\b`), | ||
| httpmock.StringResponse(fmt.Sprintf(` | ||
| { "data": { "repository": { "hasIssuesEnabled": true, "issue": { | ||
| "id": "%[1]d", | ||
| "number": %[1]d, | ||
| "url": "https://github.com/OWNER/REPO/issue/123", | ||
| "assignees": { | ||
| "nodes": [ | ||
| { | ||
| "id": "HUBOTID", | ||
| "login": "hubot", | ||
| "name": "" | ||
| }, | ||
| { | ||
| "id": "MONAID", | ||
| "login": "MonaLisa", | ||
| "name": "Mona Display Name" | ||
| } | ||
| ], | ||
| "totalCount": 2 | ||
| } | ||
| } } } }`, 123)), | ||
| ) | ||
| reg.Register( | ||
| httpmock.GraphQL(`query RepositoryAssignableUsers\b`), | ||
| httpmock.StringResponse(` | ||
| { "data": { "repository": { "assignableUsers": { | ||
| "nodes": [ | ||
| { "login": "hubot", "id": "HUBOTID", "name": "" }, | ||
| { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name" } | ||
| ], | ||
| "pageInfo": { "hasNextPage": false } | ||
| } } } } | ||
| `)) | ||
| reg.Register( | ||
| httpmock.GraphQL(`mutation IssueUpdate\b`), | ||
| httpmock.GraphQLMutation(` | ||
| { "data": { "updateIssue": { "__typename": "" } } }`, | ||
| func(inputs map[string]interface{}) { | ||
| // Checking that we still assigned the expected ID. | ||
| require.Contains(t, inputs["assigneeIds"], "MONAID") | ||
| }), | ||
| ) | ||
| }, | ||
| stdout: "https://github.com/OWNER/REPO/issue/123\n", | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
|
|
@@ -678,6 +795,34 @@ func mockIssueNumberGet(_ *testing.T, reg *httpmock.Registry, number int) { | |
| ) | ||
| } | ||
|
|
||
| func mockIsssueNumberGetWithAssignedActors(_ *testing.T, reg *httpmock.Registry, number int) { | ||
| reg.Register( | ||
| httpmock.GraphQL(`query IssueByNumber\b`), | ||
| httpmock.StringResponse(fmt.Sprintf(` | ||
| { "data": { "repository": { "hasIssuesEnabled": true, "issue": { | ||
| "id": "%[1]d", | ||
| "number": %[1]d, | ||
| "url": "https://github.com/OWNER/REPO/issue/%[1]d", | ||
| "assignedActors": { | ||
| "nodes": [ | ||
| { | ||
| "id": "HUBOTID", | ||
| "login": "hubot", | ||
| "__typename": "Bot" | ||
| }, | ||
| { | ||
| "id": "MONAID", | ||
| "login": "MonaLisa", | ||
| "name": "Mona Display Name", | ||
| "__typename": "User" | ||
| } | ||
| ], | ||
| "totalCount": 2 | ||
| } | ||
| } } } }`, number)), | ||
| ) | ||
| } | ||
|
|
||
| func mockIssueProjectItemsGet(_ *testing.T, reg *httpmock.Registry) { | ||
| reg.Register( | ||
| httpmock.GraphQL(`query IssueProjectItems\b`), | ||
|
|
@@ -699,7 +844,7 @@ func mockRepoMetadata(_ *testing.T, reg *httpmock.Registry) { | |
| { "data": { "repository": { "suggestedActors": { | ||
| "nodes": [ | ||
| { "login": "hubot", "id": "HUBOTID", "__typename": "Bot" }, | ||
| { "login": "MonaLisa", "id": "MONAID", "__typename": "User" } | ||
| { "login": "MonaLisa", "id": "MONAID", "name": "Mona Display Name", "__typename": "User" } | ||
| ], | ||
| "pageInfo": { "hasNextPage": false } | ||
| } } } } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.