github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/shared/survey.go (about)

     1  package shared
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/AlecAivazis/survey/v2"
     8  	"github.com/ungtb10d/cli/v2/api"
     9  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    10  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    11  	"github.com/ungtb10d/cli/v2/pkg/prompt"
    12  	"github.com/ungtb10d/cli/v2/pkg/surveyext"
    13  )
    14  
    15  type Action int
    16  
    17  const (
    18  	SubmitAction Action = iota
    19  	PreviewAction
    20  	CancelAction
    21  	MetadataAction
    22  	EditCommitMessageAction
    23  	EditCommitSubjectAction
    24  	SubmitDraftAction
    25  
    26  	noMilestone = "(none)"
    27  
    28  	submitLabel      = "Submit"
    29  	submitDraftLabel = "Submit as draft"
    30  	previewLabel     = "Continue in browser"
    31  	metadataLabel    = "Add metadata"
    32  	cancelLabel      = "Cancel"
    33  )
    34  
    35  func ConfirmIssueSubmission(allowPreview bool, allowMetadata bool) (Action, error) {
    36  	return confirmSubmission(allowPreview, allowMetadata, false, false)
    37  }
    38  
    39  func ConfirmPRSubmission(allowPreview, allowMetadata, isDraft bool) (Action, error) {
    40  	return confirmSubmission(allowPreview, allowMetadata, true, isDraft)
    41  }
    42  
    43  func confirmSubmission(allowPreview, allowMetadata, allowDraft, isDraft bool) (Action, error) {
    44  	var options []string
    45  	if !isDraft {
    46  		options = append(options, submitLabel)
    47  	}
    48  	if allowDraft {
    49  		options = append(options, submitDraftLabel)
    50  	}
    51  	if allowPreview {
    52  		options = append(options, previewLabel)
    53  	}
    54  	if allowMetadata {
    55  		options = append(options, metadataLabel)
    56  	}
    57  	options = append(options, cancelLabel)
    58  
    59  	confirmAnswers := struct {
    60  		Confirmation int
    61  	}{}
    62  	confirmQs := []*survey.Question{
    63  		{
    64  			Name: "confirmation",
    65  			Prompt: &survey.Select{
    66  				Message: "What's next?",
    67  				Options: options,
    68  			},
    69  		},
    70  	}
    71  
    72  	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
    73  	err := prompt.SurveyAsk(confirmQs, &confirmAnswers)
    74  	if err != nil {
    75  		return -1, fmt.Errorf("could not prompt: %w", err)
    76  	}
    77  
    78  	switch options[confirmAnswers.Confirmation] {
    79  	case submitLabel:
    80  		return SubmitAction, nil
    81  	case submitDraftLabel:
    82  		return SubmitDraftAction, nil
    83  	case previewLabel:
    84  		return PreviewAction, nil
    85  	case metadataLabel:
    86  		return MetadataAction, nil
    87  	case cancelLabel:
    88  		return CancelAction, nil
    89  	default:
    90  		return -1, fmt.Errorf("invalid index: %d", confirmAnswers.Confirmation)
    91  	}
    92  }
    93  
    94  func BodySurvey(state *IssueMetadataState, templateContent, editorCommand string) error {
    95  	if templateContent != "" {
    96  		if state.Body != "" {
    97  			// prevent excessive newlines between default body and template
    98  			state.Body = strings.TrimRight(state.Body, "\n")
    99  			state.Body += "\n\n"
   100  		}
   101  		state.Body += templateContent
   102  	}
   103  
   104  	preBody := state.Body
   105  
   106  	// TODO should just be an AskOne but ran into problems with the stubber
   107  	qs := []*survey.Question{
   108  		{
   109  			Name: "Body",
   110  			Prompt: &surveyext.GhEditor{
   111  				BlankAllowed:  true,
   112  				EditorCommand: editorCommand,
   113  				Editor: &survey.Editor{
   114  					Message:       "Body",
   115  					FileName:      "*.md",
   116  					Default:       state.Body,
   117  					HideDefault:   true,
   118  					AppendDefault: true,
   119  				},
   120  			},
   121  		},
   122  	}
   123  
   124  	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
   125  	err := prompt.SurveyAsk(qs, state)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	if preBody != state.Body {
   131  		state.MarkDirty()
   132  	}
   133  
   134  	return nil
   135  }
   136  
   137  func TitleSurvey(state *IssueMetadataState) error {
   138  	preTitle := state.Title
   139  
   140  	// TODO should just be an AskOne but ran into problems with the stubber
   141  	qs := []*survey.Question{
   142  		{
   143  			Name: "Title",
   144  			Prompt: &survey.Input{
   145  				Message: "Title",
   146  				Default: state.Title,
   147  			},
   148  		},
   149  	}
   150  
   151  	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
   152  	err := prompt.SurveyAsk(qs, state)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	if preTitle != state.Title {
   158  		state.MarkDirty()
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  type MetadataFetcher struct {
   165  	IO        *iostreams.IOStreams
   166  	APIClient *api.Client
   167  	Repo      ghrepo.Interface
   168  	State     *IssueMetadataState
   169  }
   170  
   171  func (mf *MetadataFetcher) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) {
   172  	mf.IO.StartProgressIndicator()
   173  	metadataResult, err := api.RepoMetadata(mf.APIClient, mf.Repo, input)
   174  	mf.IO.StopProgressIndicator()
   175  	mf.State.MetadataResult = metadataResult
   176  	return metadataResult, err
   177  }
   178  
   179  type RepoMetadataFetcher interface {
   180  	RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error)
   181  }
   182  
   183  func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error {
   184  	isChosen := func(m string) bool {
   185  		for _, c := range state.Metadata {
   186  			if m == c {
   187  				return true
   188  			}
   189  		}
   190  		return false
   191  	}
   192  
   193  	allowReviewers := state.Type == PRMetadata
   194  
   195  	extraFieldsOptions := []string{}
   196  	if allowReviewers {
   197  		extraFieldsOptions = append(extraFieldsOptions, "Reviewers")
   198  	}
   199  	extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
   200  
   201  	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
   202  	err := prompt.SurveyAsk([]*survey.Question{
   203  		{
   204  			Name: "metadata",
   205  			Prompt: &survey.MultiSelect{
   206  				Message: "What would you like to add?",
   207  				Options: extraFieldsOptions,
   208  			},
   209  		},
   210  	}, state)
   211  	if err != nil {
   212  		return fmt.Errorf("could not prompt: %w", err)
   213  	}
   214  
   215  	metadataInput := api.RepoMetadataInput{
   216  		Reviewers:  isChosen("Reviewers"),
   217  		Assignees:  isChosen("Assignees"),
   218  		Labels:     isChosen("Labels"),
   219  		Projects:   isChosen("Projects"),
   220  		Milestones: isChosen("Milestone"),
   221  	}
   222  	metadataResult, err := fetcher.RepoMetadataFetch(metadataInput)
   223  	if err != nil {
   224  		return fmt.Errorf("error fetching metadata options: %w", err)
   225  	}
   226  
   227  	var users []string
   228  	for _, u := range metadataResult.AssignableUsers {
   229  		users = append(users, u.DisplayName())
   230  	}
   231  	var teams []string
   232  	for _, t := range metadataResult.Teams {
   233  		teams = append(teams, fmt.Sprintf("%s/%s", baseRepo.RepoOwner(), t.Slug))
   234  	}
   235  	var labels []string
   236  	for _, l := range metadataResult.Labels {
   237  		labels = append(labels, l.Name)
   238  	}
   239  	var projects []string
   240  	for _, l := range metadataResult.Projects {
   241  		projects = append(projects, l.Name)
   242  	}
   243  	milestones := []string{noMilestone}
   244  	for _, m := range metadataResult.Milestones {
   245  		milestones = append(milestones, m.Title)
   246  	}
   247  
   248  	var mqs []*survey.Question
   249  	if isChosen("Reviewers") {
   250  		if len(users) > 0 || len(teams) > 0 {
   251  			mqs = append(mqs, &survey.Question{
   252  				Name: "reviewers",
   253  				Prompt: &survey.MultiSelect{
   254  					Message: "Reviewers",
   255  					Options: append(users, teams...),
   256  					Default: state.Reviewers,
   257  				},
   258  			})
   259  		} else {
   260  			fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
   261  		}
   262  	}
   263  	if isChosen("Assignees") {
   264  		if len(users) > 0 {
   265  			mqs = append(mqs, &survey.Question{
   266  				Name: "assignees",
   267  				Prompt: &survey.MultiSelect{
   268  					Message: "Assignees",
   269  					Options: users,
   270  					Default: state.Assignees,
   271  				},
   272  			})
   273  		} else {
   274  			fmt.Fprintln(io.ErrOut, "warning: no assignable users")
   275  		}
   276  	}
   277  	if isChosen("Labels") {
   278  		if len(labels) > 0 {
   279  			mqs = append(mqs, &survey.Question{
   280  				Name: "labels",
   281  				Prompt: &survey.MultiSelect{
   282  					Message: "Labels",
   283  					Options: labels,
   284  					Default: state.Labels,
   285  				},
   286  			})
   287  		} else {
   288  			fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
   289  		}
   290  	}
   291  	if isChosen("Projects") {
   292  		if len(projects) > 0 {
   293  			mqs = append(mqs, &survey.Question{
   294  				Name: "projects",
   295  				Prompt: &survey.MultiSelect{
   296  					Message: "Projects",
   297  					Options: projects,
   298  					Default: state.Projects,
   299  				},
   300  			})
   301  		} else {
   302  			fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
   303  		}
   304  	}
   305  	if isChosen("Milestone") {
   306  		if len(milestones) > 1 {
   307  			var milestoneDefault string
   308  			if len(state.Milestones) > 0 {
   309  				milestoneDefault = state.Milestones[0]
   310  			}
   311  			mqs = append(mqs, &survey.Question{
   312  				Name: "milestone",
   313  				Prompt: &survey.Select{
   314  					Message: "Milestone",
   315  					Options: milestones,
   316  					Default: milestoneDefault,
   317  				},
   318  			})
   319  		} else {
   320  			fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
   321  		}
   322  	}
   323  
   324  	values := struct {
   325  		Reviewers []string
   326  		Assignees []string
   327  		Labels    []string
   328  		Projects  []string
   329  		Milestone string
   330  	}{}
   331  
   332  	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
   333  	err = prompt.SurveyAsk(mqs, &values)
   334  	if err != nil {
   335  		return fmt.Errorf("could not prompt: %w", err)
   336  	}
   337  
   338  	if isChosen("Reviewers") {
   339  		var logins []string
   340  		for _, r := range values.Reviewers {
   341  			// Extract user login from display name
   342  			logins = append(logins, (strings.Split(r, " "))[0])
   343  		}
   344  		state.Reviewers = logins
   345  	}
   346  	if isChosen("Assignees") {
   347  		var logins []string
   348  		for _, a := range values.Assignees {
   349  			// Extract user login from display name
   350  			logins = append(logins, (strings.Split(a, " "))[0])
   351  		}
   352  		state.Assignees = logins
   353  	}
   354  	if isChosen("Labels") {
   355  		state.Labels = values.Labels
   356  	}
   357  	if isChosen("Projects") {
   358  		state.Projects = values.Projects
   359  	}
   360  	if isChosen("Milestone") {
   361  		if values.Milestone != "" && values.Milestone != noMilestone {
   362  			state.Milestones = []string{values.Milestone}
   363  		} else {
   364  			state.Milestones = []string{}
   365  		}
   366  	}
   367  
   368  	return nil
   369  }