kanban

Kanban client in Gio
Log | Files | Refs | README | LICENSE

commit c2dca132470300c0b68dcf1a2e365d767918d9d5
parent 8651dd584f14b0c98fb4d4c844825b638f13e05d
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Fri, 19 Feb 2021 14:34:33 +0800

fix: create/update tickets based on ID comparison

Diffstat:
Mcmd/kanban/ui.go | 21++++++++++++++-------
Mcmd/kanban/widgets.go | 15+++++++++------
Mgo.mod | 1+
Mgo.sum | 18++----------------
Mkanban.go | 48++++++++++++++++++++++++++++++++++++++----------
5 files changed, 64 insertions(+), 39 deletions(-)

diff --git a/cmd/kanban/ui.go b/cmd/kanban/ui.go @@ -22,6 +22,7 @@ import ( "git.sr.ht/~jackmordaunt/kanban/cmd/kanban/util" "git.sr.ht/~jackmordaunt/kanban/icons" "git.sr.ht/~jackmordaunt/kanban/storage" + "github.com/google/uuid" ) type ( @@ -220,7 +221,7 @@ func (ui *UI) Update(gtx C) { ui.Project.RegressTicket(t.Ticket) } if t.EditButton.Clicked() { - ui.EditTicket(&t.Ticket) + ui.EditTicket(t.Ticket) } if t.DeleteButton.Clicked() { ui.DeleteTicket(t.Ticket) @@ -230,9 +231,16 @@ func (ui *UI) Update(gtx C) { } } if ui.TicketForm.SubmitBtn.Clicked() { - // @todo handle create/update ambiguity. - _ = ui.TicketForm.Submit() - ui.Project.AssignTicket(ui.TicketForm.Stage, *ui.TicketForm.Ticket) + t := ui.TicketForm.Submit() + if t.ID == uuid.Nil { + if err := ui.Project.AssignTicket(ui.TicketForm.Stage, t); err != nil { + log.Printf("assigning ticket: %v", err) + } + } else { + if err := ui.Project.UpdateTicket(t); err != nil { + log.Printf("updating ticket: %v", err) + } + } ui.Clear() } if ui.TicketForm.CancelBtn.Clicked() { @@ -246,7 +254,7 @@ func (ui *UI) Update(gtx C) { ui.Clear() } if ui.TicketDetails.Edit.Clicked() { - ui.EditTicket(&ui.TicketDetails.Ticket) + ui.EditTicket(ui.TicketDetails.Ticket) } if ui.TicketDetails.Cancel.Clicked() { ui.Clear() @@ -459,7 +467,7 @@ func (ui *UI) InspectTicket(t kanban.Ticket) { } // EditTicket opens the ticket form for editing ticket data. -func (ui *UI) EditTicket(t *kanban.Ticket) { +func (ui *UI) EditTicket(t kanban.Ticket) { ui.TicketForm.Set(t) ui.Modal = func(gtx C) D { return control.Card{ @@ -472,7 +480,6 @@ func (ui *UI) EditTicket(t *kanban.Ticket) { // AddTicket opens the ticket form for creating ticket data. func (ui *UI) AddTicket(stage string) { - ui.TicketForm.Set(&kanban.Ticket{}) ui.Modal = func(gtx C) D { return control.Card{ Title: "Add Ticket", diff --git a/cmd/kanban/widgets.go b/cmd/kanban/widgets.go @@ -21,7 +21,7 @@ import ( // // @Todo use form pattern from avisha. type TicketForm struct { - *kanban.Ticket + kanban.Ticket Stage string Title component.TextField Summary component.TextField @@ -30,21 +30,24 @@ type TicketForm struct { CancelBtn widget.Clickable } -func (f *TicketForm) Set(t *kanban.Ticket) { +func (f *TicketForm) Set(t kanban.Ticket) { f.Ticket = t f.Title.SetText(t.Title) f.Summary.SetText(t.Summary) f.Details.SetText(t.Details) } -// Submit validates inputs and writes to the ticket. -func (f TicketForm) Submit() error { - *f.Ticket = kanban.Ticket{ +// Submit uses form data to create a Ticket. +func (f TicketForm) Submit() kanban.Ticket { + defer func() { + f.Ticket = kanban.Ticket{} + }() + return kanban.Ticket{ + ID: f.ID, Title: f.Title.Text(), Summary: f.Summary.Text(), Details: f.Details.Text(), } - return nil } func (form *TicketForm) Layout(gtx C, th *material.Theme, stage string) D { diff --git a/go.mod b/go.mod @@ -6,6 +6,7 @@ require ( gioui.org v0.0.0-20210210205757-7286b075e2fb gioui.org/x v0.0.0-20210120222453-b55819bc712b github.com/asdine/storm/v3 v3.2.1 + github.com/google/uuid v1.2.0 github.com/spf13/pflag v1.0.5 go.etcd.io/bbolt v1.3.5 // indirect golang.org/x/exp v0.0.0-20210212053707-62dc52270d37 diff --git a/go.sum b/go.sum @@ -1,8 +1,6 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210116085804-99bfa6a33cdf/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= -gioui.org v0.0.0-20210127212131-b698c8ed8229 h1:K60CRdV3HIRwujzfukYpS8DtcwhKljeaTa7BGjGNkmA= -gioui.org v0.0.0-20210127212131-b698c8ed8229/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= gioui.org v0.0.0-20210210205757-7286b075e2fb h1:UKF3b0qSQED+HO1weY+H4GpYJALVn3suJxkavMgNDVQ= gioui.org v0.0.0-20210210205757-7286b075e2fb/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B2LGoT8JtFgvmYmRzTw= gioui.org/x v0.0.0-20210120222453-b55819bc712b h1:C7/1fHOBkcypkgWopz8Mf+eDJggTWQWlL315KjhDVDQ= @@ -12,9 +10,6 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM= github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= -github.com/asdine/storm v1.1.0 h1:lwDLqMMPhokfYk8EuU1RRHTi54T68EI+QnCqK5t4TCM= -github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q= -github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ= github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac= github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -26,6 +21,8 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -39,7 +36,6 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -48,18 +44,13 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 h1:n9HxLrNxWWtEb1cA950nuEEj3QnKbtsCJ6KjcgisNUs= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= -golang.org/x/exp v0.0.0-20210126221216-84987778548c h1:sWZb7hc7UoMhB5/VYk5+nsHuiHq8J5l0osfBYs9C3gw= -golang.org/x/exp v0.0.0-20210126221216-84987778548c/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= golang.org/x/exp v0.0.0-20210212053707-62dc52270d37 h1:8LMx3JFCswBZrnLWtxzpogDG5g1Hb7KWy/16Msz0hQk= golang.org/x/exp v0.0.0-20210212053707-62dc52270d37/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -81,17 +72,12 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9 h1:1/DFK4b7JH8DmkqhUk48onnSfrPzImPoVxuomtbT2nk= golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/kanban.go b/kanban.go @@ -34,6 +34,8 @@ package kanban import ( "fmt" "time" + + "github.com/google/uuid" ) // Project is a context for a given set of tickets. @@ -61,8 +63,19 @@ func (p *Project) MoveStage(name string, dir Direction) bool { } // AssignTicket assigns a ticket to the given stage. -func (p *Project) AssignTicket(stage string, ticket Ticket) { - p.Stages.Find(stage).Assign(ticket) +func (p *Project) AssignTicket(stage string, ticket Ticket) error { + return p.Stages.Find(stage).Assign(ticket) +} + +// Update an existing ticket. +// It is an error to attempt to update a ticket that does not exist. +func (p *Project) UpdateTicket(ticket Ticket) error { + for _, s := range p.Stages { + if s.Update(ticket) { + return nil + } + } + return fmt.Errorf("ticket does not exist: %v", ticket) } // ProgressTicket moves a ticket to the "next" stage. @@ -70,7 +83,7 @@ func (p *Project) ProgressTicket(ticket Ticket) { for ii, s := range p.Stages { if s.Contains(ticket) { if ii < len(p.Stages)-1 { - p.Stages[ii+1].Assign(p.Stages[ii].Take(ticket)) + _ = p.Stages[ii+1].Assign(p.Stages[ii].Take(ticket)) } break } @@ -82,7 +95,7 @@ func (p *Project) RegressTicket(ticket Ticket) { for ii, s := range p.Stages { if s.Contains(ticket) { if ii > 0 { - p.Stages[ii-1].Assign(p.Stages[ii].Take(ticket)) + _ = p.Stages[ii-1].Assign(p.Stages[ii].Take(ticket)) } break } @@ -126,14 +139,16 @@ type Stage struct { Tickets []Ticket } -// Assign appends a ticket id to the stage. -func (s *Stage) Assign(ticket Ticket) { - for _, t := range s.Tickets { - if t == ticket { - return - } +// Assign appends a ticket to the stage with a unique ID. +// Existing tickets will be duplicated, butwith different IDs. +func (s *Stage) Assign(ticket Ticket) error { + id, err := uuid.NewUUID() + if err != nil { + return fmt.Errorf("generating ID: %v", err) } + ticket.ID = id s.Tickets = append(s.Tickets, ticket) + return nil } // UnAssign removes a ticket id from the stage. @@ -199,8 +214,21 @@ func (s *Stage) Contains(ticket Ticket) bool { return false } +// Update a ticket, returning a bool to indicate success. +// False means ticket does not exist and therefore nothing was updated. +func (s *Stage) Update(ticket Ticket) bool { + for ii, t := range s.Tickets { + if t.ID == ticket.ID { + s.Tickets[ii] = ticket + return true + } + } + return false +} + // Ticket in a stage. type Ticket struct { + ID uuid.UUID // Title of the ticket. Title string // Summary contains short and concise overview of the ticket.