kanban

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

commit 40391070a55f0cc94dfce699f34739fdd0153822
parent dc2c80d4735d41cc7ba14a3c42a4576220d94c62
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Sat, 10 Apr 2021 16:27:19 +0800

fix: ensure project pointer points into project slice

Project pointer was pointing a to a loop variable which is a copy.

Not sure how to enforce this invariant across the code, since it's a pattern that the UI relies on.

Diffstat:
Mcmd/kanban/ui.go | 47++++++++++++++++++++++++-----------------------
Mcmd/kanban/widgets.go | 35++++++++++++++++++++++++++---------
Mstorage/bolt/bolt.go | 29++++++-----------------------
Mstorage/lazy/lazy.go | 16+++++-----------
Mstorage/mem/mem.go | 31++++++++++++-------------------
Mstorage/storage.go | 2--
6 files changed, 73 insertions(+), 87 deletions(-)

diff --git a/cmd/kanban/ui.go b/cmd/kanban/ui.go @@ -175,25 +175,27 @@ func (ui *UI) Update(gtx C) { } } if ui.ProjectForm.SubmitBtn.Clicked() { - // @cleanup - // Could we enforce this relationship with the api? - // Perhaps pass in the projects slice and have it append to it. - if err := ui.Storage.Create(kanban.Project{ - ID: uuid.New(), - Name: ui.ProjectForm.Name.Text(), - Stages: []kanban.Stage{ - {Name: "Todo"}, - {Name: "In Progress"}, - {Name: "Testing"}, - {Name: "Done"}, - }, - }); err != nil { - log.Printf("creating new project: %v", err) - } else { - if projects, err := ui.Storage.List(); err == nil { - ui.Projects = projects + if ui.ProjectForm.Mode() == ModeEdit { + ui.ProjectForm.Submit() + } + if ui.ProjectForm.Mode() == ModeCreate { + if err := ui.Storage.Create(kanban.Project{ + ID: uuid.New(), + Name: ui.ProjectForm.Name.Text(), + Stages: []kanban.Stage{ + {Name: "Todo"}, + {Name: "In Progress"}, + {Name: "Testing"}, + {Name: "Done"}, + }, + }); err != nil { + log.Printf("creating new project: %v", err) } else { - log.Printf("listing projects: %v", err) + if projects, err := ui.Storage.List(); err == nil { + ui.Projects = projects + } else { + log.Printf("listing projects: %v", err) + } } } ui.Clear() @@ -560,7 +562,7 @@ func (ui *UI) EditProject() { if ui.Project == nil { return } - // ui.ProjectForm.Edit(*ui.Project) + ui.ProjectForm.Edit(ui.Project) ui.ProjectForm.Name.Focus() ui.Modal = func(gtx C) D { return ui.ProjectForm.Layout(gtx, ui.Th) @@ -573,10 +575,9 @@ type Projects []kanban.Project // Find and return project by name. // Boolean indicates whether the project exists. func (plist Projects) Find(name string) (*kanban.Project, bool) { - for _, p := range plist { - p := p - if p.Name == name { - return &p, true + for ii := range plist { + if plist[ii].Name == name { + return &plist[ii], true } } return nil, false diff --git a/cmd/kanban/widgets.go b/cmd/kanban/widgets.go @@ -20,6 +20,16 @@ import ( "github.com/google/uuid" ) +// Mode signals the form mode. +// Typically a form is either dealing with an entity to be created, or +// manipulating an existing entity. +type Mode int + +const ( + ModeCreate Mode = iota + ModeEdit +) + // TicketForm renders the form for ticket information. // // @Todo use form pattern from avisha. @@ -99,30 +109,37 @@ func (f *TicketForm) Layout(gtx C, th *material.Theme, stage string) D { // ProjectForm renders a form for manipulating projects. type ProjectForm struct { - kanban.Project + *kanban.Project Name component.TextField SubmitBtn widget.Clickable CancelBtn widget.Clickable } // Edit the provided project. -func (f *ProjectForm) Edit(p kanban.Project) { +func (f *ProjectForm) Edit(p *kanban.Project) { f.Project = p f.Name.SetText(p.Name) } -func (f *ProjectForm) Submit() kanban.Project { - defer func() { - f.Project = kanban.Project{} - }() - return kanban.Project{ - Name: f.Name.Text(), +// Submit writes form data to the entity. +func (f *ProjectForm) Submit() { + f.Project.Name = f.Name.Text() +} + +func (f *ProjectForm) Mode() Mode { + if f.Project.ID != uuid.Nil { + return ModeEdit } + return ModeCreate } func (f *ProjectForm) Layout(gtx C, th *material.Theme) D { + var title = "Create a new Project" + if f.Project != nil { + title = "Edit Project" + } return control.Card{ - Title: "Create a new Project", + Title: title, Body: func(gtx C) D { return layout.Flex{ Axis: layout.Vertical, diff --git a/storage/bolt/bolt.go b/storage/bolt/bolt.go @@ -10,6 +10,12 @@ import ( "github.com/google/uuid" ) +var _ storage.Storer = (*Storer)(nil) + +type Storer struct { + *bolt.DB +} + type Bucket []byte func (b Bucket) String() string { @@ -20,12 +26,6 @@ var ( BucketProject Bucket = Bucket("Project") ) -var _ storage.Storer = (*Storer)(nil) - -type Storer struct { - *bolt.DB -} - func Open(path string) (*Storer, error) { db, err := bolt.Open(path, 0660, nil) if err != nil { @@ -85,23 +85,6 @@ func (db *Storer) Save(projects ...kanban.Project) error { }) } -func (db *Storer) Lookup(name string) (p kanban.Project, ok bool, err error) { - // @enhance: index by name - return p, ok, db.View(func(tx *bolt.Tx) error { - c := tx.Bucket(BucketProject).Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - if err := json.Unmarshal(v, &p); err != nil { - return fmt.Errorf("deserializing project: %w", err) - } - if p.Name == name { - ok = true - break - } - } - return nil - }) -} - func (db *Storer) Find(id uuid.UUID) (p kanban.Project, ok bool, err error) { key, err := id.MarshalBinary() if err != nil { diff --git a/storage/lazy/lazy.go b/storage/lazy/lazy.go @@ -49,7 +49,7 @@ func (s *Storer) Create(p kanban.Project) error { // Save a project. Only saves to disk if changed. func (s *Storer) Save(projects ...kanban.Project) error { for _, p := range projects { - old, ok, err := s.Cache.Lookup(p.Name) + old, ok, err := s.Cache.Find(p.ID) if err != nil { return err } @@ -60,18 +60,12 @@ func (s *Storer) Save(projects ...kanban.Project) error { if err := s.Disk.Save(p); err != nil { return fmt.Errorf("saving to disk: %v", err) } - return s.Refresh(p.Name) + return s.Refresh(p.ID) } } return nil } -// Load a project by name. -// Bool indicates whether a project exists for that name. -func (s *Storer) Lookup(name string) (kanban.Project, bool, error) { - return s.Cache.Lookup(name) -} - // Load a project by ID. // Bool indicates whether a project exists for that ID. func (s *Storer) Find(id uuid.UUID) (kanban.Project, bool, error) { @@ -84,13 +78,13 @@ func (s *Storer) List() ([]kanban.Project, error) { } // Refresh a project entity by loading from disk. -func (s *Storer) Refresh(name string) error { - p, ok, err := s.Disk.Lookup(name) +func (s *Storer) Refresh(id uuid.UUID) error { + p, ok, err := s.Disk.Find(id) if err != nil { return fmt.Errorf("loading from disk: %v", err) } if !ok { - return fmt.Errorf("project does not exist: %v", name) + return fmt.Errorf("project does not exist: %v", id) } return s.Cache.Save(p) } diff --git a/storage/mem/mem.go b/storage/mem/mem.go @@ -13,29 +13,29 @@ var _ storage.Storer = (*Storer)(nil) // Storer implements in-memory storage for Projects. type Storer struct { - Data map[string]kanban.Project - Order []string + Data map[uuid.UUID]kanban.Project + Order []uuid.UUID } func New() *Storer { return &Storer{ - Data: make(map[string]kanban.Project), + Data: make(map[uuid.UUID]kanban.Project), } } func (s *Storer) Create(p kanban.Project) error { - if _, ok := s.Data[p.Name]; ok { + if _, ok := s.Data[p.ID]; ok { return fmt.Errorf("project %q exists", p.Name) } - s.Data[p.Name] = p - s.Order = append(s.Order, p.Name) + s.Data[p.ID] = p + s.Order = append(s.Order, p.ID) return nil } func (s *Storer) Save(projects ...kanban.Project) error { for _, p := range projects { - if _, ok := s.Data[p.Name]; ok { - s.Data[p.Name] = p + if _, ok := s.Data[p.ID]; ok { + s.Data[p.ID] = p } else { return fmt.Errorf("project %q does not exist", p.Name) } @@ -43,17 +43,10 @@ func (s *Storer) Save(projects ...kanban.Project) error { return nil } -func (s *Storer) Lookup(name string) (kanban.Project, bool, error) { - if p, ok := s.Data[name]; ok { - return p, ok, nil - } - return kanban.Project{}, false, nil -} - func (s *Storer) Find(id uuid.UUID) (kanban.Project, bool, error) { for _, p := range s.Data { if p.ID == id { - return s.Data[p.Name], true, nil + return s.Data[p.ID], true, nil } } return kanban.Project{}, false, nil @@ -64,8 +57,8 @@ func (s *Storer) Count() (int, error) { } func (s *Storer) List() (list []kanban.Project, err error) { - for _, name := range s.Order { - if p, ok := s.Data[name]; ok { + for _, id := range s.Order { + if p, ok := s.Data[id]; ok { list = append(list, p) } } @@ -74,7 +67,7 @@ func (s *Storer) List() (list []kanban.Project, err error) { func (s *Storer) Load(projects []kanban.Project) error { for ii := range projects { - p := s.Data[projects[ii].Name] + p := s.Data[projects[ii].ID] projects[ii] = p } return nil diff --git a/storage/storage.go b/storage/storage.go @@ -17,8 +17,6 @@ type Storer interface { // Allows caller to allocate and control memory. // Avoids copying. Load([]kanban.Project) error - // Lookup a Project by name. - Lookup(name string) (kanban.Project, bool, error) // Find a Project by ID. Find(id uuid.UUID) (kanban.Project, bool, error) // List all existing Projects.