commit 31a605c80b8e0605761e810f246f2a9f7d6bd61f
parent a41d4f9531907e42cc088ba85bbb1a13d150c89c
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Wed, 4 Nov 2020 16:28:14 +0800
feat: use database for persitant storage
Diffstat:
| M | cmd/kanban/main.go | | | 60 | +++++++++++++++++++++++++++++++++++++++--------------------- |
| M | go.mod | | | 1 | + |
| M | go.sum | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
| M | kanban.go | | | 161 | ++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- |
4 files changed, 176 insertions(+), 84 deletions(-)
diff --git a/cmd/kanban/main.go b/cmd/kanban/main.go
@@ -6,9 +6,12 @@ import (
"image/color"
"log"
"os"
+ "path/filepath"
+ "strconv"
"unsafe"
"git.sr.ht/~jackmordaunt/kanban"
+ "github.com/asdine/storm/v3"
"gioui.org/f32"
"gioui.org/font/gofont"
@@ -27,26 +30,41 @@ import (
)
func main() {
- w := app.NewWindow(app.Title("Kanban"))
- th := material.NewTheme(gofont.Collection())
+ db, err := func() (*storm.DB, error) {
+ path := filepath.Join(os.TempDir(), "kanban.db")
+ fmt.Printf("%s\n", path)
+ var init = false
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ init = true
+ }
+ db, err := storm.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("opening data file: %w", err)
+ }
+ if err := db.Init(&kanban.Stage{}); err != nil {
+ return nil, err
+ }
+ if err := db.ReIndex(&kanban.Stage{}); err != nil {
+ return nil, err
+ }
+ if init {
+ for ii, stage := range []string{"Todo", "In Progress", "Testing", "Done"} {
+ if err := db.Save(&kanban.Stage{ID: ii + 1, Name: stage}); err != nil {
+ return nil, fmt.Errorf("creating default stages: %w", err)
+ }
+ }
+ }
+ return db, nil
+ }()
+ if err != nil {
+ log.Fatalf("error: initializing data: %v", err)
+ }
+ defer db.Close()
ui := &UI{
- Window: w,
- Th: th,
+ Window: app.NewWindow(app.Title("Kanban")),
+ Th: material.NewTheme(gofont.Collection()),
Kanban: &kanban.Kanban{
- Stages: []kanban.Stage{
- {
- Name: "Todo",
- },
- {
- Name: "In Progress",
- },
- {
- Name: "Testing",
- },
- {
- Name: "Done",
- },
- },
+ Store: db,
},
// TODO: render dynamically from storage.
Panels: []Panel{
@@ -143,11 +161,11 @@ func (ui *UI) Update(gtx C) {
if ui.TicketForm.Submit.Clicked() {
ticket, err := ui.TicketForm.Validate()
if err != nil {
- fmt.Printf("error: %s", err)
+ fmt.Printf("error: %s\n", err)
return
}
if err := ui.Kanban.Assign(ui.TicketForm.Stage, ticket); err != nil {
- fmt.Printf("error: %s", err)
+ fmt.Printf("error: assigning ticket: %s\n", err)
return
}
ui.TicketForm = TicketForm{}
@@ -171,7 +189,7 @@ func (ui *UI) Layout(gtx C) D {
stage, _ := ui.Kanban.Stage(panel.Label)
var cards = make([]layout.ListElement, len(stage.Tickets))
for ii, ticket := range stage.Tickets {
- id := ticket.ID.String()
+ id := strconv.Itoa(ticket.ID)
cards[ii] = func(gtx C, ii int) D {
t := (*Ticket)(ui.TicketStates.Next(id, unsafe.Pointer(&Ticket{})))
t.Ticket = stage.Tickets[ii]
diff --git a/go.mod b/go.mod
@@ -5,5 +5,6 @@ go 1.15
require (
gioui.org v0.0.0-20201101161300-a55302a49867
git.sr.ht/~whereswaldon/materials v0.0.0-20201101220027-a518d42fd730
+ github.com/asdine/storm/v3 v3.2.1
golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925
)
diff --git a/go.sum b/go.sum
@@ -5,8 +5,34 @@ gioui.org v0.0.0-20201101161300-a55302a49867/go.mod h1:Y+uS7hHMvku1Q+ooaoq6fYD5B
git.sr.ht/~whereswaldon/materials v0.0.0-20201101220027-a518d42fd730 h1:pAOILGMbZpo3BzS0NXX41bCd4yhIzlG00fkXE1ItN/E=
git.sr.ht/~whereswaldon/materials v0.0.0-20201101220027-a518d42fd730/go.mod h1:kLyZfWzOWp15bbVIiZXdNHBg+lluQq66KnB67d70O0U=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+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/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=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+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/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=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -26,7 +52,10 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191105084925-a882066a44e0 h1:QPlSTtPE2k6PZPasQUbzuK3p9JbS+vMXYVto8g/yrsg=
+golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -34,10 +63,19 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
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/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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/kanban.go b/kanban.go
@@ -2,24 +2,33 @@ package kanban
import (
"fmt"
- "strconv"
+
+ "github.com/asdine/storm/v3"
)
// Kanban manipulates the model.
type Kanban struct {
- Stages []Stage
+ Store *storm.DB
}
// ListStages returns a list of stages.
func (k Kanban) ListStages() ([]Stage, error) {
- return k.Stages, nil
+ var stages []Stage
+ if err := k.Store.AllByIndex("ID", &stages); err != nil {
+ return nil, fmt.Errorf("collecting stage: %w", err)
+ }
+ return stages, nil
}
// NextStage returns the stage that follows the specified one.
func (k Kanban) NextStage(current string) (string, error) {
- for ii, stage := range k.Stages {
- if stage.Name == current && ii < len(k.Stages)-1 {
- return k.Stages[ii+1].Name, nil
+ stages, err := k.ListStages()
+ if err != nil {
+ return "", err
+ }
+ for ii, stage := range stages {
+ if stage.Name == current && ii < len(stages)-1 {
+ return stages[ii+1].Name, nil
}
}
return current, fmt.Errorf("no more stages after: %q", current)
@@ -27,9 +36,13 @@ func (k Kanban) NextStage(current string) (string, error) {
// NextStage returns the stage that preceeds the specified one.
func (k Kanban) PreviousStage(current string) (string, error) {
- for ii, stage := range k.Stages {
+ stages, err := k.ListStages()
+ if err != nil {
+ return "", err
+ }
+ for ii, stage := range stages {
if stage.Name == current && ii > 0 {
- return k.Stages[ii-1].Name, nil
+ return stages[ii-1].Name, nil
}
}
return current, fmt.Errorf("no more stages before: %q", current)
@@ -38,51 +51,64 @@ func (k Kanban) PreviousStage(current string) (string, error) {
// Stage returns a stage by the given name.
// Creates an empty stage if it doesn't exist.
func (k *Kanban) Stage(name string) (Stage, error) {
- for _, stage := range k.Stages {
+ stages, err := k.ListStages()
+ if err != nil {
+ return Stage{}, err
+ }
+ for _, stage := range stages {
if stage.Name == name {
return stage, nil
}
}
- k.Stages = append(k.Stages, Stage{Name: name})
- return k.Stages[len(k.Stages)-1], nil
+ id, err := k.nextID()
+ if err != nil {
+ return Stage{}, err
+ }
+ stage := Stage{ID: id, Name: name}
+ if err := k.Store.Save(&stage); err != nil {
+ return Stage{}, err
+ }
+ return stage, nil
}
-func (k *Kanban) Move(stage string, ticket ID) error {
- var found *Ticket
- for ii := range k.Stages {
- stage := k.Stages[ii]
- for kk := range stage.Tickets {
- t := stage.Tickets[kk]
+func (k *Kanban) Move(stage string, ticket int) error {
+ stages, err := k.ListStages()
+ if err != nil {
+ return err
+ }
+ for ii := range stages {
+ s := stages[ii]
+ for kk := range s.Tickets {
+ t := s.Tickets[kk]
if t.ID == ticket {
- found = &t
if err := k.Delete(ticket); err != nil {
- return err
+ return fmt.Errorf("deleting: %w", err)
}
- break
+ return k.Assign(stage, t)
}
}
}
- if found == nil {
- return fmt.Errorf("ticket %q does not exist", ticket)
- }
- return k.Assign(stage, *found)
+ return fmt.Errorf("ticket %q does not exist", ticket)
}
// Progress a ticket to the next stage.
-func (k *Kanban) Progress(ticket ID) error {
+func (k *Kanban) Progress(ticket int) error {
stage, err := k.StageFor(ticket)
if err != nil {
- return err
+ return fmt.Errorf("finding stage: %w", err)
}
next, err := k.NextStage(stage)
if err != nil {
- return err
+ return fmt.Errorf("loading stage: %w", err)
}
- return k.Move(next, ticket)
+ if err := k.Move(next, ticket); err != nil {
+ return fmt.Errorf("moving ticket: %w", err)
+ }
+ return nil
}
// Regress a ticket to the previous stage.
-func (k *Kanban) Regress(ticket ID) error {
+func (k *Kanban) Regress(ticket int) error {
stage, err := k.StageFor(ticket)
if err != nil {
return err
@@ -94,8 +120,12 @@ func (k *Kanban) Regress(ticket ID) error {
return k.Move(next, ticket)
}
-func (k *Kanban) StageFor(ticket ID) (string, error) {
- for _, s := range k.Stages {
+func (k *Kanban) StageFor(ticket int) (string, error) {
+ stages, err := k.ListStages()
+ if err != nil {
+ return "", err
+ }
+ for _, s := range stages {
for _, t := range s.Tickets {
if t.ID == ticket {
return s.Name, nil
@@ -105,77 +135,82 @@ func (k *Kanban) StageFor(ticket ID) (string, error) {
return "", fmt.Errorf("ticket %q does not exist", ticket)
}
-func (k *Kanban) Assign(stage string, ticket Ticket) error {
- ticket.ID = k.nextID()
- for ii := range k.Stages {
- s := &k.Stages[ii]
- if s.Name == stage {
- s.Tickets = append(s.Tickets, ticket)
- return nil
- }
+func (k *Kanban) Assign(name string, ticket Ticket) error {
+ id, err := k.nextID()
+ if err != nil {
+ return fmt.Errorf("generating ID: %w", err)
}
- k.Stages = append(k.Stages, Stage{
- Name: stage,
- Tickets: []Ticket{ticket},
- })
- return nil
+ ticket.ID = id
+ stage, err := k.Stage(name)
+ if err != nil {
+ return fmt.Errorf("finding stage: %w", err)
+ }
+ stage.Tickets = append(stage.Tickets, ticket)
+ return k.Store.Update(&stage)
}
-func (k *Kanban) Delete(ticket ID) error {
- for kk := range k.Stages {
- s := &k.Stages[kk]
+func (k *Kanban) Delete(ticket int) error {
+ stages, err := k.ListStages()
+ if err != nil {
+ return err
+ }
+ for kk := range stages {
+ s := &stages[kk]
for ii := range s.Tickets {
if s.Tickets[ii].ID == ticket {
s.Tickets = append(s.Tickets[:ii], s.Tickets[ii+1:]...)
- return nil
+ return k.Store.Update(s)
}
}
}
- return nil
+ return fmt.Errorf("ticket %q does not exist", ticket)
}
func (k *Kanban) Update(ticket Ticket) error {
- for _, s := range k.Stages {
+ stages, err := k.ListStages()
+ if err != nil {
+ return err
+ }
+ for _, s := range stages {
for ii := range s.Tickets {
if s.Tickets[ii].ID == ticket.ID {
s.Tickets[ii] = ticket
- return nil
+ return k.Store.Update(&s)
}
}
}
- return nil
+ return fmt.Errorf("ticket %q does not exist", ticket)
}
-func (k *Kanban) nextID() ID {
+func (k *Kanban) nextID() (int, error) {
+ stages, err := k.ListStages()
+ if err != nil {
+ return -1, err
+ }
var max int
- for _, stage := range k.Stages {
+ for _, stage := range stages {
for _, t := range stage.Tickets {
if int(t.ID) > max {
max = int(t.ID)
}
}
}
- return ID(max + 1)
+ return max + 1, nil
}
-type ID int
-
// Stage in the kanban pipeline, can hold a number of tickets.
type Stage struct {
+ ID int `storm:"id,index"`
Name string
Tickets []Ticket
}
// Ticket in a stage.
type Ticket struct {
- ID ID
+ ID int
Title string
Category string
Summary string
Details string
- References []ID
-}
-
-func (id ID) String() string {
- return strconv.Itoa(int(id))
+ References []int
}