mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
issue list renders through the shared table: a context line, ID TITLE LABELS [STATE] [KIND] AUTHOR UPDATED with relative times on a terminal, the ID colored by state and linked, and a stable tab-separated column set when piped; empty results say so on stderr. It gains -s -L -l -a -A -m -S -w, @me, comma-separated label and milestone filters, and --web. issue view, edit, close and reopen accept #N, OWNER/REPO#N and issue URLs. Mutations report "✓ Created/Edited/Closed issue alice/x#5 (title)" on a terminal; issue create prints the new issue's URL on stdout (also as issue new), and close and reopen skip issues already in that state with a ! line and take -c/--comment. issue create and edit gain the gh short flags and singular --add-label, --remove-label, --add-assignee and --remove-assignee (the plural names still work), and create gains --web. The interactive create form asks for the title and body, then "What's next?" (submit, add metadata or cancel), without a confirmation after it; the edit form names the issue it edits. Every issue command has help text with examples and completes its flags and issue numbers. Signed-off-by: bircni <bircni@icloud.com>
92 lines
3.2 KiB
Go
92 lines
3.2 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"charm.land/huh/v2"
|
|
)
|
|
|
|
// IssueEditFormChoices supplies current issue values and selectable options for edit.
|
|
// Current values missing from the options stay selectable.
|
|
type IssueEditFormChoices struct {
|
|
// Heading names the edited issue above the form, such as "Editing alice/x#2 (Fix login)".
|
|
Heading string
|
|
Title, Body string
|
|
Assignees []string
|
|
Milestone *IssueMilestoneChoice
|
|
Labels []IssueLabelChoice
|
|
Deadline *time.Time
|
|
CanPush bool
|
|
AvailableAssignees []string
|
|
Milestones []IssueMilestoneChoice
|
|
AvailableLabels []IssueLabelChoice
|
|
EditBody BodyEditor
|
|
}
|
|
|
|
// IssueEditFormValues holds interactively edited issue fields. Deadline is set
|
|
// only when the due date changed.
|
|
type IssueEditFormValues struct {
|
|
Title, Body string
|
|
Assignees []string
|
|
Milestone int64
|
|
ClearMilestone bool
|
|
Labels []int64
|
|
Deadline *time.Time
|
|
ClearDeadline bool
|
|
Draft Draft
|
|
}
|
|
|
|
// IssueEditForm collects interactive issue edit input.
|
|
type IssueEditForm interface {
|
|
EditIssue(context.Context, IssueEditFormChoices) (IssueEditFormValues, error)
|
|
}
|
|
|
|
// EditIssue runs the interactive issue edit form on stderr, seeded with the current values.
|
|
func (p *FormPrompter) EditIssue(ctx context.Context, choices IssueEditFormChoices) (IssueEditFormValues, error) {
|
|
deadline := ""
|
|
if choices.Deadline != nil && !choices.Deadline.IsZero() {
|
|
// Keep the server's offset: converting to UTC can move the date by a day.
|
|
deadline = choices.Deadline.Format(time.DateOnly)
|
|
}
|
|
fields := issueFields{title: choices.Title, body: choices.Body, deadline: deadline, assignees: choices.Assignees}
|
|
milestones := choices.Milestones
|
|
if choices.Milestone != nil {
|
|
fields.milestone, milestones = choices.Milestone.ID, withCurrent(milestones, *choices.Milestone)
|
|
}
|
|
for _, label := range choices.Labels {
|
|
fields.labels = append(fields.labels, label.ID)
|
|
}
|
|
groups := fields.groups(choices.Heading, choices.EditBody, choices.CanPush, withCurrent(choices.AvailableAssignees, choices.Assignees...), milestones, withCurrent(choices.AvailableLabels, choices.Labels...))
|
|
if err := p.runForm(ctx, huh.NewForm(groups...)); err != nil {
|
|
return IssueEditFormValues{}, err
|
|
}
|
|
values := IssueEditFormValues{Title: strings.TrimSpace(fields.title), Assignees: fields.assignees, Milestone: fields.milestone, Labels: fields.labels}
|
|
values.ClearMilestone = fields.milestone == 0 && choices.Milestone != nil
|
|
var err error
|
|
if values.Body, values.Draft, err = editBody(ctx, choices.EditBody, fields.body); err != nil {
|
|
return values, err
|
|
}
|
|
if strings.TrimSpace(fields.deadline) == deadline {
|
|
return values, nil
|
|
}
|
|
values.Deadline, err = parseDueDate(fields.deadline)
|
|
values.ClearDeadline = values.Deadline == nil && deadline != ""
|
|
return values, err
|
|
}
|
|
|
|
// withCurrent appends current values missing from options; selects drop values they do not offer.
|
|
func withCurrent[T comparable](options []T, current ...T) []T {
|
|
for _, value := range current {
|
|
if !slices.Contains(options, value) {
|
|
options = append(slices.Clip(options), value)
|
|
}
|
|
}
|
|
return options
|
|
}
|