feat: move a card between statuses #3
8 changed files with 664 additions and 250 deletions
|
@ -48,12 +48,17 @@ func Open(path string) (Board, error) {
|
|||
return board, nil
|
||||
}
|
||||
|
||||
// Close closes the project's Kanban board.
|
||||
func (b *Board) Close() error {
|
||||
if b.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.db.Close()
|
||||
if err := b.db.Close(); err != nil {
|
||||
return fmt.Errorf("error closing the database, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StatusList returns the ordered list of statuses from the database.
|
||||
|
@ -65,18 +70,18 @@ func (b *Board) StatusList() ([]Status, error) {
|
|||
|
||||
statuses := make([]Status, len(data))
|
||||
|
||||
for i, d := range data {
|
||||
for ind, d := range data {
|
||||
buf := bytes.NewBuffer(d)
|
||||
|
||||
decoder := gob.NewDecoder(buf)
|
||||
|
||||
var s Status
|
||||
var status Status
|
||||
|
||||
if err := decoder.Decode(&s); err != nil {
|
||||
if err := decoder.Decode(&status); err != nil {
|
||||
return []Status{}, fmt.Errorf("unable to decode data, %w", err)
|
||||
}
|
||||
|
||||
statuses[i] = s
|
||||
statuses[ind] = status
|
||||
}
|
||||
|
||||
sort.Sort(ByStatusOrder(statuses))
|
||||
|
@ -84,23 +89,106 @@ func (b *Board) StatusList() ([]Status, error) {
|
|||
return statuses, nil
|
||||
}
|
||||
|
||||
// TODO: Finish implementation.
|
||||
func (b *Board) ReadStatus() (Status, error) {
|
||||
return Status{}, nil
|
||||
// Status returns a single status from the database.
|
||||
func (b *Board) Status(id int) (Status, error) {
|
||||
data, err := database.Read(b.db, database.StatusBucket, id)
|
||||
if err != nil {
|
||||
return Status{}, fmt.Errorf("unable to read status [%d] from the database, %w", id, err)
|
||||
}
|
||||
|
||||
var status Status
|
||||
|
||||
buf := bytes.NewBuffer(data)
|
||||
|
||||
decoder := gob.NewDecoder(buf)
|
||||
|
||||
if err := decoder.Decode(&status); err != nil {
|
||||
return Status{}, fmt.Errorf("unable to decode data into a Status object, %w", err)
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
type StatusArgs struct {
|
||||
Name string
|
||||
Order int
|
||||
}
|
||||
|
||||
// CreateStatus creates a status in the database.
|
||||
func (b *Board) CreateStatus(args StatusArgs) error {
|
||||
status := Status{
|
||||
ID: -1,
|
||||
Name: args.Name,
|
||||
Order: args.Order,
|
||||
CardIds: nil,
|
||||
}
|
||||
|
||||
if _, err := database.Write(b.db, database.StatusBucket, &status); err != nil {
|
||||
return fmt.Errorf("unable to write the status to the database, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateStatusArgs struct {
|
||||
StatusID int
|
||||
StatusArgs
|
||||
}
|
||||
|
||||
// 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 database, %w", err)
|
||||
}
|
||||
|
||||
if len(args.Name) > 0 {
|
||||
status.Name = args.Name
|
||||
}
|
||||
|
||||
if args.Order > 0 {
|
||||
status.Order = args.Order
|
||||
}
|
||||
|
||||
if _, err := database.Write(b.db, database.StatusBucket, &status); err != nil {
|
||||
return fmt.Errorf("unable to write the status to the database, %w", err)
|
||||
}
|
||||
|
||||
// TODO: Finish implementation.
|
||||
func (b *Board) NewStatus() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Finish implementation.
|
||||
func (b *Board) UpdateStatus() error {
|
||||
return nil
|
||||
// func (b *Board) DeleteStatus() error {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
type MoveToStatusArgs struct {
|
||||
CardID int
|
||||
CurrentStatusID int
|
||||
NextStatusID int
|
||||
}
|
||||
|
||||
// MoveToStatus moves a card between statuses.
|
||||
func (b *Board) MoveToStatus(args MoveToStatusArgs) error {
|
||||
currentStatus, err := b.Status(args.CurrentStatusID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get the card's current status [%d], %w", args.CurrentStatusID, err)
|
||||
}
|
||||
|
||||
nextStatus, err := b.Status(args.NextStatusID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get the card's next status [%d], %w", args.NextStatusID, err)
|
||||
}
|
||||
|
||||
nextStatus.AddCardID(args.CardID)
|
||||
currentStatus.RemoveCardID(args.CardID)
|
||||
|
||||
boltItems := []database.BoltItem{¤tStatus, &nextStatus}
|
||||
|
||||
if _, err := database.WriteMany(b.db, database.StatusBucket, boltItems); err != nil {
|
||||
return fmt.Errorf("unable to update the statuses in the database, %w", err)
|
||||
}
|
||||
|
||||
// TODO: Finish implementation.
|
||||
func (b *Board) DeleteStatus() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -110,14 +198,14 @@ type CardArgs struct {
|
|||
}
|
||||
|
||||
// CreateCard creates a card in the database.
|
||||
func (b *Board) CreateCard(args CardArgs) error {
|
||||
func (b *Board) CreateCard(args CardArgs) (int, error) {
|
||||
statusList, err := b.StatusList()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read the status list, %w", err)
|
||||
return 0, fmt.Errorf("unable to read the status list, %w", err)
|
||||
}
|
||||
|
||||
if len(statusList) == 0 {
|
||||
return statusListEmptyError{}
|
||||
return 0, statusListEmptyError{}
|
||||
}
|
||||
|
||||
card := Card{
|
||||
|
@ -128,18 +216,19 @@ func (b *Board) CreateCard(args CardArgs) error {
|
|||
|
||||
cardID, err := database.Write(b.db, database.CardBucket, &card)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write card to the database, %w", err)
|
||||
return 0, fmt.Errorf("unable to write card to the database, %w", err)
|
||||
}
|
||||
|
||||
initialStatus := statusList[0]
|
||||
|
||||
initialStatus.AddCardID(cardID)
|
||||
|
||||
if _, err := database.Write(b.db, database.StatusBucket, &initialStatus); err != nil {
|
||||
return fmt.Errorf("unable to write the %s status to the database, %w", initialStatus.Name, err)
|
||||
id, err := database.Write(b.db, database.StatusBucket, &initialStatus)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to write the %s status to the database, %w", initialStatus.Name, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Card returns a Card value from the database.
|
||||
|
@ -172,18 +261,18 @@ func (b *Board) CardList(ids []int) ([]Card, error) {
|
|||
|
||||
cards := make([]Card, len(data))
|
||||
|
||||
for i, d := range data {
|
||||
for ind, d := range data {
|
||||
buf := bytes.NewBuffer(d)
|
||||
|
||||
decoder := gob.NewDecoder(buf)
|
||||
|
||||
var c Card
|
||||
var card Card
|
||||
|
||||
if err := decoder.Decode(&c); err != nil {
|
||||
if err := decoder.Decode(&card); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode data, %w", err)
|
||||
}
|
||||
|
||||
cards[i] = c
|
||||
cards[ind] = card
|
||||
}
|
||||
|
||||
return cards, nil
|
||||
|
@ -216,20 +305,8 @@ func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type UpdateCardStatusArgs struct {
|
||||
CardID int
|
||||
OldStatusID int
|
||||
NewStatusID int
|
||||
}
|
||||
|
||||
// UpdateCardStatus moves a card between statuses.
|
||||
// TODO: finish implementation.
|
||||
func (b *Board) UpdateCardStatus(args UpdateCardStatusArgs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCard deletes a card from the database.
|
||||
// TODO: finish implementation.
|
||||
func (b *Board) DeleteCard(id int) error {
|
||||
return nil
|
||||
}
|
||||
//func (b *Board) DeleteCard(id int) error {
|
||||
// return nil
|
||||
//}
|
||||
|
|
|
@ -1,178 +0,0 @@
|
|||
package board_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/canal/internal/board"
|
||||
)
|
||||
|
||||
func TestCardLifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
projectDir, err := projectRoot()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
testDBPath := filepath.Join(projectDir, "test", "databases", "Board_TestCardLifecycle.db")
|
||||
os.Remove(testDBPath)
|
||||
|
||||
b, err := board.Open(testDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to open the test database %s, %s.", testDBPath, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = b.Close()
|
||||
}()
|
||||
|
||||
initialCardTitle := "A test card."
|
||||
initialCardContent := "Ensure that this card is safely stored in the database."
|
||||
expectedCardID := 1
|
||||
|
||||
testCreateCard(t, b, initialCardTitle, initialCardContent, expectedCardID)
|
||||
testReadCard(t, b, expectedCardID, initialCardTitle, initialCardContent)
|
||||
|
||||
modifiedCardTitle := "Test card updated."
|
||||
modifiedCardContent1 := "Ensure that this card is safely updated in the database."
|
||||
|
||||
testUpdateCard(t, b, expectedCardID, modifiedCardTitle, modifiedCardContent1)
|
||||
|
||||
modifiedCardContent2 := "Updated card content only."
|
||||
|
||||
testUpdateCardContent(t, b, expectedCardID, modifiedCardTitle, modifiedCardContent2)
|
||||
}
|
||||
|
||||
func testCreateCard(t *testing.T, b board.Board, title, content string, wantID int) {
|
||||
t.Helper()
|
||||
|
||||
args := board.CardArgs{
|
||||
NewTitle: title,
|
||||
NewContent: content,
|
||||
}
|
||||
|
||||
if err := b.CreateCard(args); err != nil {
|
||||
t.Fatalf("Unable to create the test card, %s.", err)
|
||||
}
|
||||
|
||||
statusList, err := b.StatusList()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to run `ReadStatusList`, %s.", err)
|
||||
}
|
||||
|
||||
if len(statusList) == 0 {
|
||||
t.Fatal("The status list appears to be empty.")
|
||||
}
|
||||
|
||||
cardIDs := statusList[0].CardIds
|
||||
|
||||
if len(cardIDs) != 1 {
|
||||
t.Fatalf("Unexpected number of cards in the default status, want: %d, got %d.", 1, len(cardIDs))
|
||||
}
|
||||
|
||||
if gotID := cardIDs[0]; wantID != gotID {
|
||||
t.Errorf("Unexpected card ID found in the default status, want: %d, got %d.", wantID, gotID)
|
||||
} else {
|
||||
t.Logf("Expected card ID found in the default status, got %d.", gotID)
|
||||
}
|
||||
}
|
||||
|
||||
func testReadCard(t *testing.T, b board.Board, cardID int, wantTitle, wantContent string) {
|
||||
t.Helper()
|
||||
|
||||
card, err := b.Card(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read test card, %s.", err)
|
||||
}
|
||||
|
||||
if card.Title != wantTitle {
|
||||
t.Errorf("Unexpected card title received, want: %s, got: %s.", wantTitle, card.Title)
|
||||
} else {
|
||||
t.Logf("Expected card title received, got: %s.", card.Title)
|
||||
}
|
||||
|
||||
if card.Content != wantContent {
|
||||
t.Errorf("Unexpected card content received, want: %s, got: %s.", wantContent, card.Content)
|
||||
} else {
|
||||
t.Logf("Expected card title received, got: %s.", card.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateCard(t *testing.T, b board.Board, cardID int, newTitle, newContent string) {
|
||||
t.Helper()
|
||||
|
||||
args := board.UpdateCardArgs{
|
||||
CardID: cardID,
|
||||
CardArgs: board.CardArgs{
|
||||
NewTitle: newTitle,
|
||||
NewContent: newContent,
|
||||
},
|
||||
}
|
||||
|
||||
if err := b.UpdateCard(args); err != nil {
|
||||
t.Fatalf("Unable to update the test card, %s", err)
|
||||
}
|
||||
|
||||
got, err := b.Card(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read the modified test card, %s", err)
|
||||
}
|
||||
|
||||
want := board.Card{
|
||||
ID: cardID,
|
||||
Title: newTitle,
|
||||
Content: newContent,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Unexpected card read from the database: want %+v, got %+v", want, got)
|
||||
} else {
|
||||
t.Logf("Expected card read from the database: got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateCardContent(t *testing.T, b board.Board, cardID int, expectedTitle, newContent string) {
|
||||
t.Helper()
|
||||
|
||||
args := board.UpdateCardArgs{
|
||||
CardID: cardID,
|
||||
CardArgs: board.CardArgs{
|
||||
NewTitle: "",
|
||||
NewContent: newContent,
|
||||
},
|
||||
}
|
||||
|
||||
if err := b.UpdateCard(args); err != nil {
|
||||
t.Fatalf("Unable to update the test card, %s", err)
|
||||
}
|
||||
|
||||
got, err := b.Card(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to read the modified test card, %s", err)
|
||||
}
|
||||
|
||||
want := board.Card{
|
||||
ID: cardID,
|
||||
Title: expectedTitle,
|
||||
Content: newContent,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Unexpected card read from the database, want: %+v, got: %+v", want, got)
|
||||
} else {
|
||||
t.Logf("Expected card read from the database, got: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func projectRoot() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get the current working directory, %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(cwd, "..", ".."), nil
|
||||
}
|
177
internal/board/card_lifecycle_test.go
Normal file
177
internal/board/card_lifecycle_test.go
Normal file
|
@ -0,0 +1,177 @@
|
|||
package board_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/canal/internal/board"
|
||||
)
|
||||
|
||||
func TestCardLifecycle(t *testing.T) {
|
||||
t.Log("Testing the lifecycle of a card.")
|
||||
|
||||
projectDir, err := projectRoot()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
testDBPath := filepath.Join(projectDir, "test", "databases", "Board_TestCardLifecycle.db")
|
||||
os.Remove(testDBPath)
|
||||
|
||||
b, err := board.Open(testDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to open the test Kanban board, %s.", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = b.Close()
|
||||
}()
|
||||
|
||||
initialCardTitle := "A test card."
|
||||
initialCardContent := "Ensure that this card is safely stored in the database."
|
||||
expectedCardID := 1
|
||||
|
||||
t.Run("Test Create Card", testCreateCard(b, initialCardTitle, initialCardContent, expectedCardID))
|
||||
|
||||
t.Run("Test Read Card", testReadCard(b, expectedCardID, initialCardTitle, initialCardContent))
|
||||
|
||||
modifiedCardTitle := "Test card updated."
|
||||
modifiedCardContent1 := "Ensure that this card is safely updated in the database."
|
||||
|
||||
t.Run("Test Update Card", testUpdateCard(b, expectedCardID, modifiedCardTitle, modifiedCardContent1))
|
||||
|
||||
modifiedCardContent2 := "Updated card content only."
|
||||
|
||||
t.Run("Test Update Card Content", testUpdateCardContent(b, expectedCardID, modifiedCardTitle, modifiedCardContent2))
|
||||
}
|
||||
|
||||
func testCreateCard(b board.Board, title, content string, wantID int) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When the card is created and saved to the database.")
|
||||
|
||||
args := board.CardArgs{
|
||||
NewTitle: title,
|
||||
NewContent: content,
|
||||
}
|
||||
|
||||
if _, err := b.CreateCard(args); err != nil {
|
||||
t.Fatalf("ERROR: Unable to create the test card, %s.", err)
|
||||
}
|
||||
|
||||
statusList, err := b.StatusList()
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to run `ReadStatusList`, %s.", err)
|
||||
}
|
||||
|
||||
if len(statusList) == 0 {
|
||||
t.Fatal("ERROR: The status list appears to be empty.")
|
||||
}
|
||||
|
||||
cardIDs := statusList[0].CardIds
|
||||
|
||||
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 {
|
||||
t.Logf("%s\tExpected card ID found in the default status, got %d.", success, gotID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testReadCard(b board.Board, cardID int, wantTitle, wantContent string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When a card is read from the database.")
|
||||
|
||||
card, err := b.Card(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to read test card, %s.", err)
|
||||
}
|
||||
|
||||
if card.Title != wantTitle {
|
||||
t.Errorf("%s\tUnexpected card title received, want: %s, got: %s.", failure, wantTitle, card.Title)
|
||||
} else {
|
||||
t.Logf("%s\tExpected card title received, got: %s.", success, card.Title)
|
||||
}
|
||||
|
||||
if card.Content != wantContent {
|
||||
t.Errorf("%s\tUnexpected card content received, want: %s, got: %s.", failure, wantContent, card.Content)
|
||||
} else {
|
||||
t.Logf("%s\tExpected card content received, got: %s.", success, card.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateCard(b board.Board, cardID int, newTitle, newContent string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When a card is updated in the database.")
|
||||
|
||||
args := board.UpdateCardArgs{
|
||||
CardID: cardID,
|
||||
CardArgs: board.CardArgs{
|
||||
NewTitle: newTitle,
|
||||
NewContent: newContent,
|
||||
},
|
||||
}
|
||||
|
||||
if err := b.UpdateCard(args); err != nil {
|
||||
t.Fatalf("ERROR: Unable to update the test card, %s", err)
|
||||
}
|
||||
|
||||
got, err := b.Card(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to read the modified test card, %s", err)
|
||||
}
|
||||
|
||||
want := board.Card{
|
||||
ID: cardID,
|
||||
Title: newTitle,
|
||||
Content: newContent,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s\tUnexpected card read from the database: want %+v, got %+v", failure, want, got)
|
||||
} else {
|
||||
t.Logf("%s\tExpected card read from the database: got %+v", success, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateCardContent(b board.Board, cardID int, expectedTitle, newContent string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When (and only when) a card's content is updated in the database.")
|
||||
|
||||
args := board.UpdateCardArgs{
|
||||
CardID: cardID,
|
||||
CardArgs: board.CardArgs{
|
||||
NewTitle: "",
|
||||
NewContent: newContent,
|
||||
},
|
||||
}
|
||||
|
||||
if err := b.UpdateCard(args); err != nil {
|
||||
t.Fatalf("ERROR: Unable to update the test card, %s", err)
|
||||
}
|
||||
|
||||
got, err := b.Card(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to read the modified test card, %s", err)
|
||||
}
|
||||
|
||||
want := board.Card{
|
||||
ID: cardID,
|
||||
Title: expectedTitle,
|
||||
Content: newContent,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s\tUnexpected card read from the database, want: %+v, got: %+v", failure, want, got)
|
||||
} else {
|
||||
t.Logf("%s\tExpected card read from the database, got: %+v", success, got)
|
||||
}
|
||||
}
|
||||
}
|
21
internal/board/helpers_test.go
Normal file
21
internal/board/helpers_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package board_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
success = "\u2713"
|
||||
failure = "\u2717"
|
||||
)
|
||||
|
||||
func projectRoot() (string, error) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get the current working directory, %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(cwd, "..", ".."), nil
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
package board
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Status represents the status of the Kanban board.
|
||||
type Status struct {
|
||||
ID int
|
||||
|
@ -18,8 +22,61 @@ func (s *Status) Id() int {
|
|||
return s.ID
|
||||
}
|
||||
|
||||
func (s *Status) AddCardID(id int) {
|
||||
s.CardIds = append(s.CardIds, id)
|
||||
// AddCardID adds a card ID to the status' list of card IDs.
|
||||
func (s *Status) AddCardID(cardID int) {
|
||||
// Create a new list if it does not exist
|
||||
// and then return.
|
||||
if s.CardIds == nil {
|
||||
s.CardIds = []int{cardID}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Sort list if not sorted.
|
||||
if !sort.IntsAreSorted(s.CardIds) {
|
||||
sort.Ints(s.CardIds)
|
||||
}
|
||||
|
||||
// Get index of the card's ID if it already exists in the list.
|
||||
// Return if it already exists in the list
|
||||
ind := sort.SearchInts(s.CardIds, cardID)
|
||||
|
||||
if ind <= len(s.CardIds) && cardID == s.CardIds[ind] {
|
||||
return
|
||||
}
|
||||
|
||||
s.CardIds = append(s.CardIds, cardID)
|
||||
sort.Ints(s.CardIds)
|
||||
}
|
||||
|
||||
// RemoveCardID removes a card ID from the status' list of card IDs.
|
||||
func (s *Status) RemoveCardID(cardID int) {
|
||||
if s.CardIds == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Sort list if not sorted.
|
||||
if !sort.IntsAreSorted(s.CardIds) {
|
||||
sort.Ints(s.CardIds)
|
||||
}
|
||||
|
||||
// Get index of id.
|
||||
// If the card ID is somehow not in the list, then ind
|
||||
// will be the index where the id can be inserted.
|
||||
ind := sort.SearchInts(s.CardIds, cardID)
|
||||
|
||||
if ind >= len(s.CardIds) || cardID != s.CardIds[ind] {
|
||||
return
|
||||
}
|
||||
|
||||
if len(s.CardIds) == 1 {
|
||||
s.CardIds = nil
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// use append to eliminate the id from the new slice
|
||||
s.CardIds = append(s.CardIds[:ind], s.CardIds[ind+1:]...)
|
||||
}
|
||||
|
||||
// ByStatusOrder implements sort.Interface for []Status based on the Order field.
|
||||
|
@ -44,16 +101,19 @@ func defaultStatusList() []Status {
|
|||
ID: -1,
|
||||
Name: "To Do",
|
||||
Order: 1,
|
||||
CardIds: nil,
|
||||
},
|
||||
{
|
||||
ID: -1,
|
||||
Name: "Doing",
|
||||
Order: 2,
|
||||
CardIds: nil,
|
||||
},
|
||||
{
|
||||
ID: -1,
|
||||
Name: "Done",
|
||||
Order: 3,
|
||||
CardIds: nil,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
192
internal/board/status_lifecycle_test.go
Normal file
192
internal/board/status_lifecycle_test.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
package board_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/canal/internal/board"
|
||||
)
|
||||
|
||||
func TestStatusLifecycle(t *testing.T) {
|
||||
t.Log("Testing the lifecycle of a status.")
|
||||
|
||||
projectDir, err := projectRoot()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
testDBPath := filepath.Join(projectDir, "test", "databases", "Board_TestStatusLifecycle.db")
|
||||
os.Remove(testDBPath)
|
||||
|
||||
kanban, err := board.Open(testDBPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to open the test Kanban board, %s.", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = kanban.Close()
|
||||
}()
|
||||
|
||||
// We've opened a new board with the default list of statuses: To Do, Doing, Done
|
||||
statusOnHoldName := "On Hold"
|
||||
statusOnHoldExpectedID := 4
|
||||
statusOnHoldOrder := 4
|
||||
|
||||
t.Run("Test Create Status", testCreateStatus(kanban, statusOnHoldName, statusOnHoldOrder))
|
||||
|
||||
t.Run("Test Read Status", testReadStatus(kanban, statusOnHoldExpectedID, statusOnHoldName))
|
||||
|
||||
t.Run("Test Status Update", testUpdateStatus(kanban, 2, "In Progress"))
|
||||
// Rearrange the board so the order is To Do, On Hold, In Progress, Done
|
||||
// Move a Card ID from To Do to In Progress
|
||||
t.Run("Test Move Card To Status", testMoveCardToStatus(kanban))
|
||||
}
|
||||
|
||||
func testCreateStatus(b board.Board, name string, order int) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When the status is created and saved to the database.")
|
||||
|
||||
args := board.StatusArgs{
|
||||
Name: name,
|
||||
Order: order,
|
||||
}
|
||||
|
||||
if err := b.CreateStatus(args); err != nil {
|
||||
t.Fatalf("ERROR: Unable to create the new status, %v.", err)
|
||||
}
|
||||
|
||||
t.Logf("%s\tStatus '%s' was successfully saved to the database.", success, name)
|
||||
}
|
||||
}
|
||||
|
||||
func testReadStatus(b board.Board, statusID int, wantName string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When the status is read from the database.")
|
||||
|
||||
status, err := b.Status(statusID)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to read the test status, %v", err)
|
||||
}
|
||||
|
||||
if status.Name != wantName {
|
||||
t.Errorf("%s\tUnexpected status received from the database, want: '%s', got: '%s'", failure, wantName, status.Name)
|
||||
} else {
|
||||
t.Logf("%s\tSuccessfully received the '%s' status from the database.", success, status.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testUpdateStatus(kanban board.Board, statusID int, newName string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When the status' name is updated in the database.")
|
||||
|
||||
args := board.UpdateStatusArgs{
|
||||
StatusID: statusID,
|
||||
StatusArgs: board.StatusArgs{
|
||||
Name: newName,
|
||||
Order: -1,
|
||||
},
|
||||
}
|
||||
if err := kanban.UpdateStatus(args); err != nil {
|
||||
t.Fatalf("ERROR: Unable to update the status, %v", err)
|
||||
} else {
|
||||
t.Logf("%s\tStatus successfully updated.", success)
|
||||
}
|
||||
|
||||
t.Log("Verifying the new status.")
|
||||
|
||||
status, err := kanban.Status(statusID)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to retrieve the status from the database, %v", err)
|
||||
}
|
||||
|
||||
want := board.Status{
|
||||
ID: statusID,
|
||||
Name: newName,
|
||||
CardIds: nil,
|
||||
Order: 2,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(status, want) {
|
||||
t.Errorf("%s\tUnexpected status received from the database, want: %+v, got: %+v", failure, want, status)
|
||||
} else {
|
||||
t.Logf("%s\tExpected status name received from the database, got: %+v", success, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// func testRearrangeBoard() func(t *testing.T) {
|
||||
// return func(t *testing.T) {
|
||||
// }
|
||||
// }
|
||||
|
||||
func testMoveCardToStatus(b board.Board) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Log("When moving a card between statuses.")
|
||||
|
||||
title := "Test card."
|
||||
|
||||
cardArgs := board.CardArgs{
|
||||
NewTitle: title,
|
||||
NewContent: "",
|
||||
}
|
||||
|
||||
cardID, err := b.CreateCard(cardArgs)
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
||||
}
|
||||
|
||||
statusList, err := b.StatusList()
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
||||
}
|
||||
|
||||
status0 := statusList[0]
|
||||
status2 := statusList[2]
|
||||
|
||||
moveArgs := board.MoveToStatusArgs{
|
||||
CardID: cardID,
|
||||
CurrentStatusID: status0.ID,
|
||||
NextStatusID: status2.ID,
|
||||
}
|
||||
|
||||
if err := b.MoveToStatus(moveArgs); err != nil {
|
||||
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)
|
||||
|
||||
statusList, err = b.StatusList()
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
||||
}
|
||||
|
||||
status0 = statusList[0]
|
||||
status2 = statusList[2]
|
||||
|
||||
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))
|
||||
} else {
|
||||
t.Logf("%s\tThe number of card IDs in '%s' is now %d", success, status0.Name, len(status0.CardIds))
|
||||
}
|
||||
|
||||
if len(status2.CardIds) != 1 {
|
||||
t.Errorf("%s\tUnexpected number of card IDs found in '%s', want: 1, got: %d", failure, status2.Name, len(status2.CardIds))
|
||||
} else {
|
||||
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])
|
||||
if err != nil {
|
||||
t.Fatalf("ERROR: Unable to retrieve the card from the database, %v", err)
|
||||
}
|
||||
|
||||
if card.Title != title {
|
||||
t.Errorf("%s\tUnexpected card title found in '%s', want: '%s', got: '%s'", success, status2.Name, title, card.Title)
|
||||
} else {
|
||||
t.Logf("%s\tExpected card title found in '%s', got: '%s'", success, status2.Name, card.Title)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,16 +2,15 @@ package ui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/canal/internal/board"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type column struct {
|
||||
statusID int
|
||||
statusName string
|
||||
cards *tview.List
|
||||
}
|
||||
|
||||
|
@ -49,19 +48,23 @@ func (u *UI) newColumn(status board.Status) (column, error) {
|
|||
cur := cardList.GetCurrentItem()
|
||||
cur--
|
||||
cardList.SetCurrentItem(cur)
|
||||
case 'm':
|
||||
u.pages.ShowPage(movePageName)
|
||||
u.SetFocus(u.move)
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
|
||||
if len(status.CardIds) > 0 {
|
||||
if status.CardIds != nil && len(status.CardIds) > 0 {
|
||||
cards, err := u.board.CardList(status.CardIds)
|
||||
if err != nil {
|
||||
return column{}, fmt.Errorf("unable to get the card list. %w", err)
|
||||
}
|
||||
|
||||
for _, c := range cards {
|
||||
cardList.AddItem(fmt.Sprintf("[%d] %s", c.Id(), c.Title), "", 0, nil)
|
||||
id := strconv.Itoa(c.ID)
|
||||
cardList.AddItem(fmt.Sprintf("[%s] %s", id, c.Title), id, 0, nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +72,6 @@ func (u *UI) newColumn(status board.Status) (column, error) {
|
|||
|
||||
c := column{
|
||||
statusID: status.ID,
|
||||
statusName: status.Name,
|
||||
cards: cardList,
|
||||
}
|
||||
|
||||
|
@ -99,7 +101,7 @@ func (u *UI) shiftColumnFocus(s int) {
|
|||
u.setColumnFocus()
|
||||
}
|
||||
|
||||
func (u *UI) updateColumns(statusList []board.Status) error {
|
||||
func (u *UI) updateColumns(statusList []board.Status) {
|
||||
u.flex.Clear()
|
||||
|
||||
columns := make([]column, len(statusList))
|
||||
|
@ -109,6 +111,4 @@ func (u *UI) updateColumns(statusList []board.Status) error {
|
|||
}
|
||||
|
||||
u.columns = columns
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"codeflow.dananglin.me.uk/apollo/canal/internal/board"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
|
@ -17,6 +18,7 @@ const (
|
|||
mainPageName string = "main"
|
||||
quitPageName string = "quit"
|
||||
addPageName string = "add"
|
||||
movePageName string = "move"
|
||||
)
|
||||
|
||||
type UI struct {
|
||||
|
@ -29,22 +31,26 @@ type UI struct {
|
|||
board board.Board
|
||||
quit *tview.Modal
|
||||
add *modalInput
|
||||
move *tview.Flex
|
||||
}
|
||||
|
||||
// NewUI returns a new UI value.
|
||||
func NewUI() UI {
|
||||
u := UI{
|
||||
ui := UI{
|
||||
Application: tview.NewApplication(),
|
||||
pages: tview.NewPages(),
|
||||
flex: tview.NewFlex(),
|
||||
quit: tview.NewModal(),
|
||||
add: NewModalInput(),
|
||||
focusedColumn: 0,
|
||||
columns: nil,
|
||||
move: nil,
|
||||
board: board.Board{},
|
||||
}
|
||||
|
||||
u.init()
|
||||
ui.init()
|
||||
|
||||
return u
|
||||
return ui
|
||||
}
|
||||
|
||||
// closeBoard closes the board.
|
||||
|
@ -66,7 +72,7 @@ func (u *UI) init() {
|
|||
switch event.Rune() {
|
||||
case 'o':
|
||||
if u.flex.HasFocus() && len(u.columns) == 0 {
|
||||
u.openBoard("")
|
||||
_ = u.openBoard("")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,6 +88,7 @@ func (u *UI) initAddInputModal() {
|
|||
if success {
|
||||
_ = u.newCard(text, "")
|
||||
}
|
||||
|
||||
u.pages.HidePage(addPageName)
|
||||
u.setColumnFocus()
|
||||
}
|
||||
|
@ -113,11 +120,11 @@ func (u *UI) newCard(title, content string) error {
|
|||
NewContent: content,
|
||||
}
|
||||
|
||||
if err := u.board.CreateCard(args); err != nil {
|
||||
if _, err := u.board.CreateCard(args); err != nil {
|
||||
return fmt.Errorf("unable to create card, %w", err)
|
||||
}
|
||||
|
||||
u.refresh()
|
||||
_ = u.refresh()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -132,7 +139,7 @@ func (u *UI) openBoard(path string) error {
|
|||
u.board = b
|
||||
|
||||
if err = u.refresh(); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("error refreshing the board, %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -147,9 +154,9 @@ func (u *UI) refresh() error {
|
|||
|
||||
u.updateColumns(statusList)
|
||||
|
||||
u.setColumnFocus()
|
||||
u.updateMovePage(statusList)
|
||||
|
||||
// TODO: update move status page here
|
||||
u.setColumnFocus()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -159,3 +166,61 @@ func (u *UI) shutdown() {
|
|||
u.closeBoard()
|
||||
u.Stop()
|
||||
}
|
||||
|
||||
func (u *UI) updateMovePage(statusList []board.Status) {
|
||||
if u.pages.HasPage(movePageName) {
|
||||
u.pages.RemovePage(movePageName)
|
||||
}
|
||||
|
||||
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(movePageName)
|
||||
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
|
||||
}
|
||||
|
||||
currentItem := u.columns[u.focusedColumn].cards.GetCurrentItem()
|
||||
_, cardIDText := u.columns[u.focusedColumn].cards.GetItemText(currentItem)
|
||||
cardID, _ := strconv.Atoi(cardIDText)
|
||||
|
||||
args := board.MoveToStatusArgs{
|
||||
CardID: cardID,
|
||||
CurrentStatusID: currentStatusID,
|
||||
NextStatusID: nextStatusID,
|
||||
}
|
||||
_ = u.board.MoveToStatus(args)
|
||||
|
||||
u.pages.HidePage(movePageName)
|
||||
_ = u.refresh()
|
||||
}
|
||||
|
||||
statusSelection.SetSelectedFunc(selectedFunc)
|
||||
|
||||
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(movePageName, move, false, false)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue