mirror of
https://gitea.com/gitea/tea.git
synced 2025-03-12 12:49:55 +01:00
Feat: interactive issue edit command (#708)
If there are no flags passed to the `issues edit` command, it prompts for changes to the properties like title, description, labels, etc. This is a follow-up to <https://gitea.com/gitea/tea/pulls/506>. Closes: <https://gitea.com/gitea/tea/issues/605> Co-authored-by: techknowlogick <techknowlogick@noreply.gitea.com> Reviewed-on: https://gitea.com/gitea/tea/pulls/708 Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com> Co-authored-by: Vincent Neubauer <v.neubauer@darlor.de> Co-committed-by: Vincent Neubauer <v.neubauer@darlor.de>
This commit is contained in:
parent
60636cd7d8
commit
57e3400f0f
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
"code.gitea.io/tea/modules/context"
|
"code.gitea.io/tea/modules/context"
|
||||||
|
"code.gitea.io/tea/modules/interact"
|
||||||
"code.gitea.io/tea/modules/print"
|
"code.gitea.io/tea/modules/print"
|
||||||
"code.gitea.io/tea/modules/task"
|
"code.gitea.io/tea/modules/task"
|
||||||
"code.gitea.io/tea/modules/utils"
|
"code.gitea.io/tea/modules/utils"
|
||||||
@ -47,6 +48,14 @@ func runIssuesEdit(cmd *cli.Context) error {
|
|||||||
|
|
||||||
client := ctx.Login.Client()
|
client := ctx.Login.Client()
|
||||||
for _, opts.Index = range indices {
|
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)
|
issue, err := task.EditIssue(ctx, client, *opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -70,7 +70,7 @@ func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.Cre
|
|||||||
|
|
||||||
// milestone
|
// milestone
|
||||||
if len(selectables.MilestoneList) != 0 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
o.Milestone = selectables.MilestoneMap[milestoneName]
|
o.Milestone = selectables.MilestoneMap[milestoneName]
|
||||||
|
153
modules/interact/issue_edit.go
Normal file
153
modules/interact/issue_edit.go
Normal file
@ -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
|
||||||
|
}
|
@ -96,7 +96,7 @@ func CreateLogin() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sshKey == "" {
|
if sshKey == "" {
|
||||||
sshKey, err = promptSelect("Select ssh-key: ", task.ListSSHPubkey(), "", "")
|
sshKey, err = promptSelect("Select ssh-key: ", task.ListSSHPubkey(), "", "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,12 @@ import (
|
|||||||
|
|
||||||
// Multiline represents options for a prompt that expects multiline input
|
// Multiline represents options for a prompt that expects multiline input
|
||||||
type Multiline struct {
|
type Multiline struct {
|
||||||
Message string
|
Message string
|
||||||
Default string
|
Default string
|
||||||
Syntax string
|
Syntax string
|
||||||
UseEditor bool
|
UseEditor bool
|
||||||
|
EditorAppendDefault bool
|
||||||
|
EditorHideDefault bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMultiline creates a prompt that switches between the inline multiline text
|
// 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) {
|
func NewMultiline(opts Multiline) (prompt survey.Prompt) {
|
||||||
if opts.UseEditor {
|
if opts.UseEditor {
|
||||||
prompt = &survey.Editor{
|
prompt = &survey.Editor{
|
||||||
Message: opts.Message,
|
Message: opts.Message,
|
||||||
Default: opts.Default,
|
Default: opts.Default,
|
||||||
FileName: "*." + opts.Syntax,
|
FileName: "*." + opts.Syntax,
|
||||||
|
AppendDefault: opts.EditorAppendDefault,
|
||||||
|
HideDefault: opts.EditorHideDefault,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prompt = &survey.Multiline{Message: opts.Message, Default: opts.Default}
|
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.
|
// 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
|
var selection string
|
||||||
|
if defaultVal == "" && noneVal != "" {
|
||||||
|
defaultVal = noneVal
|
||||||
|
|
||||||
|
}
|
||||||
promptA := &survey.Select{
|
promptA := &survey.Select{
|
||||||
Message: prompt,
|
Message: prompt,
|
||||||
Options: makeSelectOpts(options, customVal, noneVal),
|
Options: makeSelectOpts(options, customVal, noneVal),
|
||||||
VimMode: true,
|
VimMode: true,
|
||||||
Default: noneVal,
|
Default: defaultVal,
|
||||||
}
|
}
|
||||||
if err := survey.AskOne(promptA, &selection); err != nil {
|
if err := survey.AskOne(promptA, &selection); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
Loading…
x
Reference in New Issue
Block a user