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 (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
)
|
||||
|
||||
type AuthTransport struct {
|
||||
@ -15,7 +17,7 @@ type CustomClient struct {
|
||||
|
||||
// RoundTrip transport method implementation with jwt in header
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -95,7 +95,7 @@ func loginAndGetToken(url, username, password string) (string, error) {
|
||||
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 {
|
||||
return "", fmt.Errorf("error creating request: %w", err)
|
||||
}
|
||||
|
||||
@ -2,15 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net/mail"
|
||||
"os"
|
||||
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/clientCommon/config"
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/common"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
@ -49,28 +46,7 @@ func commands() []*cli.Command {
|
||||
config.Category(),
|
||||
Login(),
|
||||
Sync(),
|
||||
{
|
||||
Name: "todo",
|
||||
Usage: "print todo items",
|
||||
Action: printTodo,
|
||||
HideHelpCommand: true,
|
||||
},
|
||||
Add(),
|
||||
Todo(),
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"net/http"
|
||||
|
||||
"gitea.kleinsense.nl/DariusKlein/kleinTodo/client/todo/httpClient"
|
||||
@ -21,7 +19,6 @@ func Sync() *cli.Command {
|
||||
Name: "sync",
|
||||
Usage: "sync with kleinTodo server",
|
||||
Action: syncAction,
|
||||
Flags: loginFlags(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,20 +28,19 @@ func syncAction(context context.Context, c *cli.Command) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serverTodos := store.GetTodos(cfg.Server.Credentials.Username)
|
||||
serverTodos := store.GetTodoList(cfg.Server.Credentials.Username)
|
||||
|
||||
var todos []common.Todo
|
||||
|
||||
for todo := range maps.Values(serverTodos) {
|
||||
todos = append(todos, todo)
|
||||
for _, t := range serverTodos {
|
||||
todos = append(todos, t)
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(common.TodoList{Todos: todos})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling credentials: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", cfg.Server.Url, bytes.NewBuffer(payload))
|
||||
req, err := http.NewRequest("GET", cfg.Server.Url+"/sync", bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
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, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate json: %s", err)
|
||||
var index = 1
|
||||
|
||||
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.Printf("%s\n", prettyJSON)
|
||||
fmt.Println("Successfully synced with the server:")
|
||||
for _, todo := range response.SyncedTodos {
|
||||
err := todo.Store(store, cfg.Server.Credentials.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
todo.PrintIndexed(index)
|
||||
index++
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (s *BoltStore) GetTodos(user string) map[string]Todo {
|
||||
func (s *BoltStore) GetTodoMap(user string) map[string]Todo {
|
||||
storedTodoJsons := s.GetAllFromBucket(user)
|
||||
|
||||
serverTodos := make(map[string]Todo)
|
||||
@ -192,3 +192,16 @@ func (s *BoltStore) GetTodos(user string) map[string]Todo {
|
||||
}
|
||||
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
|
||||
const (
|
||||
Done = "done"
|
||||
WIP = "work in progress"
|
||||
Pending = "pending"
|
||||
Blocked = "blocked"
|
||||
Failed = "failed"
|
||||
NotStarted = "not started"
|
||||
Done = "done"
|
||||
WIP = "in progress"
|
||||
Pending = "pending"
|
||||
Blocked = "blocked"
|
||||
Failed = "failed"
|
||||
)
|
||||
|
||||
@ -49,7 +49,7 @@ func (todo Todo) PrintIndexed(index int) {
|
||||
statusColor = ColorGreen
|
||||
case WIP:
|
||||
statusColor = ColorYellow
|
||||
case Pending:
|
||||
case Pending, NotStarted:
|
||||
statusColor = ColorBlue
|
||||
case Blocked, Failed:
|
||||
statusColor = ColorRed
|
||||
|
||||
@ -26,7 +26,7 @@ func SyncHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
serverTodos := store.GetTodos(user)
|
||||
serverTodos := store.GetTodoMap(user)
|
||||
|
||||
var response = common.SyncResponse{
|
||||
SyncedTodos: []common.Todo{},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user