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
|
||||
}
|
||||
|
||||
// UpdateStatus modifies an existing status in the db.
|
||||
// UpdateStatus modifies an existing status in the database.
|
||||
func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
|
||||
status, err := b.Status(args.StatusID)
|
||||
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 {
|
||||
|
@ -182,10 +182,53 @@ func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO: Finish implementation.
|
||||
// func (b *Board) DeleteStatus() error {
|
||||
// return nil
|
||||
// }
|
||||
// DeleteStatus deletes a status from the database.
|
||||
// A status can only be deleted if it does not contain any cards.
|
||||
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 {
|
||||
CardID int
|
||||
|
@ -346,18 +389,18 @@ type DeleteCardArgs struct {
|
|||
// DeleteCard deletes a card from the database.
|
||||
func (b *Board) DeleteCard(args DeleteCardArgs) error {
|
||||
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)
|
||||
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)
|
||||
|
||||
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
|
||||
|
|
|
@ -19,6 +19,14 @@ func (e StatusNotExistError) Error() string {
|
|||
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.
|
||||
type Status struct {
|
||||
ID int
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package board_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
@ -50,13 +51,28 @@ func TestStatusLifecycle(t *testing.T) {
|
|||
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.Run("Test Status Update (In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
||||
t.Run("Test Status Update (Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
||||
t.Run("Test Status Update (Doing to In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
||||
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.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) {
|
||||
|
@ -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.focusedColumn = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -47,23 +48,28 @@ func (a *App) initCardForm() {
|
|||
a.cardForm.setDoneFunc(doneFunc)
|
||||
}
|
||||
|
||||
// initDeleteCardModal initialises the modal for deleting cards.
|
||||
func (a *App) initDeleteCardModal() {
|
||||
// initDeleteModal initialises the modal for deleting cards or statuses.
|
||||
func (a *App) initDeleteModal() {
|
||||
doneFunc := func(_ int, buttonLabel string) {
|
||||
if buttonLabel == "Confirm" {
|
||||
a.deleteFocusedCard()
|
||||
switch a.mode {
|
||||
case normal:
|
||||
a.deleteFocusedCard()
|
||||
case boardEdit:
|
||||
a.deleteFocusedStatusColumn()
|
||||
}
|
||||
}
|
||||
|
||||
a.pages.HidePage(deleteCardModalPage)
|
||||
a.pages.HidePage(deleteModalPage)
|
||||
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.deleteCardModal.SetBackgroundColor(tcell.ColorBlack.TrueColor())
|
||||
a.deleteCardModal.SetButtonBackgroundColor(tcell.ColorBlueViolet.TrueColor())
|
||||
a.deleteCardModal.SetTextColor(tcell.ColorWhite.TrueColor())
|
||||
a.deleteModal.SetBorder(true).SetBorderColor(tcell.ColorOrangeRed)
|
||||
a.deleteModal.SetBackgroundColor(tcell.ColorBlack.TrueColor())
|
||||
a.deleteModal.SetButtonBackgroundColor(tcell.ColorBlueViolet.TrueColor())
|
||||
a.deleteModal.SetTextColor(tcell.ColorWhite.TrueColor())
|
||||
}
|
||||
|
||||
// initQuitModal initialises the quit modal.
|
||||
|
|
|
@ -87,16 +87,22 @@ func (a *App) edit() {
|
|||
}
|
||||
|
||||
func (a *App) delete() {
|
||||
if a.mode == normal {
|
||||
switch a.mode {
|
||||
case normal:
|
||||
card, ok := a.getFocusedCard()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("Do you want to delete '%s'?", card.Title)
|
||||
a.deleteCardModal.SetText(text)
|
||||
a.pages.ShowPage(deleteCardModalPage)
|
||||
a.SetFocus(a.deleteCardModal)
|
||||
text := fmt.Sprintf("Do you want to delete the CARD %q?", card.Title)
|
||||
a.deleteModal.SetText(text)
|
||||
a.pages.ShowPage(deleteModalPage)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ const (
|
|||
)
|
||||
|
||||
const (
|
||||
mainPage string = "main"
|
||||
quitPage string = "quit"
|
||||
cardFormPage string = "card form"
|
||||
deleteCardModalPage string = "delete card modal"
|
||||
viewPage string = "view"
|
||||
statusFormPage string = "status form"
|
||||
mainPage string = "main"
|
||||
quitPage string = "quit"
|
||||
cardFormPage string = "card form"
|
||||
deleteModalPage string = "delete modal"
|
||||
viewPage string = "view"
|
||||
statusFormPage string = "status form"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -51,7 +51,7 @@ type App struct {
|
|||
modeView *modeView
|
||||
quitModal *tview.Modal
|
||||
cardForm *cardForm
|
||||
deleteCardModal *tview.Modal
|
||||
deleteModal *tview.Modal
|
||||
statusSelection statusSelection
|
||||
cardView *cardView
|
||||
statusbar *statusbar
|
||||
|
@ -75,7 +75,7 @@ func NewApp(path string) (App, error) {
|
|||
focusedColumn: 0,
|
||||
columns: nil,
|
||||
board: kanban,
|
||||
deleteCardModal: tview.NewModal(),
|
||||
deleteModal: tview.NewModal(),
|
||||
mode: normal,
|
||||
modeView: newModeView(),
|
||||
statusSelection: statusSelection{0, 0, 0},
|
||||
|
@ -108,8 +108,8 @@ func (a *App) Init() error {
|
|||
a.initCardForm()
|
||||
a.pages.AddPage(cardFormPage, a.cardForm, false, false)
|
||||
|
||||
a.initDeleteCardModal()
|
||||
a.pages.AddPage(deleteCardModalPage, a.deleteCardModal, false, false)
|
||||
a.initDeleteModal()
|
||||
a.pages.AddPage(deleteModalPage, a.deleteModal, false, false)
|
||||
|
||||
a.initCardView()
|
||||
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})
|
||||
}
|
||||
|
||||
// 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