From e77c798fbece267f13e1cdc70df40a9dcd38e089 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Fri, 12 Jan 2024 15:07:42 +0000 Subject: [PATCH] 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 --- README.asciidoc | 7 ++-- internal/ui/cardview.go | 69 +++++++++++++++++++++++++++++++++++ internal/ui/column.go | 2 ++ internal/ui/ui.go | 80 +++++++++++++++++++++++++++++------------ 4 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 internal/ui/cardview.go diff --git a/README.asciidoc b/README.asciidoc index 7618f94..97a1d6e 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -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 diff --git a/internal/ui/cardview.go b/internal/ui/cardview.go new file mode 100644 index 0000000..8b7ead7 --- /dev/null +++ b/internal/ui/cardview.go @@ -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) +} diff --git a/internal/ui/column.go b/internal/ui/column.go index 72b8af8..e1c9454 100644 --- a/internal/ui/column.go +++ b/internal/ui/column.go @@ -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, diff --git a/internal/ui/ui.go b/internal/ui/ui.go index e6a32df..a116f3c 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -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 +}