pelican/internal/board/board.go
Dan Anglin c1bb834a7f
All checks were successful
/ test (pull_request) Successful in 34s
/ lint (pull_request) Successful in 46s
feat(ui): add support for creating status columns
This commit adds support for creating new status columns. When the user
is in the 'board edit' mode, they can press the 'c' key to create a new
column. When a new column is created it automatically assumes the last
position on the board. We will add support later on to allow the user to
re-arrange the columns on the board.

Part of apollo/pelican#22
2024-01-17 17:10:36 +00:00

364 lines
7.9 KiB
Go

package board
import (
"bytes"
"encoding/gob"
"fmt"
"sort"
"time"
"codeflow.dananglin.me.uk/apollo/pelican/internal/db"
bolt "go.etcd.io/bbolt"
)
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.WriteMany(database, 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 db.
// 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) {
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
}
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{
ID: -1,
Name: name,
Position: pos,
CardIds: nil,
}
if _, err := db.Write(b.db, db.StatusBucket, &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 db.
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 db. %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.StatusBucket, &status); err != nil {
return fmt.Errorf("unable to write the status to the db. %w", err)
}
return nil
}
// TODO: Finish implementation.
// func (b *Board) DeleteStatus() error {
// return nil
// }
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{&currentStatus, &nextStatus}
if _, err := db.WriteMany(b.db, db.StatusBucket, boltItems); err != nil {
return fmt.Errorf("unable to update the statuses in the db. %w", err)
}
return nil
}
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{}
}
card := Card{
ID: -1,
Title: args.NewTitle,
Description: args.NewDescription,
Created: timestamp,
}
cardID, err := db.Write(b.db, db.CardBucket, &card)
if err != nil {
return 0, fmt.Errorf("unable to write card to the db. %w", err)
}
initialStatus := statusList[0]
initialStatus.AddCardID(cardID)
id, err := db.Write(b.db, db.StatusBucket, &initialStatus)
if err != nil {
return 0, fmt.Errorf("unable to write the %s status to the db. %w", initialStatus.Name, err)
}
return id, nil
}
// Card returns a Card value from the database.
// TODO: Handle edge case where the card does not exist in the db.
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.
// TODO: function needs testing.
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
}
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.CardBucket, &card); err != nil {
return fmt.Errorf("unable to write card to the db. %w", err)
}
return nil
}
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.StatusBucket, &status); err != nil {
return fmt.Errorf("unable to update the status in the database, %w", err)
}
return nil
}