pelican/internal/ui/ui.go
Dan Anglin fc5fa7b0ca
All checks were successful
/ test (pull_request) Successful in 28s
/ lint (pull_request) Successful in 37s
feat(BREAKING): specify project path
This PR allows users to specify the path to the database file
Pelican now expects the user to specify the path to the project's
database file which allows users to open different projects.

This is a breaking change because Pelican no longer opens the
default path automatically. If no path is set then Pelican stops
with an error message.
2023-12-12 12:47:58 +00:00

250 lines
5.4 KiB
Go

package ui
import (
"fmt"
"strconv"
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
"github.com/rivo/tview"
)
const (
shiftLeft int = iota
shiftRight
)
const (
mainPageName string = "main"
quitPageName string = "quit"
addPageName string = "add"
movePageName string = "move"
deleteCardPageName string = "delete card"
)
type UI struct {
*tview.Application
columns []column
flex *tview.Flex
pages *tview.Pages
focusedColumn int
board board.Board
quitModal *tview.Modal
addModal *modalInput
move *tview.Flex
deleteCardModal *tview.Modal
}
// NewUI returns a new UI value.
func NewUI(path string) (UI, error) {
b, err := board.Open(path)
if err != nil {
return UI{}, fmt.Errorf("unable to open the project's board; %w", err)
}
ui := UI{
Application: tview.NewApplication(),
pages: tview.NewPages(),
flex: tview.NewFlex(),
quitModal: tview.NewModal(),
addModal: NewModalInput(),
focusedColumn: 0,
columns: nil,
move: nil,
board: b,
deleteCardModal: tview.NewModal(),
}
if err := ui.init(); err != nil {
return UI{}, fmt.Errorf("received an error after running the initialisation; %w", err)
}
return ui, nil
}
// closeBoard closes the board.
func (u *UI) closeBoard() {
_ = u.board.Close()
}
// deleteCard deletes a card from the board.
func (u *UI) deleteCard() {
currentItem := u.columns[u.focusedColumn].cards.GetCurrentItem()
_, cardIDText := u.columns[u.focusedColumn].cards.GetItemText(currentItem)
cardID, _ := strconv.Atoi(cardIDText)
statusID := u.columns[u.focusedColumn].statusID
args := board.DeleteCardArgs{
CardID: cardID,
StatusID: statusID,
}
_ = u.board.DeleteCard(args)
}
// init initialises the UI.
func (u *UI) init() error {
u.pages.AddPage(mainPageName, u.flex, true, true)
u.initQuitModal()
u.pages.AddPage(quitPageName, u.quitModal, false, false)
u.initAddInputModal()
u.pages.AddPage(addPageName, u.addModal, false, false)
u.initDeleteCardModal()
u.pages.AddPage(deleteCardPageName, u.deleteCardModal, false, false)
u.SetRoot(u.pages, true)
if err := u.refresh(); err != nil {
return fmt.Errorf("error refreshing the board, %w", err)
}
return nil
}
// initAddInputModal initialises the add input modal.
func (u *UI) initAddInputModal() {
doneFunc := func(text string, success bool) {
if success {
_ = u.newCard(text, "")
}
u.pages.HidePage(addPageName)
u.setColumnFocus()
}
u.addModal.SetDoneFunc(doneFunc)
}
// initDeleteCardModal initialises the modal for deleting cards.
func (u *UI) initDeleteCardModal() {
doneFunc := func(_ int, buttonLabel string) {
if buttonLabel == "Confirm" {
u.deleteCard()
_ = u.refresh()
}
u.pages.HidePage(deleteCardPageName)
u.setColumnFocus()
}
u.deleteCardModal.SetText("Do you want to delete this card?").
AddButtons([]string{"Confirm", "Cancel"}).
SetDoneFunc(doneFunc)
}
// initQuitModal initialises the quit modal.
func (u *UI) initQuitModal() {
doneFunc := func(_ int, buttonLabel string) {
switch buttonLabel {
case "Quit":
u.shutdown()
default:
u.pages.HidePage(quitPageName)
u.setColumnFocus()
}
}
u.quitModal.SetText("Do you want to quit the application?").
AddButtons([]string{"Quit", "Cancel"}).
SetDoneFunc(doneFunc)
}
// newCard creates and saves a new card to the database.
func (u *UI) newCard(title, content string) error {
args := board.CardArgs{
NewTitle: title,
NewContent: content,
}
if _, err := u.board.CreateCard(args); err != nil {
return fmt.Errorf("unable to create card, %w", err)
}
_ = u.refresh()
return nil
}
// refresh refreshes the UI.
func (u *UI) refresh() error {
statusList, err := u.board.StatusList()
if err != nil {
return fmt.Errorf("unable to get the status list, %w", err)
}
u.updateColumns(statusList)
u.updateMovePage(statusList)
u.setColumnFocus()
return nil
}
// shutdown shuts down the application.
func (u *UI) shutdown() {
u.closeBoard()
u.Stop()
}
func (u *UI) updateMovePage(statusList []board.Status) {
if u.pages.HasPage(movePageName) {
u.pages.RemovePage(movePageName)
}
move := tview.NewFlex()
statusSelection := tview.NewList()
statusSelection.SetBorder(true)
statusSelection.ShowSecondaryText(false)
statusSelection.SetHighlightFullLine(true)
statusSelection.SetSelectedFocusOnly(true)
statusSelection.SetWrapAround(false)
doneFunc := func() {
u.pages.HidePage(movePageName)
u.setColumnFocus()
}
statusSelection.SetDoneFunc(doneFunc)
selectedFunc := func(_ int, _, secondary string, _ rune) {
currentStatusID := u.columns[u.focusedColumn].statusID
nextStatusID, err := strconv.Atoi(secondary)
if err != nil {
nextStatusID = 0
}
currentItem := u.columns[u.focusedColumn].cards.GetCurrentItem()
_, cardIDText := u.columns[u.focusedColumn].cards.GetItemText(currentItem)
cardID, _ := strconv.Atoi(cardIDText)
args := board.MoveToStatusArgs{
CardID: cardID,
CurrentStatusID: currentStatusID,
NextStatusID: nextStatusID,
}
_ = u.board.MoveToStatus(args)
u.pages.HidePage(movePageName)
_ = u.refresh()
}
statusSelection.SetSelectedFunc(selectedFunc)
for _, status := range statusList {
id := strconv.Itoa(status.ID)
statusSelection.AddItem(fmt.Sprintf("\u25C9 %s", status.Name), id, 0, nil)
}
move.AddItem(statusSelection, 0, 1, true)
u.move = move
u.pages.AddPage(movePageName, move, false, false)
}