feat(ui): add support for editing cards
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:
parent
f956b7da59
commit
d167039176
3 changed files with 107 additions and 58 deletions
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue