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:
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.