From ff676e8dc5a0b11137016eb0b5154f0b0d7eb83d Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Tue, 9 Jan 2024 15:51:23 +0000 Subject: [PATCH] feat(ui): add new board mode 'Status Selection' Replace the existing 'move' tview Page with a new board mode called 'Status Selection'. This mode is used to move cards between statuses. Resolves apollo/pelican#11 --- internal/ui/column.go | 93 +++++++++++++----- internal/ui/statusselection.go | 20 ++++ internal/ui/ui.go | 168 ++++++++++++++------------------- 3 files changed, 157 insertions(+), 124 deletions(-) create mode 100644 internal/ui/statusselection.go diff --git a/internal/ui/column.go b/internal/ui/column.go index 2f9d57b..72b8af8 100644 --- a/internal/ui/column.go +++ b/internal/ui/column.go @@ -18,22 +18,23 @@ type card struct { type column struct { *tview.Box - statusID int - cards []card - focusedCard int + statusID int + cards []card + focusedCard int + getBoardModeFunc func() boardMode } -func newColumn(statusID int, statusName string) *column { +func newColumn(statusID int, statusName string, getBoardModeFunc func() boardMode) *column { column := column{ - Box: tview.NewBox(), - statusID: statusID, - cards: nil, - focusedCard: 0, + Box: tview.NewBox(), + statusID: statusID, + cards: nil, + focusedCard: 0, + getBoardModeFunc: getBoardModeFunc, } column.SetBorder(true) column.SetTitle(" " + statusName + " ") - column.SetBorderColor(tcell.ColorOrangeRed.TrueColor()) return &column } @@ -53,17 +54,17 @@ func (c *column) addCard(cardID int, title, created string) { c.cards = append(c.cards, card) } -func (c *column) shiftCardFocus(shift int) { +func (c *column) shiftCardFocus(movement boardMovement) { numCards := len(c.cards) - switch shift { - case shiftToNext: + switch movement { + case next: if c.focusedCard == numCards-1 { c.focusedCard = 0 } else { c.focusedCard++ } - case shiftToPrevious: + case previous: if c.focusedCard == 0 { c.focusedCard = numCards - 1 } else { @@ -105,26 +106,48 @@ func (c *column) InputHandler() func(event *tcell.EventKey, setFocus func(p tvie key, letter := event.Key(), event.Rune() switch { case key == tcell.KeyDown || letter == 'j': - c.shiftCardFocus(shiftToNext) + c.shiftCardFocus(next) case key == tcell.KeyUp || letter == 'k': - c.shiftCardFocus(shiftToPrevious) + c.shiftCardFocus(previous) } }) } func (c *column) Draw(screen tcell.Screen) { + var mode boardMode + + // Update border before calling c.DrawForSubclass() + if c.getBoardModeFunc == nil { + mode = normal + } else { + mode = c.getBoardModeFunc() + } + + if mode == selection { + if c.HasFocus() { + c.SetBorderColor(tcell.ColorTurquoise.TrueColor()) + } else { + c.SetBorderColor(tcell.ColorOrangeRed.TrueColor()) + } + } else { + c.SetBorderColor(tcell.ColorOrangeRed.TrueColor()) + } + c.DrawForSubclass(screen, c) + x, y, width, _ := c.GetInnerRect() xOffset := x + 1 cursor := y gap := 1 - for i := 0; i < len(c.cards); i++ { - var style tcell.Style - var vertLine rune + for ind := 0; ind < len(c.cards); ind++ { + var ( + style tcell.Style + vertLine rune + ) - if c.HasFocus() && i == c.focusedCard { + if c.HasFocus() && ind == c.focusedCard && mode == normal { vertLine = tview.BoxDrawingsHeavyVertical style = tcell.StyleDefault.Foreground(tcell.ColorBlue.TrueColor()) } else { @@ -133,15 +156,15 @@ func (c *column) Draw(screen tcell.Screen) { } // Draw a vertical line down the left side of each card - for cy := cursor; cy < cursor+c.cards[i].height; cy++ { + for cy := cursor; cy < cursor+c.cards[ind].height; cy++ { screen.SetContent(xOffset, cy, vertLine, nil, style) } // Print the card's title - for j := range c.cards[i].wrappedTitle { + for j := range c.cards[ind].wrappedTitle { tview.Print( screen, - c.cards[i].wrappedTitle[j], + c.cards[ind].wrappedTitle[j], xOffset+2, cursor+j, width, @@ -156,14 +179,34 @@ func (c *column) Draw(screen tcell.Screen) { tview.Print( screen, - fmt.Sprintf(cardTextFmt, c.cards[i].id, cal, c.cards[i].created), + fmt.Sprintf(cardTextFmt, c.cards[ind].id, cal, c.cards[ind].created), xOffset+2, - cursor+len(c.cards[i].wrappedTitle), + cursor+len(c.cards[ind].wrappedTitle), width, tview.AlignLeft|tview.AlignTop, tcell.ColorGrey.TrueColor(), ) - cursor = cursor + c.cards[i].height + gap + cursor = cursor + c.cards[ind].height + gap + } + + // draw a 'placeholder card' if in status selection board mode + if mode == selection && c.HasFocus() { + vertLine := tview.BoxDrawingsHeavyVertical + style := tcell.StyleDefault.Foreground(tcell.ColorWhite.TrueColor()) + + for cy := cursor; cy < cursor+1; cy++ { + screen.SetContent(xOffset, cy, vertLine, nil, style) + } + + tview.Print( + screen, + "Move the card to this column", + xOffset+2, + cursor, + width, + tview.AlignLeft|tview.AlignTop, + tcell.ColorWhite.TrueColor(), + ) } } diff --git a/internal/ui/statusselection.go b/internal/ui/statusselection.go new file mode 100644 index 0000000..0fc6085 --- /dev/null +++ b/internal/ui/statusselection.go @@ -0,0 +1,20 @@ +package ui + +import "codeflow.dananglin.me.uk/apollo/pelican/internal/board" + +type statusSelection struct { + cardID int + currentStatusID int + nextStatusID int +} + +func (s statusSelection) moveCard(kanban board.Board) { + moveArgs := board.MoveToStatusArgs{ + CardID: s.cardID, + CurrentStatusID: s.currentStatusID, + NextStatusID: s.nextStatusID, + } + + // TODO: grab error for status line. + _ = kanban.MoveToStatus(moveArgs) +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index d25a6ad..1a33c91 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -2,23 +2,31 @@ package ui import ( "fmt" - "strconv" "codeflow.dananglin.me.uk/apollo/pelican/internal/board" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) +type ( + boardMovement int + boardMode int +) + const ( - shiftToNext int = iota - shiftToPrevious + next boardMovement = iota + previous +) + +const ( + normal boardMode = iota + selection ) const ( mainPage string = "main" quitPage string = "quit" addPage string = "add" - movePage string = "move" deleteCardPage string = "delete card" ) @@ -30,20 +38,21 @@ type UI struct { pages *tview.Pages focusedColumn int board board.Board + mode boardMode quitModal *tview.Modal addModal *modalInput - move *tview.Flex deleteCardModal *tview.Modal + statusSelection statusSelection } // NewUI returns a new UI value. func NewUI(path string) (UI, error) { - b, err := board.Open(path) + kanban, err := board.Open(path) if err != nil { return UI{}, fmt.Errorf("unable to open the project's board; %w", err) } - ui := UI{ + userInterface := UI{ Application: tview.NewApplication(), pages: tview.NewPages(), flex: tview.NewFlex(), @@ -51,16 +60,17 @@ func NewUI(path string) (UI, error) { addModal: NewModalInput(), focusedColumn: 0, columns: nil, - move: nil, - board: b, + board: kanban, deleteCardModal: tview.NewModal(), + mode: normal, + statusSelection: statusSelection{0, 0, 0}, } - if err := ui.init(); err != nil { + if err := userInterface.init(); err != nil { return UI{}, fmt.Errorf("received an error after running the initialisation; %w", err) } - return ui, nil + return userInterface, nil } // closeBoard closes the board. @@ -79,22 +89,46 @@ func (u *UI) init() error { key, letter := event.Key(), event.Rune() switch { - case letter == 'a': - u.pages.ShowPage(addPage) - u.SetFocus(u.addModal) case letter == 'h' || key == tcell.KeyLeft: - u.shiftColumnFocus(shiftToPrevious) + u.shiftColumnFocus(previous) case letter == 'l' || key == tcell.KeyRight: - u.shiftColumnFocus(shiftToNext) + u.shiftColumnFocus(next) + case letter == 'a': + if u.mode == normal { + u.pages.ShowPage(addPage) + u.SetFocus(u.addModal) + } case letter == 'm': - u.pages.ShowPage(movePage) - u.SetFocus(u.move) + if u.mode == normal { + focusedCard := u.columns[u.focusedColumn].focusedCard + u.statusSelection.cardID = u.columns[u.focusedColumn].cards[focusedCard].id + u.statusSelection.currentStatusID = u.columns[u.focusedColumn].statusID + u.mode = selection + } case key == tcell.KeyCtrlD: - u.pages.ShowPage(deleteCardPage) - u.SetFocus(u.deleteCardModal) + if u.mode == normal { + u.pages.ShowPage(deleteCardPage) + u.SetFocus(u.deleteCardModal) + } case key == tcell.KeyCtrlQ: - u.pages.ShowPage(quitPage) - u.SetFocus(u.quitModal) + 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: + if u.mode == selection { + u.statusSelection.nextStatusID = u.columns[u.focusedColumn].statusID + 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 @@ -113,7 +147,7 @@ func (u *UI) init() error { u.SetRoot(u.pages, true) - if err := u.refresh(false, true); err != nil { + if err := u.refresh(false); err != nil { return fmt.Errorf("error refreshing the board; %w", err) } @@ -139,7 +173,7 @@ func (u *UI) initDeleteCardModal() { doneFunc := func(_ int, buttonLabel string) { if buttonLabel == "Confirm" { u.deleteCard() - _ = u.refresh(true, false) + _ = u.refresh(true) } u.pages.HidePage(deleteCardPage) @@ -193,71 +227,7 @@ func (u *UI) newCard(title, content string) error { return fmt.Errorf("unable to create card, %w", err) } - _ = u.refresh(false, false) - - return nil -} - -func (u *UI) updateMovePage() error { - if u.pages.HasPage(movePage) { - u.pages.RemovePage(movePage) - } - - move := tview.NewFlex() - - statusSelection := tview.NewList() - statusSelection.SetBorder(true) - statusSelection.ShowSecondaryText(false) - statusSelection.SetHighlightFullLine(true) - statusSelection.SetSelectedFocusOnly(true) - statusSelection.SetWrapAround(false) - - doneFunc := func() { - u.pages.HidePage(movePage) - u.setColumnFocus() - } - - statusSelection.SetDoneFunc(doneFunc) - - selectedFunc := func(_ int, _, secondary string, _ rune) { - currentStatusID := u.columns[u.focusedColumn].statusID - - nextStatusID, err := strconv.Atoi(secondary) - if err != nil { - nextStatusID = 0 - } - - focusedCard := u.columns[u.focusedColumn].focusedCard - cardID := u.columns[u.focusedColumn].cards[focusedCard].id - - args := board.MoveToStatusArgs{ - CardID: cardID, - CurrentStatusID: currentStatusID, - NextStatusID: nextStatusID, - } - _ = u.board.MoveToStatus(args) - - u.pages.HidePage(movePage) - _ = u.refresh(false, false) - } - - statusSelection.SetSelectedFunc(selectedFunc) - - statusList, err := u.board.StatusList() - if err != nil { - return fmt.Errorf("unable to get the list of statuses; %w", err) - } - - for _, status := range statusList { - id := strconv.Itoa(status.ID) - statusSelection.AddItem(fmt.Sprintf("\u25C9 %s", status.Name), id, 0, nil) - } - - move.AddItem(statusSelection, 0, 1, true) - - u.move = move - - u.pages.AddPage(movePage, move, false, false) + _ = u.refresh(false) return nil } @@ -273,7 +243,7 @@ func (u *UI) initColumns() error { columns := make([]*column, len(statusList)) for i := range statusList { - column := newColumn(statusList[i].ID, statusList[i].Name) + column := newColumn(statusList[i].ID, statusList[i].Name, u.boardMode) u.flex.AddItem(column, 50, 1, true) columns[i] = column } @@ -301,15 +271,15 @@ func (u *UI) updateColumn(index int) error { return nil } -func (u *UI) shiftColumnFocus(s int) { - switch s { - case shiftToNext: +func (u *UI) shiftColumnFocus(movement boardMovement) { + switch movement { + case next: if u.focusedColumn == len(u.columns)-1 { u.focusedColumn = 0 } else { u.focusedColumn++ } - case shiftToPrevious: + case previous: if u.focusedColumn == 0 { u.focusedColumn = len(u.columns) - 1 } else { @@ -325,7 +295,7 @@ func (u *UI) setColumnFocus() { } // refresh refreshes the UI. -func (u *UI) refresh(updateFocusedColumnOnly, updateMovePage bool) error { +func (u *UI) refresh(updateFocusedColumnOnly bool) error { if updateFocusedColumnOnly { if err := u.updateColumn(u.focusedColumn); err != nil { return err @@ -336,10 +306,6 @@ func (u *UI) refresh(updateFocusedColumnOnly, updateMovePage bool) error { } } - if updateMovePage { - _ = u.updateMovePage() - } - u.setColumnFocus() return nil @@ -350,3 +316,7 @@ func (u *UI) shutdown() { u.closeBoard() u.Stop() } + +func (u *UI) boardMode() boardMode { + return u.mode +}