Allow moving tickets between projects with P.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"task/internal/command"
|
||||
"task/internal/ticket"
|
||||
)
|
||||
|
||||
func TestMoveTicketUndo(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store := ticket.NewJSONStore(dir)
|
||||
timeStore := ticket.NewJSONTimeStore(dir)
|
||||
h := command.NewHistory()
|
||||
|
||||
created, err := store.Create(ticket.Ticket{
|
||||
Title: "Misfiled", ProjectID: "inbox", Status: ticket.StatusTodo,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := timeStore.Create(ticket.TimeEntry{
|
||||
TicketID: created.ID, ProjectID: "inbox", Duration: 15 * time.Minute, Source: ticket.SourceManual,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
after := created
|
||||
after.ProjectID = "client"
|
||||
if err := h.Do(&command.MoveTicket{
|
||||
Store: store, TimeStore: timeStore, Before: created, After: after,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list, _ := store.List("client")
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("client len=%d", len(list))
|
||||
}
|
||||
entries, _ := timeStore.ListByTicket(created.ID)
|
||||
if len(entries) != 1 || entries[0].ProjectID != "client" {
|
||||
t.Fatalf("time entries: %+v", entries)
|
||||
}
|
||||
|
||||
if err := h.Undo(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
list, _ = store.List("inbox")
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("inbox after undo len=%d", len(list))
|
||||
}
|
||||
entries, _ = timeStore.ListByTicket(created.ID)
|
||||
if len(entries) != 1 || entries[0].ProjectID != "inbox" {
|
||||
t.Fatalf("time after undo: %+v", entries)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ const (
|
||||
typeAddTime = "add_time"
|
||||
typeRemoveTime = "remove_time"
|
||||
typeUpdateTime = "update_time"
|
||||
typeMove = "move"
|
||||
)
|
||||
|
||||
// HistoryPath returns DataDir/history.json.
|
||||
@@ -73,6 +74,9 @@ func encodeCmd(cmd Command) (persistedCmd, error) {
|
||||
case *UpdateTimeEntry:
|
||||
b, a := c.Before, c.After
|
||||
return persistedCmd{Type: typeUpdateTime, TimeBefore: &b, TimeAfter: &a}, nil
|
||||
case *MoveTicket:
|
||||
b, a := c.Before, c.After
|
||||
return persistedCmd{Type: typeMove, Before: &b, After: &a}, nil
|
||||
default:
|
||||
return persistedCmd{}, fmt.Errorf("unsupported command type %T", cmd)
|
||||
}
|
||||
@@ -119,6 +123,11 @@ func decodeCmd(rec persistedCmd, store ticket.Store, timeStore ticket.TimeStore)
|
||||
return nil, fmt.Errorf("update_time: time store not configured")
|
||||
}
|
||||
return &UpdateTimeEntry{Store: timeStore, Before: *rec.TimeBefore, After: *rec.TimeAfter}, nil
|
||||
case typeMove:
|
||||
if rec.Before == nil || rec.After == nil {
|
||||
return nil, fmt.Errorf("move record missing before/after")
|
||||
}
|
||||
return &MoveTicket{Store: store, TimeStore: timeStore, Before: *rec.Before, After: *rec.After}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown command type %q", rec.Type)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
// CreateTicket creates a ticket; Undo deletes it.
|
||||
type CreateTicket struct {
|
||||
Store ticket.Store
|
||||
Ticket ticket.Ticket
|
||||
Store ticket.Store
|
||||
Ticket ticket.Ticket
|
||||
created ticket.Ticket
|
||||
}
|
||||
|
||||
@@ -79,3 +79,43 @@ func (c *UpdateTicket) Undo(ctx context.Context) error {
|
||||
_ = ctx
|
||||
return c.Store.Update(c.Before)
|
||||
}
|
||||
|
||||
// MoveTicket relocates a ticket to another project; Undo moves it back.
|
||||
type MoveTicket struct {
|
||||
Store ticket.Store
|
||||
TimeStore ticket.TimeStore
|
||||
Before ticket.Ticket
|
||||
After ticket.Ticket // ProjectID is the destination; other fields from Before
|
||||
}
|
||||
|
||||
func (c *MoveTicket) Name() string { return "move ticket" }
|
||||
|
||||
func (c *MoveTicket) Execute(ctx context.Context) error {
|
||||
_ = ctx
|
||||
moved, err := c.Store.Move(c.Before.ID, c.After.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.After = moved
|
||||
if c.TimeStore != nil {
|
||||
if err := c.TimeStore.RetargetTicket(moved.ID, moved.ProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MoveTicket) Undo(ctx context.Context) error {
|
||||
_ = ctx
|
||||
moved, err := c.Store.Move(c.After.ID, c.Before.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Before = moved
|
||||
if c.TimeStore != nil {
|
||||
if err := c.TimeStore.RetargetTicket(moved.ID, moved.ProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user