kanban

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

commit 601b0cee6481f3c9e40889df12540206f96645b6
parent f3af1138a54960c8c2519792b1e375fb0dbcee5d
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Sun,  8 Nov 2020 15:29:51 +0800

feat(ux): navigate tickets with arrow keys

Diffstat:
Mcmd/kanban/main.go | 112++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mkanban.go | 14++++++++++++++
2 files changed, 122 insertions(+), 4 deletions(-)

diff --git a/cmd/kanban/main.go b/cmd/kanban/main.go @@ -117,12 +117,20 @@ type UI struct { TicketForm TicketForm TicketDetails TicketDetails DeleteDialog DeleteDialog + FocusedTicket struct { + ID int + Index int + Stage int + } } func (ui *UI) Loop() error { - var ops op.Ops - for { - switch event := (<-ui.Events()).(type) { + var ( + ops op.Ops + events = ui.Events() + ) + for event := range events { + switch event := (event).(type) { case system.DestroyEvent: return event.Err case system.ClipboardEvent: @@ -134,6 +142,7 @@ func (ui *UI) Loop() error { event.Frame(gtx.Ops) } } + return nil } // TODO: investigate best way to handle errors. Some errors are for the user @@ -142,9 +151,44 @@ func (ui *UI) Loop() error { func (ui *UI) Update(gtx C) { for _, event := range gtx.Events(ui) { if k, ok := event.(key.Event); ok { - if k.Name == key.NameEscape { + switch k.Name { + case key.NameEscape: ui.Modal = nil ui.TicketForm = TicketForm{} + ui.FocusedTicket = struct { + ID int + Index int + Stage int + }{} + case key.NameEnter, key.NameReturn: + // TODO: query for a single Ticket by ID, quickly. + tickets, err := ui.Kanban.Tickets() + if err != nil { + fmt.Printf("error: %v", err) + break + } + for _, t := range tickets { + t := t + if t.ID == ui.FocusedTicket.ID { + ui.TicketDetails.Ticket = t + ui.Modal = func(gtx C) D { + return Card{ + Title: fmt.Sprintf("%q", t.Title), + }.Layout(gtx, ui.Th, func(gtx C) D { + return ui.TicketDetails.Layout(gtx, ui.Th) + }) + } + break + } + } + case key.NameDownArrow: + ui.Refocus(NextTicket) + case key.NameUpArrow: + ui.Refocus(PreviousTicket) + case key.NameRightArrow: + ui.Refocus(PreviousStage) + case key.NameLeftArrow: + ui.Refocus(NextStage) } } } @@ -273,6 +317,14 @@ func (ui *UI) Layout(gtx C) D { t := (*Ticket)(ui.TicketStates.New(id, unsafe.Pointer(&Ticket{}))) t.Ticket = stage.Tickets[ii] t.Stage = stage.Name + if ui.FocusedTicket.ID == t.ID { + return widget.Border{ + Color: color.RGBA{B: 200, A: 200}, + Width: unit.Dp(2), + }.Layout(gtx, func(gtx C) D { + return t.Layout(gtx, ui.Th) + }) + } return t.Layout(gtx, ui.Th) } } @@ -298,6 +350,58 @@ func (ui *UI) Layout(gtx C) D { ) } +type Direction uint8 + +const ( + NextTicket Direction = iota + PreviousTicket + NextStage + PreviousStage +) + +// Refocus to the ticket in the given direction. +// Allows movement between tickets and stages in sequential order. +func (ui *UI) Refocus(d Direction) { + stages, err := ui.Kanban.ListStages() + if err != nil { + fmt.Printf("error: querying stages: %v", err) + return + } + switch d { + case NextTicket: + ui.FocusedTicket.Index++ + if ui.FocusedTicket.Index > len(stages[ui.FocusedTicket.Stage].Tickets) { + ui.FocusedTicket.Index = 1 + ui.FocusedTicket.Stage++ + if ui.FocusedTicket.Stage > len(stages)-1 { + ui.FocusedTicket.Stage = 0 + } + } + case PreviousTicket: + ui.FocusedTicket.Index-- + if ui.FocusedTicket.Index < 1 { + ui.FocusedTicket.Stage-- + if ui.FocusedTicket.Stage < 0 { + ui.FocusedTicket.Stage = len(stages) - 1 + } + ui.FocusedTicket.Index = len(stages[ui.FocusedTicket.Stage].Tickets) + } + case PreviousStage: + ui.FocusedTicket.Index = 1 + ui.FocusedTicket.Stage++ + if ui.FocusedTicket.Stage > len(stages)-1 { + ui.FocusedTicket.Stage = 0 + } + case NextStage: + ui.FocusedTicket.Index = 1 + ui.FocusedTicket.Stage-- + if ui.FocusedTicket.Stage < 0 { + ui.FocusedTicket.Stage = len(stages) - 1 + } + } + ui.FocusedTicket.ID = stages[ui.FocusedTicket.Stage].Tickets[ui.FocusedTicket.Index-1].ID +} + // TicketForm renders the form for ticket information. // // TODO: tab navigation through form fields. diff --git a/kanban.go b/kanban.go @@ -30,6 +30,20 @@ type Ticket struct { Created time.Time } +func (k Kanban) Tickets() ([]Ticket, error) { + stages, err := k.ListStages() + if err != nil { + return nil, err + } + var tickets []Ticket + for _, stage := range stages { + for _, t := range stage.Tickets { + tickets = append(tickets, t) + } + } + return tickets, nil +} + // ListStages returns a list of stages. func (k Kanban) ListStages() ([]Stage, error) { var stages []Stage