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
This commit is contained in:
parent
d921231017
commit
e77c798fbe
4 changed files with 134 additions and 24 deletions
|
@ -81,10 +81,13 @@ and initialises the database with an empty project.
|
||||||
|Create a new card
|
|Create a new card
|
||||||
|
|
||||||
|CTRL + d
|
|CTRL + d
|
||||||
|Delete card
|
|Delete a card
|
||||||
|
|
||||||
|m
|
|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
|
== Inspiration
|
||||||
|
|
69
internal/ui/cardview.go
Normal file
69
internal/ui/cardview.go
Normal 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)
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ type column struct {
|
||||||
*tview.Box
|
*tview.Box
|
||||||
|
|
||||||
statusID int
|
statusID int
|
||||||
|
statusName string
|
||||||
cards []card
|
cards []card
|
||||||
focusedCard int
|
focusedCard int
|
||||||
getBoardModeFunc func() boardMode
|
getBoardModeFunc func() boardMode
|
||||||
|
@ -28,6 +29,7 @@ func newColumn(statusID int, statusName string, getBoardModeFunc func() boardMod
|
||||||
column := column{
|
column := column{
|
||||||
Box: tview.NewBox(),
|
Box: tview.NewBox(),
|
||||||
statusID: statusID,
|
statusID: statusID,
|
||||||
|
statusName: statusName,
|
||||||
cards: nil,
|
cards: nil,
|
||||||
focusedCard: 0,
|
focusedCard: 0,
|
||||||
getBoardModeFunc: getBoardModeFunc,
|
getBoardModeFunc: getBoardModeFunc,
|
||||||
|
|
|
@ -28,6 +28,7 @@ const (
|
||||||
quitPage string = "quit"
|
quitPage string = "quit"
|
||||||
cardFormPage string = "card form"
|
cardFormPage string = "card form"
|
||||||
deleteCardModalPage string = "delete card modal"
|
deleteCardModalPage string = "delete card modal"
|
||||||
|
viewPage string = "view"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UI struct {
|
type UI struct {
|
||||||
|
@ -43,6 +44,7 @@ type UI struct {
|
||||||
cardForm *cardForm
|
cardForm *cardForm
|
||||||
deleteCardModal *tview.Modal
|
deleteCardModal *tview.Modal
|
||||||
statusSelection statusSelection
|
statusSelection statusSelection
|
||||||
|
view *cardView
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUI returns a new UI value.
|
// NewUI returns a new UI value.
|
||||||
|
@ -64,6 +66,7 @@ func NewUI(path string) (UI, error) {
|
||||||
deleteCardModal: tview.NewModal(),
|
deleteCardModal: tview.NewModal(),
|
||||||
mode: normal,
|
mode: normal,
|
||||||
statusSelection: statusSelection{0, 0, 0},
|
statusSelection: statusSelection{0, 0, 0},
|
||||||
|
view: newCardView(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := userInterface.init(); err != nil {
|
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)
|
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()
|
key, letter := event.Key(), event.Rune()
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -131,11 +160,19 @@ func (u *UI) init() error {
|
||||||
u.mode = normal
|
u.mode = normal
|
||||||
}
|
}
|
||||||
case key == tcell.KeyEnter:
|
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()
|
u.statusSelection.nextStatusID = u.focusedStatusID()
|
||||||
if u.statusSelection.currentStatusID != u.statusSelection.nextStatusID {
|
if u.statusSelection.currentStatusID != u.statusSelection.nextStatusID {
|
||||||
u.statusSelection.moveCard(u.board)
|
u.statusSelection.moveCard(u.board)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.statusSelection = statusSelection{0, 0, 0}
|
u.statusSelection = statusSelection{0, 0, 0}
|
||||||
u.mode = normal
|
u.mode = normal
|
||||||
_ = u.refresh(false)
|
_ = u.refresh(false)
|
||||||
|
@ -143,26 +180,7 @@ func (u *UI) init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return event
|
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.
|
// initCardForm initialises the card form.
|
||||||
|
@ -218,6 +236,19 @@ func (u *UI) initQuitModal() {
|
||||||
SetDoneFunc(doneFunc)
|
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.
|
// newCard creates and saves a new card to the database.
|
||||||
func (u *UI) newCard(title, description string) error {
|
func (u *UI) newCard(title, description string) error {
|
||||||
args := board.CardArgs{
|
args := board.CardArgs{
|
||||||
|
@ -376,7 +407,12 @@ func (u *UI) focusedCardID() int {
|
||||||
return id
|
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 {
|
func (u *UI) focusedStatusID() int {
|
||||||
return u.columns[u.focusedColumn].statusID
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue