feat: delete cards #6

Closed
dananglin wants to merge 35 commits from delete-cards into main
12 changed files with 549 additions and 208 deletions
Showing only changes of commit 0839d566b5 - Show all commits

View file

@ -7,7 +7,9 @@ import (
)
func main() {
if err := ui.App(); err != nil {
fmt.Printf("Error: %s", err)
canal := ui.NewApp()
if err := canal.Run(); err != nil {
fmt.Printf("Error: an error occurred while running Canal, %s", err)
}
}

3
go.mod
View file

@ -3,7 +3,8 @@ module forge.dananglin.me.uk/code/dananglin/canal
go 1.16
require (
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
github.com/magefile/mage v1.11.0
github.com/rivo/tview v0.0.0-20210923051754-2cb20002bc4c
go.etcd.io/bbolt v1.3.6
)

25
go.sum
View file

@ -1,8 +1,27 @@
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/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/tview v0.0.0-20210923051754-2cb20002bc4c h1:ye4bWm8SafYmr0DADOKSfeVZ1Swzm9aLW+baCOcHDWE=
github.com/rivo/tview v0.0.0-20210923051754-2cb20002bc4c/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

17
internal/board/args.go Normal file
View file

@ -0,0 +1,17 @@
package board
type CardArgs struct {
NewTitle string
NewContent string
}
type UpdateCardArgs struct {
CardID int
CardArgs
}
type UpdateCardStatusArgs struct {
CardID int
OldStatusID int
NewStatusID int
}

View file

@ -10,8 +10,8 @@ import (
bolt "go.etcd.io/bbolt"
)
// LoadBoard reads the board from the database. If no board exists then a new one will be created.
func LoadBoard(path string) (*bolt.DB, error) {
// OpenProject reads the project from the database. If no board exists then a new one will be created.
func OpenProject(path string) (*bolt.DB, error) {
db, err := database.OpenDatabase(path)
if err != nil {
return nil, fmt.Errorf("unable to open the database, %w", err)
@ -31,7 +31,7 @@ func LoadBoard(path string) (*bolt.DB, error) {
boltItems[i] = &newStatusList[i]
}
if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
if _, err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
return nil, fmt.Errorf("unable to save the default status list to the database, %w", err)
}
}
@ -88,7 +88,7 @@ func DeleteStatus(db *bolt.DB) error {
}
// CreateCard creates a card in the database.
func CreateCard(db *bolt.DB, title, content string) error {
func CreateCard(db *bolt.DB, args CardArgs) error {
statusList, err := ReadStatusList(db)
if err != nil {
return fmt.Errorf("unable to read the status list, %w", err)
@ -100,8 +100,8 @@ func CreateCard(db *bolt.DB, title, content string) error {
card := Card{
ID: -1,
Title: title,
Content: content,
Title: args.NewTitle,
Content: args.NewContent,
}
cardID, err := database.Write(db, database.CardBucket, &card)
@ -140,19 +140,46 @@ func ReadCard(db *bolt.DB, id int) (Card, error) {
return card, nil
}
// ReadCardList returns a list of Card values from the database.
// TODO: function needs testing.
func ReadCardList(db *bolt.DB, ids []int) ([]Card, error) {
data, err := database.ReadMany(db, database.CardBucket, ids)
if err != nil {
return nil, fmt.Errorf("unable to read card list from the database, %w", err)
}
cards := make([]Card, len(data))
for i, d := range data {
buf := bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
var c Card
if err := decoder.Decode(&c); err != nil {
return nil, fmt.Errorf("unable to decode data, %w", err)
}
cards[i] = c
}
return cards, nil
}
// UpdateCard modifies an existing card and saves the modification to the database.
func UpdateCard(db *bolt.DB, id int, title, content string) error {
card, err := ReadCard(db, id)
func UpdateCard(db *bolt.DB, args UpdateCardArgs) error {
card, err := ReadCard(db, args.CardID)
if err != nil {
return err
}
if len(title) > 0 {
card.Title = title
if len(args.NewTitle) > 0 {
card.Title = args.NewTitle
}
if len(content) > 0 {
card.Content = content
if len(args.NewContent) > 0 {
card.Content = args.NewContent
}
if _, err := database.Write(db, database.CardBucket, &card); err != nil {
@ -162,9 +189,9 @@ func UpdateCard(db *bolt.DB, id int, title, content string) error {
return nil
}
// MoveCard moves a card between statuses.
// UpdateCardStatus moves a card between statuses.
// TODO: finish implementation.
func MoveCard(db *bolt.DB, fromStatusID, toStatusID int) error {
func UpdateCardStatus(db *bolt.DB, args UpdateCardStatusArgs) error {
return nil
}

View file

@ -22,7 +22,7 @@ func TestCardLifecycle(t *testing.T) {
testDBPath := filepath.Join(projectDir, "test", "databases", "Board_TestCardLifecycle.db")
os.Remove(testDBPath)
db, err := board.LoadBoard(testDBPath)
db, err := board.OpenProject(testDBPath)
if err != nil {
t.Fatalf("Unable to open the test database %s, %s.", testDBPath, err)
}

View file

@ -51,55 +51,57 @@ func OpenDatabase(path string) (*bolt.DB, error) {
return db, nil
}
// WriteMany saves one or more statuses to the status bucket.
func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) error {
if len(items) == 0 {
return nil
}
// Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) {
bucket := []byte(bucketName)
err := db.Update(func(tx *bolt.Tx) error {
var data []byte
if err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
for _, i := range items {
var err error
data = b.Get([]byte(strconv.Itoa(id)))
if i.Id() < 1 {
var id uint64
if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate ID, %w", err)
}
i.UpdateId(int(id))
return nil
}); err != nil {
return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err)
}
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
if err = encoder.Encode(i); err != nil {
return fmt.Errorf("unable to encode data, %w", err)
return data, nil
}
if err = b.Put([]byte(strconv.Itoa(i.Id())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to add the status to the bucket, %w", err)
// ReadMany reads one or more Bolt items from the specified bucket.
func ReadMany(db *bolt.DB, bucketName string, ids []int) ([][]byte, error) {
bucket := []byte(bucketName)
var output [][]byte
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: bucketName}
}
for _, v := range ids {
data := b.Get([]byte(strconv.Itoa(v)))
output = append(output, data)
}
return nil
})
if err != nil {
return fmt.Errorf("error while saving the statuses to the database, %w", err)
return output, fmt.Errorf("error while retrieving the data from the database, %w", err)
}
return nil
return output, nil
}
// TODO: Create ReadMany
// ReadAll retrieves all the statuses from the status bucket.
// ReadAll retrieves all the Bolt Items from the specified bucket.
func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
bucket := []byte(bucketName)
@ -109,7 +111,7 @@ func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
return bucketNotExistError{bucket: bucketName}
}
if err := b.ForEach(func(_, v []byte) error {
@ -168,27 +170,54 @@ func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) {
return item.Id(), nil
}
// Read retrieves a Bolt item from a specified bucket and returns the data in bytes.
func Read(db *bolt.DB, bucketName string, id int) ([]byte, error) {
// WriteMany saves one or more Bolt items to the status bucket.
func WriteMany(db *bolt.DB, bucketName string, items []BoltItem) ([]int, error) {
if len(items) == 0 {
return []int{}, nil
}
ids := make([]int, len(items))
bucket := []byte(bucketName)
var data []byte
if err := db.View(func(tx *bolt.Tx) error {
err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(bucket)
if b == nil {
return bucketNotExistError{bucket: string(bucket)}
}
data = b.Get([]byte(strconv.Itoa(id)))
for i, v := range items {
var err error
return nil
}); err != nil {
return []byte{}, fmt.Errorf("error while reading the Bolt item from the database, %w", err)
if v.Id() < 1 {
var id uint64
if id, err = b.NextSequence(); err != nil {
return fmt.Errorf("unable to generate ID, %w", err)
}
v.UpdateId(int(id))
}
return data, nil
buf := new(bytes.Buffer)
encoder := gob.NewEncoder(buf)
if err = encoder.Encode(v); err != nil {
return fmt.Errorf("unable to encode data, %w", err)
}
if err = b.Put([]byte(strconv.Itoa(v.Id())), buf.Bytes()); err != nil {
return fmt.Errorf("unable to add the Bolt Item to the %s bucket, %w", bucketName, err)
}
ids[i] = v.Id()
}
return nil
})
if err != nil {
return nil, fmt.Errorf("error while saving the Bolt Items to the database, %w", err)
}
return ids, nil
}
// dbPath returns the path to the database file. If a path is given then that is returned. Otherwise the default path is returned.

View file

@ -112,7 +112,7 @@ func testWriteStatusList(t *testing.T, db *bolt.DB) {
boltItems[i] = &newStatusList[i]
}
if err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
if _, err := database.WriteMany(db, database.StatusBucket, boltItems); err != nil {
t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err)
}
}
@ -175,7 +175,7 @@ func testReadStatusList(t *testing.T, db *bolt.DB) {
}
}
func TestReadAndWriteCard(t *testing.T) {
func TestReadAndWriteCards(t *testing.T) {
t.Parallel()
var db *bolt.DB
@ -198,20 +198,41 @@ func TestReadAndWriteCard(t *testing.T) {
_ = db.Close()
}()
cardID := testWriteCard(t, db)
testReadCard(t, db, cardID)
}
func testWriteCard(t *testing.T, db *bolt.DB) int {
t.Helper()
newCard := board.Card{
singleCard := board.Card{
ID: -1,
Title: "A test task.",
Content: "This task should be completed.",
}
cardID, err := database.Write(db, database.CardBucket, &newCard)
singleCardID := testWriteOneCard(t, db, singleCard)
testReadOneCard(t, db, singleCardID)
manyCards := []board.Card{
{
ID: -1,
Title: "Test card A.",
Content: "This is test card A.",
},
{
ID: -1,
Title: "Test card B.",
Content: "This is test card B.",
},
{
ID: -1,
Title: "Test card C.",
Content: "This is test card C.",
},
}
manyCardIDs := testWriteManyCard(t, db, manyCards)
testReadManyCards(t, db, manyCardIDs)
}
func testWriteOneCard(t *testing.T, db *bolt.DB, card board.Card) int {
t.Helper()
cardID, err := database.Write(db, database.CardBucket, &card)
if err != nil {
t.Fatalf("An error occurred whilst writing the card to the database, %s", err)
}
@ -219,7 +240,7 @@ func testWriteCard(t *testing.T, db *bolt.DB) int {
return cardID
}
func testReadCard(t *testing.T, db *bolt.DB, cardID int) {
func testReadOneCard(t *testing.T, db *bolt.DB, cardID int) {
t.Helper()
data, err := database.Read(db, database.CardBucket, cardID)
@ -250,6 +271,72 @@ func testReadCard(t *testing.T, db *bolt.DB, cardID int) {
}
}
func testWriteManyCard(t *testing.T, db *bolt.DB, cards []board.Card) []int {
t.Helper()
boltItems := make([]database.BoltItem, len(cards))
for i := range cards {
boltItems[i] = &cards[i]
}
ids, err := database.WriteMany(db, database.CardBucket, boltItems)
if err != nil {
t.Fatalf("An error occurred whilst writing many cards to the database, %s", err)
}
return ids
}
func testReadManyCards(t *testing.T, db *bolt.DB, cardIDs []int) {
t.Helper()
data, err := database.ReadMany(db, database.CardBucket, cardIDs)
if err != nil {
t.Fatalf("An error occurred whilst reading the data from the database, %s", err)
}
got := make([]board.Card, len(data))
for i, d := range data {
buf := bytes.NewBuffer(d)
decoder := gob.NewDecoder(buf)
var c board.Card
if err := decoder.Decode(&c); err != nil {
t.Fatalf("An error occurred whilst decoding data, %s", err)
}
got[i] = c
}
want := []board.Card{
{
ID: 2,
Title: "Test card A.",
Content: "This is test card A.",
},
{
ID: 3,
Title: "Test card B.",
Content: "This is test card B.",
},
{
ID: 4,
Title: "Test card C.",
Content: "This is test card C.",
},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("Unexpected list of cards read from the database: got %+v, want %+v", got, want)
} else {
t.Logf("Expected list of cards read from the database: got %+v", got)
}
}
func projectRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {

154
internal/ui/app.go Normal file
View file

@ -0,0 +1,154 @@
package ui
import (
"fmt"
"forge.dananglin.me.uk/code/dananglin/canal/internal/board"
"github.com/rivo/tview"
bolt "go.etcd.io/bbolt"
)
type shiftDirection int
const (
shiftLeft shiftDirection = iota
shiftRight
)
const (
mainPage string = "main"
quitPage string = "quit"
addPage string = "add"
)
// App does some magical stuff.
type App struct {
*tview.Application
columns []column
flex *tview.Flex
pages *tview.Pages
focusedColumn int
db *bolt.DB
}
// shutdown shuts down the application.
func (a *App) shutdown() {
a.closeDB()
a.Stop()
}
// closeDB closes the BoltDB database.
func (a *App) closeDB() {
if a.db != nil {
_ = a.db.Close()
}
}
// openProject opens the kanban project.
func (a *App) openProject(path string) error {
if a.db != nil && len(a.db.Path()) > 0 {
a.db.Close()
}
db, err := board.OpenProject(path)
if err != nil {
return fmt.Errorf("unable to load board, %w", err)
}
a.db = db
if err = a.refresh(); err != nil {
return err
}
return nil
}
// refresh refreshes the UI.
func (a *App) refresh() error {
statusList, err := board.ReadStatusList(a.db)
if err != nil {
return fmt.Errorf("unable to get the status list, %w", err)
}
a.updateBoard(statusList)
a.setColumnFocus()
return nil
}
func (a *App) updateBoard(statusList []board.Status) error {
a.flex.Clear()
columns := make([]column, len(statusList))
for i := range statusList {
columns[i] = a.newColumn(statusList[i].ID, statusList[i].Name)
if len(statusList[i].CardIds) > 0 {
cards, err := board.ReadCardList(a.db, statusList[i].CardIds)
if err != nil {
return fmt.Errorf("unable to get the card list. %w", err)
}
for _, c := range cards {
columns[i].cards.AddItem(fmt.Sprintf("[%d] %s", c.Id(), c.Title), "", 0, nil)
}
}
a.flex.AddItem(columns[i].cards, 0, 1, false)
}
a.columns = columns
return nil
}
func (a *App) shiftColumnFocus(s shiftDirection) {
switch s {
case shiftRight:
if a.focusedColumn == len(a.columns)-1 {
a.focusedColumn = 0
} else {
a.focusedColumn++
}
case shiftLeft:
if a.focusedColumn == 0 {
a.focusedColumn = len(a.columns) - 1
} else {
a.focusedColumn--
}
}
a.setColumnFocus()
}
func (a *App) setColumnFocus() {
a.SetFocus(a.columns[a.focusedColumn].cards)
}
// newCard creates a new card and saves it to the database.
func (a *App) newCard(title, content string) error {
args := board.CardArgs{
NewTitle: title,
NewContent: content,
}
if err := board.CreateCard(a.db, args); err != nil {
return fmt.Errorf("unable to create card, %w", err)
}
a.refresh()
return nil
}
// TODO: Move 'Add' to the centre of the app
// TODO: Customize list primitive or create a new one
// TODO: If customizing exisiing list primitive, wrap list around a column type. Add statusID to it.
// TODO: Update card status (card ID, oldStatus, newStatus)
//func viewCard() error {
//return nil
//}

41
internal/ui/column.go Normal file
View file

@ -0,0 +1,41 @@
package ui
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
type column struct {
statusID int
statusName string
cards *tview.List
}
func (a *App) newColumn(statusID int, statusName string) column {
l := tview.NewList()
l.SetBorder(true)
l.ShowSecondaryText(false)
l.SetTitle(" " + statusName + " ")
l.SetHighlightFullLine(true)
l.SetSelectedFocusOnly(true)
l.SetWrapAround(false)
l.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Rune() == 'h' || event.Key() == tcell.KeyLeft {
a.shiftColumnFocus(shiftLeft)
} else if event.Rune() == 'l' || event.Key() == tcell.KeyRight {
a.shiftColumnFocus(shiftRight)
}
return event
})
c := column{
statusID: statusID,
statusName: statusName,
cards: l,
}
return c
}

100
internal/ui/init.go Normal file
View file

@ -0,0 +1,100 @@
package ui
import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// NewApp returns a new App.
func NewApp() App {
a := App{
Application: tview.NewApplication(),
pages: tview.NewPages(),
flex: tview.NewFlex(),
focusedColumn: 0,
}
initApp(&a)
return a
}
// initApp initialises App
func initApp(a *App) {
a.pages.AddPage(mainPage, a.flex, true, true)
quit := newQuitModal(a)
a.pages.AddPage(quitPage, quit, false, false)
add := newAddForm(a)
a.pages.AddPage(addPage, add, false, false)
a.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Rune() == 'q' {
a.pages.ShowPage(quitPage)
a.SetFocus(quit)
} else if event.Rune() == 'o' {
a.openProject("")
} else if event.Rune() == 'a' {
a.pages.ShowPage(addPage)
a.SetFocus(add)
}
return event
})
a.SetRoot(a.pages, true)
}
// newQuitModal returns a new modal for the user's confirmation
// for quitting the application.
func newQuitModal(a *App) *tview.Modal {
quit := *tview.NewModal()
quitDoneFunc := func(_ int, buttonLabel string) {
switch buttonLabel {
case "Quit":
a.shutdown()
default:
a.pages.SwitchToPage("main")
a.setColumnFocus()
}
}
quit.SetText("Do you want to quit the application?").
AddButtons([]string{"Quit", "Cancel"}).
SetDoneFunc(quitDoneFunc)
return &quit
}
// newAddForm creates a new Form primitive for creating a new card.
func newAddForm(a *App) *tview.Form {
add := tview.NewForm()
titleField := "Title"
add.AddInputField(titleField, "", 0, nil, nil)
add.AddButton("Save", func() {
title := add.GetFormItemByLabel(titleField).(*tview.InputField).GetText()
// TODO: error value needs handling
_ = a.newCard(title, "")
add.GetFormItemByLabel(titleField).(*tview.InputField).SetText("")
a.pages.SwitchToPage(mainPage)
a.setColumnFocus()
})
add.AddButton("Cancel", func() {
a.pages.SwitchToPage(mainPage)
add.GetFormItemByLabel(titleField).(*tview.InputField).SetText("")
a.setColumnFocus()
})
add.SetBorder(true)
add.SetTitle(" New Card ")
return add
}

View file

@ -1,136 +0,0 @@
// Right now this will be a very simple, scuffed interface.
package ui
import (
"fmt"
"forge.dananglin.me.uk/code/dananglin/canal/internal/board"
"github.com/eiannone/keyboard"
bolt "go.etcd.io/bbolt"
)
func App() error {
var db *bolt.DB
var err error
keysEvents, err := keyboard.GetKeys(10)
if err != nil {
return fmt.Errorf("unable to create the keysEvent channel, %w", err)
}
defer func() {
_ = keyboard.Close()
}()
fmt.Println(usage())
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 = refresh(db); err != nil {
fmt.Printf("Error: Unable to refresh board, %s\n", err)
}
case 'a':
if err = newCard(db); err != nil {
fmt.Printf("Error: Unable to add a card, %s\n", err)
}
case 'v':
if err = viewCard(db, 1); err != nil {
fmt.Printf("Error: Unable to view card, %s\n", err)
}
case 'o':
// TODO: How do we close the db?
db, err = openProject("")
if err != nil {
fmt.Printf("Error: Unable to open the project, %s\n", err)
}
default:
fmt.Println("Error: Unknown key event.")
}
}
db.Close()
return nil
}
func refresh(db *bolt.DB) error {
statusList, err := board.ReadStatusList(db)
if err != nil {
return fmt.Errorf("unable to get the status list, %w", 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 newCard(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 := board.CreateCard(db, title, content); err != nil {
return fmt.Errorf("unable to create card, %w", err)
}
fmt.Println("Sample card created successfully.")
return nil
}
func viewCard(db *bolt.DB, id int) error {
card, err := board.ReadCard(db, id)
if err != nil {
return fmt.Errorf("unable to read card, %w", 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 openProject(path string) (*bolt.DB, error) {
db, err := board.LoadBoard(path)
if err != nil {
return nil, fmt.Errorf("unable to load board, %w", err)
}
if err = refresh(db); err != nil {
return nil, err
}
return db, nil
}
func usage() string {
usage := `
Press 'o' to open the project
Press 'r' to refresh the board
Press 'a' to add a sample card
Press 'v' to view the first sample card
Press 'q' to quit
`
return usage
}