feat: add backend support for status repositioning
All checks were successful
/ test (pull_request) Successful in 36s
/ lint (pull_request) Successful in 38s

Add backend support for repositioning statuses on the Kanban board.
Part of apollo/pelican#27
This commit is contained in:
Dan Anglin 2024-01-25 01:36:22 +00:00
parent 73547c49c6
commit e99c8cd877
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
3 changed files with 137 additions and 20 deletions

View file

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

26
internal/board/shuffle.go Normal file
View file

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

View file

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