194 lines
5.5 KiB
Go
194 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func runTodos(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error {
|
|
if len(args) == 0 {
|
|
return runTodosList(ctx, client, nil, stdout)
|
|
}
|
|
if args[0] == "help" || args[0] == "--help" || args[0] == "-h" {
|
|
_, _ = io.WriteString(stdout, "Usage: cmsctl todos [list] [--done=true|false|open|done|all] [--open] [--prio low|medium|high] [--q query] [--limit N] [--offset N]\n cmsctl todos done <id>\n cmsctl todos reopen <id>\n cmsctl todos get <id>\n cmsctl todos create --note '...' [--prio low|medium|high] [--location '...'] [--url '...']\n cmsctl todos delete <id> --yes\n")
|
|
return nil
|
|
}
|
|
|
|
// If the first argument is a flag (e.g. `cmsctl todos --open`), treat as `list`
|
|
if strings.HasPrefix(args[0], "-") {
|
|
return runTodosList(ctx, client, args, stdout)
|
|
}
|
|
|
|
switch args[0] {
|
|
case "list":
|
|
return runTodosList(ctx, client, args[1:], stdout)
|
|
case "done", "mark-done":
|
|
return runTodosSetDone(ctx, client, args[1:], true, stdout)
|
|
case "reopen", "undone", "open":
|
|
return runTodosSetDone(ctx, client, args[1:], false, stdout)
|
|
case "get":
|
|
id, err := requireID(args[1:], "cmsctl todos get requires a todo id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, err := client.get(ctx, client.cmsURL("/nodes/"+escapePathSegment(id)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeRaw(stdout, body)
|
|
case "create":
|
|
return runTodosCreate(ctx, client, args[1:], stdin, stdout)
|
|
case "delete":
|
|
return cmsDeleteNode(ctx, client, args[1:], stdout)
|
|
default:
|
|
return fmt.Errorf("unknown todos command %q (use --help)", args[0])
|
|
}
|
|
}
|
|
|
|
type doneFlag struct {
|
|
set bool
|
|
value string
|
|
}
|
|
|
|
func (d *doneFlag) String() string {
|
|
return d.value
|
|
}
|
|
|
|
func (d *doneFlag) Set(s string) error {
|
|
d.set = true
|
|
d.value = strings.TrimSpace(strings.ToLower(s))
|
|
return nil
|
|
}
|
|
|
|
func (d *doneFlag) IsBoolFlag() bool {
|
|
return true
|
|
}
|
|
|
|
func runTodosList(ctx context.Context, client *Client, args []string, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("todos list", flag.ContinueOnError)
|
|
var done doneFlag
|
|
fs.Var(&done, "done", "filter by completion status: true|false, open|done, or all")
|
|
openOnly := fs.Bool("open", false, "filter to open / uncompleted todos (shorthand for --done=false)")
|
|
prio := fs.String("prio", "", "priority filter: low, medium, or high")
|
|
q := fs.String("q", "", "search substring over note/location/url")
|
|
limit := fs.Int("limit", 100, "page size; use 0 for all")
|
|
offset := fs.Int("offset", 0, "page offset")
|
|
fs.SetOutput(io.Discard)
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
query := url.Values{
|
|
"limit": {strconv.Itoa(*limit)},
|
|
"offset": {strconv.Itoa(*offset)},
|
|
}
|
|
if *prio != "" {
|
|
query.Set("prio", strings.ToLower(*prio))
|
|
}
|
|
if *q != "" {
|
|
query.Set("q", *q)
|
|
}
|
|
|
|
if *openOnly {
|
|
query.Set("done", "false")
|
|
} else if done.set {
|
|
switch done.value {
|
|
case "true", "t", "1", "done", "yes":
|
|
query.Set("done", "true")
|
|
case "false", "f", "0", "open", "no":
|
|
query.Set("done", "false")
|
|
case "all", "":
|
|
// omit parameter to fetch all
|
|
default:
|
|
return fmt.Errorf("invalid --done value %q: use true, false, open, done, or all", done.value)
|
|
}
|
|
}
|
|
|
|
body, err := client.get(ctx, client.cmsURL("/todos")+"?"+query.Encode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeRaw(stdout, body)
|
|
}
|
|
|
|
func runTodosSetDone(ctx context.Context, client *Client, args []string, done bool, stdout io.Writer) error {
|
|
id, err := requireID(args, "cmsctl todos done requires a todo id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
patch := map[string]any{
|
|
"data": map[string]any{
|
|
"done": done,
|
|
},
|
|
}
|
|
body, err := json.Marshal(patch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
response, err := client.patchBody(ctx, client.cmsURL("/nodes/"+escapePathSegment(id)), body, "application/json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeRaw(stdout, response)
|
|
}
|
|
|
|
func runTodosCreate(ctx context.Context, client *Client, args []string, stdin io.Reader, stdout io.Writer) error {
|
|
fs := flag.NewFlagSet("todos create", flag.ContinueOnError)
|
|
note := fs.String("note", "", "improvement note / description")
|
|
prio := fs.String("prio", "medium", "priority: low, medium, or high")
|
|
done := fs.Bool("done", false, "initial completion status")
|
|
location := fs.String("location", "", "admin view title where suggestion was filed")
|
|
rawURL := fs.String("url", "", "page URL where suggestion was filed")
|
|
tags := fs.String("tags", "", "comma-separated tags")
|
|
input := fs.String("input", "", "complete request JSON file or - for stdin")
|
|
fs.SetOutput(io.Discard)
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
body, err := readInput(stdin, *input, fs.Args())
|
|
if err != nil {
|
|
return fmt.Errorf("read todo input: %w", err)
|
|
}
|
|
if len(body) == 0 {
|
|
if strings.TrimSpace(*note) == "" {
|
|
return errors.New("todos create requires --note")
|
|
}
|
|
data := map[string]any{
|
|
"note": strings.TrimSpace(*note),
|
|
"prio": *prio,
|
|
"done": *done,
|
|
}
|
|
if *location != "" {
|
|
data["location"] = *location
|
|
}
|
|
if *rawURL != "" {
|
|
data["url"] = *rawURL
|
|
}
|
|
node := map[string]any{
|
|
"dataType": "todo",
|
|
"data": data,
|
|
"tags": splitCSV(*tags),
|
|
}
|
|
body, err = json.Marshal(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if !json.Valid(body) {
|
|
return errors.New("create todo input is not valid JSON")
|
|
}
|
|
response, err := client.postBody(ctx, client.cmsURL("/nodes"), body, "application/json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeRaw(stdout, response)
|
|
}
|