Dan Anglin
e99c8cd877
Add backend support for repositioning statuses on the Kanban board. Part of apollo/pelican#27
440 lines
11 KiB
Go
440 lines
11 KiB
Go
package board
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/gob"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"codeflow.dananglin.me.uk/apollo/pelican/internal/db"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
// Board is probably the heart of Pelican.
|
|
type Board struct {
|
|
db *bolt.DB
|
|
}
|
|
|
|
// Open reads the board from the database.
|
|
// If no board exists then a new one will be created.
|
|
func Open(path string) (Board, error) {
|
|
database, err := db.OpenDatabase(path)
|
|
if err != nil {
|
|
return Board{}, fmt.Errorf("unable to open the db. %w", err)
|
|
}
|
|
|
|
board := Board{
|
|
db: database,
|
|
}
|
|
|
|
statusList, err := board.StatusList()
|
|
if err != nil {
|
|
return Board{}, err
|
|
}
|
|
|
|
if len(statusList) == 0 {
|
|
newStatusList := defaultStatusList()
|
|
|
|
boltItems := make([]db.BoltItem, len(newStatusList))
|
|
|
|
for i := range newStatusList {
|
|
boltItems[i] = &newStatusList[i]
|
|
}
|
|
|
|
if _, err := db.Write(database, db.WriteModeCreate, db.StatusBucket, boltItems); err != nil {
|
|
return Board{}, fmt.Errorf("unable to save the default status list to the db. %w", err)
|
|
}
|
|
}
|
|
|
|
return board, nil
|
|
}
|
|
|
|
// Close closes the project's Kanban board.
|
|
func (b *Board) Close() error {
|
|
if b.db == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := b.db.Close(); err != nil {
|
|
return fmt.Errorf("error closing the db. %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StatusList returns the ordered list of statuses from the db.
|
|
func (b *Board) StatusList() ([]Status, error) {
|
|
data, err := db.ReadAll(b.db, db.StatusBucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read the status list, %w", err)
|
|
}
|
|
|
|
statuses := make([]Status, len(data))
|
|
|
|
for ind, d := range data {
|
|
buf := bytes.NewBuffer(d)
|
|
|
|
decoder := gob.NewDecoder(buf)
|
|
|
|
var status Status
|
|
|
|
if err := decoder.Decode(&status); err != nil {
|
|
return nil, fmt.Errorf("unable to decode data, %w", err)
|
|
}
|
|
|
|
statuses[ind] = status
|
|
}
|
|
|
|
sort.Sort(ByStatusPosition(statuses))
|
|
|
|
return statuses, nil
|
|
}
|
|
|
|
// 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 {
|
|
return Status{}, fmt.Errorf("unable to read status [%d] from the database; %w", statusID, err)
|
|
}
|
|
|
|
if data == nil {
|
|
return Status{}, StatusNotExistError{ID: statusID}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// StatusArgs is an argument type for creating or updating statuses.
|
|
type StatusArgs struct {
|
|
Name string
|
|
Position int
|
|
}
|
|
|
|
// CreateStatus creates a status in the database.
|
|
func (b *Board) CreateStatus(args StatusArgs) error {
|
|
name := args.Name
|
|
pos := args.Position
|
|
|
|
if pos < 1 {
|
|
statuses, err := b.StatusList()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get the list of statuses; %w", err)
|
|
}
|
|
|
|
length := len(statuses)
|
|
if length == 0 {
|
|
pos = 1
|
|
} else {
|
|
pos = statuses[length-1].Position + 1
|
|
}
|
|
}
|
|
|
|
status := Status{
|
|
Identity: Identity{ID: -1},
|
|
Name: name,
|
|
Position: pos,
|
|
CardIds: nil,
|
|
}
|
|
|
|
if _, err := db.Write(b.db, db.WriteModeCreate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
|
|
return fmt.Errorf("unable to write the status to the database; %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateStatusArgs is an argument type required for updating statuses.
|
|
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.Position > 0 {
|
|
status.Position = args.Position
|
|
}
|
|
|
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
|
|
return fmt.Errorf("unable to write the status to the db. %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteStatus deletes a status from the database.
|
|
// A status can only be deleted if it does not contain any cards.
|
|
func (b *Board) DeleteStatus(statusID int) error {
|
|
status, err := b.Status(statusID)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to retrieve the status from the database; %w", err)
|
|
}
|
|
|
|
if len(status.CardIds) > 0 {
|
|
return StatusNotEmptyError{ID: statusID}
|
|
}
|
|
|
|
if err := db.Delete(b.db, db.StatusBucket, statusID); err != nil {
|
|
return fmt.Errorf("unable to delete the status from the database; %w", err)
|
|
}
|
|
|
|
if err := b.normaliseStatusesPositionValuesFromDatabase(); err != nil {
|
|
return fmt.Errorf("unable to normalise the statuses position values; %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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,
|
|
StatusArgs: StatusArgs{
|
|
Name: "",
|
|
Position: i + 1,
|
|
},
|
|
}
|
|
|
|
if err := b.UpdateStatus(updateArgs); err != nil {
|
|
return fmt.Errorf("unable to update the status %q; %w", status.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MoveToStatusArgs is an argument type for moving a card between statuses.
|
|
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 := []db.BoltItem{¤tStatus, &nextStatus}
|
|
|
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, boltItems); err != nil {
|
|
return fmt.Errorf("unable to update the statuses in the db. %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CardArgs is an argument type for creating or updating cards.
|
|
type CardArgs struct {
|
|
NewTitle string
|
|
NewDescription string
|
|
}
|
|
|
|
// CreateCard creates a card in the database.
|
|
func (b *Board) CreateCard(args CardArgs) (int, error) {
|
|
timestamp := time.Now().Format(time.DateTime)
|
|
|
|
statusList, err := b.StatusList()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to read the status list, %w", err)
|
|
}
|
|
|
|
if len(statusList) == 0 {
|
|
return 0, StatusListEmptyError{}
|
|
}
|
|
|
|
boltItems := []db.BoltItem{
|
|
&Card{
|
|
Identity: Identity{ID: -1},
|
|
Title: args.NewTitle,
|
|
Description: args.NewDescription,
|
|
Created: timestamp,
|
|
},
|
|
}
|
|
|
|
cardIDs, err := db.Write(b.db, db.WriteModeCreate, db.CardBucket, boltItems)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to write card to the db. %w", err)
|
|
}
|
|
|
|
if len(cardIDs) != 1 {
|
|
return 0, fmt.Errorf("unexpected number of card IDs returned after writing the card to the database; want: 1, got %d", len(cardIDs))
|
|
}
|
|
|
|
cardID := cardIDs[0]
|
|
|
|
statusInFirstPos := statusList[0]
|
|
|
|
statusInFirstPos.AddCardID(cardID)
|
|
|
|
_, err = db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&statusInFirstPos})
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to write the %s status to the db. %w", statusInFirstPos.Name, err)
|
|
}
|
|
|
|
return cardID, nil
|
|
}
|
|
|
|
// Card returns a Card value from the database.
|
|
func (b *Board) Card(cardID int) (Card, error) {
|
|
data, err := db.Read(b.db, db.CardBucket, cardID)
|
|
if err != nil {
|
|
return Card{}, fmt.Errorf("unable to read card [%d] from the db. %w", cardID, err)
|
|
}
|
|
|
|
if data == nil {
|
|
return Card{}, CardNotExistError{ID: cardID}
|
|
}
|
|
|
|
var card Card
|
|
|
|
buf := bytes.NewBuffer(data)
|
|
|
|
decoder := gob.NewDecoder(buf)
|
|
|
|
if err := decoder.Decode(&card); err != nil {
|
|
return Card{}, fmt.Errorf("unable to decode data, %w", err)
|
|
}
|
|
|
|
return card, nil
|
|
}
|
|
|
|
// CardList returns a list of Card values from the db.
|
|
func (b *Board) CardList(ids []int) ([]Card, error) {
|
|
data, err := db.ReadMany(b.db, db.CardBucket, ids)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read card list from the db. %w", err)
|
|
}
|
|
|
|
cards := make([]Card, len(data))
|
|
|
|
for ind, d := range data {
|
|
buf := bytes.NewBuffer(d)
|
|
|
|
decoder := gob.NewDecoder(buf)
|
|
|
|
var card Card
|
|
|
|
if err := decoder.Decode(&card); err != nil {
|
|
return nil, fmt.Errorf("unable to decode data, %w", err)
|
|
}
|
|
|
|
cards[ind] = card
|
|
}
|
|
|
|
return cards, nil
|
|
}
|
|
|
|
// UpdateCardArgs is an argument type for updating a card.
|
|
type UpdateCardArgs struct {
|
|
CardID int
|
|
CardArgs
|
|
}
|
|
|
|
// UpdateCard modifies an existing card in the database.
|
|
func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
|
card, err := b.Card(args.CardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(args.NewTitle) > 0 {
|
|
card.Title = args.NewTitle
|
|
}
|
|
|
|
if len(args.NewDescription) > 0 {
|
|
card.Description = args.NewDescription
|
|
}
|
|
|
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.CardBucket, []db.BoltItem{&card}); err != nil {
|
|
return fmt.Errorf("unable to write the card to the database; %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteCardArgs is an argument type for deleting a card.
|
|
type DeleteCardArgs struct {
|
|
CardID int
|
|
StatusID int
|
|
}
|
|
|
|
// DeleteCard deletes a card from the database.
|
|
func (b *Board) DeleteCard(args DeleteCardArgs) error {
|
|
if err := db.Delete(b.db, db.CardBucket, args.CardID); err != nil {
|
|
return fmt.Errorf("unable to delete the card from the database; %w", err)
|
|
}
|
|
|
|
status, err := b.Status(args.StatusID)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to read Status '%d' from the database; %w", args.StatusID, err)
|
|
}
|
|
|
|
status.RemoveCardID(args.CardID)
|
|
|
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
|
|
return fmt.Errorf("unable to update the status in the database; %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|