diff --git a/card.go b/card.go new file mode 100644 index 0000000..7cbff20 --- /dev/null +++ b/card.go @@ -0,0 +1,83 @@ +package main + +import ( + "errors" + + bolt "go.etcd.io/bbolt" +) + +// Card represents a card on a Kanban board. +type Card struct { + ID int + Title string + Content string +} + +// TODO: this function needs to be unit tested and documented. +func createCard(db *bolt.DB, title, content string) error { + // first ensure that there is a status list in the database + statusList, err := loadAllStatuses(db) + if err != nil { + return err + } + + if len(statusList) == 0 { + return errors.New("the status list cannot be empty") + } + + card := Card{ + ID: -1, + Title: title, + Content: content, + } + + cardID, err := saveCard(db, card) + if err != nil { + return err + } + + cardIDs := statusList[0].CardIds + + cardIDs = append(cardIDs, cardID) + + statusList[0].CardIds = cardIDs + + // TODO: change the below to save a single status + if err := saveStatuses(db, statusList); err != nil { + return err + } + + return nil +} + +// readCard returns a Card value from the database. +func readCard(db *bolt.DB, id int) (Card, error) { + return loadCard(db, id) +} + +// TODO: unit test and document this function. +func updateCard(db *bolt.DB, id int, title, content string) error { + card, err := loadCard(db, id) + if err != nil { + return nil + } + + card.Title = title + card.Content = content + + if _, err := saveCard(db, card); err != nil { + return err + } + + return nil +} + +// TODO: finish implementation +func moveCard(db *bolt.DB, fromStatusID, toStatusID int) error { + return nil +} + +// TODO: finish implementation +func deleteCard(db *bolt.DB, id int) error { + return nil +} diff --git a/go.mod b/go.mod index 8245989..de79f56 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module forge.dananglin.me.uk/code/dananglin/pelican go 1.16 require ( + github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 github.com/magefile/mage v1.11.0 go.etcd.io/bbolt v1.3.6 ) diff --git a/go.sum b/go.sum index 6fef076..c09086e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ= +github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8= github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= diff --git a/main.go b/main.go index da29a2c..040a1f1 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,53 @@ package main +import ( + "fmt" + "os" +) + func main() { + path, err := dbPath("") + if err != nil { + fmt.Printf("Error: Unable to get the database path, %s", err) + os.Exit(1) + } + + db, err := openDatabase(path) + if err != nil { + fmt.Printf("Error: Unable to open the database, %s", err) + os.Exit(1) + } + + defer db.Close() + + if err := ensureBuckets(db); err != nil { + fmt.Printf("Error: Unable to ensure buckets exist, %s", err) + os.Exit(1) + } + + var statusList []Status + + statusList, err = readStatuses(db) + if err != nil { + fmt.Printf("Error: Unable to get status list, %s", err) + os.Exit(1) + } + + if len(statusList) == 0 { + newStatusList := newDefaultStatusList() + if err := saveStatuses(db, newStatusList); err != nil { + fmt.Printf("Error: Unable to save the default status list to the database, %s", err) + os.Exit(1) + } + + _, err = readStatuses(db) + if err != nil { + fmt.Printf("Error: Unable to get status list, %s", err) + os.Exit(1) + } + } + + if err := badUI(db); err != nil { + fmt.Printf("Error: %s", err) + } } diff --git a/kanban.go b/status.go similarity index 55% rename from kanban.go rename to status.go index 37ccb8e..4b65367 100644 --- a/kanban.go +++ b/status.go @@ -7,13 +7,6 @@ import ( bolt "go.etcd.io/bbolt" ) -// Card represents a card on a Kanban board. -type Card struct { - ID int - Title string - Content string -} - // Status represents the status of the Kanban board. type Status struct { ID int @@ -37,9 +30,7 @@ func (s ByStatusOrder) Less(i, j int) bool { return s[i].Order < s[j].Order } -// readStatuses reads all statuses from the database. If there are no statuses in the database then the default -// status list is created and stored in the database. Before returning the status list, the list is ordered based -// on the status' 'Order' field. +// readStatuses returns an ordered list of statuses from the database. // TODO: function needs to be unit tested. func readStatuses(db *bolt.DB) ([]Status, error) { statuses, err := loadAllStatuses(db) @@ -47,34 +38,45 @@ func readStatuses(db *bolt.DB) ([]Status, error) { return statuses, fmt.Errorf("unable to read statuses, %w", err) } - if len(statuses) == 0 { - if err := createDefaultStatusList(db); err != nil { - return statuses, fmt.Errorf("unable to create the default statuses, %w", err) - } - readStatuses(db) - } - sort.Sort(ByStatusOrder(statuses)) return statuses, nil } -// createDefaultStatusList creates the default list of statuses and saves the statuses to the database. -func createDefaultStatusList(db *bolt.DB) error { - s := []Status{ +func readStatus(db *bolt.DB) (Status, error) { + return Status{}, nil +} + +func createStatus(db *bolt.DB) error { + return nil +} + +func updateStatus(db *bolt.DB) error { + return nil +} + +func deleteStatus(db *bolt.DB) error { + return nil +} + +// newDefaultStatusList creates the default list of statuses and saves the statuses to the database. +// TODO: function needs to be unit tested. +func newDefaultStatusList() []Status { + return []Status{ { + ID: -1, Name: "To Do", Order: 1, }, { + ID: -1, Name: "Doing", Order: 2, }, { + ID: -1, Name: "Done", Order: 3, }, } - - return saveStatuses(db, s) } diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..4c5fe31 --- /dev/null +++ b/ui.go @@ -0,0 +1,113 @@ +// Right now this will be a very simple, scuffed interface. +package main + +import ( + "fmt" + + bolt "go.etcd.io/bbolt" + "github.com/eiannone/keyboard" +) + +func badUI(db *bolt.DB) error { + keysEvents, err := keyboard.GetKeys(10) + if err != nil { + return fmt.Errorf("unable to create the keysEvent channel, %w", err) + } + + defer func() { + _ = keyboard.Close() + }() + + // infinite loop + fmt.Println(uiUsage()) + + app: + for { + event := <-keysEvents + if event.Err != nil { + return fmt.Errorf("keys event error: %w", event.Err) + } + + switch event.Rune { + case 'q': + break app + case 'r': + if err := uiRefreshScuffedBoard(db); err != nil { + fmt.Printf("Error: Unable to refresh board, %s\n", err) + } + case 'a': + if err := uiCreateCard(db); err != nil { + fmt.Printf("Error: Unable to add a card, %s\n", err) + } + case 'v': + if err := uiViewCard(db, 1); err != nil { + fmt.Printf("Error: Unable to view card, %s\n", err) + } + default: + fmt.Println("Error: Unknown key event.") + } + } + + // wait for a keyboard press + // take action + // a to add a card + // u to update a card + return nil +} + +func uiRefreshScuffedBoard(db *bolt.DB) error { + statusList, err := readStatuses(db) + if err != nil { + return err + } + + fmt.Printf("--------------------\n") + for _, s := range statusList{ + fmt.Printf("Status ID: %d\nStatus Name: \"%s\"\nCard IDs: %v\n\n", s.ID, s.Name, s.CardIds) + } + fmt.Printf("--------------------\n\n\n") + + return nil +} + +func uiCreateCard(db *bolt.DB) error { + title := "A card title" + + content := "As a user, this is a ticket for me.\nAs a user, I want to close it." + + if err := createCard(db, title, content); err != nil { + return err + } + + fmt.Println("Sample card created successfully.") + + return nil +} + +func uiViewCard(db *bolt.DB, id int) error { + card, err := readCard(db, id) + if err != nil { + return err + } + + + fmt.Printf("====================\n") + fmt.Printf("[%d] %s\n", card.ID, card.Title) + fmt.Printf("--------------------\n") + fmt.Println(card.Content) + fmt.Printf("====================\n") + + return nil +} + +func uiUsage() string { + usage := ` +Press 'q' to quit +Press 'r' to refresh the board +Press 'a' to add a sample card +Press 'v' to view the first sample card + +` + + return usage +}