github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/shared/editable.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/set"
    11  	"github.com/ungtb10d/cli/v2/pkg/surveyext"
    12  )
    13  
    14  type Editable struct {
    15  	Title     EditableString
    16  	Body      EditableString
    17  	Base      EditableString
    18  	Reviewers EditableSlice
    19  	Assignees EditableSlice
    20  	Labels    EditableSlice
    21  	Projects  EditableSlice
    22  	Milestone EditableString
    23  	Metadata  api.RepoMetadataResult
    24  }
    25  
    26  type EditableString struct {
    27  	Value   string
    28  	Default string
    29  	Options []string
    30  	Edited  bool
    31  }
    32  
    33  type EditableSlice struct {
    34  	Value   []string
    35  	Add     []string
    36  	Remove  []string
    37  	Default []string
    38  	Options []string
    39  	Edited  bool
    40  	Allowed bool
    41  }
    42  
    43  func (e Editable) Dirty() bool {
    44  	return e.Title.Edited ||
    45  		e.Body.Edited ||
    46  		e.Base.Edited ||
    47  		e.Reviewers.Edited ||
    48  		e.Assignees.Edited ||
    49  		e.Labels.Edited ||
    50  		e.Projects.Edited ||
    51  		e.Milestone.Edited
    52  }
    53  
    54  func (e Editable) TitleValue() *string {
    55  	if !e.Title.Edited {
    56  		return nil
    57  	}
    58  	return &e.Title.Value
    59  }
    60  
    61  func (e Editable) BodyValue() *string {
    62  	if !e.Body.Edited {
    63  		return nil
    64  	}
    65  	return &e.Body.Value
    66  }
    67  
    68  func (e Editable) ReviewerIds() (*[]string, *[]string, error) {
    69  	if !e.Reviewers.Edited {
    70  		return nil, nil, nil
    71  	}
    72  	if len(e.Reviewers.Add) != 0 || len(e.Reviewers.Remove) != 0 {
    73  		s := set.NewStringSet()
    74  		s.AddValues(e.Reviewers.Default)
    75  		s.AddValues(e.Reviewers.Add)
    76  		s.RemoveValues(e.Reviewers.Remove)
    77  		e.Reviewers.Value = s.ToSlice()
    78  	}
    79  	var userReviewers []string
    80  	var teamReviewers []string
    81  	for _, r := range e.Reviewers.Value {
    82  		if strings.ContainsRune(r, '/') {
    83  			teamReviewers = append(teamReviewers, r)
    84  		} else {
    85  			userReviewers = append(userReviewers, r)
    86  		}
    87  	}
    88  	userIds, err := e.Metadata.MembersToIDs(userReviewers)
    89  	if err != nil {
    90  		return nil, nil, err
    91  	}
    92  	teamIds, err := e.Metadata.TeamsToIDs(teamReviewers)
    93  	if err != nil {
    94  		return nil, nil, err
    95  	}
    96  	return &userIds, &teamIds, nil
    97  }
    98  
    99  func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]string, error) {
   100  	if !e.Assignees.Edited {
   101  		return nil, nil
   102  	}
   103  	if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 {
   104  		meReplacer := NewMeReplacer(client, repo.RepoHost())
   105  		s := set.NewStringSet()
   106  		s.AddValues(e.Assignees.Default)
   107  		add, err := meReplacer.ReplaceSlice(e.Assignees.Add)
   108  		if err != nil {
   109  			return nil, err
   110  		}
   111  		s.AddValues(add)
   112  		remove, err := meReplacer.ReplaceSlice(e.Assignees.Remove)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		s.RemoveValues(remove)
   117  		e.Assignees.Value = s.ToSlice()
   118  	}
   119  	a, err := e.Metadata.MembersToIDs(e.Assignees.Value)
   120  	return &a, err
   121  }
   122  
   123  func (e Editable) ProjectIds() (*[]string, error) {
   124  	if !e.Projects.Edited {
   125  		return nil, nil
   126  	}
   127  	if len(e.Projects.Add) != 0 || len(e.Projects.Remove) != 0 {
   128  		s := set.NewStringSet()
   129  		s.AddValues(e.Projects.Default)
   130  		s.AddValues(e.Projects.Add)
   131  		s.RemoveValues(e.Projects.Remove)
   132  		e.Projects.Value = s.ToSlice()
   133  	}
   134  	p, err := e.Metadata.ProjectsToIDs(e.Projects.Value)
   135  	return &p, err
   136  }
   137  
   138  func (e Editable) MilestoneId() (*string, error) {
   139  	if !e.Milestone.Edited {
   140  		return nil, nil
   141  	}
   142  	if e.Milestone.Value == noMilestone || e.Milestone.Value == "" {
   143  		s := ""
   144  		return &s, nil
   145  	}
   146  	m, err := e.Metadata.MilestoneToID(e.Milestone.Value)
   147  	return &m, err
   148  }
   149  
   150  func EditFieldsSurvey(editable *Editable, editorCommand string) error {
   151  	var err error
   152  	if editable.Title.Edited {
   153  		editable.Title.Value, err = titleSurvey(editable.Title.Default)
   154  		if err != nil {
   155  			return err
   156  		}
   157  	}
   158  	if editable.Body.Edited {
   159  		editable.Body.Value, err = bodySurvey(editable.Body.Default, editorCommand)
   160  		if err != nil {
   161  			return err
   162  		}
   163  	}
   164  	if editable.Reviewers.Edited {
   165  		editable.Reviewers.Value, err = multiSelectSurvey("Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
   166  		if err != nil {
   167  			return err
   168  		}
   169  	}
   170  	if editable.Assignees.Edited {
   171  		editable.Assignees.Value, err = multiSelectSurvey("Assignees", editable.Assignees.Default, editable.Assignees.Options)
   172  		if err != nil {
   173  			return err
   174  		}
   175  	}
   176  	if editable.Labels.Edited {
   177  		editable.Labels.Add, err = multiSelectSurvey("Labels", editable.Labels.Default, editable.Labels.Options)
   178  		if err != nil {
   179  			return err
   180  		}
   181  		for _, prev := range editable.Labels.Default {
   182  			var found bool
   183  			for _, selected := range editable.Labels.Add {
   184  				if prev == selected {
   185  					found = true
   186  					break
   187  				}
   188  			}
   189  			if !found {
   190  				editable.Labels.Remove = append(editable.Labels.Remove, prev)
   191  			}
   192  		}
   193  	}
   194  	if editable.Projects.Edited {
   195  		editable.Projects.Value, err = multiSelectSurvey("Projects", editable.Projects.Default, editable.Projects.Options)
   196  		if err != nil {
   197  			return err
   198  		}
   199  	}
   200  	if editable.Milestone.Edited {
   201  		editable.Milestone.Value, err = milestoneSurvey(editable.Milestone.Default, editable.Milestone.Options)
   202  		if err != nil {
   203  			return err
   204  		}
   205  	}
   206  	confirm, err := confirmSurvey()
   207  	if err != nil {
   208  		return err
   209  	}
   210  	if !confirm {
   211  		return fmt.Errorf("Discarding...")
   212  	}
   213  
   214  	return nil
   215  }
   216  
   217  func FieldsToEditSurvey(editable *Editable) error {
   218  	contains := func(s []string, str string) bool {
   219  		for _, v := range s {
   220  			if v == str {
   221  				return true
   222  			}
   223  		}
   224  		return false
   225  	}
   226  
   227  	opts := []string{"Title", "Body"}
   228  	if editable.Reviewers.Allowed {
   229  		opts = append(opts, "Reviewers")
   230  	}
   231  	opts = append(opts, "Assignees", "Labels", "Projects", "Milestone")
   232  	results, err := multiSelectSurvey("What would you like to edit?", []string{}, opts)
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	if contains(results, "Title") {
   238  		editable.Title.Edited = true
   239  	}
   240  	if contains(results, "Body") {
   241  		editable.Body.Edited = true
   242  	}
   243  	if contains(results, "Reviewers") {
   244  		editable.Reviewers.Edited = true
   245  	}
   246  	if contains(results, "Assignees") {
   247  		editable.Assignees.Edited = true
   248  	}
   249  	if contains(results, "Labels") {
   250  		editable.Labels.Edited = true
   251  	}
   252  	if contains(results, "Projects") {
   253  		editable.Projects.Edited = true
   254  	}
   255  	if contains(results, "Milestone") {
   256  		editable.Milestone.Edited = true
   257  	}
   258  
   259  	return nil
   260  }
   261  
   262  func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable) error {
   263  	input := api.RepoMetadataInput{
   264  		Reviewers:  editable.Reviewers.Edited,
   265  		Assignees:  editable.Assignees.Edited,
   266  		Labels:     editable.Labels.Edited,
   267  		Projects:   editable.Projects.Edited,
   268  		Milestones: editable.Milestone.Edited,
   269  	}
   270  	metadata, err := api.RepoMetadata(client, repo, input)
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	var users []string
   276  	for _, u := range metadata.AssignableUsers {
   277  		users = append(users, u.Login)
   278  	}
   279  	var teams []string
   280  	for _, t := range metadata.Teams {
   281  		teams = append(teams, fmt.Sprintf("%s/%s", repo.RepoOwner(), t.Slug))
   282  	}
   283  	var labels []string
   284  	for _, l := range metadata.Labels {
   285  		labels = append(labels, l.Name)
   286  	}
   287  	var projects []string
   288  	for _, l := range metadata.Projects {
   289  		projects = append(projects, l.Name)
   290  	}
   291  	milestones := []string{noMilestone}
   292  	for _, m := range metadata.Milestones {
   293  		milestones = append(milestones, m.Title)
   294  	}
   295  
   296  	editable.Metadata = *metadata
   297  	editable.Reviewers.Options = append(users, teams...)
   298  	editable.Assignees.Options = users
   299  	editable.Labels.Options = labels
   300  	editable.Projects.Options = projects
   301  	editable.Milestone.Options = milestones
   302  
   303  	return nil
   304  }
   305  
   306  func titleSurvey(title string) (string, error) {
   307  	var result string
   308  	q := &survey.Input{
   309  		Message: "Title",
   310  		Default: title,
   311  	}
   312  	err := survey.AskOne(q, &result)
   313  	return result, err
   314  }
   315  
   316  func bodySurvey(body, editorCommand string) (string, error) {
   317  	var result string
   318  	q := &surveyext.GhEditor{
   319  		EditorCommand: editorCommand,
   320  		Editor: &survey.Editor{
   321  			Message:       "Body",
   322  			FileName:      "*.md",
   323  			Default:       body,
   324  			HideDefault:   true,
   325  			AppendDefault: true,
   326  		},
   327  	}
   328  	err := survey.AskOne(q, &result)
   329  	return result, err
   330  }
   331  
   332  func multiSelectSurvey(message string, defaults, options []string) ([]string, error) {
   333  	if len(options) == 0 {
   334  		return nil, nil
   335  	}
   336  	var results []string
   337  	q := &survey.MultiSelect{
   338  		Message: message,
   339  		Options: options,
   340  		Default: defaults,
   341  	}
   342  	err := survey.AskOne(q, &results)
   343  	return results, err
   344  }
   345  
   346  func milestoneSurvey(title string, opts []string) (string, error) {
   347  	if len(opts) == 0 {
   348  		return "", nil
   349  	}
   350  	var result string
   351  	q := &survey.Select{
   352  		Message: "Milestone",
   353  		Options: opts,
   354  		Default: title,
   355  	}
   356  	err := survey.AskOne(q, &result)
   357  	return result, err
   358  }
   359  
   360  func confirmSurvey() (bool, error) {
   361  	var result bool
   362  	q := &survey.Confirm{
   363  		Message: "Submit?",
   364  		Default: true,
   365  	}
   366  	err := survey.AskOne(q, &result)
   367  	return result, err
   368  }