mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
feat(milestone): milestone tables, views, forms and one selection model
milestone view printed the list row, and milestone list hid the due date and progress. milestone list now shows ID TITLE [STATE] DUE PROGRESS, open milestones first and by due date; milestone view shows the state, due date, progress, description and the milestone's web page (also --web). Every milestone command addresses milestones by title or ID. Mutations report ✓ lines: milestone create also prints the new milestone's URL and takes the title as an argument; close and reopen skip milestones already in that state; delete names the milestone in its confirmation; milestone issue add and remove report one line such as "✓ Added #1, #2 to milestone "v1.0" in alice/x". milestone issue list uses the issue table. milestone edit without flags on a terminal opens the milestone form with the current values, and every milestone command completes milestone titles and has help text with examples. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
14 files changed
+606
-257
No files matched your search
@@ -6,6 +6,7 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
|
||||
type milestoneFormFunc func(context.Context) (terminal.MilestoneFormValues, error)
|
||||
|
||||
func (f milestoneFormFunc) CreateMilestone(ctx context.Context) (terminal.MilestoneFormValues, error) {
|
||||
func (f milestoneFormFunc) EditMilestone(ctx context.Context, _ string, _ terminal.MilestoneFormValues) (terminal.MilestoneFormValues, error) {
|
||||
return f(ctx)
|
||||
}
|
||||
|
||||
@@ -67,3 +68,96 @@ func TestMilestoneCreateState(t *testing.T) {
|
||||
require.Equal(t, "closed", payload["state"])
|
||||
require.JSONEq(t, `{"id":9,"state":"closed"}`, out.String())
|
||||
}
|
||||
|
||||
func TestMilestoneTablesViewsAndDoneLines(t *testing.T) {
|
||||
var changed []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/milestones":
|
||||
writeMilestones(w, r, `[{"id":5,"title":"later","state":"open"},{"id":4,"title":"v0.9","state":"closed","closed_issues":3},`+
|
||||
`{"id":3,"title":"v1.0","state":"open","description":"First release","due_on":"2026-12-31T00:00:00Z","open_issues":3,"closed_issues":1}]`)
|
||||
case "GET /api/v1/repos/alice/project/issues/1", "GET /api/v1/repos/alice/project/issues/2":
|
||||
_, _ = w.Write([]byte(`{"number":1,"milestone":{"id":4,"title":"v0.9"}}`))
|
||||
case "PATCH /api/v1/repos/alice/project/issues/1", "PATCH /api/v1/repos/alice/project/issues/2":
|
||||
changed = append(changed, r.Method+" "+r.URL.Path)
|
||||
_, _ = fmt.Fprintf(w, `{"number":%s}`, r.URL.Path[len(r.URL.Path)-1:])
|
||||
default:
|
||||
changed = append(changed, r.Method+" "+r.URL.Path)
|
||||
_, _ = w.Write([]byte(`{"id":3,"title":"v1.0","state":"closed"}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
deps, out, diagnostics := serverDeps(t, server.URL)
|
||||
deps.Now = func() time.Time { return time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC) }
|
||||
deps.Getenv = serverEnv(server.URL, map[string]string{"NO_COLOR": "1", "TEA_MARKDOWN": "raw", "TEA_HYPERLINKS": "never"})
|
||||
run := func(tty bool, args ...string) {
|
||||
t.Helper()
|
||||
out.Reset()
|
||||
diagnostics.Reset()
|
||||
deps.Streams.OutTTY = tty
|
||||
require.NoError(t, Execute(context.Background(), deps, append(args, "-R", "alice/project", "--no-input")))
|
||||
}
|
||||
run(true, "milestone", "list", "--state", "all")
|
||||
require.Equal(t, "Showing 3 of 3 milestones in alice/project\n\n"+
|
||||
"ID TITLE STATE DUE PROGRESS\n"+
|
||||
"3 v1.0 open in about 2 months 1/4 (25%)\n"+
|
||||
"5 later open 0/0\n"+
|
||||
"4 v0.9 closed 3/3 (100%)\n", out.String())
|
||||
run(false, "milestone", "list", "--state", "all")
|
||||
require.Equal(t, "3\tv1.0\topen\t2026-12-31T00:00:00Z\t1/4 (25%)\n5\tlater\topen\t\t0/0\n4\tv0.9\tclosed\t\t3/3 (100%)\n", out.String())
|
||||
run(true, "milestone", "view", "v1.0")
|
||||
require.Equal(t, "v1.0 #3\nOpen • due Dec 31, 2026 (in about 2 months) • 1 of 4 closed (25%)\n\nFirst release\n\n"+
|
||||
"View this milestone on Gitea: "+server.URL+"/alice/project/milestone/3\n", out.String())
|
||||
run(true, "milestone", "close", "v1.0", "v0.9")
|
||||
require.Equal(t, "✓ Closed milestone \"v1.0\" in alice/project\n! Milestone \"v0.9\" in alice/project is already closed\n", diagnostics.String())
|
||||
run(true, "milestone", "issue", "add", "v1.0", "1", "#2")
|
||||
require.Equal(t, "✓ Added #1, #2 to milestone \"v1.0\" in alice/project\n", diagnostics.String())
|
||||
run(true, "milestone", "issue", "remove", "v0.9", "1")
|
||||
require.Equal(t, "✓ Removed #1 from milestone \"v0.9\" in alice/project\n", diagnostics.String())
|
||||
run(false, "milestone", "create", "v2.0")
|
||||
require.Equal(t, server.URL+"/alice/project/milestone/3\n", out.String())
|
||||
require.Equal(t, []string{
|
||||
"GET /api/v1/repos/alice/project/milestones/3",
|
||||
"PATCH /api/v1/repos/alice/project/milestones/3",
|
||||
"PATCH /api/v1/repos/alice/project/issues/1",
|
||||
"PATCH /api/v1/repos/alice/project/issues/2",
|
||||
"PATCH /api/v1/repos/alice/project/issues/1",
|
||||
"POST /api/v1/repos/alice/project/milestones",
|
||||
}, changed)
|
||||
}
|
||||
|
||||
func TestMilestoneEditFormStartsFromTheMilestone(t *testing.T) {
|
||||
var edited map[string]any
|
||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method + " " + r.URL.Path {
|
||||
case "GET /api/v1/repos/alice/project/milestones":
|
||||
writeMilestones(w, r, `[{"id":3,"title":"v1.0","description":"First","due_on":"2026-12-31T00:00:00Z"}]`)
|
||||
case "PATCH /api/v1/repos/alice/project/milestones/3":
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&edited))
|
||||
_, _ = w.Write([]byte(`{"id":3,"title":"v1.0.0"}`))
|
||||
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
|
||||
var heading string
|
||||
var current terminal.MilestoneFormValues
|
||||
deps.MilestoneForm = milestoneFormEditor(func(h string, c terminal.MilestoneFormValues) terminal.MilestoneFormValues {
|
||||
heading, current = h, c
|
||||
return terminal.MilestoneFormValues{Title: "v1.0.0", Description: c.Description}
|
||||
})
|
||||
require.NoError(t, Execute(context.Background(), deps, []string{"milestone", "edit", "v1.0", "-R", "alice/project"}))
|
||||
require.Equal(t, "Editing milestone \"v1.0\" in alice/project", heading)
|
||||
require.Equal(t, "First", current.Description)
|
||||
require.Equal(t, "2026-12-31", current.Deadline.Format(time.DateOnly))
|
||||
require.Equal(t, "v1.0.0", edited["title"])
|
||||
require.Equal(t, "First", edited["description"])
|
||||
}
|
||||
|
||||
type milestoneFormEditor func(string, terminal.MilestoneFormValues) terminal.MilestoneFormValues
|
||||
|
||||
func (f milestoneFormEditor) EditMilestone(_ context.Context, heading string, current terminal.MilestoneFormValues) (terminal.MilestoneFormValues, error) {
|
||||
return f(heading, current), nil
|
||||
}
|
||||
Reference in new issue
Block a user