feat: move a card between statuses

Add a very simple interface to allow a user to move a
card from one status to another.
This commit is contained in:
Dan Anglin 2023-04-26 08:32:33 +01:00
parent 0e7cb2388d
commit 571bce3a16
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
8 changed files with 664 additions and 250 deletions

View file

@ -48,12 +48,17 @@ func Open(path string) (Board, error) {
return board, nil return board, nil
} }
// Close closes the project's Kanban board.
func (b *Board) Close() error { func (b *Board) Close() error {
if b.db == nil { if b.db == nil {
return 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. // 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)) statuses := make([]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 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) return []Status{}, fmt.Errorf("unable to decode data, %w", err)
} }
statuses[i] = s statuses[ind] = status
} }
sort.Sort(ByStatusOrder(statuses)) sort.Sort(ByStatusOrder(statuses))
@ -84,23 +89,106 @@ func (b *Board) StatusList() ([]Status, error) {
return statuses, nil return statuses, nil
} }
// TODO: Finish implementation. // Status returns a single status from the database.
func (b *Board) ReadStatus() (Status, error) { func (b *Board) Status(id int) (Status, error) {
return Status{}, nil 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 return nil
} }
// TODO: Finish implementation. // TODO: Finish implementation.
func (b *Board) UpdateStatus() error { // func (b *Board) DeleteStatus() error {
return nil // 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{&currentStatus, &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 return nil
} }
@ -110,14 +198,14 @@ type CardArgs struct {
} }
// CreateCard creates a card in the database. // 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() statusList, err := b.StatusList()
if err != nil { 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 { if len(statusList) == 0 {
return statusListEmptyError{} return 0, statusListEmptyError{}
} }
card := Card{ card := Card{
@ -128,18 +216,19 @@ func (b *Board) CreateCard(args CardArgs) error {
cardID, err := database.Write(b.db, database.CardBucket, &card) cardID, err := database.Write(b.db, database.CardBucket, &card)
if err != nil { 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 := statusList[0]
initialStatus.AddCardID(cardID) initialStatus.AddCardID(cardID)
if _, err := database.Write(b.db, database.StatusBucket, &initialStatus); err != nil { id, err := database.Write(b.db, database.StatusBucket, &initialStatus)
return fmt.Errorf("unable to write the %s status to the database, %w", initialStatus.Name, err) 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. // 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)) cards := make([]Card, 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 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) return nil, fmt.Errorf("unable to decode data, %w", err)
} }
cards[i] = c cards[ind] = card
} }
return cards, nil return cards, nil
@ -216,20 +305,8 @@ func (b *Board) UpdateCard(args UpdateCardArgs) error {
return nil 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. // DeleteCard deletes a card from the database.
// TODO: finish implementation. // TODO: finish implementation.
func (b *Board) DeleteCard(id int) error { //func (b *Board) DeleteCard(id int) error {
return nil // return nil
} //}

View file

@ -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
}

View 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)
}
}
}

View 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
}

View file

@ -1,5 +1,9 @@
package board package board
import (
"sort"
)
// 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
@ -18,8 +22,61 @@ func (s *Status) Id() int {
return s.ID return s.ID
} }
func (s *Status) AddCardID(id int) { // AddCardID adds a card ID to the status' list of card IDs.
s.CardIds = append(s.CardIds, id) 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. // ByStatusOrder implements sort.Interface for []Status based on the Order field.
@ -44,16 +101,19 @@ func defaultStatusList() []Status {
ID: -1, ID: -1,
Name: "To Do", Name: "To Do",
Order: 1, Order: 1,
CardIds: nil,
}, },
{ {
ID: -1, ID: -1,
Name: "Doing", Name: "Doing",
Order: 2, Order: 2,
CardIds: nil,
}, },
{ {
ID: -1, ID: -1,
Name: "Done", Name: "Done",
Order: 3, Order: 3,
CardIds: nil,
}, },
} }
} }

View 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)
}
}
}

View file

@ -2,16 +2,15 @@ package ui
import ( import (
"fmt" "fmt"
"strconv"
"codeflow.dananglin.me.uk/apollo/canal/internal/board" "codeflow.dananglin.me.uk/apollo/canal/internal/board"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/rivo/tview" "github.com/rivo/tview"
) )
type column struct { type column struct {
statusID int statusID int
statusName string
cards *tview.List cards *tview.List
} }
@ -49,19 +48,23 @@ func (u *UI) newColumn(status board.Status) (column, error) {
cur := cardList.GetCurrentItem() cur := cardList.GetCurrentItem()
cur-- cur--
cardList.SetCurrentItem(cur) cardList.SetCurrentItem(cur)
case 'm':
u.pages.ShowPage(movePageName)
u.SetFocus(u.move)
} }
return event return event
}) })
if len(status.CardIds) > 0 { if status.CardIds != nil && len(status.CardIds) > 0 {
cards, err := u.board.CardList(status.CardIds) cards, err := u.board.CardList(status.CardIds)
if err != nil { if err != nil {
return column{}, fmt.Errorf("unable to get the card list. %w", err) return column{}, fmt.Errorf("unable to get the card list. %w", err)
} }
for _, c := range cards { 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{ c := column{
statusID: status.ID, statusID: status.ID,
statusName: status.Name,
cards: cardList, cards: cardList,
} }
@ -99,7 +101,7 @@ func (u *UI) shiftColumnFocus(s int) {
u.setColumnFocus() u.setColumnFocus()
} }
func (u *UI) updateColumns(statusList []board.Status) error { func (u *UI) updateColumns(statusList []board.Status) {
u.flex.Clear() u.flex.Clear()
columns := make([]column, len(statusList)) columns := make([]column, len(statusList))
@ -109,6 +111,4 @@ func (u *UI) updateColumns(statusList []board.Status) error {
} }
u.columns = columns u.columns = columns
return nil
} }

View file

@ -2,6 +2,7 @@ package ui
import ( import (
"fmt" "fmt"
"strconv"
"codeflow.dananglin.me.uk/apollo/canal/internal/board" "codeflow.dananglin.me.uk/apollo/canal/internal/board"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
@ -17,6 +18,7 @@ const (
mainPageName string = "main" mainPageName string = "main"
quitPageName string = "quit" quitPageName string = "quit"
addPageName string = "add" addPageName string = "add"
movePageName string = "move"
) )
type UI struct { type UI struct {
@ -29,22 +31,26 @@ type UI struct {
board board.Board board board.Board
quit *tview.Modal quit *tview.Modal
add *modalInput add *modalInput
move *tview.Flex
} }
// NewUI returns a new UI value. // NewUI returns a new UI value.
func NewUI() UI { func NewUI() UI {
u := UI{ ui := UI{
Application: tview.NewApplication(), Application: tview.NewApplication(),
pages: tview.NewPages(), pages: tview.NewPages(),
flex: tview.NewFlex(), flex: tview.NewFlex(),
quit: tview.NewModal(), quit: tview.NewModal(),
add: NewModalInput(), add: NewModalInput(),
focusedColumn: 0, focusedColumn: 0,
columns: nil,
move: nil,
board: board.Board{},
} }
u.init() ui.init()
return u return ui
} }
// closeBoard closes the board. // closeBoard closes the board.
@ -66,7 +72,7 @@ func (u *UI) init() {
switch event.Rune() { switch event.Rune() {
case 'o': case 'o':
if u.flex.HasFocus() && len(u.columns) == 0 { if u.flex.HasFocus() && len(u.columns) == 0 {
u.openBoard("") _ = u.openBoard("")
} }
} }
@ -82,6 +88,7 @@ func (u *UI) initAddInputModal() {
if success { if success {
_ = u.newCard(text, "") _ = u.newCard(text, "")
} }
u.pages.HidePage(addPageName) u.pages.HidePage(addPageName)
u.setColumnFocus() u.setColumnFocus()
} }
@ -113,11 +120,11 @@ func (u *UI) newCard(title, content string) error {
NewContent: content, 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) return fmt.Errorf("unable to create card, %w", err)
} }
u.refresh() _ = u.refresh()
return nil return nil
} }
@ -132,7 +139,7 @@ func (u *UI) openBoard(path string) error {
u.board = b u.board = b
if err = u.refresh(); err != nil { if err = u.refresh(); err != nil {
return err return fmt.Errorf("error refreshing the board, %w", err)
} }
return nil return nil
@ -147,9 +154,9 @@ func (u *UI) refresh() error {
u.updateColumns(statusList) u.updateColumns(statusList)
u.setColumnFocus() u.updateMovePage(statusList)
// TODO: update move status page here u.setColumnFocus()
return nil return nil
} }
@ -159,3 +166,61 @@ func (u *UI) shutdown() {
u.closeBoard() u.closeBoard()
u.Stop() 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)
}