pelican/internal/ui/column.go
Dan Anglin e77c798fbe
All checks were successful
/ test (pull_request) Successful in 28s
/ lint (pull_request) Successful in 31s
feat(ui): add support for viewing cards
This commit adds support for viewing all the details of a card in a
separate widget. The user simply needs to press the Enter key to view
the card. To return to the Kanban board the user needs to press the
Escape key.

Part of apollo/pelican#16
2024-01-12 15:07:42 +00:00

214 lines
4.7 KiB
Go

package ui
import (
"fmt"
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type card struct {
id int
wrappedTitle []string
created string
height int
}
type column struct {
*tview.Box
statusID int
statusName string
cards []card
focusedCard int
getBoardModeFunc func() boardMode
}
func newColumn(statusID int, statusName string, getBoardModeFunc func() boardMode) *column {
column := column{
Box: tview.NewBox(),
statusID: statusID,
statusName: statusName,
cards: nil,
focusedCard: 0,
getBoardModeFunc: getBoardModeFunc,
}
column.SetBorder(true)
column.SetTitle(" " + statusName + " ")
return &column
}
func (c *column) addCard(cardID int, title, created string) {
wrappedTitle := tview.WordWrap(title, 40)
metadataHeight := 1
height := len(wrappedTitle) + metadataHeight
card := card{
id: cardID,
wrappedTitle: wrappedTitle,
created: created,
height: height,
}
c.cards = append(c.cards, card)
}
func (c *column) shiftCardFocus(movement boardMovement) {
numCards := len(c.cards)
switch movement {
case next:
if c.focusedCard == numCards-1 {
c.focusedCard = 0
} else {
c.focusedCard++
}
case previous:
if c.focusedCard == 0 {
c.focusedCard = numCards - 1
} else {
c.focusedCard--
}
}
}
func (c *column) clear() {
c.cards = nil
c.focusedCard = 0
}
func (c *column) update(kanban board.Board) error {
c.clear()
status, err := kanban.Status(c.statusID)
if err != nil {
return fmt.Errorf("unable to retrieve details about Status %d from the database; %w", c.statusID, err)
}
if status.CardIds != nil && len(status.CardIds) > 0 {
cards, err := kanban.CardList(status.CardIds)
if err != nil {
return fmt.Errorf("unable to get the list of cards from Status %d; %w", c.statusID, err)
}
for _, card := range cards {
c.addCard(card.ID, card.Title, card.Created)
}
}
return nil
}
// InputHandler returns the handler for this primitive.
func (c *column) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
return c.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) {
key, letter := event.Key(), event.Rune()
switch {
case key == tcell.KeyDown || letter == 'j':
c.shiftCardFocus(next)
case key == tcell.KeyUp || letter == 'k':
c.shiftCardFocus(previous)
}
})
}
func (c *column) Draw(screen tcell.Screen) {
var mode boardMode
// Update border before calling c.DrawForSubclass()
if c.getBoardModeFunc == nil {
mode = normal
} else {
mode = c.getBoardModeFunc()
}
if mode == selection {
if c.HasFocus() {
c.SetBorderColor(tcell.ColorTurquoise.TrueColor())
} else {
c.SetBorderColor(tcell.ColorOrangeRed.TrueColor())
}
} else {
c.SetBorderColor(tcell.ColorOrangeRed.TrueColor())
}
c.DrawForSubclass(screen, c)
x, y, width, _ := c.GetInnerRect()
xOffset := x + 1
cursor := y
gap := 1
for ind := 0; ind < len(c.cards); ind++ {
var (
style tcell.Style
vertLine rune
)
if c.HasFocus() && ind == c.focusedCard && mode == normal {
vertLine = tview.BoxDrawingsHeavyVertical
style = tcell.StyleDefault.Foreground(tcell.ColorBlue.TrueColor())
} else {
vertLine = tview.BoxDrawingsLightVertical
style = tcell.StyleDefault.Foreground(tcell.ColorYellow.TrueColor())
}
// Draw a vertical line down the left side of each card
for cy := cursor; cy < cursor+c.cards[ind].height; cy++ {
screen.SetContent(xOffset, cy, vertLine, nil, style)
}
// Print the card's title
for j := range c.cards[ind].wrappedTitle {
tview.Print(
screen,
c.cards[ind].wrappedTitle[j],
xOffset+2,
cursor+j,
width,
tview.AlignLeft|tview.AlignTop,
tcell.ColorGreen.TrueColor(),
)
}
// Print the card's metadata
cal := "📅"
cardTextFmt := "#%d %s%s"
tview.Print(
screen,
fmt.Sprintf(cardTextFmt, c.cards[ind].id, cal, c.cards[ind].created),
xOffset+2,
cursor+len(c.cards[ind].wrappedTitle),
width,
tview.AlignLeft|tview.AlignTop,
tcell.ColorGrey.TrueColor(),
)
cursor = cursor + c.cards[ind].height + gap
}
// draw a 'placeholder card' if in status selection board mode
if mode == selection && c.HasFocus() {
vertLine := tview.BoxDrawingsHeavyVertical
style := tcell.StyleDefault.Foreground(tcell.ColorWhite.TrueColor())
for cy := cursor; cy < cursor+1; cy++ {
screen.SetContent(xOffset, cy, vertLine, nil, style)
}
tview.Print(
screen,
"Move the card to this column",
xOffset+2,
cursor,
width,
tview.AlignLeft|tview.AlignTop,
tcell.ColorWhite.TrueColor(),
)
}
}