2023-05-06 12:49:40 +01:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
|
|
"github.com/rivo/tview"
|
|
|
|
)
|
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
type card struct {
|
|
|
|
id int
|
|
|
|
wrappedTitle []string
|
|
|
|
created string
|
|
|
|
height int
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
type column struct {
|
|
|
|
*tview.Box
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
statusID int
|
2024-01-12 15:07:42 +00:00
|
|
|
statusName string
|
2024-01-09 15:51:23 +00:00
|
|
|
cards []card
|
|
|
|
focusedCard int
|
|
|
|
getBoardModeFunc func() boardMode
|
2024-01-08 04:52:13 +00:00
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
func newColumn(statusID int, statusName string, getBoardModeFunc func() boardMode) *column {
|
2024-01-08 04:52:13 +00:00
|
|
|
column := column{
|
2024-01-09 15:51:23 +00:00
|
|
|
Box: tview.NewBox(),
|
|
|
|
statusID: statusID,
|
2024-01-12 15:07:42 +00:00
|
|
|
statusName: statusName,
|
2024-01-09 15:51:23 +00:00
|
|
|
cards: nil,
|
|
|
|
focusedCard: 0,
|
|
|
|
getBoardModeFunc: getBoardModeFunc,
|
2024-01-08 04:52:13 +00:00
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
column.SetBorder(true)
|
|
|
|
column.SetTitle(" " + statusName + " ")
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
return &column
|
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
func (c *column) addCard(cardID int, title, created string) {
|
|
|
|
wrappedTitle := tview.WordWrap(title, 40)
|
|
|
|
metadataHeight := 1
|
|
|
|
height := len(wrappedTitle) + metadataHeight
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
card := card{
|
|
|
|
id: cardID,
|
|
|
|
wrappedTitle: wrappedTitle,
|
|
|
|
created: created,
|
|
|
|
height: height,
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
c.cards = append(c.cards, card)
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
func (c *column) shiftCardFocus(movement boardMovement) {
|
2024-01-08 04:52:13 +00:00
|
|
|
numCards := len(c.cards)
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
switch movement {
|
|
|
|
case next:
|
2024-01-08 04:52:13 +00:00
|
|
|
if c.focusedCard == numCards-1 {
|
|
|
|
c.focusedCard = 0
|
2023-05-06 12:49:40 +01:00
|
|
|
} else {
|
2024-01-08 04:52:13 +00:00
|
|
|
c.focusedCard++
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
2024-01-09 15:51:23 +00:00
|
|
|
case previous:
|
2024-01-08 04:52:13 +00:00
|
|
|
if c.focusedCard == 0 {
|
|
|
|
c.focusedCard = numCards - 1
|
2023-05-06 12:49:40 +01:00
|
|
|
} else {
|
2024-01-08 04:52:13 +00:00
|
|
|
c.focusedCard--
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
}
|
2024-01-08 04:52:13 +00:00
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
func (c *column) clear() {
|
|
|
|
c.cards = nil
|
|
|
|
c.focusedCard = 0
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
func (c *column) update(kanban board.Board) error {
|
|
|
|
c.clear()
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
for _, card := range cards {
|
|
|
|
c.addCard(card.ID, card.Title, card.Created)
|
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
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':
|
2024-01-09 15:51:23 +00:00
|
|
|
c.shiftCardFocus(next)
|
2024-01-08 04:52:13 +00:00
|
|
|
case key == tcell.KeyUp || letter == 'k':
|
2024-01-09 15:51:23 +00:00
|
|
|
c.shiftCardFocus(previous)
|
2024-01-08 04:52:13 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *column) Draw(screen tcell.Screen) {
|
2024-01-09 15:51:23 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
c.DrawForSubclass(screen, c)
|
2024-01-09 15:51:23 +00:00
|
|
|
|
2024-01-08 04:52:13 +00:00
|
|
|
x, y, width, _ := c.GetInnerRect()
|
|
|
|
|
|
|
|
xOffset := x + 1
|
|
|
|
cursor := y
|
|
|
|
gap := 1
|
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
for ind := 0; ind < len(c.cards); ind++ {
|
|
|
|
var (
|
|
|
|
style tcell.Style
|
|
|
|
vertLine rune
|
|
|
|
)
|
2024-01-08 04:52:13 +00:00
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
if c.HasFocus() && ind == c.focusedCard && mode == normal {
|
2024-01-08 04:52:13 +00:00
|
|
|
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
|
2024-01-09 15:51:23 +00:00
|
|
|
for cy := cursor; cy < cursor+c.cards[ind].height; cy++ {
|
2024-01-08 04:52:13 +00:00
|
|
|
screen.SetContent(xOffset, cy, vertLine, nil, style)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Print the card's title
|
2024-01-09 15:51:23 +00:00
|
|
|
for j := range c.cards[ind].wrappedTitle {
|
2024-01-08 04:52:13 +00:00
|
|
|
tview.Print(
|
|
|
|
screen,
|
2024-01-09 15:51:23 +00:00
|
|
|
c.cards[ind].wrappedTitle[j],
|
2024-01-08 04:52:13 +00:00
|
|
|
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,
|
2024-01-09 15:51:23 +00:00
|
|
|
fmt.Sprintf(cardTextFmt, c.cards[ind].id, cal, c.cards[ind].created),
|
2024-01-08 04:52:13 +00:00
|
|
|
xOffset+2,
|
2024-01-09 15:51:23 +00:00
|
|
|
cursor+len(c.cards[ind].wrappedTitle),
|
2024-01-08 04:52:13 +00:00
|
|
|
width,
|
|
|
|
tview.AlignLeft|tview.AlignTop,
|
|
|
|
tcell.ColorGrey.TrueColor(),
|
|
|
|
)
|
|
|
|
|
2024-01-09 15:51:23 +00:00
|
|
|
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(),
|
|
|
|
)
|
2024-01-08 04:52:13 +00:00
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|