refactor(issue edit): wire up search-based assignee selection

Add AssigneeSearchFunc to gh issue edit interactive flow, matching
the pattern already used in gh pr edit. This eliminates the bulk
RepositoryAssignableActors fetch for interactive assignee selection,
using dynamic SuggestedAssignableActors search instead.

Also clean up pr edit assigneeSearchFunc signature to remove the
unused editable parameter (no longer needed after removing the
actor accumulation hack).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kynan Ware 2026-03-23 15:37:50 -06:00
parent e24f55d5a4
commit 947f8fb1b7
4 changed files with 53 additions and 32 deletions

View file

@ -12,6 +12,7 @@ import (
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/internal/text"
shared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
@ -248,6 +249,13 @@ func editRun(opts *EditOptions) error {
// Fetch editable shared fields once for all issues.
apiClient := api.NewClientFromHTTP(httpClient)
// Wire up search function for assignees when ActorIsAssignable is available.
// Interactive mode only supports a single issue, so we use its ID for the search query.
if issueFeatures.ActorIsAssignable && opts.Interactive && len(issues) == 1 {
editable.AssigneeSearchFunc = assigneeSearchFunc(apiClient, baseRepo, issues[0].ID)
}
opts.IO.StartProgressIndicatorWithLabel("Fetching repository information")
err = opts.FetchOptions(apiClient, baseRepo, &editable, opts.Detector.ProjectsV1())
opts.IO.StopProgressIndicator()
@ -351,3 +359,36 @@ func editRun(opts *EditOptions) error {
return nil
}
func assigneeSearchFunc(apiClient *api.Client, repo ghrepo.Interface, assignableID string) func(string) prompter.MultiSelectSearchResult {
return func(input string) prompter.MultiSelectSearchResult {
actors, availableAssigneesCount, err := api.SuggestedAssignableActors(
apiClient,
repo,
assignableID,
input)
if err != nil {
return prompter.MultiSelectSearchResult{Err: err}
}
logins := make([]string, 0, len(actors))
displayNames := make([]string, 0, len(actors))
for _, a := range actors {
if a.Login() == "" {
continue
}
logins = append(logins, a.Login())
if a.DisplayName() != "" {
displayNames = append(displayNames, a.DisplayName())
} else {
displayNames = append(displayNames, a.Login())
}
}
return prompter.MultiSelectSearchResult{
Keys: logins,
Labels: displayNames,
MoreResults: availableAssigneesCount,
}
}
}

View file

@ -607,17 +607,6 @@ func Test_editRun(t *testing.T) {
mockIssueGet(t, reg)
mockIssueProjectItemsGet(t, reg)
mockRepoMetadata(t, reg)
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)
mockIssueUpdateActorAssignees(t, reg)
mockIssueUpdateLabels(t, reg)
@ -649,17 +638,6 @@ func Test_editRun(t *testing.T) {
},
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`),

View file

@ -303,7 +303,7 @@ func editRun(opts *EditOptions) error {
// to legacy reviewer/assignee fetching.
// TODO actorIsAssignableCleanup
if issueFeatures.ActorIsAssignable {
editable.AssigneeSearchFunc = assigneeSearchFunc(apiClient, repo, &editable, pr.ID)
editable.AssigneeSearchFunc = assigneeSearchFunc(apiClient, repo, pr.ID)
editable.ReviewerSearchFunc = reviewerSearchFunc(apiClient, repo, &editable, pr.ID)
}
@ -348,7 +348,7 @@ func editRun(opts *EditOptions) error {
// assigneeSearchFunc is intended to be an arg for MultiSelectWithSearch
// to return potential assignee actors.
func assigneeSearchFunc(apiClient *api.Client, repo ghrepo.Interface, editable *shared.Editable, assignableID string) func(string) prompter.MultiSelectSearchResult {
func assigneeSearchFunc(apiClient *api.Client, repo ghrepo.Interface, assignableID string) func(string) prompter.MultiSelectSearchResult {
searchFunc := func(input string) prompter.MultiSelectSearchResult {
actors, availableAssigneesCount, err := api.SuggestedAssignableActors(
apiClient,

View file

@ -267,14 +267,16 @@ func (e Editable) MilestoneId() (*string, error) {
// go routines. Fields that would be mutated will be copied.
func (e *Editable) Clone() Editable {
return Editable{
Title: e.Title.clone(),
Body: e.Body.clone(),
Base: e.Base.clone(),
Reviewers: e.Reviewers.clone(),
Assignees: e.Assignees.clone(),
Labels: e.Labels.clone(),
Projects: e.Projects.clone(),
Milestone: e.Milestone.clone(),
Title: e.Title.clone(),
Body: e.Body.clone(),
Base: e.Base.clone(),
Reviewers: e.Reviewers.clone(),
ReviewerSearchFunc: e.ReviewerSearchFunc,
Assignees: e.Assignees.clone(),
AssigneeSearchFunc: e.AssigneeSearchFunc,
Labels: e.Labels.clone(),
Projects: e.Projects.clone(),
Milestone: e.Milestone.clone(),
// Shallow copy since no mutation.
Metadata: e.Metadata,
}