diff --git a/cmd/issues/edit.go b/cmd/issues/edit.go index 0af37fb..9de38dd 100644 --- a/cmd/issues/edit.go +++ b/cmd/issues/edit.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/modules/context" + "code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/utils" @@ -47,6 +48,14 @@ func runIssuesEdit(cmd *cli.Context) error { client := ctx.Login.Client() for _, opts.Index = range indices { + if ctx.NumFlags() == 0 { + var err error + opts, err = interact.EditIssue(*ctx, opts.Index) + if err != nil { + return err + } + } + issue, err := task.EditIssue(ctx, client, *opts) if err != nil { return err diff --git a/modules/interact/issue_create.go b/modules/interact/issue_create.go index 2d4f995..db64472 100644 --- a/modules/interact/issue_create.go +++ b/modules/interact/issue_create.go @@ -70,7 +70,7 @@ func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.Cre // milestone if len(selectables.MilestoneList) != 0 { - if milestoneName, err = promptSelect("Milestone:", selectables.MilestoneList, "", "[none]"); err != nil { + if milestoneName, err = promptSelect("Milestone:", selectables.MilestoneList, "", "[none]", ""); err != nil { return err } o.Milestone = selectables.MilestoneMap[milestoneName] diff --git a/modules/interact/issue_edit.go b/modules/interact/issue_edit.go new file mode 100644 index 0000000..fc13301 --- /dev/null +++ b/modules/interact/issue_edit.go @@ -0,0 +1,153 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package interact + +import ( + "slices" + + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/context" + "code.gitea.io/tea/modules/task" + + "github.com/AlecAivazis/survey/v2" +) + +// EditIssue interactively edits an issue +func EditIssue(ctx context.TeaContext, index int64) (*task.EditIssueOption, error) { + var opts = task.EditIssueOption{} + var err error + + ctx.Owner, ctx.Repo, err = promptRepoSlug(ctx.Owner, ctx.Repo) + if err != nil { + return &opts, err + } + + c := ctx.Login.Client() + i, _, err := c.GetIssue(ctx.Owner, ctx.Repo, index) + if err != nil { + return &opts, err + } + + opts = task.EditIssueOption{ + Index: index, + Title: &i.Title, + Body: &i.Body, + Deadline: i.Deadline, + } + + if len(i.Assignees) != 0 { + for _, a := range i.Assignees { + opts.AddAssignees = append(opts.AddAssignees, a.UserName) + } + } + + if len(i.Labels) != 0 { + for _, l := range i.Labels { + opts.AddLabels = append(opts.AddLabels, l.Name) + } + } + + if i.Milestone != nil { + opts.Milestone = &i.Milestone.Title + } + + if err := promptIssueEditProperties(&ctx, &opts); err != nil { + return &opts, err + } + + return &opts, err +} + +func promptIssueEditProperties(ctx *context.TeaContext, o *task.EditIssueOption) error { + var milestoneName string + var labelsSelected []string + var err error + + selectableChan := make(chan (issueSelectables), 1) + go fetchIssueSelectables(ctx.Login, ctx.Owner, ctx.Repo, selectableChan) + + // title + promptOpts := survey.WithValidator(survey.Required) + promptI := &survey.Input{Message: "Issue title:", Default: *o.Title} + if err = survey.AskOne(promptI, o.Title, promptOpts); err != nil { + return err + } + + // description + promptD := NewMultiline(Multiline{ + Message: "Issue description:", + Default: *o.Body, + Syntax: "md", + UseEditor: config.GetPreferences().Editor, + EditorAppendDefault: true, + EditorHideDefault: true, + }) + + if err = survey.AskOne(promptD, o.Body); err != nil { + return err + } + + // wait until selectables are fetched + selectables := <-selectableChan + if selectables.Err != nil { + return selectables.Err + } + + // skip remaining props if we don't have permission to set them + if !selectables.Repo.Permissions.Push { + return nil + } + + currAssignees := o.AddAssignees + newAssignees := selectables.Assignees + + for _, c := range currAssignees { + if i := slices.Index(newAssignees, c); i != -1 { + newAssignees = slices.Delete(newAssignees, i, i+1) + } + } + + // assignees + if o.AddAssignees, err = promptMultiSelect("Add Assignees:", newAssignees, "[other]"); err != nil { + return err + } + + // milestone + if len(selectables.MilestoneList) != 0 { + var defaultMS string + if o.Milestone != nil { + defaultMS = *o.Milestone + } + if milestoneName, err = promptSelect("Milestone:", selectables.MilestoneList, "", "[none]", defaultMS); err != nil { + return err + } + o.Milestone = &milestoneName + } + + // labels + if len(selectables.LabelList) != 0 { + promptL := &survey.MultiSelect{Message: "Labels:", Options: selectables.LabelList, VimMode: true, Default: o.AddLabels} + if err := survey.AskOne(promptL, &labelsSelected); err != nil { + return err + } + // removed labels + for _, l := range o.AddLabels { + if !slices.Contains(labelsSelected, l) { + o.RemoveLabels = append(o.RemoveLabels, l) + } + } + // added labels + o.AddLabels = make([]string, len(labelsSelected)) + for i, l := range labelsSelected { + o.AddLabels[i] = l + } + } + + // deadline + if o.Deadline, err = promptDatetime("Due date:"); err != nil { + return err + } + + return nil +} diff --git a/modules/interact/login.go b/modules/interact/login.go index 4698fc6..5c820fa 100644 --- a/modules/interact/login.go +++ b/modules/interact/login.go @@ -96,7 +96,7 @@ func CreateLogin() error { } if sshKey == "" { - sshKey, err = promptSelect("Select ssh-key: ", task.ListSSHPubkey(), "", "") + sshKey, err = promptSelect("Select ssh-key: ", task.ListSSHPubkey(), "", "", "") if err != nil { return err } diff --git a/modules/interact/prompts.go b/modules/interact/prompts.go index cb5c375..66205bb 100644 --- a/modules/interact/prompts.go +++ b/modules/interact/prompts.go @@ -15,10 +15,12 @@ import ( // Multiline represents options for a prompt that expects multiline input type Multiline struct { - Message string - Default string - Syntax string - UseEditor bool + Message string + Default string + Syntax string + UseEditor bool + EditorAppendDefault bool + EditorHideDefault bool } // NewMultiline creates a prompt that switches between the inline multiline text @@ -26,9 +28,11 @@ type Multiline struct { func NewMultiline(opts Multiline) (prompt survey.Prompt) { if opts.UseEditor { prompt = &survey.Editor{ - Message: opts.Message, - Default: opts.Default, - FileName: "*." + opts.Syntax, + Message: opts.Message, + Default: opts.Default, + FileName: "*." + opts.Syntax, + AppendDefault: opts.EditorAppendDefault, + HideDefault: opts.EditorHideDefault, } } else { prompt = &survey.Multiline{Message: opts.Message, Default: opts.Default} @@ -146,13 +150,17 @@ func promptSelectV2(prompt string, options []string) (string, error) { } // promptSelect creates a generic select prompt, with processing of custom values or none-option. -func promptSelect(prompt string, options []string, customVal, noneVal string) (string, error) { +func promptSelect(prompt string, options []string, customVal, noneVal, defaultVal string) (string, error) { var selection string + if defaultVal == "" && noneVal != "" { + defaultVal = noneVal + + } promptA := &survey.Select{ Message: prompt, Options: makeSelectOpts(options, customVal, noneVal), VimMode: true, - Default: noneVal, + Default: defaultVal, } if err := survey.AskOne(promptA, &selection); err != nil { return "", err