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"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Board is probably the heart of Pelican.
|
||||||
type Board struct {
|
type Board struct {
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
}
|
}
|
||||||
|
@ -90,9 +91,7 @@ func (b *Board) StatusList() ([]Status, error) {
|
||||||
return statuses, nil
|
return statuses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status returns a single status from the db.
|
// Status returns a single status from the database.
|
||||||
// TODO: Add a test case that handles when a status does not exist.
|
|
||||||
// Or use in delete status case.
|
|
||||||
func (b *Board) Status(statusID int) (Status, error) {
|
func (b *Board) Status(statusID int) (Status, error) {
|
||||||
data, err := db.Read(b.db, db.StatusBucket, statusID)
|
data, err := db.Read(b.db, db.StatusBucket, statusID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -116,6 +115,7 @@ func (b *Board) Status(statusID int) (Status, error) {
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StatusArgs is an argument type for creating or updating statuses.
|
||||||
type StatusArgs struct {
|
type StatusArgs struct {
|
||||||
Name string
|
Name string
|
||||||
Position int
|
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)
|
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 fmt.Errorf("unable to normalise the statuses position values; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// normaliseStatusesPositionValues retrieves the ordered list of statuses from the database and sets
|
// RepositionStatus re-positions a Status value on a slice of Statuses.
|
||||||
// each status' positional value based on its position in the list.
|
func (b *Board) RepositionStatus(currentIndex, targetIndex int) error {
|
||||||
func (b *Board) normaliseStatusesPositionValues() error {
|
|
||||||
statuses, err := b.StatusList()
|
statuses, err := b.StatusList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get the list of statuses; %w", err)
|
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 {
|
for i, status := range statuses {
|
||||||
updateArgs := UpdateStatusArgs{
|
updateArgs := UpdateStatusArgs{
|
||||||
StatusID: status.ID,
|
StatusID: status.ID,
|
||||||
|
@ -230,6 +253,7 @@ func (b *Board) normaliseStatusesPositionValues() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoveToStatusArgs is an argument type for moving a card between statuses.
|
||||||
type MoveToStatusArgs struct {
|
type MoveToStatusArgs struct {
|
||||||
CardID int
|
CardID int
|
||||||
CurrentStatusID int
|
CurrentStatusID int
|
||||||
|
@ -260,6 +284,7 @@ func (b *Board) MoveToStatus(args MoveToStatusArgs) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CardArgs is an argument type for creating or updating cards.
|
||||||
type CardArgs struct {
|
type CardArgs struct {
|
||||||
NewTitle string
|
NewTitle string
|
||||||
NewDescription string
|
NewDescription string
|
||||||
|
@ -360,6 +385,7 @@ func (b *Board) CardList(ids []int) ([]Card, error) {
|
||||||
return cards, nil
|
return cards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateCardArgs is an argument type for updating a card.
|
||||||
type UpdateCardArgs struct {
|
type UpdateCardArgs struct {
|
||||||
CardID int
|
CardID int
|
||||||
CardArgs
|
CardArgs
|
||||||
|
@ -387,6 +413,7 @@ func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteCardArgs is an argument type for deleting a card.
|
||||||
type DeleteCardArgs struct {
|
type DeleteCardArgs struct {
|
||||||
CardID int
|
CardID int
|
||||||
StatusID 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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"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 (Doing to In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
||||||
t.Run("Test Status Update (To Do to Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
t.Run("Test Status Update (To Do to Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
||||||
|
|
||||||
// (TODO: Rearranging statuses still needs to be implemented)
|
t.Logf("Our current column positioning is: Backlog, In Progress, Done, On Hold, Next.")
|
||||||
// Rearrange the board so the order is: Backlog, Next, In Progress, On Hold, Done
|
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.Logf("Let us now try moving a card from one status to another...")
|
||||||
t.Run("Test Move Card To Status", testMoveCardToStatus(kanban))
|
t.Run("Test Move Card To Status", testMoveCardToStatus(kanban))
|
||||||
|
|
||||||
// TODO: This needs to be updated when we re-arrange the board.
|
expectedPositionsAfterDelete := map[int]string{
|
||||||
expectedPositions := map[int]string{
|
|
||||||
1: "Backlog",
|
1: "Backlog",
|
||||||
2: "In Progress",
|
2: "Next",
|
||||||
3: "Done",
|
3: "In Progress",
|
||||||
4: "Next",
|
4: "Done",
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Let us now delete the 'On Hold' status from the database...")
|
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.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) {
|
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) {
|
func testRepositionStatuses(kanban board.Board, currentIndex, targetIndex int, wantPositions map[int]string) func(t *testing.T) {
|
||||||
// return 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) {
|
func testMoveCardToStatus(kanban board.Board) func(t *testing.T) {
|
||||||
return 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 {
|
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)
|
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)
|
t.Logf("\tVerifying that the card has moved to '%s'...", status2.Name)
|
||||||
|
|
Loading…
Reference in a new issue