mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
feat(label): edit a label in a form without flags
label edit without flags on a terminal now opens a form with the label's name, color, description and flags, like milestone edit and issue edit, instead of failing; without a terminal it reports the shared "nothing to edit" error. The help of issue and milestone close and reopen names its commands without generating the text from the verb. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
6 files changed
+143
-8
No files matched your search
@@ -414,11 +414,11 @@ func issueTemplate(ctx context.Context, service services.Issues, name string) (s
|
|||||||
// changing them.
|
// changing them.
|
||||||
func (a *application) issueStateCommand(verb string) *cobra.Command {
|
func (a *application) issueStateCommand(verb string) *cobra.Command {
|
||||||
var comment string
|
var comment string
|
||||||
state, past, completeState := "closed", "Closed", "open"
|
state, past, completeState, short := "closed", "Closed", "open", "Close issues"
|
||||||
if verb == "reopen" {
|
if verb == "reopen" {
|
||||||
state, past, completeState = "open", "Reopened", "closed"
|
state, past, completeState, short = "open", "Reopened", "closed", "Reopen issues"
|
||||||
}
|
}
|
||||||
cmd := &cobra.Command{Use: verb + " {<number> | <url>}...", Short: titleCase(verb) + " issues", Example: fmt.Sprintf(`$ tea issue %[1]s 12
|
cmd := &cobra.Command{Use: verb + " {<number> | <url>}...", Short: short, Example: fmt.Sprintf(`$ tea issue %[1]s 12
|
||||||
$ tea issue %[1]s 12 13 --comment "Done in #14"`, verb), Annotations: map[string]string{"tea.arguments": issueArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
$ tea issue %[1]s 12 13 --comment "Done in #14"`, verb), Annotations: map[string]string{"tea.arguments": issueArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
target, numbers, err := a.resolveRefs(cmd, args, issueRef)
|
target, numbers, err := a.resolveRefs(cmd, args, issueRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/tea/internal/terminal"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -193,3 +194,45 @@ func TestLabelCreatePicksAColorAndPrompts(t *testing.T) {
|
|||||||
require.Equal(t, "From a prompt", created[1]["description"])
|
require.Equal(t, "From a prompt", created[1]["description"])
|
||||||
require.Equal(t, []string{"Label name", "Color (hex, e.g. d73a4a; empty = random):", "Description (optional):"}, prompt.asked)
|
require.Equal(t, []string{"Label name", "Color (hex, e.g. d73a4a; empty = random):", "Description (optional):"}, prompt.asked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// labelFormPrompter answers the label form with edit applied to the current values.
|
||||||
|
type labelFormPrompter struct {
|
||||||
|
scriptedPrompter
|
||||||
|
heading string
|
||||||
|
edit func(*terminal.LabelFormValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *labelFormPrompter) EditLabel(_ context.Context, heading string, current terminal.LabelFormValues) (terminal.LabelFormValues, error) {
|
||||||
|
p.heading = heading
|
||||||
|
p.edit(¤t)
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabelEditFormStartsFromTheLabel(t *testing.T) {
|
||||||
|
var edited string
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method + " " + r.URL.Path {
|
||||||
|
case "GET /api/v1/repos/alice/project/labels":
|
||||||
|
if r.URL.Query().Get("page") != "1" {
|
||||||
|
_, _ = w.Write([]byte(`[]`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`[{"id":11,"name":"bug","color":"ee0701","description":"Broken"}]`))
|
||||||
|
case "PATCH /api/v1/repos/alice/project/labels/11":
|
||||||
|
edited = requestFields(t, r, "name", "color", "description", "exclusive")
|
||||||
|
_, _ = w.Write([]byte(`{"id":11,"name":"defect"}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, _, _ := serverDeps(t, server.URL)
|
||||||
|
deps.Streams.InTTY, deps.Streams.ErrTTY = true, true
|
||||||
|
prompt := &labelFormPrompter{edit: func(v *terminal.LabelFormValues) { v.Name, v.Exclusive = "defect", true }}
|
||||||
|
deps.Prompter = prompt
|
||||||
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "edit", "bug", "-R", "alice/project"}))
|
||||||
|
require.Equal(t, `Editing label "bug" in alice/project`, prompt.heading)
|
||||||
|
require.Equal(t, "name=defect color=ee0701 description=Broken exclusive=true", edited)
|
||||||
|
deps.Streams.InTTY, deps.Streams.ErrTTY = false, false
|
||||||
|
require.ErrorIs(t, Execute(context.Background(), deps, []string{"label", "edit", "bug", "-R", "alice/project"}), errNothingToEdit)
|
||||||
|
}
|
||||||
+17
-2
@@ -240,14 +240,19 @@ func (a *application) reportLabel(cmd *cobra.Command, s services.Labels, item se
|
|||||||
func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command {
|
func (a *application) labelEditCommand(scope labelScope, complete cobra.CompletionFunc) *cobra.Command {
|
||||||
var fields labelFields
|
var fields labelFields
|
||||||
var byName bool
|
var byName bool
|
||||||
cmd := &cobra.Command{Use: "edit <label>", Short: "Edit a label", Example: `$ tea label edit bug --name defect --color b60205
|
cmd := &cobra.Command{Use: "edit <label>", Short: "Edit a label", Long: "Edit a label. Without flags on a terminal, a form starts from the current values.", Example: `$ tea label edit bug --name defect --color b60205
|
||||||
$ tea label edit "kind/question" --description "Further information is requested"
|
$ tea label edit "kind/question" --description "Further information is requested"
|
||||||
$ tea label edit old-stuff --archived`, Annotations: map[string]string{"tea.arguments": labelArguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
$ tea label edit old-stuff --archived`, Annotations: map[string]string{"tea.arguments": labelArguments}, Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
opts, err := fields.options(cmd)
|
opts, err := fields.options(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if opts == (services.LabelOptions{}) {
|
prompt, err := a.canPrompt()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
form, hasForm := a.deps.Prompter.(terminal.LabelForm)
|
||||||
|
if opts == (services.LabelOptions{}) && (!prompt || !hasForm) {
|
||||||
return errNothingToEdit
|
return errNothingToEdit
|
||||||
}
|
}
|
||||||
s, err := scope(cmd)
|
s, err := scope(cmd)
|
||||||
@@ -258,6 +263,16 @@ $ tea label edit old-stuff --archived`, Annotations: map[string]string{"tea.argu
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if opts == (services.LabelOptions{}) {
|
||||||
|
current := found[0]
|
||||||
|
values, err := form.EditLabel(cmd.Context(), "Editing "+labelName(s, current), terminal.LabelFormValues{
|
||||||
|
Name: current.Name, Color: current.Color, Description: current.Description, Exclusive: current.Exclusive, Archived: current.Archived,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts = services.LabelOptions{Name: &values.Name, Color: &values.Color, Description: &values.Description, Exclusive: &values.Exclusive, Archived: &values.Archived}
|
||||||
|
}
|
||||||
item, err := s.Edit(cmd.Context(), found[0].ID, opts)
|
item, err := s.Edit(cmd.Context(), found[0].ID, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import (
|
|||||||
// of changing them.
|
// of changing them.
|
||||||
func (a *application) milestoneStateCommand(verb string, service milestoneService) *cobra.Command {
|
func (a *application) milestoneStateCommand(verb string, service milestoneService) *cobra.Command {
|
||||||
var byName bool
|
var byName bool
|
||||||
state, past := "closed", "Closed"
|
state, past, short := "closed", "Closed", "Close milestones"
|
||||||
if verb == "reopen" {
|
if verb == "reopen" {
|
||||||
state, past = "open", "Reopened"
|
state, past, short = "open", "Reopened", "Reopen milestones"
|
||||||
}
|
}
|
||||||
cmd := &cobra.Command{Use: verb + " <milestone>...", Short: titleCase(verb) + " milestones", Example: fmt.Sprintf(`$ tea milestone %[1]s v1.0
|
cmd := &cobra.Command{Use: verb + " <milestone>...", Short: short, Example: fmt.Sprintf(`$ tea milestone %[1]s v1.0
|
||||||
$ tea milestone %[1]s v1.0 v1.1`, verb), Annotations: map[string]string{"tea.arguments": milestoneArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
$ tea milestone %[1]s v1.0 v1.1`, verb), Annotations: map[string]string{"tea.arguments": milestoneArguments}, Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if slices.ContainsFunc(args, func(arg string) bool { return strings.TrimSpace(arg) == "" }) {
|
if slices.ContainsFunc(args, func(arg string) bool { return strings.TrimSpace(arg) == "" }) {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
|
return clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"charm.land/huh/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LabelFormValues holds interactively edited label fields.
|
||||||
|
type LabelFormValues struct {
|
||||||
|
Name, Color, Description string
|
||||||
|
Exclusive, Archived bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabelForm collects interactive label input.
|
||||||
|
type LabelForm interface {
|
||||||
|
// EditLabel asks for the fields of a label, starting from current, under heading, such as
|
||||||
|
// `Editing label "bug" in alice/x`.
|
||||||
|
EditLabel(ctx context.Context, heading string, current LabelFormValues) (LabelFormValues, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hexColor = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`)
|
||||||
|
|
||||||
|
// EditLabel runs the interactive label form on stderr.
|
||||||
|
func (p *FormPrompter) EditLabel(ctx context.Context, heading string, current LabelFormValues) (LabelFormValues, error) {
|
||||||
|
values := current
|
||||||
|
if err := p.runForm(ctx, huh.NewForm(huh.NewGroup(
|
||||||
|
huh.NewInput().Title("Label name (required):").Value(&values.Name).Validate(huh.ValidateNotEmpty()),
|
||||||
|
huh.NewInput().Title("Color (hex, e.g. d73a4a):").Value(&values.Color).Validate(func(s string) error {
|
||||||
|
if !hexColor.MatchString(strings.TrimSpace(s)) {
|
||||||
|
return errors.New("use six hexadecimal digits")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
huh.NewInput().Title("Description (optional):").Value(&values.Description),
|
||||||
|
huh.NewConfirm().Title("Exclusive (one label of its scope per issue):").Value(&values.Exclusive),
|
||||||
|
huh.NewConfirm().Title("Archived:").Value(&values.Archived),
|
||||||
|
).Title(heading))); err != nil {
|
||||||
|
return LabelFormValues{}, err
|
||||||
|
}
|
||||||
|
values.Name, values.Color = strings.TrimSpace(values.Name), strings.TrimPrefix(strings.TrimSpace(values.Color), "#")
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package terminal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLabelFormStartsFromCurrentValues(t *testing.T) {
|
||||||
|
p, _ := testPrompter(strings.NewReader("\r\r\r\r\r"))
|
||||||
|
current := LabelFormValues{Name: "bug", Color: "#EE0701", Description: "Broken", Exclusive: true}
|
||||||
|
values, err := p.EditLabel(context.Background(), `Editing label "bug" in alice/x`, current)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, LabelFormValues{Name: "bug", Color: "EE0701", Description: "Broken", Exclusive: true}, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabelFormMarksNameRequired(t *testing.T) {
|
||||||
|
p, rendered := accessiblePrompter(t, "bug\nee0701\n\nn\nn\n")
|
||||||
|
values, err := p.EditLabel(context.Background(), "", LabelFormValues{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, rendered.String(), "Label name (required):")
|
||||||
|
require.Equal(t, "bug", values.Name)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user