c99a4a2d95
- Add filter.go: Parse filters (+tag, -tag, attribute:value, IDs) - Implement Filter.ToSQL() for WHERE clause generation - Add modifier.go: Parse modifiers (set/clear attributes, add/remove tags) - Implement Modifier.Apply() to update existing tasks - Add dateparse.go: Smart date parsing (ISO, today, tomorrow, weekdays) - Implement nextWeekday logic (smart Sunday interpretation) - Update GetTasks() to accept Filter parameter - Add CreateTaskWithModifier() for task creation with modifiers - Add comprehensive test suite (13 new tests, all passing) - Support filtering by status, project, priority, tags, UUIDs, display IDs - Support modifying priority, project, dates, recurrence, tags
205 lines
4.5 KiB
Go
205 lines
4.5 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type Modifier struct {
|
|
SetAttributes map[string]*string // key -> value (nil = clear)
|
|
AddTags []string
|
|
RemoveTags []string
|
|
}
|
|
|
|
func NewModifier() *Modifier {
|
|
return &Modifier{
|
|
SetAttributes: make(map[string]*string),
|
|
AddTags: []string{},
|
|
RemoveTags: []string{},
|
|
}
|
|
}
|
|
|
|
// ParseModifier parses command-line args into Modifier
|
|
func ParseModifier(args []string) (*Modifier, error) {
|
|
m := NewModifier()
|
|
|
|
for _, arg := range args {
|
|
if strings.HasPrefix(arg, "+") {
|
|
// Add tag
|
|
m.AddTags = append(m.AddTags, strings.TrimPrefix(arg, "+"))
|
|
} else if strings.HasPrefix(arg, "-") && !strings.Contains(arg, ":") {
|
|
// Remove tag
|
|
m.RemoveTags = append(m.RemoveTags, strings.TrimPrefix(arg, "-"))
|
|
} else if strings.Contains(arg, ":") {
|
|
// Attribute modification
|
|
parts := strings.SplitN(arg, ":", 2)
|
|
key := parts[0]
|
|
value := parts[1]
|
|
|
|
if value == "" {
|
|
// Clear attribute (priority: with no value)
|
|
m.SetAttributes[key] = nil
|
|
} else {
|
|
m.SetAttributes[key] = &value
|
|
}
|
|
}
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// Apply applies modifier to task
|
|
func (m *Modifier) Apply(task *Task) error {
|
|
// Apply attribute changes
|
|
for key, valuePtr := range m.SetAttributes {
|
|
switch key {
|
|
case "priority":
|
|
if valuePtr == nil {
|
|
task.Priority = PriorityDefault
|
|
} else {
|
|
task.Priority = Priority(priorityStringToInt(*valuePtr))
|
|
}
|
|
case "project":
|
|
task.Project = valuePtr
|
|
case "due":
|
|
if valuePtr == nil {
|
|
task.Due = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid due date: %w", err)
|
|
}
|
|
task.Due = &parsed
|
|
}
|
|
case "scheduled":
|
|
if valuePtr == nil {
|
|
task.Scheduled = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid scheduled date: %w", err)
|
|
}
|
|
task.Scheduled = &parsed
|
|
}
|
|
case "wait":
|
|
if valuePtr == nil {
|
|
task.Wait = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid wait date: %w", err)
|
|
}
|
|
task.Wait = &parsed
|
|
}
|
|
case "until":
|
|
if valuePtr == nil {
|
|
task.Until = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid until date: %w", err)
|
|
}
|
|
task.Until = &parsed
|
|
}
|
|
case "recur":
|
|
if valuePtr == nil {
|
|
task.RecurrenceDuration = nil
|
|
} else {
|
|
duration, err := ParseRecurrencePattern(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid recurrence: %w", err)
|
|
}
|
|
task.RecurrenceDuration = &duration
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply tag changes
|
|
for _, tag := range m.AddTags {
|
|
if err := task.AddTag(tag); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, tag := range m.RemoveTags {
|
|
if err := task.RemoveTag(tag); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
task.Modified = timeNow()
|
|
|
|
return task.Save()
|
|
}
|
|
|
|
// ApplyToNew applies modifier to a new task (before it's saved)
|
|
// This is used when creating tasks with modifiers
|
|
func (m *Modifier) ApplyToNew(task *Task) error {
|
|
// Apply attribute changes (same as Apply but without Save)
|
|
for key, valuePtr := range m.SetAttributes {
|
|
switch key {
|
|
case "priority":
|
|
if valuePtr == nil {
|
|
task.Priority = PriorityDefault
|
|
} else {
|
|
task.Priority = Priority(priorityStringToInt(*valuePtr))
|
|
}
|
|
case "project":
|
|
task.Project = valuePtr
|
|
case "due":
|
|
if valuePtr == nil {
|
|
task.Due = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid due date: %w", err)
|
|
}
|
|
task.Due = &parsed
|
|
}
|
|
case "scheduled":
|
|
if valuePtr == nil {
|
|
task.Scheduled = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid scheduled date: %w", err)
|
|
}
|
|
task.Scheduled = &parsed
|
|
}
|
|
case "wait":
|
|
if valuePtr == nil {
|
|
task.Wait = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid wait date: %w", err)
|
|
}
|
|
task.Wait = &parsed
|
|
}
|
|
case "until":
|
|
if valuePtr == nil {
|
|
task.Until = nil
|
|
} else {
|
|
parsed, err := ParseDate(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid until date: %w", err)
|
|
}
|
|
task.Until = &parsed
|
|
}
|
|
case "recur":
|
|
if valuePtr == nil {
|
|
task.RecurrenceDuration = nil
|
|
} else {
|
|
duration, err := ParseRecurrencePattern(*valuePtr)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid recurrence: %w", err)
|
|
}
|
|
task.RecurrenceDuration = &duration
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: Tags are added after task is saved (in CreateTask function)
|
|
return nil
|
|
}
|