feat(ui): add support for viewing cards
All checks were successful
/ test (pull_request) Successful in 28s
/ lint (pull_request) Successful in 31s

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
This commit is contained in:
Dan Anglin 2024-01-12 15:07:42 +00:00
parent d921231017
commit e77c798fbe
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
4 changed files with 134 additions and 24 deletions

View file

@ -81,10 +81,13 @@ and initialises the database with an empty project.
|Create a new card
|CTRL + d
|Delete card
|Delete a card
|m
|Move card between statuses
|Move a card from one column to another (press `Enter` to confirm your selection).
|Enter
|View the details of a card (press `ESC` to go back to the main board).
|===
== Inspiration

69
internal/ui/cardview.go Normal file
View file

@ -0,0 +1,69 @@
package ui
import (
"fmt"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type cardView struct {
*tview.TextView
frame *tview.Frame
contentFormat string
}
func newCardView() *cardView {
border := tcell.ColorOrangeRed.TrueColor()
background := tcell.ColorBlack.TrueColor()
content := tview.NewTextView()
// Stylise the TextView
content.SetDynamicColors(true).
SetBorder(true).
SetBorderPadding(0, 0, 1, 1).
SetBorderColor(border).
SetBackgroundColor(background)
cardContentFormat := `[green::b][#%d[] %s[-:-:-:-]
[green::b]Status:[white::-] %s [green::b]Created:[white::-] %s
[green::b]Description:[white::-]
%s
`
view := cardView{
TextView: content,
frame: tview.NewFrame(content),
contentFormat: cardContentFormat,
}
return &view
}
func (c *cardView) setDoneFunc(handler func(key tcell.Key)) {
c.SetDoneFunc(handler)
}
func (c *cardView) print(id int, title, status, created, description string) {
fmt.Fprintf(c, c.contentFormat, id, title, status, created, description)
}
func (c *cardView) Draw(screen tcell.Screen) {
height := 25
width := 50
screenWidth, screenHeight := screen.Size()
// Set the form's position and size.
x := (screenWidth - width) / 2
y := (screenHeight - height) / 2
c.SetRect(x, y, width, height)
// Draw the frame.
c.frame.SetRect(x, y, width, height)
c.frame.Draw(screen)
}

View file

@ -19,6 +19,7 @@ type column struct {
*tview.Box
statusID int
statusName string
cards []card
focusedCard int
getBoardModeFunc func() boardMode
@ -28,6 +29,7 @@ func newColumn(statusID int, statusName string, getBoardModeFunc func() boardMod
column := column{
Box: tview.NewBox(),
statusID: statusID,
statusName: statusName,
cards: nil,
focusedCard: 0,
getBoardModeFunc: getBoardModeFunc,

View file

@ -28,6 +28,7 @@ const (
quitPage string = "quit"
cardFormPage string = "card form"
deleteCardModalPage string = "delete card modal"
viewPage string = "view"
)
type UI struct {
@ -43,6 +44,7 @@ type UI struct {
cardForm *cardForm
deleteCardModal *tview.Modal
statusSelection statusSelection
view *cardView
}
// NewUI returns a new UI value.
@ -64,6 +66,7 @@ func NewUI(path string) (UI, error) {
deleteCardModal: tview.NewModal(),
mode: normal,
statusSelection: statusSelection{0, 0, 0},
view: newCardView(),
}
if err := userInterface.init(); err != nil {
@ -85,7 +88,33 @@ func (u *UI) init() error {
return fmt.Errorf("error initialising the status columns; %w", err)
}
u.flex.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
u.flex.SetInputCapture(u.inputCapture())
u.pages.AddPage(mainPage, u.flex, true, true)
u.initQuitModal()
u.pages.AddPage(quitPage, u.quitModal, false, false)
u.initCardForm()
u.pages.AddPage(cardFormPage, u.cardForm, false, false)
u.initDeleteCardModal()
u.pages.AddPage(deleteCardModalPage, u.deleteCardModal, false, false)
u.initView()
u.pages.AddPage(viewPage, u.view, false, false)
u.SetRoot(u.pages, true)
if err := u.refresh(false); err != nil {
return fmt.Errorf("error refreshing the board; %w", err)
}
return nil
}
func (u *UI) inputCapture() func(event *tcell.EventKey) *tcell.EventKey {
return func(event *tcell.EventKey) *tcell.EventKey {
key, letter := event.Key(), event.Rune()
switch {
@ -131,11 +160,19 @@ func (u *UI) init() error {
u.mode = normal
}
case key == tcell.KeyEnter:
if u.mode == selection {
switch u.mode {
case normal:
card, _ := u.getFocusedCard()
status := u.focusedStatusName()
u.view.print(card.ID, card.Title, status, card.Created, card.Description)
u.pages.ShowPage(viewPage)
u.SetFocus(u.view)
case selection:
u.statusSelection.nextStatusID = u.focusedStatusID()
if u.statusSelection.currentStatusID != u.statusSelection.nextStatusID {
u.statusSelection.moveCard(u.board)
}
u.statusSelection = statusSelection{0, 0, 0}
u.mode = normal
_ = u.refresh(false)
@ -143,26 +180,7 @@ func (u *UI) init() error {
}
return event
})
u.pages.AddPage(mainPage, u.flex, true, true)
u.initQuitModal()
u.pages.AddPage(quitPage, u.quitModal, false, false)
u.initCardForm()
u.pages.AddPage(cardFormPage, u.cardForm, false, false)
u.initDeleteCardModal()
u.pages.AddPage(deleteCardModalPage, u.deleteCardModal, false, false)
u.SetRoot(u.pages, true)
if err := u.refresh(false); err != nil {
return fmt.Errorf("error refreshing the board; %w", err)
}
return nil
}
// initCardForm initialises the card form.
@ -218,6 +236,19 @@ func (u *UI) initQuitModal() {
SetDoneFunc(doneFunc)
}
// initView initialises the view window for displaying the card.
func (u *UI) initView() {
doneFunc := func(key tcell.Key) {
if key == tcell.KeyEsc {
u.pages.HidePage(viewPage)
u.view.Clear()
u.setColumnFocus()
}
}
u.view.setDoneFunc(doneFunc)
}
// newCard creates and saves a new card to the database.
func (u *UI) newCard(title, description string) error {
args := board.CardArgs{
@ -376,7 +407,12 @@ func (u *UI) focusedCardID() int {
return id
}
// focusedStatusID returns the ID of thestatus (column) in focus.
// focusedStatusID returns the ID of the status (column) in focus.
func (u *UI) focusedStatusID() int {
return u.columns[u.focusedColumn].statusID
}
// focusedStatusName returns the name of the status (column) in focus.
func (u *UI) focusedStatusName() string {
return u.columns[u.focusedColumn].statusName
}