This commit is contained in:
parent
673d56903d
commit
bb685be5d5
55
client/todo/add.go
Normal file
55
client/todo/add.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add Command
|
||||||
|
func Add() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "add",
|
||||||
|
Usage: "add todo item (s)",
|
||||||
|
Action: addAction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addAction logic for Template
|
||||||
|
func addAction(context context.Context, c *cli.Command) error {
|
||||||
|
store, err := common.GetTodoDataStore()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var newTodos []common.Todo
|
||||||
|
var adding = true
|
||||||
|
for adding {
|
||||||
|
newTodos = append(newTodos, createNewTodo())
|
||||||
|
if !common.AskUserBool("Want to add more?") {
|
||||||
|
adding = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range newTodos {
|
||||||
|
err := t.Store(store, cfg.Server.Credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewTodo() common.Todo {
|
||||||
|
return common.Todo{
|
||||||
|
Name: common.AskUserString("Name:\n"),
|
||||||
|
Description: common.AskUserString("Description:\n"),
|
||||||
|
Status: common.AskUserString(fmt.Sprintf("Status(%s, %s, %s, %s, %s, %s):\n",
|
||||||
|
common.NotStarted, common.Done, common.WIP, common.Pending, common.Blocked, common.Failed,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Owner: cfg.Server.Credentials.Username,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ package httpClient
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthTransport struct {
|
type AuthTransport struct {
|
||||||
@ -15,7 +17,7 @@ type CustomClient struct {
|
|||||||
|
|
||||||
// RoundTrip transport method implementation with jwt in header
|
// RoundTrip transport method implementation with jwt in header
|
||||||
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (t *AuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("Beare%s", t.Token))
|
req.Header.Add(common.AuthHeader, fmt.Sprintf("Bearer %s", t.Token))
|
||||||
return http.DefaultTransport.RoundTrip(req)
|
return http.DefaultTransport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,7 @@ func loginAndGetToken(url, username, password string) (string, error) {
|
|||||||
return "", fmt.Errorf("error marshaling credentials: %w", err)
|
return "", fmt.Errorf("error marshaling credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
req, err := http.NewRequest("POST", url+"/login", bytes.NewBuffer(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error creating request: %w", err)
|
return "", fmt.Errorf("error creating request: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,15 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"maps"
|
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config"
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config"
|
||||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
|
||||||
"github.com/urfave/cli/v3"
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,28 +46,7 @@ func commands() []*cli.Command {
|
|||||||
config.Category(),
|
config.Category(),
|
||||||
Login(),
|
Login(),
|
||||||
Sync(),
|
Sync(),
|
||||||
{
|
Add(),
|
||||||
Name: "todo",
|
Todo(),
|
||||||
Usage: "print todo items",
|
|
||||||
Action: printTodo,
|
|
||||||
HideHelpCommand: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printTodo(context context.Context, c *cli.Command) error {
|
|
||||||
fmt.Printf("Todo items:\n")
|
|
||||||
store, err := common.GetTodoDataStore()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serverTodos := store.GetTodos(cfg.Server.Credentials.Username)
|
|
||||||
|
|
||||||
var index = 1
|
|
||||||
|
|
||||||
for todo := range maps.Values(serverTodos) {
|
|
||||||
todo.PrintIndexed(index)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"maps"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient"
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient"
|
||||||
@ -21,7 +19,6 @@ func Sync() *cli.Command {
|
|||||||
Name: "sync",
|
Name: "sync",
|
||||||
Usage: "sync with kleinTodo server",
|
Usage: "sync with kleinTodo server",
|
||||||
Action: syncAction,
|
Action: syncAction,
|
||||||
Flags: loginFlags(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,20 +28,19 @@ func syncAction(context context.Context, c *cli.Command) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
serverTodos := store.GetTodos(cfg.Server.Credentials.Username)
|
serverTodos := store.GetTodoList(cfg.Server.Credentials.Username)
|
||||||
|
|
||||||
var todos []common.Todo
|
var todos []common.Todo
|
||||||
|
|
||||||
for todo := range maps.Values(serverTodos) {
|
for _, t := range serverTodos {
|
||||||
todos = append(todos, todo)
|
todos = append(todos, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := json.Marshal(common.TodoList{Todos: todos})
|
payload, err := json.Marshal(common.TodoList{Todos: todos})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error marshaling credentials: %w", err)
|
return fmt.Errorf("error marshaling credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
req, err := http.NewRequest("GET", cfg.Server.Url+"/sync", bytes.NewBuffer(payload))
|
||||||
req, err := http.NewRequest("POST", cfg.Server.Url, bytes.NewBuffer(payload))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating request: %w", err)
|
return fmt.Errorf("error creating request: %w", err)
|
||||||
}
|
}
|
||||||
@ -68,16 +64,35 @@ func syncAction(context context.Context, c *cli.Command) error {
|
|||||||
var response common.SyncResponse
|
var response common.SyncResponse
|
||||||
|
|
||||||
if err := json.Unmarshal(body, &response); err != nil {
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
return fmt.Errorf("failed to decode successful response: %w", err)
|
return fmt.Errorf("failed to decode successful response: %w\n%s", err, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
prettyJSON, err := json.MarshalIndent(response, "", " ")
|
var index = 1
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to generate json: %s", err)
|
if len(response.MisMatchingTodos) > 0 {
|
||||||
|
for _, todo := range response.MisMatchingTodos {
|
||||||
|
fmt.Println("Mismatch between server and client")
|
||||||
|
fmt.Print("local:")
|
||||||
|
todo.LocalTodo.PrintIndexed(1)
|
||||||
|
fmt.Print("server:")
|
||||||
|
todo.ServerTodo.PrintIndexed(2)
|
||||||
|
if common.AskUserBool("Do you wish to override you local version with the server version?") {
|
||||||
|
response.SyncedTodos = append(response.SyncedTodos, todo.ServerTodo)
|
||||||
|
} else {
|
||||||
|
response.SyncedTodos = append(response.SyncedTodos, todo.LocalTodo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print the string version of the byte slice.
|
fmt.Println("Successfully synced with the server:")
|
||||||
fmt.Printf("%s\n", prettyJSON)
|
for _, todo := range response.SyncedTodos {
|
||||||
|
err := todo.Store(store, cfg.Server.Credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
todo.PrintIndexed(index)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
173
client/todo/todo.go
Normal file
173
client/todo/todo.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Todo Command
|
||||||
|
func Todo() *cli.Command {
|
||||||
|
return &cli.Command{
|
||||||
|
Name: "todo",
|
||||||
|
Usage: "print todo items and allow for updating",
|
||||||
|
Action: todo,
|
||||||
|
HideHelpCommand: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func todo(context context.Context, c *cli.Command) error {
|
||||||
|
// bufio.Scanner is a great way to read user input line by line.
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
|
||||||
|
store, err := common.GetTodoDataStore()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
todos := store.GetTodoList(cfg.Server.Credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the main application loop for the interactive mode.
|
||||||
|
for {
|
||||||
|
clearScreen()
|
||||||
|
printTodos(todos)
|
||||||
|
|
||||||
|
fmt.Print("What would you like to do? (update, delete, add, quit) [u/d/a/q]: ")
|
||||||
|
|
||||||
|
// Wait for and read the user's next command.
|
||||||
|
scanner.Scan()
|
||||||
|
command := strings.ToLower(strings.TrimSpace(scanner.Text()))
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "u", "update":
|
||||||
|
todos = handleUpdate(scanner, todos, store)
|
||||||
|
case "d", "delete":
|
||||||
|
todos = handleDelete(scanner, todos, store)
|
||||||
|
case "a", "add":
|
||||||
|
todos = handleAdd(scanner, todos, store)
|
||||||
|
case "q", "quit":
|
||||||
|
fmt.Println("Goodbye!")
|
||||||
|
return nil // Exit the program
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid command. Please try again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearScreen() {
|
||||||
|
fmt.Print("\033[H\033[2J")
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTodos(todos []common.Todo) {
|
||||||
|
fmt.Printf("Todo items:\n")
|
||||||
|
for i, t := range todos {
|
||||||
|
t.PrintIndexed(i + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDelete prompts for an index and removes the item.
|
||||||
|
func handleDelete(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltStore) []common.Todo {
|
||||||
|
fmt.Print("Enter the number of the item to delete: ")
|
||||||
|
scanner.Scan()
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
index, err := strconv.Atoi(input)
|
||||||
|
|
||||||
|
if err != nil || index < 1 || index > len(todos) {
|
||||||
|
fmt.Println("Invalid number. Returning to main menu.")
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
removedItem := todos[index-1]
|
||||||
|
|
||||||
|
err = store.RemoveValueFromBucket(cfg.Server.Credentials.Username, removedItem.Name)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
fmt.Printf("Item '%s' deleted.\n", removedItem.Name)
|
||||||
|
|
||||||
|
todos = append(todos[:index-1], todos[index:]...)
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUpdate prompts for an index and new values.
|
||||||
|
func handleUpdate(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltStore) []common.Todo {
|
||||||
|
fmt.Print("Enter the number of the item to update: ")
|
||||||
|
scanner.Scan()
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
index, err := strconv.Atoi(input)
|
||||||
|
|
||||||
|
if err != nil || index < 1 || index > len(todos) {
|
||||||
|
fmt.Println("Invalid number. Returning to main menu.")
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for 0-based slice index
|
||||||
|
itemToUpdate := todos[index-1]
|
||||||
|
|
||||||
|
fmt.Printf("Updating '%s'. Press Enter to keep current value.\n", itemToUpdate.Name)
|
||||||
|
|
||||||
|
fmt.Printf("New name [%s]: ", itemToUpdate.Name)
|
||||||
|
scanner.Scan()
|
||||||
|
newName := strings.TrimSpace(scanner.Text())
|
||||||
|
if newName != "" {
|
||||||
|
itemToUpdate.Name = newName
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("New description [%s]: ", itemToUpdate.Description)
|
||||||
|
scanner.Scan()
|
||||||
|
newDescription := strings.TrimSpace(scanner.Text())
|
||||||
|
if newDescription != "" {
|
||||||
|
itemToUpdate.Description = newDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("New status [%s]: ", itemToUpdate.Status)
|
||||||
|
scanner.Scan()
|
||||||
|
newStatus := strings.TrimSpace(scanner.Text())
|
||||||
|
if newStatus != "" {
|
||||||
|
itemToUpdate.Status = newStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
err = itemToUpdate.Store(store, cfg.Server.Credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Item updated.")
|
||||||
|
todos[index-1] = itemToUpdate
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAdd prompts for the details of a new item.
|
||||||
|
func handleAdd(scanner *bufio.Scanner, todos []common.Todo, store *common.BoltStore) []common.Todo {
|
||||||
|
fmt.Print("Enter the name of the new task: ")
|
||||||
|
scanner.Scan()
|
||||||
|
name := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
fmt.Print("Enter the description: ")
|
||||||
|
scanner.Scan()
|
||||||
|
description := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
newTodo := common.Todo{
|
||||||
|
Name: name, Description: description, Status: common.NotStarted, Owner: cfg.Server.Credentials.Username,
|
||||||
|
}
|
||||||
|
err := newTodo.Store(store, cfg.Server.Credentials.Username)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(err.Error())
|
||||||
|
return todos
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("New item added.")
|
||||||
|
return append(todos, newTodo)
|
||||||
|
}
|
||||||
@ -180,7 +180,7 @@ func (s *BoltStore) ExistsByKey(bucket, key string) (bool, error) {
|
|||||||
return exists, err
|
return exists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *BoltStore) GetTodos(user string) map[string]Todo {
|
func (s *BoltStore) GetTodoMap(user string) map[string]Todo {
|
||||||
storedTodoJsons := s.GetAllFromBucket(user)
|
storedTodoJsons := s.GetAllFromBucket(user)
|
||||||
|
|
||||||
serverTodos := make(map[string]Todo)
|
serverTodos := make(map[string]Todo)
|
||||||
@ -192,3 +192,16 @@ func (s *BoltStore) GetTodos(user string) map[string]Todo {
|
|||||||
}
|
}
|
||||||
return serverTodos
|
return serverTodos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *BoltStore) GetTodoList(user string) []Todo {
|
||||||
|
storedTodoJsons := s.GetAllFromBucket(user)
|
||||||
|
|
||||||
|
var serverTodos []Todo
|
||||||
|
for _, val := range storedTodoJsons {
|
||||||
|
var todo Todo
|
||||||
|
if json.Unmarshal([]byte(val), &todo) == nil {
|
||||||
|
serverTodos = append(serverTodos, todo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverTodos
|
||||||
|
}
|
||||||
|
|||||||
@ -16,9 +16,10 @@ const (
|
|||||||
|
|
||||||
// statuses
|
// statuses
|
||||||
const (
|
const (
|
||||||
Done = "done"
|
NotStarted = "not started"
|
||||||
WIP = "work in progress"
|
Done = "done"
|
||||||
Pending = "pending"
|
WIP = "in progress"
|
||||||
Blocked = "blocked"
|
Pending = "pending"
|
||||||
Failed = "failed"
|
Blocked = "blocked"
|
||||||
|
Failed = "failed"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func (todo Todo) PrintIndexed(index int) {
|
|||||||
statusColor = ColorGreen
|
statusColor = ColorGreen
|
||||||
case WIP:
|
case WIP:
|
||||||
statusColor = ColorYellow
|
statusColor = ColorYellow
|
||||||
case Pending:
|
case Pending, NotStarted:
|
||||||
statusColor = ColorBlue
|
statusColor = ColorBlue
|
||||||
case Blocked, Failed:
|
case Blocked, Failed:
|
||||||
statusColor = ColorRed
|
statusColor = ColorRed
|
||||||
|
|||||||
@ -26,7 +26,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverTodos := store.GetTodos(user)
|
serverTodos := store.GetTodoMap(user)
|
||||||
|
|
||||||
var response = common.SyncResponse{
|
var response = common.SyncResponse{
|
||||||
SyncedTodos: []common.Todo{},
|
SyncedTodos: []common.Todo{},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user