feat(board): add backend support for status repositioning #34
3 changed files with 137 additions and 20 deletions
|
@ -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
26
internal/board/shuffle.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue