feat: delete cards #6
10 changed files with 315 additions and 95 deletions
|
@ -14,7 +14,7 @@ type Board struct {
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open reads the board from the db.
|
// Open reads the board from the database.
|
||||||
// If no board exists then a new one will be created.
|
// If no board exists then a new one will be created.
|
||||||
func Open(path string) (Board, error) {
|
func Open(path string) (Board, error) {
|
||||||
database, err := db.OpenDatabase(path)
|
database, err := db.OpenDatabase(path)
|
||||||
|
@ -65,7 +65,7 @@ func (b *Board) Close() error {
|
||||||
func (b *Board) StatusList() ([]Status, error) {
|
func (b *Board) StatusList() ([]Status, error) {
|
||||||
data, err := db.ReadAll(b.db, db.StatusBucket)
|
data, err := db.ReadAll(b.db, db.StatusBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []Status{}, fmt.Errorf("unable to read the status list, %w", err)
|
return nil, fmt.Errorf("unable to read the status list, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses := make([]Status, len(data))
|
statuses := make([]Status, len(data))
|
||||||
|
@ -78,7 +78,7 @@ func (b *Board) StatusList() ([]Status, error) {
|
||||||
var status Status
|
var status Status
|
||||||
|
|
||||||
if err := decoder.Decode(&status); err != nil {
|
if err := decoder.Decode(&status); err != nil {
|
||||||
return []Status{}, fmt.Errorf("unable to decode data, %w", err)
|
return nil, fmt.Errorf("unable to decode data, %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses[ind] = status
|
statuses[ind] = status
|
||||||
|
@ -90,12 +90,18 @@ func (b *Board) StatusList() ([]Status, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns a single status from the db.
|
// Status returns a single status from the db.
|
||||||
|
// TODO: Add a test case that handles when a status does not exist.
|
||||||
|
// Or use in delete status case.
|
||||||
func (b *Board) Status(id int) (Status, error) {
|
func (b *Board) Status(id int) (Status, error) {
|
||||||
data, err := db.Read(b.db, db.StatusBucket, id)
|
data, err := db.Read(b.db, db.StatusBucket, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Status{}, fmt.Errorf("unable to read status [%d] from the db. %w", id, err)
|
return Status{}, fmt.Errorf("unable to read status [%d] from the db. %w", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data == nil {
|
||||||
|
return Status{}, StatusNotExistError{ID: id}
|
||||||
|
}
|
||||||
|
|
||||||
var status Status
|
var status Status
|
||||||
|
|
||||||
buf := bytes.NewBuffer(data)
|
buf := bytes.NewBuffer(data)
|
||||||
|
@ -197,7 +203,7 @@ type CardArgs struct {
|
||||||
NewContent string
|
NewContent string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCard creates a card in the db.
|
// CreateCard creates a card in the database.
|
||||||
func (b *Board) CreateCard(args CardArgs) (int, error) {
|
func (b *Board) CreateCard(args CardArgs) (int, error) {
|
||||||
statusList, err := b.StatusList()
|
statusList, err := b.StatusList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -205,7 +211,7 @@ func (b *Board) CreateCard(args CardArgs) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(statusList) == 0 {
|
if len(statusList) == 0 {
|
||||||
return 0, statusListEmptyError{}
|
return 0, StatusListEmptyError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
card := Card{
|
card := Card{
|
||||||
|
@ -231,11 +237,16 @@ func (b *Board) CreateCard(args CardArgs) (int, error) {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card returns a Card value from the db.
|
// Card returns a Card value from the database.
|
||||||
func (b *Board) Card(id int) (Card, error) {
|
// TODO: Handle edge case where the card does not exist in the db.
|
||||||
data, err := db.Read(b.db, db.CardBucket, id)
|
func (b *Board) Card(cardID int) (Card, error) {
|
||||||
|
data, err := db.Read(b.db, db.CardBucket, cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Card{}, fmt.Errorf("unable to read card [%d] from the db. %w", id, err)
|
return Card{}, fmt.Errorf("unable to read card [%d] from the db. %w", cardID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data == nil {
|
||||||
|
return Card{}, CardNotExistError{ID: cardID}
|
||||||
}
|
}
|
||||||
|
|
||||||
var card Card
|
var card Card
|
||||||
|
@ -283,7 +294,7 @@ type UpdateCardArgs struct {
|
||||||
CardArgs
|
CardArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCard modifies an existing card in the db.
|
// UpdateCard modifies an existing card in the database.
|
||||||
func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
||||||
card, err := b.Card(args.CardID)
|
card, err := b.Card(args.CardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -305,8 +316,27 @@ func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCard deletes a card from the db.
|
type DeleteCardArgs struct {
|
||||||
// TODO: finish implementation.
|
CardID int
|
||||||
// func (b *Board) DeleteCard(id int) error {
|
StatusID int
|
||||||
// return nil
|
}
|
||||||
// }
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := b.Status(args.StatusID)
|
||||||
|
if err != nil {
|
||||||
|
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 nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
package board
|
package board
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type CardNotExistError struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e CardNotExistError) Error() string {
|
||||||
|
return fmt.Sprintf("card ID '%d' does not exist in the database", e.ID)
|
||||||
|
}
|
||||||
|
|
||||||
// Card represents a card on a Kanban board.
|
// Card represents a card on a Kanban board.
|
||||||
type Card struct {
|
type Card 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"
|
||||||
|
@ -32,8 +33,9 @@ func TestCardLifecycle(t *testing.T) {
|
||||||
initialCardTitle := "A test card."
|
initialCardTitle := "A test card."
|
||||||
initialCardContent := "Ensure that this card is safely stored in the database."
|
initialCardContent := "Ensure that this card is safely stored in the database."
|
||||||
expectedCardID := 1
|
expectedCardID := 1
|
||||||
|
expectedStatusID := 1
|
||||||
|
|
||||||
t.Run("Test Create Card", testCreateCard(kanban, initialCardTitle, initialCardContent, expectedCardID))
|
t.Run("Test Create Card", testCreateCard(kanban, initialCardTitle, initialCardContent, expectedCardID, expectedStatusID))
|
||||||
|
|
||||||
t.Run("Test Read Card", testReadCard(kanban, expectedCardID, initialCardTitle, initialCardContent))
|
t.Run("Test Read Card", testReadCard(kanban, expectedCardID, initialCardTitle, initialCardContent))
|
||||||
|
|
||||||
|
@ -45,9 +47,11 @@ func TestCardLifecycle(t *testing.T) {
|
||||||
modifiedCardContent2 := "Updated card content only."
|
modifiedCardContent2 := "Updated card content only."
|
||||||
|
|
||||||
t.Run("Test Update Card Content", testUpdateCardContent(kanban, expectedCardID, modifiedCardTitle, modifiedCardContent2))
|
t.Run("Test Update Card Content", testUpdateCardContent(kanban, expectedCardID, modifiedCardTitle, modifiedCardContent2))
|
||||||
|
|
||||||
|
t.Run("Test Card Delete", testDeleteCard(kanban, expectedCardID, expectedStatusID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateCard(kanban board.Board, title, content string, wantID int) func(t *testing.T) {
|
func testCreateCard(kanban board.Board, title, content string, expectedCardID, expectedStatusID int) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When the card is created and saved to the database.")
|
t.Log("When the card is created and saved to the database.")
|
||||||
|
|
||||||
|
@ -60,25 +64,23 @@ func testCreateCard(kanban board.Board, title, content string, wantID int) func(
|
||||||
t.Fatalf("ERROR: Unable to create the test card, %s.", err)
|
t.Fatalf("ERROR: Unable to create the test card, %s.", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusList, err := kanban.StatusList()
|
t.Logf("\t\tVerifying that the card's ID is in the expected status...")
|
||||||
|
|
||||||
|
status, err := kanban.Status(expectedStatusID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to run `ReadStatusList`, %s.", err)
|
t.Fatalf("ERROR: Unable to read status '%d', %v", expectedStatusID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(statusList) == 0 {
|
numCardIDs := len(status.CardIds)
|
||||||
t.Fatal("ERROR: The status list appears to be empty.")
|
|
||||||
|
if numCardIDs != 1 {
|
||||||
|
t.Fatalf("ERROR: Unexpected number of cards in status '%d', want: %d, got %d.", expectedStatusID, 1, numCardIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
cardIDs := statusList[0].CardIds
|
if expectedCardID != status.CardIds[0] {
|
||||||
|
t.Errorf("%s\tUnexpected card ID found in the default status, want: %d, got %d.", failure, expectedCardID, status.CardIds[0])
|
||||||
if len(cardIDs) != 1 {
|
|
||||||
t.Fatalf("ERROR: Unexpected number of cards in the default status, want: %d, got %d.", 1, len(cardIDs))
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotID := cardIDs[0]; wantID != gotID {
|
|
||||||
t.Errorf("%s\tUnexpected card ID found in the default status, want: %d, got %d.", failure, wantID, gotID)
|
|
||||||
} else {
|
} else {
|
||||||
t.Logf("%s\tExpected card ID found in the default status, got %d.", success, gotID)
|
t.Logf("%s\tExpected card ID found in the default status, got %d.", success, status.CardIds[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -175,3 +177,51 @@ func testUpdateCardContent(kanban board.Board, cardID int, expectedTitle, newCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDeleteCard(kanban board.Board, cardID, statusID int) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Log("When deleting a card from the database.")
|
||||||
|
|
||||||
|
args := board.DeleteCardArgs{
|
||||||
|
CardID: cardID,
|
||||||
|
StatusID: statusID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := kanban.DeleteCard(args); err != nil {
|
||||||
|
t.Fatalf("ERROR: An error occurred when deleting the card from the database, %v", err)
|
||||||
|
} else {
|
||||||
|
t.Logf("%s\tNo errors occurred when deleting the card from the database.", success)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\tVerifying that the card is removed from the database...")
|
||||||
|
|
||||||
|
_, err := kanban.Card(cardID)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("%s\tDid not receive the expected error when attempting to read the deleted card.", failure)
|
||||||
|
} else {
|
||||||
|
if errors.Is(err, board.CardNotExistError{}) {
|
||||||
|
t.Errorf(
|
||||||
|
"%s\tDid not receive the expected board.CardNotExistError when attempting to retrieve the deleted card, instead got '%v'.",
|
||||||
|
failure,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
t.Logf("%s\tSuccessfully received board.CardNotExistError when attempting to retrieve the deleted card.", success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\tVerifying that the card's ID is removed from the status list...")
|
||||||
|
|
||||||
|
status, err := kanban.Status(statusID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: Unable to read status '%d' from the database; %v", statusID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
numCardIDs := len(status.CardIds)
|
||||||
|
if numCardIDs != 0 {
|
||||||
|
t.Errorf("%s\tUnexpected non-empty list of card IDs in status '%d', got '%+v' card IDs.", failure, statusID, status.CardIds)
|
||||||
|
} else {
|
||||||
|
t.Logf("%s\tThe card ID was successfully removed from the list of card in status '%d'.", success, statusID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package board
|
|
||||||
|
|
||||||
type statusListEmptyError struct{}
|
|
||||||
|
|
||||||
func (e statusListEmptyError) Error() string {
|
|
||||||
return "the status list must not be empty"
|
|
||||||
}
|
|
|
@ -2,8 +2,23 @@ package board
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type StatusListEmptyError struct{}
|
||||||
|
|
||||||
|
func (e StatusListEmptyError) Error() string {
|
||||||
|
return "the status list must not be empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusNotExistError struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e StatusNotExistError) Error() string {
|
||||||
|
return fmt.Sprintf("status ID '%d' does not exist in the database", 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
|
||||||
|
|
|
@ -95,7 +95,7 @@ func testUpdateStatus(kanban board.Board, statusID int, newName string) func(t *
|
||||||
t.Logf("%s\tStatus successfully updated.", success)
|
t.Logf("%s\tStatus successfully updated.", success)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("Verifying the new status.")
|
t.Log("\tVerifying the new status...")
|
||||||
|
|
||||||
status, err := kanban.Status(statusID)
|
status, err := kanban.Status(statusID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,49 +122,40 @@ func testUpdateStatus(kanban board.Board, statusID int, newName string) func(t *
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func testMoveCardToStatus(b board.Board) func(t *testing.T) {
|
func testMoveCardToStatus(kanban board.Board) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When moving a card between statuses.")
|
t.Log("When moving a card between statuses.")
|
||||||
|
|
||||||
title := "Test card."
|
title := "Test card."
|
||||||
|
|
||||||
cardArgs := board.CardArgs{
|
cardArgs := board.CardArgs{NewTitle: title, NewContent: ""}
|
||||||
NewTitle: title,
|
|
||||||
NewContent: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
cardID, err := b.CreateCard(cardArgs)
|
cardID, err := kanban.CreateCard(cardArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusList, err := b.StatusList()
|
statusList, err := kanban.StatusList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
status0 := statusList[0]
|
status0, status2 := statusList[0], statusList[2]
|
||||||
status2 := statusList[2]
|
|
||||||
|
|
||||||
moveArgs := board.MoveToStatusArgs{
|
moveArgs := board.MoveToStatusArgs{CardID: cardID, CurrentStatusID: status0.ID, NextStatusID: status2.ID}
|
||||||
CardID: cardID,
|
|
||||||
CurrentStatusID: status0.ID,
|
|
||||||
NextStatusID: status2.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.MoveToStatus(moveArgs); err != nil {
|
if err := kanban.MoveToStatus(moveArgs); err != nil {
|
||||||
t.Fatalf("ERROR: Unable to move the Card ID from '%s' to '%s', %v", status0.Name, status2.Name, err)
|
t.Fatalf("ERROR: Unable to move the Card ID from '%s' to '%s', %v", status0.Name, status2.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Verifying that the card has moved to '%s'", status2.Name)
|
t.Logf("\tVerifying that the card has moved to '%s'...", status2.Name)
|
||||||
|
|
||||||
statusList, err = b.StatusList()
|
statusList, err = kanban.StatusList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
status0 = statusList[0]
|
status0, status2 = statusList[0], statusList[2]
|
||||||
status2 = statusList[2]
|
|
||||||
|
|
||||||
if len(status0.CardIds) != 0 {
|
if len(status0.CardIds) != 0 {
|
||||||
t.Errorf("%s\tUnexpected number of card IDs found in '%s', want: 0, got: %d", failure, status0.Name, len(status0.CardIds))
|
t.Errorf("%s\tUnexpected number of card IDs found in '%s', want: 0, got: %d", failure, status0.Name, len(status0.CardIds))
|
||||||
|
@ -178,7 +169,7 @@ func testMoveCardToStatus(b board.Board) func(t *testing.T) {
|
||||||
t.Logf("%s\tThe number of card IDs in '%s' is now %d", success, status2.Name, len(status2.CardIds))
|
t.Logf("%s\tThe number of card IDs in '%s' is now %d", success, status2.Name, len(status2.CardIds))
|
||||||
}
|
}
|
||||||
|
|
||||||
card, err := b.Card(status2.CardIds[0])
|
card, err := kanban.Card(status2.CardIds[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to retrieve the card from the database, %v", err)
|
t.Fatalf("ERROR: Unable to retrieve the card from the database, %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,6 +220,29 @@ func WriteMany(database *bolt.DB, bucketName string, items []BoltItem) ([]int, e
|
||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete deletes a Bolt item from a specified bucket.
|
||||||
|
func Delete(db *bolt.DB, bucketName string, itemID int) error {
|
||||||
|
bucketNameBytes := []byte(bucketName)
|
||||||
|
|
||||||
|
if err := db.Update(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket(bucketNameBytes)
|
||||||
|
|
||||||
|
if bucket == nil {
|
||||||
|
return bucketNotExistError{bucket: bucketName}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bucket.Delete([]byte(strconv.Itoa(itemID))); err != nil {
|
||||||
|
return fmt.Errorf("an error occurred when deleting Bolt item '%d', %w", itemID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("error deleting data from the '%s' bucket, %w", bucketName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.
|
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.
|
||||||
// For linux, the default location of the database file is $XDG_DATA_HOME/canal/canal.db. If the XDG_DATA_HOME environment
|
// For linux, the default location of the database file is $XDG_DATA_HOME/canal/canal.db. If the XDG_DATA_HOME environment
|
||||||
// variable is not set then it will default to $HOME/.local/share/canal/canal.db. For all other operating systems the default
|
// variable is not set then it will default to $HOME/.local/share/canal/canal.db. For all other operating systems the default
|
||||||
|
|
|
@ -31,7 +31,7 @@ func TestOpenDataBaseXDGDataDir(t *testing.T) {
|
||||||
|
|
||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
|
|
||||||
wantDB := filepath.Join(testXdgDataHome, "canal", "canal.db")
|
wantDB := filepath.Join(testXdgDataHome, "pelican", "pelican.db")
|
||||||
|
|
||||||
// ensure that the database file exists
|
// ensure that the database file exists
|
||||||
_, err = os.Stat(wantDB)
|
_, err = os.Stat(wantDB)
|
||||||
|
@ -118,18 +118,18 @@ func testReadStatusList(t *testing.T, database *bolt.DB) {
|
||||||
|
|
||||||
got := make([]board.Status, len(data))
|
got := make([]board.Status, len(data))
|
||||||
|
|
||||||
for i, d := range data {
|
for ind, d := range data {
|
||||||
buf := bytes.NewBuffer(d)
|
buf := bytes.NewBuffer(d)
|
||||||
|
|
||||||
decoder := gob.NewDecoder(buf)
|
decoder := gob.NewDecoder(buf)
|
||||||
|
|
||||||
var s board.Status
|
var status board.Status
|
||||||
|
|
||||||
if err := decoder.Decode(&s); err != nil {
|
if err := decoder.Decode(&status); err != nil {
|
||||||
t.Fatalf("An error occurred whilst decoding data, %s", err)
|
t.Fatalf("An error occurred whilst decoding data, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got[i] = s
|
got[ind] = status
|
||||||
}
|
}
|
||||||
|
|
||||||
want := []board.Status{
|
want := []board.Status{
|
||||||
|
@ -327,3 +327,66 @@ func testReadManyCards(t *testing.T, database *bolt.DB, cardIDs []int) {
|
||||||
t.Logf("Expected list of cards read from the database: got %+v", got)
|
t.Logf("Expected list of cards read from the database: got %+v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteOneCard(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var database *bolt.DB
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
projectDir, err := projectRoot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
testDB := filepath.Join(projectDir, "test", "databases", "Database_TestDeleteOneCard.db")
|
||||||
|
os.Remove(testDB)
|
||||||
|
|
||||||
|
if database, err = db.OpenDatabase(testDB); err != nil {
|
||||||
|
t.Fatalf("An error occurred whilst opening the test database %s, %s.", testDB, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = database.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create one card, get card ID.
|
||||||
|
card := board.Card{
|
||||||
|
ID: -1,
|
||||||
|
Title: "Test card",
|
||||||
|
Content: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
cardID, err := db.Write(database, db.CardBucket, &card)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cards, err := db.ReadAll(database, db.CardBucket)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: Unable to read the cards from the database, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
numCards := len(cards)
|
||||||
|
if numCards != 1 {
|
||||||
|
t.Fatalf("ERROR: Unexpected number of cards returned from the card bucket; want 1; got %d", numCards)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Delete(database, db.CardBucket, cardID); err != nil {
|
||||||
|
t.Fatalf("ERROR: Unable to delete the card from the database, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all cards, expect length = 0; error if not 0
|
||||||
|
cards, err = db.ReadAll(database, db.CardBucket)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: Unable to read the cards from the database, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
numCards = len(cards)
|
||||||
|
if numCards != 0 {
|
||||||
|
t.Errorf("%s\tUnexpected number of cards returned from the card bucket; want 0; got %d", failure, numCards)
|
||||||
|
} else {
|
||||||
|
t.Logf("%s\tThe card was successfully deleted from the database.", success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,12 +26,9 @@ func (u *UI) newColumn(status board.Status) (column, error) {
|
||||||
|
|
||||||
cardList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
cardList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
switch event.Rune() {
|
switch event.Rune() {
|
||||||
case 'q':
|
|
||||||
u.pages.ShowPage(quitPageName)
|
|
||||||
u.SetFocus(u.quit)
|
|
||||||
case 'a':
|
case 'a':
|
||||||
u.pages.ShowPage(addPageName)
|
u.pages.ShowPage(addPageName)
|
||||||
u.SetFocus(u.add)
|
u.SetFocus(u.addModal)
|
||||||
case 'h':
|
case 'h':
|
||||||
u.shiftColumnFocus(shiftLeft)
|
u.shiftColumnFocus(shiftLeft)
|
||||||
case 'l':
|
case 'l':
|
||||||
|
@ -53,6 +50,15 @@ func (u *UI) newColumn(status board.Status) (column, error) {
|
||||||
u.SetFocus(u.move)
|
u.SetFocus(u.move)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch event.Key() {
|
||||||
|
case tcell.KeyCtrlQ:
|
||||||
|
u.pages.ShowPage(quitPageName)
|
||||||
|
u.SetFocus(u.quitModal)
|
||||||
|
case tcell.KeyCtrlD:
|
||||||
|
u.pages.ShowPage(deleteCardPageName)
|
||||||
|
u.SetFocus(u.deleteCardModal)
|
||||||
|
}
|
||||||
|
|
||||||
return event
|
return event
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -15,37 +15,40 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mainPageName string = "main"
|
mainPageName string = "main"
|
||||||
quitPageName string = "quit"
|
quitPageName string = "quit"
|
||||||
addPageName string = "add"
|
addPageName string = "add"
|
||||||
movePageName string = "move"
|
movePageName string = "move"
|
||||||
|
deleteCardPageName string = "delete card"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UI struct {
|
type UI struct {
|
||||||
*tview.Application
|
*tview.Application
|
||||||
|
|
||||||
columns []column
|
columns []column
|
||||||
flex *tview.Flex
|
flex *tview.Flex
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
focusedColumn int
|
focusedColumn int
|
||||||
board board.Board
|
board board.Board
|
||||||
quit *tview.Modal
|
quitModal *tview.Modal
|
||||||
add *modalInput
|
addModal *modalInput
|
||||||
move *tview.Flex
|
move *tview.Flex
|
||||||
|
deleteCardModal *tview.Modal
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUI returns a new UI value.
|
// NewUI returns a new UI value.
|
||||||
func NewUI() UI {
|
func NewUI() UI {
|
||||||
ui := UI{
|
ui := UI{
|
||||||
Application: tview.NewApplication(),
|
Application: tview.NewApplication(),
|
||||||
pages: tview.NewPages(),
|
pages: tview.NewPages(),
|
||||||
flex: tview.NewFlex(),
|
flex: tview.NewFlex(),
|
||||||
quit: tview.NewModal(),
|
quitModal: tview.NewModal(),
|
||||||
add: NewModalInput(),
|
addModal: NewModalInput(),
|
||||||
focusedColumn: 0,
|
focusedColumn: 0,
|
||||||
columns: nil,
|
columns: nil,
|
||||||
move: nil,
|
move: nil,
|
||||||
board: board.Board{},
|
board: board.Board{},
|
||||||
|
deleteCardModal: tview.NewModal(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.init()
|
ui.init()
|
||||||
|
@ -58,15 +61,34 @@ func (u *UI) closeBoard() {
|
||||||
_ = u.board.Close()
|
_ = u.board.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteCard deletes a card from the board.
|
||||||
|
func (u *UI) deleteCard() {
|
||||||
|
currentItem := u.columns[u.focusedColumn].cards.GetCurrentItem()
|
||||||
|
_, cardIDText := u.columns[u.focusedColumn].cards.GetItemText(currentItem)
|
||||||
|
cardID, _ := strconv.Atoi(cardIDText)
|
||||||
|
|
||||||
|
statusID := u.columns[u.focusedColumn].statusID
|
||||||
|
|
||||||
|
args := board.DeleteCardArgs{
|
||||||
|
CardID: cardID,
|
||||||
|
StatusID: statusID,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = u.board.DeleteCard(args)
|
||||||
|
}
|
||||||
|
|
||||||
// init initialises the UI.
|
// init initialises the UI.
|
||||||
func (u *UI) init() {
|
func (u *UI) init() {
|
||||||
u.pages.AddPage(mainPageName, u.flex, true, true)
|
u.pages.AddPage(mainPageName, u.flex, true, true)
|
||||||
|
|
||||||
u.initQuitModal()
|
u.initQuitModal()
|
||||||
u.pages.AddPage(quitPageName, u.quit, false, false)
|
u.pages.AddPage(quitPageName, u.quitModal, false, false)
|
||||||
|
|
||||||
u.initAddInputModal()
|
u.initAddInputModal()
|
||||||
u.pages.AddPage(addPageName, u.add, false, false)
|
u.pages.AddPage(addPageName, u.addModal, false, false)
|
||||||
|
|
||||||
|
u.initDeleteCardModal()
|
||||||
|
u.pages.AddPage(deleteCardPageName, u.deleteCardModal, false, false)
|
||||||
|
|
||||||
u.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
u.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
switch event.Rune() {
|
switch event.Rune() {
|
||||||
|
@ -93,24 +115,41 @@ func (u *UI) initAddInputModal() {
|
||||||
u.setColumnFocus()
|
u.setColumnFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
u.add.SetDoneFunc(doneFunc)
|
u.addModal.SetDoneFunc(doneFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDeleteCardModal initialises the modal for deleting cards.
|
||||||
|
func (u *UI) initDeleteCardModal() {
|
||||||
|
doneFunc := func(_ int, buttonLabel string) {
|
||||||
|
if buttonLabel == "Confirm" {
|
||||||
|
u.deleteCard()
|
||||||
|
_ = u.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.pages.HidePage(deleteCardPageName)
|
||||||
|
u.setColumnFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
u.deleteCardModal.SetText("Do you want to delete this card?").
|
||||||
|
AddButtons([]string{"Confirm", "Cancel"}).
|
||||||
|
SetDoneFunc(doneFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initQuitModal initialises the quit modal.
|
// initQuitModal initialises the quit modal.
|
||||||
func (u *UI) initQuitModal() {
|
func (u *UI) initQuitModal() {
|
||||||
quitDoneFunc := func(_ int, buttonLabel string) {
|
doneFunc := func(_ int, buttonLabel string) {
|
||||||
switch buttonLabel {
|
switch buttonLabel {
|
||||||
case "Quit":
|
case "Quit":
|
||||||
u.shutdown()
|
u.shutdown()
|
||||||
default:
|
default:
|
||||||
u.pages.SwitchToPage(mainPageName)
|
u.pages.HidePage(quitPageName)
|
||||||
u.setColumnFocus()
|
u.setColumnFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u.quit.SetText("Do you want to quit the application?").
|
u.quitModal.SetText("Do you want to quit the application?").
|
||||||
AddButtons([]string{"Quit", "Cancel"}).
|
AddButtons([]string{"Quit", "Cancel"}).
|
||||||
SetDoneFunc(quitDoneFunc)
|
SetDoneFunc(doneFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCard creates and saves a new card to the database.
|
// newCard creates and saves a new card to the database.
|
||||||
|
|
Loading…
Reference in a new issue