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