pelican/internal/ui/ui.go
Dan Anglin c1bb834a7f
All checks were successful
/ test (pull_request) Successful in 34s
/ lint (pull_request) Successful in 46s
feat(ui): add support for creating status columns
This commit adds support for creating new status columns. When the user
is in the 'board edit' mode, they can press the 'c' key to create a new
column. When a new column is created it automatically assumes the last
position on the board. We will add support later on to allow the user to
re-arrange the columns on the board.

Part of apollo/pelican#22
2024-01-17 17:10:36 +00:00

368 lines
8.7 KiB
Go

package ui
import (
"fmt"
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
"github.com/rivo/tview"
)
type (
boardMovement int
boardMode string
formMode int
)
const (
next boardMovement = iota
previous
)
const (
mainPage string = "main"
quitPage string = "quit"
cardFormPage string = "card form"
deleteCardModalPage string = "delete card modal"
viewPage string = "view"
statusFormPage string = "status form"
)
const (
normal boardMode = "NORMAL"
selection boardMode = "SELECTION"
boardEdit boardMode = "BOARD EDIT"
)
const (
create formMode = iota
edit
)
type App struct {
*tview.Application
columns []*column
grid *tview.Grid
columnFlex *tview.Flex
pages *tview.Pages
focusedColumn int
board board.Board
mode boardMode
modeView *modeView
quitModal *tview.Modal
cardForm *cardForm
deleteCardModal *tview.Modal
statusSelection statusSelection
cardView *cardView
statusbar *statusbar
statusForm *statusForm
}
// NewApp returns a new App value.
func NewApp(path string) (App, error) {
kanban, err := board.Open(path)
if err != nil {
return App{}, fmt.Errorf("unable to open the project's board; %w", err)
}
app := App{
Application: tview.NewApplication(),
pages: tview.NewPages(),
grid: tview.NewGrid(),
columnFlex: tview.NewFlex(),
quitModal: tview.NewModal(),
cardForm: newCardForm(),
focusedColumn: 0,
columns: nil,
board: kanban,
deleteCardModal: tview.NewModal(),
mode: normal,
modeView: newModeView(),
statusSelection: statusSelection{0, 0, 0},
cardView: newCardView(),
statusbar: newStatusbar(),
statusForm: newStatusForm(),
}
return app, nil
}
// init initialises the UI.
func (a *App) Init() error {
a.columnFlex.SetInputCapture(a.inputCapture())
a.initStatusbar()
a.modeView.update(a.mode)
a.grid.SetColumns(10, 0).SetRows(0, 1).SetBorders(false)
a.grid.AddItem(a.columnFlex, 0, 0, 1, 2, 0, 0, true)
a.grid.AddItem(a.modeView, 1, 0, 1, 1, 0, 0, false)
a.grid.AddItem(a.statusbar, 1, 1, 1, 1, 0, 0, false)
a.pages.AddPage(mainPage, a.grid, true, true)
a.initQuitModal()
a.pages.AddPage(quitPage, a.quitModal, false, false)
a.initCardForm()
a.pages.AddPage(cardFormPage, a.cardForm, false, false)
a.initDeleteCardModal()
a.pages.AddPage(deleteCardModalPage, a.deleteCardModal, false, false)
a.initCardView()
a.pages.AddPage(viewPage, a.cardView, false, false)
a.initStatusForm()
a.pages.AddPage(statusFormPage, a.statusForm, false, false)
a.SetRoot(a.pages, true)
a.refresh(refreshArgs{updateFocusedColumnOnly: false, reinitialiseColumns: true})
return nil
}
// saveCard saves a new card to the database.
func (a *App) saveCard(title, description string) {
args := board.CardArgs{
NewTitle: title,
NewDescription: description,
}
_, err := a.board.CreateCard(args)
if err != nil {
a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to create card: %v.", err))
return
}
a.statusbar.displayMessage(infoLevel, "Card created successfully.")
a.refresh(refreshArgs{updateFocusedColumnOnly: false, reinitialiseColumns: false})
}
// editFocusedCard saves an edited card to the database.
func (a *App) editFocusedCard(title, description string) {
cardID := a.focusedCardID()
args := board.UpdateCardArgs{
CardID: cardID,
CardArgs: board.CardArgs{
NewTitle: title,
NewDescription: description,
},
}
if err := a.board.UpdateCard(args); err != nil {
a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to edit card: %v.", err))
return
}
a.statusbar.displayMessage(infoLevel, "Card edited successfully.")
a.refresh(refreshArgs{updateFocusedColumnOnly: true, reinitialiseColumns: false})
}
// getFocusedCard retrieves the details of the card in focus.
func (a *App) getFocusedCard() (board.Card, bool) {
cardID := a.focusedCardID()
card, err := a.board.Card(cardID)
if err != nil {
a.statusbar.displayMessage(
errorLevel,
fmt.Sprintf("Failed to retrieve the card with card ID %d, %v.", cardID, err),
)
return card, false
}
return card, true
}
// deleteFocusedCard deletes the card in focus from the board.
func (a *App) deleteFocusedCard() {
args := board.DeleteCardArgs{
CardID: a.focusedCardID(),
StatusID: a.focusedStatusID(),
}
if err := a.board.DeleteCard(args); err != nil {
a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to delete card: %v.", err))
return
}
a.statusbar.displayMessage(infoLevel, "Card deleted successfully.")
a.refresh(refreshArgs{updateFocusedColumnOnly: true, reinitialiseColumns: false})
}
// updateAllColumns ensures that all columns are up-to-date.
func (a *App) updateAllColumns() error {
for i := range a.columns {
if err := a.updateColumn(i); err != nil {
return err
}
}
return nil
}
// updateColumn ensures that the column is up-to-date.
func (a *App) updateColumn(index int) error {
if err := a.columns[index].update(a.board); err != nil {
return fmt.Errorf("unable to update column; %w", err)
}
return nil
}
// shiftColumnFocus shifts the focus to the column to the left or right depending
// on the movement set by the user.
func (a *App) shiftColumnFocus(movement boardMovement) {
switch movement {
case next:
if a.focusedColumn == len(a.columns)-1 {
a.focusedColumn = 0
} else {
a.focusedColumn++
}
case previous:
if a.focusedColumn == 0 {
a.focusedColumn = len(a.columns) - 1
} else {
a.focusedColumn--
}
}
a.setColumnFocus()
}
// setColumnFocus sets the focus to the column primitive as specified by focusedColumn.
func (a *App) setColumnFocus() {
a.SetFocus(a.columns[a.focusedColumn])
}
// refreshArgs is an argument type for the refresh method.
type refreshArgs struct {
updateFocusedColumnOnly bool
reinitialiseColumns bool
}
// refresh refreshes the UI.
func (a *App) refresh(args refreshArgs) {
// in some cases (e.g. a new column is created)
// we need to re-initialise the columnFlex primitive
if args.reinitialiseColumns {
args.updateFocusedColumnOnly = false
a.initColumns()
}
if args.updateFocusedColumnOnly {
if err := a.updateColumn(a.focusedColumn); err != nil {
a.statusbar.displayMessage(
errorLevel,
fmt.Sprintf("Failed to update status column %q: %v", a.focusedStatusName(), err),
)
return
}
} else {
if err := a.updateAllColumns(); err != nil {
a.statusbar.displayMessage(
errorLevel,
fmt.Sprintf("Failed to update status columns: %v", err),
)
return
}
}
a.setColumnFocus()
}
// shutdown shuts down the application.
func (a *App) shutdown() {
a.closeBoard()
a.Stop()
}
// boardMode returns the current board mode.
func (a *App) boardMode() boardMode {
return a.mode
}
// updateBoardMode updates the board mode.
func (a *App) updateBoardMode(mode boardMode) {
a.mode = mode
a.modeView.update(mode)
}
// focusedCardID returns the ID of the card in focus.
func (a *App) focusedCardID() int {
focusedCard := a.columns[a.focusedColumn].focusedCard
id := a.columns[a.focusedColumn].cards[focusedCard].id
return id
}
// focusedStatusID returns the ID of the status (column) in focus.
func (a *App) focusedStatusID() int {
return a.columns[a.focusedColumn].statusID
}
// focusedStatusName returns the name of the status (column) in focus.
func (a *App) focusedStatusName() string {
return a.columns[a.focusedColumn].statusName
}
// closeBoard closes the board.
func (a *App) closeBoard() {
_ = a.board.Close()
}
// editFocusedStatusColumn updates the status column in focus and saves the changes to the database.
func (a *App) editFocusedStatusColumn(newName string) {
statusID := a.focusedStatusID()
args := board.UpdateStatusArgs{
StatusID: statusID,
StatusArgs: board.StatusArgs{
Name: newName,
},
}
if err := a.board.UpdateStatus(args); err != nil {
a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to edit status: %v.", err))
return
}
a.statusbar.displayMessage(infoLevel, "Status updated successfully.")
a.refresh(refreshArgs{updateFocusedColumnOnly: true, reinitialiseColumns: false})
}
// saveNewStatus saves a new status to the database and updates the Kanban board.
// The new status will be positioned last on the board. It's position can be modified
// by the user.
func (a *App) saveNewStatus(name string) {
args := board.StatusArgs{
Name: name,
}
err := a.board.CreateStatus(args)
if err != nil {
a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to create the status column: %v.", err))
return
}
a.statusbar.displayMessage(infoLevel, "Status column created successfully.")
a.refresh(refreshArgs{updateFocusedColumnOnly: false, reinitialiseColumns: true})
}