1d55f04a1f
When creating recurring tasks, modifiers (due, wait, priority, etc) were not being applied because AttributeOrder was not copied to the temporary modifier. This caused all date attributes to be ignored. Refactored addRecurringTask to: - Create task structs directly instead of using CreateTask (avoiding premature saves) - Use ApplyToNew() instead of Apply() for modifiers before first save - Properly copy AttributeOrder when building the temporary modifier - Save template and instance once with all fields correctly set This ensures recurring tasks now properly have due dates, wait dates, and other modifiers applied when created via 'opal add' or batch import.
200 lines
5.3 KiB
Go
200 lines
5.3 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.jnss.me/joakim/opal/internal/engine"
|
|
"github.com/google/uuid"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var addCmd = &cobra.Command{
|
|
Use: "add <description> [modifiers...]",
|
|
Short: "Add a new task",
|
|
Long: `Add a new task with optional modifiers.
|
|
|
|
Examples:
|
|
opal add buy groceries # No quotes needed!
|
|
opal add review PR priority:H project:backend
|
|
opal add buy groceries +shop carrots # Tag can be anywhere
|
|
opal add team meeting due:mon recur:1w +meetings`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
parsed := getParsedArgs(cmd)
|
|
// For add, combine filters and modifiers (all are args to parse)
|
|
allArgs := append(parsed.Filters, parsed.Modifiers...)
|
|
if err := addTask(allArgs); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
|
|
func addTask(args []string) error {
|
|
// Parse description and modifiers from args
|
|
// Description = all words that are NOT filters/modifiers
|
|
// Filters/Modifiers = words with +, -, or containing :
|
|
description, modifierArgs, err := parseAddArgs(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse modifiers
|
|
var mod *engine.Modifier
|
|
|
|
if len(modifierArgs) > 0 {
|
|
mod, err = engine.ParseModifier(modifierArgs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse modifiers: %w", err)
|
|
}
|
|
}
|
|
|
|
// Check if this is a recurring task
|
|
isRecurring := mod != nil && mod.SetAttributes["recur"] != nil
|
|
|
|
if isRecurring {
|
|
// Create recurring task (template + first instance)
|
|
return addRecurringTask(description, mod)
|
|
}
|
|
|
|
// Create regular task
|
|
task, err := engine.CreateTaskWithModifier(description, mod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create task: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Created task %s\n", task.UUID)
|
|
if len(task.Tags) > 0 {
|
|
fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseAddArgs extracts description and modifiers from args
|
|
// Description = all non-filter/modifier words joined with spaces
|
|
// Filters/Modifiers = args with +, -, or containing :
|
|
func parseAddArgs(args []string) (string, []string, error) {
|
|
var descParts []string
|
|
var modifiers []string
|
|
|
|
for _, arg := range args {
|
|
isFilterOrModifier := strings.HasPrefix(arg, "+") ||
|
|
strings.HasPrefix(arg, "-") ||
|
|
strings.Contains(arg, ":")
|
|
|
|
if isFilterOrModifier {
|
|
modifiers = append(modifiers, arg)
|
|
} else {
|
|
descParts = append(descParts, arg)
|
|
}
|
|
}
|
|
|
|
if len(descParts) == 0 {
|
|
return "", nil, fmt.Errorf("description is required")
|
|
}
|
|
|
|
description := strings.Join(descParts, " ")
|
|
return description, modifiers, nil
|
|
}
|
|
|
|
func addRecurringTask(description string, mod *engine.Modifier) error {
|
|
// Extract recurrence pattern
|
|
recurPattern := mod.SetAttributes["recur"]
|
|
if recurPattern == nil {
|
|
return fmt.Errorf("no recurrence pattern specified")
|
|
}
|
|
|
|
// Validate: recurring tasks must have due date
|
|
if mod.SetAttributes["due"] == nil {
|
|
return fmt.Errorf("recurring tasks require a due date (use due:YYYY-MM-DD or due:monday)")
|
|
}
|
|
|
|
duration, err := engine.ParseRecurrencePattern(*recurPattern)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid recurrence pattern: %w", err)
|
|
}
|
|
|
|
// Create template task (without saving yet)
|
|
now := time.Now()
|
|
template := &engine.Task{
|
|
UUID: uuid.New(),
|
|
Status: engine.StatusRecurring,
|
|
Description: description,
|
|
Priority: engine.PriorityDefault,
|
|
Created: now,
|
|
Modified: now,
|
|
RecurrenceDuration: &duration,
|
|
Tags: []string{},
|
|
}
|
|
|
|
// Create modifier without the recur attribute
|
|
tempMod := &engine.Modifier{
|
|
SetAttributes: make(map[string]*string),
|
|
AttributeOrder: []string{},
|
|
AddTags: mod.AddTags,
|
|
RemoveTags: mod.RemoveTags,
|
|
}
|
|
|
|
// Copy all attributes except recur
|
|
for _, key := range mod.AttributeOrder {
|
|
if key != "recur" {
|
|
val := mod.SetAttributes[key]
|
|
tempMod.SetAttributes[key] = val
|
|
tempMod.AttributeOrder = append(tempMod.AttributeOrder, key)
|
|
}
|
|
}
|
|
|
|
// Apply modifiers to template before first save
|
|
if err := tempMod.ApplyToNew(template); err != nil {
|
|
return fmt.Errorf("failed to apply modifiers to template: %w", err)
|
|
}
|
|
|
|
// Save template
|
|
if err := template.Save(); err != nil {
|
|
return fmt.Errorf("failed to save template: %w", err)
|
|
}
|
|
|
|
// Add tags to template (requires task.ID from save)
|
|
for _, tag := range mod.AddTags {
|
|
if err := template.AddTag(tag); err != nil {
|
|
return fmt.Errorf("failed to add tag to template: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create first instance
|
|
instance := &engine.Task{
|
|
UUID: uuid.New(),
|
|
Status: engine.StatusPending,
|
|
Description: description,
|
|
Priority: template.Priority,
|
|
Created: now,
|
|
Modified: now,
|
|
ParentUUID: &template.UUID,
|
|
Due: template.Due,
|
|
Wait: template.Wait,
|
|
Scheduled: template.Scheduled,
|
|
Project: template.Project,
|
|
Tags: []string{},
|
|
}
|
|
|
|
if err := instance.Save(); err != nil {
|
|
return fmt.Errorf("failed to save first instance: %w", err)
|
|
}
|
|
|
|
// Copy tags to instance
|
|
for _, tag := range template.Tags {
|
|
if err := instance.AddTag(tag); err != nil {
|
|
return fmt.Errorf("failed to add tag to instance: %w", err)
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Created recurring task %s\n", template.UUID)
|
|
fmt.Printf("First instance: %s\n", instance.UUID)
|
|
fmt.Printf("Recurrence: %s\n", engine.FormatRecurrenceDuration(duration))
|
|
|
|
return nil
|
|
}
|