Dan Anglin
ee861e5426
This commit updates the delete card modal so that the prompt includes the title of the card that is about to be deleted. This is to give the user the confidence that they are deleting the intended card. The modal has also been updated to match the current theme of the application. Resolves apollo/pelican#18
424 lines
9.7 KiB
Go
424 lines
9.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
)
|
|
|
|
type (
|
|
boardMovement int
|
|
boardMode int
|
|
)
|
|
|
|
const (
|
|
next boardMovement = iota
|
|
previous
|
|
)
|
|
|
|
const (
|
|
normal boardMode = iota
|
|
selection
|
|
)
|
|
|
|
const (
|
|
mainPage string = "main"
|
|
quitPage string = "quit"
|
|
cardFormPage string = "card form"
|
|
deleteCardModalPage string = "delete card modal"
|
|
viewPage string = "view"
|
|
)
|
|
|
|
type UI struct {
|
|
*tview.Application
|
|
|
|
columns []*column
|
|
flex *tview.Flex
|
|
pages *tview.Pages
|
|
focusedColumn int
|
|
board board.Board
|
|
mode boardMode
|
|
quitModal *tview.Modal
|
|
cardForm *cardForm
|
|
deleteCardModal *tview.Modal
|
|
statusSelection statusSelection
|
|
view *cardView
|
|
}
|
|
|
|
// NewUI returns a new UI value.
|
|
func NewUI(path string) (UI, error) {
|
|
kanban, err := board.Open(path)
|
|
if err != nil {
|
|
return UI{}, fmt.Errorf("unable to open the project's board; %w", err)
|
|
}
|
|
|
|
userInterface := UI{
|
|
Application: tview.NewApplication(),
|
|
pages: tview.NewPages(),
|
|
flex: tview.NewFlex(),
|
|
quitModal: tview.NewModal(),
|
|
cardForm: newCardForm(),
|
|
focusedColumn: 0,
|
|
columns: nil,
|
|
board: kanban,
|
|
deleteCardModal: tview.NewModal(),
|
|
mode: normal,
|
|
statusSelection: statusSelection{0, 0, 0},
|
|
view: newCardView(),
|
|
}
|
|
|
|
if err := userInterface.init(); err != nil {
|
|
return UI{}, fmt.Errorf("received an error after running the initialisation; %w", err)
|
|
}
|
|
|
|
return userInterface, nil
|
|
}
|
|
|
|
// closeBoard closes the board.
|
|
func (u *UI) closeBoard() {
|
|
_ = u.board.Close()
|
|
}
|
|
|
|
// init initialises the UI.
|
|
func (u *UI) init() error {
|
|
err := u.initColumns()
|
|
if err != nil {
|
|
return fmt.Errorf("error initialising the status columns; %w", err)
|
|
}
|
|
|
|
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 {
|
|
case letter == 'h' || key == tcell.KeyLeft:
|
|
u.shiftColumnFocus(previous)
|
|
case letter == 'l' || key == tcell.KeyRight:
|
|
u.shiftColumnFocus(next)
|
|
case letter == 'c':
|
|
if u.mode == normal {
|
|
u.cardForm.mode = create
|
|
u.cardForm.updateInputFields("", "")
|
|
u.cardForm.frame.SetTitle(" Create Card ")
|
|
u.pages.ShowPage(cardFormPage)
|
|
u.SetFocus(u.cardForm)
|
|
}
|
|
case letter == 'm':
|
|
if u.mode == normal {
|
|
u.statusSelection.cardID = u.focusedCardID()
|
|
u.statusSelection.currentStatusID = u.focusedStatusID()
|
|
u.mode = selection
|
|
}
|
|
case letter == 'e':
|
|
if u.mode == normal {
|
|
u.cardForm.mode = edit
|
|
card, _ := u.getFocusedCard()
|
|
u.cardForm.updateInputFields(card.Title, card.Description)
|
|
u.cardForm.frame.SetTitle(" Edit Card ")
|
|
u.pages.ShowPage(cardFormPage)
|
|
u.SetFocus(u.cardForm)
|
|
}
|
|
case key == tcell.KeyCtrlD:
|
|
if u.mode == normal {
|
|
card, _ := u.getFocusedCard()
|
|
text := fmt.Sprintf("Do you want to delete '%s'?", card.Title)
|
|
u.deleteCardModal.SetText(text)
|
|
u.pages.ShowPage(deleteCardModalPage)
|
|
u.SetFocus(u.deleteCardModal)
|
|
}
|
|
case key == tcell.KeyCtrlQ:
|
|
if u.mode == normal {
|
|
u.pages.ShowPage(quitPage)
|
|
u.SetFocus(u.quitModal)
|
|
}
|
|
case key == tcell.KeyESC:
|
|
if u.mode != normal {
|
|
u.mode = normal
|
|
}
|
|
case key == tcell.KeyEnter:
|
|
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)
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
}
|
|
|
|
// initCardForm initialises the card form.
|
|
func (u *UI) initCardForm() {
|
|
doneFunc := func(title, description string, success bool, mode cardFormMode) {
|
|
if success {
|
|
switch mode {
|
|
case create:
|
|
_ = u.newCard(title, description)
|
|
case edit:
|
|
_ = u.editFocusedCard(title, description)
|
|
}
|
|
}
|
|
|
|
u.pages.HidePage(cardFormPage)
|
|
u.setColumnFocus()
|
|
}
|
|
|
|
u.cardForm.setDoneFunc(doneFunc)
|
|
}
|
|
|
|
// initDeleteCardModal initialises the modal for deleting cards.
|
|
func (u *UI) initDeleteCardModal() {
|
|
doneFunc := func(_ int, buttonLabel string) {
|
|
if buttonLabel == "Confirm" {
|
|
u.deleteFocusedCard()
|
|
_ = u.refresh(true)
|
|
}
|
|
|
|
u.pages.HidePage(deleteCardModalPage)
|
|
u.setColumnFocus()
|
|
}
|
|
|
|
u.deleteCardModal.AddButtons([]string{"Confirm", "Cancel"}).SetDoneFunc(doneFunc)
|
|
|
|
u.deleteCardModal.SetBorder(true).SetBorderColor(tcell.ColorOrangeRed)
|
|
u.deleteCardModal.SetBackgroundColor(tcell.ColorBlack.TrueColor())
|
|
u.deleteCardModal.SetButtonBackgroundColor(tcell.ColorBlueViolet.TrueColor())
|
|
u.deleteCardModal.SetTextColor(tcell.ColorWhite.TrueColor())
|
|
}
|
|
|
|
// initQuitModal initialises the quit modal.
|
|
func (u *UI) initQuitModal() {
|
|
doneFunc := func(_ int, buttonLabel string) {
|
|
switch buttonLabel {
|
|
case "Quit":
|
|
u.shutdown()
|
|
default:
|
|
u.pages.HidePage(quitPage)
|
|
u.setColumnFocus()
|
|
}
|
|
}
|
|
|
|
u.quitModal.SetText("Do you want to quit the application?").
|
|
AddButtons([]string{"Quit", "Cancel"}).
|
|
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{
|
|
NewTitle: title,
|
|
NewDescription: description,
|
|
}
|
|
|
|
if _, err := u.board.CreateCard(args); err != nil {
|
|
return fmt.Errorf("unable to create card, %w", err)
|
|
}
|
|
|
|
_ = u.refresh(false)
|
|
|
|
return nil
|
|
}
|
|
|
|
// editFocusedCard saves and edited card to the database.
|
|
func (u *UI) editFocusedCard(title, description string) error {
|
|
cardID := u.focusedCardID()
|
|
|
|
args := board.UpdateCardArgs{
|
|
CardID: cardID,
|
|
CardArgs: board.CardArgs{
|
|
NewTitle: title,
|
|
NewDescription: description,
|
|
},
|
|
}
|
|
|
|
if err := u.board.UpdateCard(args); err != nil {
|
|
return fmt.Errorf("unable to edit card with ID: %d; %w", cardID, err)
|
|
}
|
|
|
|
_ = u.refresh(true)
|
|
|
|
return nil
|
|
}
|
|
|
|
// getFocusedCard retrieves the details of the focused card.
|
|
func (u *UI) getFocusedCard() (board.Card, error) {
|
|
cardID := u.focusedCardID()
|
|
|
|
card, err := u.board.Card(cardID)
|
|
if err != nil {
|
|
return board.Card{}, fmt.Errorf("unable to retrieve the card with card ID %d, %w", cardID, err)
|
|
}
|
|
|
|
return card, nil
|
|
}
|
|
|
|
// deleteFocusedCard deletes the focused card from the board.
|
|
func (u *UI) deleteFocusedCard() {
|
|
args := board.DeleteCardArgs{
|
|
CardID: u.focusedCardID(),
|
|
StatusID: u.focusedStatusID(),
|
|
}
|
|
|
|
_ = u.board.DeleteCard(args)
|
|
}
|
|
|
|
// initColumns initialises the columns of the Kanban board.
|
|
func (u *UI) initColumns() error {
|
|
u.flex.Clear()
|
|
|
|
statusList, err := u.board.StatusList()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to retrieve the list of statuses; %w", err)
|
|
}
|
|
|
|
columns := make([]*column, len(statusList))
|
|
|
|
for i := range statusList {
|
|
column := newColumn(statusList[i].ID, statusList[i].Name, u.boardMode)
|
|
u.flex.AddItem(column, 50, 1, true)
|
|
columns[i] = column
|
|
}
|
|
|
|
u.columns = columns
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *UI) updateAllColumns() error {
|
|
for i := range u.columns {
|
|
if err := u.updateColumn(i); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *UI) updateColumn(index int) error {
|
|
if err := u.columns[index].update(u.board); err != nil {
|
|
return fmt.Errorf("unable to update column; %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *UI) shiftColumnFocus(movement boardMovement) {
|
|
switch movement {
|
|
case next:
|
|
if u.focusedColumn == len(u.columns)-1 {
|
|
u.focusedColumn = 0
|
|
} else {
|
|
u.focusedColumn++
|
|
}
|
|
case previous:
|
|
if u.focusedColumn == 0 {
|
|
u.focusedColumn = len(u.columns) - 1
|
|
} else {
|
|
u.focusedColumn--
|
|
}
|
|
}
|
|
|
|
u.setColumnFocus()
|
|
}
|
|
|
|
func (u *UI) setColumnFocus() {
|
|
u.SetFocus(u.columns[u.focusedColumn])
|
|
}
|
|
|
|
// refresh refreshes the UI.
|
|
func (u *UI) refresh(updateFocusedColumnOnly bool) error {
|
|
if updateFocusedColumnOnly {
|
|
if err := u.updateColumn(u.focusedColumn); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := u.updateAllColumns(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
u.setColumnFocus()
|
|
|
|
return nil
|
|
}
|
|
|
|
// shutdown shuts down the application.
|
|
func (u *UI) shutdown() {
|
|
u.closeBoard()
|
|
u.Stop()
|
|
}
|
|
|
|
// boardMode returns the current board mode.
|
|
func (u *UI) boardMode() boardMode {
|
|
return u.mode
|
|
}
|
|
|
|
// focusedCardID returns the ID of the card in focus.
|
|
func (u *UI) focusedCardID() int {
|
|
focusedCard := u.columns[u.focusedColumn].focusedCard
|
|
id := u.columns[u.focusedColumn].cards[focusedCard].id
|
|
|
|
return id
|
|
}
|
|
|
|
// 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
|
|
}
|