feat(ui): add support for editing cards
All checks were successful
/ test (pull_request) Successful in 3m13s
/ lint (pull_request) Successful in 1m4s

This commit adds support for editing an existing card's title and
description. The (previously named) input modal has been enhanced to
support both creating and editing cards.

Part of apollo/pelican#14
This commit is contained in:
Dan Anglin 2024-01-10 18:21:14 +00:00
parent f956b7da59
commit d167039176
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
3 changed files with 107 additions and 58 deletions

View file

@ -13,6 +13,16 @@ output:
sort-results: true sort-results: true
linters-settings: linters-settings:
depguard:
rules:
main:
files:
- $all
allow:
- $gostd
- codeflow.dananglin.me.uk/apollo/pelican
- github.com/gdamore/tcell/v2
- github.com/rivo/tview
exhaustivestruct: exhaustivestruct:
struct-patterns: struct-patterns:
- 'forge.dananglin.me.uk/code/dananglin/pelican.Status' - 'forge.dananglin.me.uk/code/dananglin/pelican.Status'

View file

@ -5,15 +5,23 @@ import (
"github.com/rivo/tview" "github.com/rivo/tview"
) )
type modalInput struct { type cardModalMode int
const (
create cardModalMode = iota
edit
)
type cardModal struct {
*tview.Form *tview.Form
frame *tview.Frame frame *tview.Frame
title string title string
description string description string
done func(string, string, bool) done func(string, string, bool, cardModalMode)
mode cardModalMode
} }
func newModalInput() *modalInput { func newCardModal() *cardModal {
var ( var (
background = tcell.ColorBlack.TrueColor() background = tcell.ColorBlack.TrueColor()
buttonBackground = tcell.ColorBlueViolet.TrueColor() buttonBackground = tcell.ColorBlueViolet.TrueColor()
@ -24,12 +32,13 @@ func newModalInput() *modalInput {
form = tview.NewForm() form = tview.NewForm()
) )
modal := modalInput{ modal := cardModal{
Form: form, Form: form,
frame: tview.NewFrame(form), frame: tview.NewFrame(form),
title: "", title: "",
description: "", description: "",
done: nil, done: nil,
mode: create,
} }
// Stylise the buttons // Stylise the buttons
@ -51,49 +60,39 @@ func newModalInput() *modalInput {
SetBackgroundColor(background). SetBackgroundColor(background).
SetBorderPadding(1, 1, 1, 1) SetBorderPadding(1, 1, 1, 1)
modal.AddButton("Create card", func() { modal.AddButton("Save", func() {
if modal.done != nil { if modal.done != nil {
modal.done(modal.title, modal.description, true) modal.done(modal.title, modal.description, true, modal.mode)
} }
modal.reset()
}) })
modal.AddButton("Cancel", func() { modal.AddButton("Cancel", func() {
if modal.done != nil { if modal.done != nil {
modal.done(modal.title, modal.description, false) modal.done(modal.title, modal.description, false, modal.mode)
} }
modal.reset()
}) })
modal.addInputFields()
modal.frame.SetTitle(" New Card ")
return &modal return &modal
} }
func (m *modalInput) reset() { func (m *cardModal) updateInputFields(title, description string) {
m.Clear(false) m.Clear(false)
m.addInputFields() m.AddInputField("Title", title, 60, nil, func(text string) {
}
func (m *modalInput) addInputFields() {
m.AddInputField("Title", "", 60, nil, func(text string) {
m.title = text m.title = text
}) })
m.AddTextArea("Description", "", 60, 10, 0, func(text string) { m.AddTextArea("Description", description, 60, 10, 0, func(text string) {
m.description = text m.description = text
}) })
} }
func (m *modalInput) SetDoneFunc(handler func(string, string, bool)) *modalInput { func (m *cardModal) setDoneFunc(handler func(string, string, bool, cardModalMode)) *cardModal {
m.done = handler m.done = handler
return m return m
} }
func (m *modalInput) Draw(screen tcell.Screen) { func (m *cardModal) Draw(screen tcell.Screen) {
buttonsWidth := 20 buttonsWidth := 20
screenWidth, screenHeight := screen.Size() screenWidth, screenHeight := screen.Size()
width := screenWidth / 3 width := screenWidth / 3

View file

@ -24,10 +24,10 @@ const (
) )
const ( const (
mainPage string = "main" mainPage string = "main"
quitPage string = "quit" quitPage string = "quit"
addPage string = "add" cardModalPage string = "card modal"
deleteCardPage string = "delete card" deleteCardModalPage string = "delete card modal"
) )
type UI struct { type UI struct {
@ -40,7 +40,7 @@ type UI struct {
board board.Board board board.Board
mode boardMode mode boardMode
quitModal *tview.Modal quitModal *tview.Modal
addModal *modalInput cardModal *cardModal
deleteCardModal *tview.Modal deleteCardModal *tview.Modal
statusSelection statusSelection statusSelection statusSelection
} }
@ -57,7 +57,7 @@ func NewUI(path string) (UI, error) {
pages: tview.NewPages(), pages: tview.NewPages(),
flex: tview.NewFlex(), flex: tview.NewFlex(),
quitModal: tview.NewModal(), quitModal: tview.NewModal(),
addModal: newModalInput(), cardModal: newCardModal(),
focusedColumn: 0, focusedColumn: 0,
columns: nil, columns: nil,
board: kanban, board: kanban,
@ -95,8 +95,10 @@ func (u *UI) init() error {
u.shiftColumnFocus(next) u.shiftColumnFocus(next)
case letter == 'c': case letter == 'c':
if u.mode == normal { if u.mode == normal {
u.pages.ShowPage(addPage) u.cardModal.mode = create
u.SetFocus(u.addModal) u.cardModal.updateInputFields("", "")
u.pages.ShowPage(cardModalPage)
u.SetFocus(u.cardModal)
} }
case letter == 'm': case letter == 'm':
if u.mode == normal { if u.mode == normal {
@ -105,9 +107,19 @@ func (u *UI) init() error {
u.statusSelection.currentStatusID = u.columns[u.focusedColumn].statusID u.statusSelection.currentStatusID = u.columns[u.focusedColumn].statusID
u.mode = selection u.mode = selection
} }
case letter == 'e':
if u.mode == normal {
u.cardModal.mode = edit
focusedCard := u.columns[u.focusedColumn].focusedCard
cardID := u.columns[u.focusedColumn].cards[focusedCard].id
card, _ := u.board.Card(cardID)
u.cardModal.updateInputFields(card.Title, card.Description)
u.pages.ShowPage(cardModalPage)
u.SetFocus(u.cardModal)
}
case key == tcell.KeyCtrlD: case key == tcell.KeyCtrlD:
if u.mode == normal { if u.mode == normal {
u.pages.ShowPage(deleteCardPage) u.pages.ShowPage(deleteCardModalPage)
u.SetFocus(u.deleteCardModal) u.SetFocus(u.deleteCardModal)
} }
case key == tcell.KeyCtrlQ: case key == tcell.KeyCtrlQ:
@ -139,11 +151,11 @@ func (u *UI) init() error {
u.initQuitModal() u.initQuitModal()
u.pages.AddPage(quitPage, u.quitModal, false, false) u.pages.AddPage(quitPage, u.quitModal, false, false)
u.initAddInputModal() u.initCardModal()
u.pages.AddPage(addPage, u.addModal, false, false) u.pages.AddPage(cardModalPage, u.cardModal, false, false)
u.initDeleteCardModal() u.initDeleteCardModal()
u.pages.AddPage(deleteCardPage, u.deleteCardModal, false, false) u.pages.AddPage(deleteCardModalPage, u.deleteCardModal, false, false)
u.SetRoot(u.pages, true) u.SetRoot(u.pages, true)
@ -154,18 +166,25 @@ func (u *UI) init() error {
return nil return nil
} }
// initAddInputModal initialises the add input modal. // initCardModal initialises the card modal.
func (u *UI) initAddInputModal() { func (u *UI) initCardModal() {
doneFunc := func(text, description string, success bool) { doneFunc := func(title, description string, success bool, mode cardModalMode) {
if success { if success {
_ = u.newCard(text, description) switch mode {
case create:
_ = u.newCard(title, description)
case edit:
focusedCard := u.columns[u.focusedColumn].focusedCard
cardID := u.columns[u.focusedColumn].cards[focusedCard].id
_ = u.editCard(cardID, title, description)
}
} }
u.pages.HidePage(addPage) u.pages.HidePage(cardModalPage)
u.setColumnFocus() u.setColumnFocus()
} }
u.addModal.SetDoneFunc(doneFunc) u.cardModal.setDoneFunc(doneFunc)
} }
// initDeleteCardModal initialises the modal for deleting cards. // initDeleteCardModal initialises the modal for deleting cards.
@ -176,7 +195,7 @@ func (u *UI) initDeleteCardModal() {
_ = u.refresh(true) _ = u.refresh(true)
} }
u.pages.HidePage(deleteCardPage) u.pages.HidePage(deleteCardModalPage)
u.setColumnFocus() u.setColumnFocus()
} }
@ -185,20 +204,6 @@ func (u *UI) initDeleteCardModal() {
SetDoneFunc(doneFunc) SetDoneFunc(doneFunc)
} }
// deleteCard deletes a card from the board.
func (u *UI) deleteCard() {
statusID := u.columns[u.focusedColumn].statusID
focusedCard := u.columns[u.focusedColumn].focusedCard
cardID := u.columns[u.focusedColumn].cards[focusedCard].id
args := board.DeleteCardArgs{
CardID: cardID,
StatusID: statusID,
}
_ = u.board.DeleteCard(args)
}
// initQuitModal initialises the quit modal. // initQuitModal initialises the quit modal.
func (u *UI) initQuitModal() { func (u *UI) initQuitModal() {
doneFunc := func(_ int, buttonLabel string) { doneFunc := func(_ int, buttonLabel string) {
@ -217,10 +222,10 @@ func (u *UI) initQuitModal() {
} }
// 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, content string) error { func (u *UI) newCard(title, description string) error {
args := board.CardArgs{ args := board.CardArgs{
NewTitle: title, NewTitle: title,
NewDescription: content, NewDescription: description,
} }
if _, err := u.board.CreateCard(args); err != nil { if _, err := u.board.CreateCard(args); err != nil {
@ -232,6 +237,40 @@ func (u *UI) newCard(title, content string) error {
return nil return nil
} }
// editCard saves and edited card to the database.
func (u *UI) editCard(cardID int, title, description string) error {
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
}
// deleteCard deletes a card from the board.
func (u *UI) deleteCard() {
statusID := u.columns[u.focusedColumn].statusID
focusedCard := u.columns[u.focusedColumn].focusedCard
cardID := u.columns[u.focusedColumn].cards[focusedCard].id
args := board.DeleteCardArgs{
CardID: cardID,
StatusID: statusID,
}
_ = u.board.DeleteCard(args)
}
// initColumns initialises the columns of the Kanban board.
func (u *UI) initColumns() error { func (u *UI) initColumns() error {
u.flex.Clear() u.flex.Clear()
@ -317,6 +356,7 @@ func (u *UI) shutdown() {
u.Stop() u.Stop()
} }
// boardMode returns the current board mode.
func (u *UI) boardMode() boardMode { func (u *UI) boardMode() boardMode {
return u.mode return u.mode
} }