diff --git a/internal/board/board.go b/internal/board/board.go index 1a95f47..c1cb4b4 100644 --- a/internal/board/board.go +++ b/internal/board/board.go @@ -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 } -// TODO: Finish implementation. -func (b *Board) NewStatus() error { +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) + } + 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 } -// TODO: Finish implementation. -func (b *Board) DeleteStatus() error { +// 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) + } + 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 +//} diff --git a/internal/board/board_test.go b/internal/board/board_test.go deleted file mode 100644 index 78ee88e..0000000 --- a/internal/board/board_test.go +++ /dev/null @@ -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 -} diff --git a/internal/board/card_lifecycle_test.go b/internal/board/card_lifecycle_test.go new file mode 100644 index 0000000..74f0684 --- /dev/null +++ b/internal/board/card_lifecycle_test.go @@ -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) + } + } +} diff --git a/internal/board/helpers_test.go b/internal/board/helpers_test.go new file mode 100644 index 0000000..365e8cb --- /dev/null +++ b/internal/board/helpers_test.go @@ -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 +} diff --git a/internal/board/status.go b/internal/board/status.go index 546a428..f5a4e06 100644 --- a/internal/board/status.go +++ b/internal/board/status.go @@ -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. @@ -41,19 +98,22 @@ func (s ByStatusOrder) Less(i, j int) bool { func defaultStatusList() []Status { return []Status{ { - ID: -1, - Name: "To Do", - Order: 1, + ID: -1, + Name: "To Do", + Order: 1, + CardIds: nil, }, { - ID: -1, - Name: "Doing", - Order: 2, + ID: -1, + Name: "Doing", + Order: 2, + CardIds: nil, }, { - ID: -1, - Name: "Done", - Order: 3, + ID: -1, + Name: "Done", + Order: 3, + CardIds: nil, }, } } diff --git a/internal/board/status_lifecycle_test.go b/internal/board/status_lifecycle_test.go new file mode 100644 index 0000000..9e790d4 --- /dev/null +++ b/internal/board/status_lifecycle_test.go @@ -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) + } + } +} diff --git a/internal/ui/column.go b/internal/ui/column.go index fd5d433..858b20b 100644 --- a/internal/ui/column.go +++ b/internal/ui/column.go @@ -2,17 +2,16 @@ 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 + statusID int + cards *tview.List } func (u *UI) newColumn(status board.Status) (column, error) { @@ -49,28 +48,31 @@ 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) } } u.flex.AddItem(cardList, 0, 1, false) c := column{ - statusID: status.ID, - statusName: status.Name, - cards: cardList, + statusID: status.ID, + cards: cardList, } return c, nil @@ -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 } diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 229f197..cf48e3d 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -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) +}