From e99c8cd8777f61249a44683155f3001d6948daff Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Thu, 25 Jan 2024 01:36:22 +0000 Subject: [PATCH] feat: add backend support for status repositioning Add backend support for repositioning statuses on the Kanban board. Part of apollo/pelican#27 --- internal/board/board.go | 41 +++++++++-- internal/board/shuffle.go | 26 +++++++ internal/board/status_lifecycle_test.go | 90 +++++++++++++++++++++---- 3 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 internal/board/shuffle.go diff --git a/internal/board/board.go b/internal/board/board.go index a4cc16e..523905b 100644 --- a/internal/board/board.go +++ b/internal/board/board.go @@ -11,6 +11,7 @@ import ( bolt "go.etcd.io/bbolt" ) +// Board is probably the heart of Pelican. type Board struct { db *bolt.DB } @@ -90,9 +91,7 @@ func (b *Board) StatusList() ([]Status, error) { return statuses, nil } -// Status returns a single status from the db. -// TODO: Add a test case that handles when a status does not exist. -// Or use in delete status case. +// Status returns a single status from the database. func (b *Board) Status(statusID int) (Status, error) { data, err := db.Read(b.db, db.StatusBucket, statusID) if err != nil { @@ -116,6 +115,7 @@ func (b *Board) Status(statusID int) (Status, error) { return status, nil } +// StatusArgs is an argument type for creating or updating statuses. type StatusArgs struct { Name string Position int @@ -198,21 +198,44 @@ func (b *Board) DeleteStatus(statusID int) error { return fmt.Errorf("unable to delete the status from the database; %w", err) } - if err := b.normaliseStatusesPositionValues(); err != nil { + if err := b.normaliseStatusesPositionValuesFromDatabase(); err != nil { return fmt.Errorf("unable to normalise the statuses position values; %w", err) } return nil } -// normaliseStatusesPositionValues retrieves the ordered list of statuses from the database and sets -// each status' positional value based on its position in the list. -func (b *Board) normaliseStatusesPositionValues() error { +// RepositionStatus re-positions a Status value on a slice of Statuses. +func (b *Board) RepositionStatus(currentIndex, targetIndex int) error { statuses, err := b.StatusList() if err != nil { return fmt.Errorf("unable to get the list of statuses; %w", err) } + statuses = shuffle(statuses, currentIndex, targetIndex) + + if err := b.normaliseStatusesPositionValues(statuses); err != nil { + return fmt.Errorf("unable to normalise the statuses position values; %w", err) + } + + return nil +} + +// normaliseStatusesPositionValuesFromDatabase retrieves the ordered list of statuses from the database and sets +// each status' positional value based on its position in the list before saving the updates to the database. +func (b *Board) normaliseStatusesPositionValuesFromDatabase() error { + statuses, err := b.StatusList() + if err != nil { + return fmt.Errorf("unable to get the list of statuses; %w", err) + } + + return b.normaliseStatusesPositionValues(statuses) +} + +// normaliseStatusesPositionValues takes a list of statuses and sets +// each status' positional value based on its position in the list before +// saving the updates to the database. +func (b *Board) normaliseStatusesPositionValues(statuses []Status) error { for i, status := range statuses { updateArgs := UpdateStatusArgs{ StatusID: status.ID, @@ -230,6 +253,7 @@ func (b *Board) normaliseStatusesPositionValues() error { return nil } +// MoveToStatusArgs is an argument type for moving a card between statuses. type MoveToStatusArgs struct { CardID int CurrentStatusID int @@ -260,6 +284,7 @@ func (b *Board) MoveToStatus(args MoveToStatusArgs) error { return nil } +// CardArgs is an argument type for creating or updating cards. type CardArgs struct { NewTitle string NewDescription string @@ -360,6 +385,7 @@ func (b *Board) CardList(ids []int) ([]Card, error) { return cards, nil } +// UpdateCardArgs is an argument type for updating a card. type UpdateCardArgs struct { CardID int CardArgs @@ -387,6 +413,7 @@ func (b *Board) UpdateCard(args UpdateCardArgs) error { return nil } +// DeleteCardArgs is an argument type for deleting a card. type DeleteCardArgs struct { CardID int StatusID int diff --git a/internal/board/shuffle.go b/internal/board/shuffle.go new file mode 100644 index 0000000..9e57124 --- /dev/null +++ b/internal/board/shuffle.go @@ -0,0 +1,26 @@ +package board + +// shuffle takes a list of Statuses and swaps its position with the Status closer to its desired +// position until it reaches it. +// +// This is currently used to move specified columns forwards or backwards on the Kanban board. +// When a column changes position the other columns shuffle forward or backwards as required. +func shuffle(list []Status, oldIndex, newIndex int) []Status { + if newIndex == oldIndex { + return list + } + + if newIndex < oldIndex { + for i := oldIndex; i > newIndex; i-- { + list[i], list[i-1] = list[i-1], list[i] + } + + return list + } + + for i := oldIndex; i < newIndex; i++ { + list[i], list[i+1] = list[i+1], list[i] + } + + return list +} diff --git a/internal/board/status_lifecycle_test.go b/internal/board/status_lifecycle_test.go index 691b3ed..8093eb2 100644 --- a/internal/board/status_lifecycle_test.go +++ b/internal/board/status_lifecycle_test.go @@ -2,6 +2,7 @@ package board_test import ( "errors" + "fmt" "os" "path/filepath" "reflect" @@ -57,25 +58,65 @@ func TestStatusLifecycle(t *testing.T) { t.Run("Test Status Update (Doing to In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress")) t.Run("Test Status Update (To Do to Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog")) - // (TODO: Rearranging statuses still needs to be implemented) - // Rearrange the board so the order is: Backlog, Next, In Progress, On Hold, Done + t.Logf("Our current column positioning is: Backlog, In Progress, Done, On Hold, Next.") + t.Logf("Let us rearrange the board so that the order is: Backlog, Next, In Progress, On Hold, Done...") + + // NOTE: the statuses current index is the index in the slice that is received from the database. + // The wantPositions is used to evaluate each statuses Position value after all the reshuffling and renormalisation. + rearrangeCases := []struct { + statusName string + currentIndex int + targetIndex int + wantPositions map[int]string + }{ + { + statusName: "Done", + currentIndex: 2, + targetIndex: 4, + wantPositions: map[int]string{ + 1: "Backlog", + 2: "In Progress", + 3: "On Hold", + 4: "Next", + 5: "Done", + }, + }, + { + statusName: "Next", + currentIndex: 3, + targetIndex: 1, + wantPositions: map[int]string{ + 1: "Backlog", + 2: "Next", + 3: "In Progress", + 4: "On Hold", + 5: "Done", + }, + }, + } + + for i := range rearrangeCases { + t.Run( + fmt.Sprintf("Test Re-Arrange Status: %s", rearrangeCases[i].statusName), + testRepositionStatuses(kanban, rearrangeCases[i].currentIndex, rearrangeCases[i].targetIndex, rearrangeCases[i].wantPositions), + ) + } t.Logf("Let us now try moving a card from one status to another...") t.Run("Test Move Card To Status", testMoveCardToStatus(kanban)) - // TODO: This needs to be updated when we re-arrange the board. - expectedPositions := map[int]string{ + expectedPositionsAfterDelete := map[int]string{ 1: "Backlog", - 2: "In Progress", - 3: "Done", - 4: "Next", + 2: "Next", + 3: "In Progress", + 4: "Done", } t.Logf("Let us now delete the 'On Hold' status from the database...") - t.Run("Test Delete Status (On Hold)", testDeleteEmptyStatus(kanban, statusOnHoldExpectedID, expectedPositions)) + t.Run("Test Delete Status (On Hold)", testDeleteEmptyStatus(kanban, statusOnHoldExpectedID, expectedPositionsAfterDelete)) t.Logf("Additionally, let us try to delete a status that contains a card...") - t.Run("Test Delete a non-empty status", testDeleteNonEmptyStatus(kanban, 3)) + t.Run("Test Delete a non-empty status", testDeleteNonEmptyStatus(kanban, 2)) } func testCreateStatus(kanban board.Board, name string, position int) func(t *testing.T) { @@ -157,10 +198,31 @@ func testUpdateStatus(kanban board.Board, statusID, expectedPosition int, newNam } } -// func testRearrangeBoard() func(t *testing.T) { -// return func(t *testing.T) { -// } -// } +func testRepositionStatuses(kanban board.Board, currentIndex, targetIndex int, wantPositions map[int]string) func(t *testing.T) { + return func(t *testing.T) { + t.Logf("When repositioning a status on the board.") + + if err := kanban.RepositionStatus(currentIndex, targetIndex); err != nil { + t.Fatalf("ERROR: Unable to reposition the status; %v", err) + } + + statuses, err := kanban.StatusList() + if err != nil { + t.Fatalf("ERROR: an error was received when attempting to get the list of statuses from the database; %v", err) + } + + gotPositions := make(map[int]string) + for _, status := range statuses { + gotPositions[status.Position] = status.Name + } + + if !reflect.DeepEqual(wantPositions, gotPositions) { + t.Errorf("%s\tUnexpected positions received from the database; want: %v, got %v", failure, wantPositions, gotPositions) + } else { + t.Logf("%s\tExpected positions received from the database; got %v", success, gotPositions) + } + } +} func testMoveCardToStatus(kanban board.Board) func(t *testing.T) { return func(t *testing.T) { @@ -186,6 +248,8 @@ func testMoveCardToStatus(kanban board.Board) func(t *testing.T) { if err := kanban.MoveToStatus(moveArgs); err != nil { t.Fatalf("ERROR: Unable to move the Card ID from '%s' to '%s', %v", status0.Name, status2.Name, err) + } else { + t.Logf("%s\tThe MoveToStatus operation has completed without error.", success) } t.Logf("\tVerifying that the card has moved to '%s'...", status2.Name)