Dan Anglin
e77c798fbe
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
214 lines
4.7 KiB
Go
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(),
|
|
)
|
|
}
|
|
}
|