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
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:
struct-patterns:
- 'forge.dananglin.me.uk/code/dananglin/pelican.Status'

View file

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

View file

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