feat: add support for deleting status columns #30
6 changed files with 176 additions and 36 deletions
|
@ -160,11 +160,11 @@ type UpdateStatusArgs struct {
|
||||||
StatusArgs
|
StatusArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateStatus modifies an existing status in the db.
|
// UpdateStatus modifies an existing status in the database.
|
||||||
func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
|
func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
|
||||||
status, err := b.Status(args.StatusID)
|
status, err := b.Status(args.StatusID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to retrieve the status from the db. %w", err)
|
return fmt.Errorf("unable to retrieve the status from the database. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(args.Name) > 0 {
|
if len(args.Name) > 0 {
|
||||||
|
@ -182,10 +182,53 @@ func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Finish implementation.
|
// DeleteStatus deletes a status from the database.
|
||||||
// func (b *Board) DeleteStatus() error {
|
// A status can only be deleted if it does not contain any cards.
|
||||||
// return nil
|
func (b *Board) DeleteStatus(statusID int) error {
|
||||||
// }
|
status, err := b.Status(statusID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to retrieve the status from the database; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(status.CardIds) > 0 {
|
||||||
|
return StatusNotEmptyError{ID: statusID}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Delete(b.db, db.StatusBucket, statusID); err != nil {
|
||||||
|
return fmt.Errorf("unable to delete the status from the database; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.normaliseStatusesPositionValues(); err != nil {
|
||||||
|
return fmt.Errorf("unable to normalise the statuses position values; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// normaliseStatusesPositionValues retrieves the ordered list of statuses from the database and sets
|
||||||
|
// each status' positional value based on its position in the list.
|
||||||
|
func (b *Board) normaliseStatusesPositionValues() error {
|
||||||
|
statuses, err := b.StatusList()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the list of statuses; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, status := range statuses {
|
||||||
|
updateArgs := UpdateStatusArgs{
|
||||||
|
StatusID: status.ID,
|
||||||
|
StatusArgs: StatusArgs{
|
||||||
|
Name: "",
|
||||||
|
Position: i + 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.UpdateStatus(updateArgs); err != nil {
|
||||||
|
return fmt.Errorf("unable to update the status %q; %w", status.Name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type MoveToStatusArgs struct {
|
type MoveToStatusArgs struct {
|
||||||
CardID int
|
CardID int
|
||||||
|
@ -346,18 +389,18 @@ type DeleteCardArgs struct {
|
||||||
// DeleteCard deletes a card from the database.
|
// DeleteCard deletes a card from the database.
|
||||||
func (b *Board) DeleteCard(args DeleteCardArgs) error {
|
func (b *Board) DeleteCard(args DeleteCardArgs) error {
|
||||||
if err := db.Delete(b.db, db.CardBucket, args.CardID); err != nil {
|
if err := db.Delete(b.db, db.CardBucket, args.CardID); err != nil {
|
||||||
return fmt.Errorf("unable to delete the card from the database, %w", err)
|
return fmt.Errorf("unable to delete the card from the database; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
status, err := b.Status(args.StatusID)
|
status, err := b.Status(args.StatusID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read Status '%d' from the database, %w", args.StatusID, err)
|
return fmt.Errorf("unable to read Status '%d' from the database; %w", args.StatusID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
status.RemoveCardID(args.CardID)
|
status.RemoveCardID(args.CardID)
|
||||||
|
|
||||||
if _, err := db.Write(b.db, db.StatusBucket, &status); err != nil {
|
if _, err := db.Write(b.db, db.StatusBucket, &status); err != nil {
|
||||||
return fmt.Errorf("unable to update the status in the database, %w", err)
|
return fmt.Errorf("unable to update the status in the database; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -19,6 +19,14 @@ func (e StatusNotExistError) Error() string {
|
||||||
return fmt.Sprintf("status ID '%d' does not exist in the database", e.ID)
|
return fmt.Sprintf("status ID '%d' does not exist in the database", e.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StatusNotEmptyError struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StatusNotEmptyError) Error() string {
|
||||||
|
return fmt.Sprintf("status ID '%d' must contain no cards before deletion", e.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// Status represents the status of the Kanban board.
|
// Status represents the status of the Kanban board.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
ID int
|
ID int
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package board_test
|
package board_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -50,13 +51,28 @@ func TestStatusLifecycle(t *testing.T) {
|
||||||
t.Run("Test Read Status (Next)", testReadStatus(kanban, statusNextExpectedID, statusNextName, statusNextExpectedBoardPosition))
|
t.Run("Test Read Status (Next)", testReadStatus(kanban, statusNextExpectedID, statusNextName, statusNextExpectedBoardPosition))
|
||||||
|
|
||||||
t.Logf("Let us now update the names of two of our statuses...")
|
t.Logf("Let us now update the names of two of our statuses...")
|
||||||
t.Run("Test Status Update (In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
t.Run("Test Status Update (Doing to In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
||||||
t.Run("Test Status Update (Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
t.Run("Test Status Update (To Do to Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
||||||
|
|
||||||
// (TODO: Rearranging statuses still needs to be implemented) Rearrange the board so the order is To Do, On Hold, In Progress, Done
|
// (TODO: Rearranging statuses still needs to be implemented)
|
||||||
|
// Rearrange the board so the order is: Backlog, Next, In Progress, On Hold, Done
|
||||||
|
|
||||||
t.Logf("Let us now try moving a card from one status to another...")
|
t.Logf("Let us now try moving a card from one status to another...")
|
||||||
t.Run("Test Move Card To Status", testMoveCardToStatus(kanban))
|
t.Run("Test Move Card To Status", testMoveCardToStatus(kanban))
|
||||||
|
|
||||||
|
// TODO: This needs to be updated when we re-arrange the board.
|
||||||
|
expectedPositions := map[int]string{
|
||||||
|
1: "Backlog",
|
||||||
|
2: "In Progress",
|
||||||
|
3: "Done",
|
||||||
|
4: "Next",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Let us now delete the 'On Hold' status from the database...")
|
||||||
|
t.Run("Test Delete Status (On Hold)", testDeleteEmptyStatus(kanban, statusOnHoldExpectedID, expectedPositions))
|
||||||
|
|
||||||
|
t.Logf("Additionally, let us try to delete a status that contains a card...")
|
||||||
|
t.Run("Test Delete a non-empty status", testDeleteNonEmptyStatus(kanban, 3))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateStatus(kanban board.Board, name string, position int) func(t *testing.T) {
|
func testCreateStatus(kanban board.Board, name string, position int) func(t *testing.T) {
|
||||||
|
@ -202,3 +218,47 @@ func testMoveCardToStatus(kanban board.Board) func(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDeleteEmptyStatus(kanban board.Board, statusID int, wantPositions map[int]string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Log("When deleting an empty status.")
|
||||||
|
|
||||||
|
err := kanban.DeleteStatus(statusID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: an error was received when attempting to delete the status from the database; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
statuses, err := kanban.StatusList()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: an error was received when attempting to get the list of statuses from the database; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotPositions := make(map[int]string)
|
||||||
|
for _, status := range statuses {
|
||||||
|
gotPositions[status.Position] = status.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(wantPositions, gotPositions) {
|
||||||
|
t.Errorf("%s\tUnexpected positions received from the database; want: %v, got %v", failure, wantPositions, gotPositions)
|
||||||
|
} else {
|
||||||
|
t.Logf("%s\tExpected positions received from the database; got %v", success, gotPositions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDeleteNonEmptyStatus(kanban board.Board, statusID int) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Log("When deleting a non-empty status.")
|
||||||
|
|
||||||
|
err := kanban.DeleteStatus(statusID)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
t.Errorf("%s\tExpected an error for deleting a non-empty status but received 'nil'", failure)
|
||||||
|
case errors.As(err, &board.StatusNotEmptyError{}):
|
||||||
|
t.Logf("%s\tExpected error received after attempting to delete a non-empty status; got: '%v'", success, err)
|
||||||
|
default:
|
||||||
|
t.Errorf("%s\tUnexpected error received after attempting to delete a non-empty status; got: '%v'", failure, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ func (a *App) initColumns() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
a.columns = columns
|
a.columns = columns
|
||||||
|
a.focusedColumn = 0
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -47,23 +48,28 @@ func (a *App) initCardForm() {
|
||||||
a.cardForm.setDoneFunc(doneFunc)
|
a.cardForm.setDoneFunc(doneFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDeleteCardModal initialises the modal for deleting cards.
|
// initDeleteModal initialises the modal for deleting cards or statuses.
|
||||||
func (a *App) initDeleteCardModal() {
|
func (a *App) initDeleteModal() {
|
||||||
doneFunc := func(_ int, buttonLabel string) {
|
doneFunc := func(_ int, buttonLabel string) {
|
||||||
if buttonLabel == "Confirm" {
|
if buttonLabel == "Confirm" {
|
||||||
|
switch a.mode {
|
||||||
|
case normal:
|
||||||
a.deleteFocusedCard()
|
a.deleteFocusedCard()
|
||||||
|
case boardEdit:
|
||||||
|
a.deleteFocusedStatusColumn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a.pages.HidePage(deleteCardModalPage)
|
a.pages.HidePage(deleteModalPage)
|
||||||
a.setColumnFocus()
|
a.setColumnFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
a.deleteCardModal.AddButtons([]string{"Confirm", "Cancel"}).SetDoneFunc(doneFunc)
|
a.deleteModal.AddButtons([]string{"Cancel", "Confirm"}).SetDoneFunc(doneFunc)
|
||||||
|
|
||||||
a.deleteCardModal.SetBorder(true).SetBorderColor(tcell.ColorOrangeRed)
|
a.deleteModal.SetBorder(true).SetBorderColor(tcell.ColorOrangeRed)
|
||||||
a.deleteCardModal.SetBackgroundColor(tcell.ColorBlack.TrueColor())
|
a.deleteModal.SetBackgroundColor(tcell.ColorBlack.TrueColor())
|
||||||
a.deleteCardModal.SetButtonBackgroundColor(tcell.ColorBlueViolet.TrueColor())
|
a.deleteModal.SetButtonBackgroundColor(tcell.ColorBlueViolet.TrueColor())
|
||||||
a.deleteCardModal.SetTextColor(tcell.ColorWhite.TrueColor())
|
a.deleteModal.SetTextColor(tcell.ColorWhite.TrueColor())
|
||||||
}
|
}
|
||||||
|
|
||||||
// initQuitModal initialises the quit modal.
|
// initQuitModal initialises the quit modal.
|
||||||
|
|
|
@ -87,16 +87,22 @@ func (a *App) edit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) delete() {
|
func (a *App) delete() {
|
||||||
if a.mode == normal {
|
switch a.mode {
|
||||||
|
case normal:
|
||||||
card, ok := a.getFocusedCard()
|
card, ok := a.getFocusedCard()
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
text := fmt.Sprintf("Do you want to delete '%s'?", card.Title)
|
text := fmt.Sprintf("Do you want to delete the CARD %q?", card.Title)
|
||||||
a.deleteCardModal.SetText(text)
|
a.deleteModal.SetText(text)
|
||||||
a.pages.ShowPage(deleteCardModalPage)
|
a.pages.ShowPage(deleteModalPage)
|
||||||
a.SetFocus(a.deleteCardModal)
|
a.SetFocus(a.deleteModal)
|
||||||
|
case boardEdit:
|
||||||
|
text := fmt.Sprintf("Do you want to delete the STATUS %q?", a.focusedStatusName())
|
||||||
|
a.deleteModal.SetText(text)
|
||||||
|
a.pages.ShowPage(deleteModalPage)
|
||||||
|
a.SetFocus(a.deleteModal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ const (
|
||||||
mainPage string = "main"
|
mainPage string = "main"
|
||||||
quitPage string = "quit"
|
quitPage string = "quit"
|
||||||
cardFormPage string = "card form"
|
cardFormPage string = "card form"
|
||||||
deleteCardModalPage string = "delete card modal"
|
deleteModalPage string = "delete modal"
|
||||||
viewPage string = "view"
|
viewPage string = "view"
|
||||||
statusFormPage string = "status form"
|
statusFormPage string = "status form"
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ type App struct {
|
||||||
modeView *modeView
|
modeView *modeView
|
||||||
quitModal *tview.Modal
|
quitModal *tview.Modal
|
||||||
cardForm *cardForm
|
cardForm *cardForm
|
||||||
deleteCardModal *tview.Modal
|
deleteModal *tview.Modal
|
||||||
statusSelection statusSelection
|
statusSelection statusSelection
|
||||||
cardView *cardView
|
cardView *cardView
|
||||||
statusbar *statusbar
|
statusbar *statusbar
|
||||||
|
@ -75,7 +75,7 @@ func NewApp(path string) (App, error) {
|
||||||
focusedColumn: 0,
|
focusedColumn: 0,
|
||||||
columns: nil,
|
columns: nil,
|
||||||
board: kanban,
|
board: kanban,
|
||||||
deleteCardModal: tview.NewModal(),
|
deleteModal: tview.NewModal(),
|
||||||
mode: normal,
|
mode: normal,
|
||||||
modeView: newModeView(),
|
modeView: newModeView(),
|
||||||
statusSelection: statusSelection{0, 0, 0},
|
statusSelection: statusSelection{0, 0, 0},
|
||||||
|
@ -108,8 +108,8 @@ func (a *App) Init() error {
|
||||||
a.initCardForm()
|
a.initCardForm()
|
||||||
a.pages.AddPage(cardFormPage, a.cardForm, false, false)
|
a.pages.AddPage(cardFormPage, a.cardForm, false, false)
|
||||||
|
|
||||||
a.initDeleteCardModal()
|
a.initDeleteModal()
|
||||||
a.pages.AddPage(deleteCardModalPage, a.deleteCardModal, false, false)
|
a.pages.AddPage(deleteModalPage, a.deleteModal, false, false)
|
||||||
|
|
||||||
a.initCardView()
|
a.initCardView()
|
||||||
a.pages.AddPage(viewPage, a.cardView, false, false)
|
a.pages.AddPage(viewPage, a.cardView, false, false)
|
||||||
|
@ -366,3 +366,20 @@ func (a *App) saveNewStatus(name string) {
|
||||||
|
|
||||||
a.refresh(refreshArgs{updateFocusedColumnOnly: false, reinitialiseColumns: true})
|
a.refresh(refreshArgs{updateFocusedColumnOnly: false, reinitialiseColumns: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteFocusedStatusColumn deletes the focused status column from the database.
|
||||||
|
// If the column is not empty, the column will not be deleted and an error will
|
||||||
|
// be shown in the status bar.
|
||||||
|
func (a *App) deleteFocusedStatusColumn() {
|
||||||
|
statusID := a.focusedStatusID()
|
||||||
|
|
||||||
|
if err := a.board.DeleteStatus(statusID); err != nil {
|
||||||
|
a.statusbar.displayMessage(errorLevel, fmt.Sprintf("Failed to delete the status column: %v.", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.statusbar.displayMessage(infoLevel, "Status deleted successfully.")
|
||||||
|
|
||||||
|
a.refresh(refreshArgs{updateFocusedColumnOnly: false, reinitialiseColumns: true})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue