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 }