diff --git a/opal-task/internal/api/handlers/tasks.go b/opal-task/internal/api/handlers/tasks.go index b2830f3..1e26c58 100644 --- a/opal-task/internal/api/handlers/tasks.go +++ b/opal-task/internal/api/handlers/tasks.go @@ -130,35 +130,35 @@ func CreateTask(w http.ResponseWriter, r *http.Request) { mod.AddTags = req.Tags if req.Project != nil { - mod.SetAttributes["project"] = req.Project + mod.Set("project", req.Project) } if req.Priority != nil { - mod.SetAttributes["priority"] = req.Priority + mod.Set("priority", req.Priority) } if req.Due != nil { dueStr := fmt.Sprintf("%d", *req.Due) - mod.SetAttributes["due"] = &dueStr + mod.Set("due", &dueStr) } if req.Scheduled != nil { scheduledStr := fmt.Sprintf("%d", *req.Scheduled) - mod.SetAttributes["scheduled"] = &scheduledStr + mod.Set("scheduled", &scheduledStr) } if req.Wait != nil { waitStr := fmt.Sprintf("%d", *req.Wait) - mod.SetAttributes["wait"] = &waitStr + mod.Set("wait", &waitStr) } if req.Until != nil { untilStr := fmt.Sprintf("%d", *req.Until) - mod.SetAttributes["until"] = &untilStr + mod.Set("until", &untilStr) } if req.Recurrence != nil { - mod.SetAttributes["recurrence"] = req.Recurrence + mod.Set("recur", req.Recurrence) } // Create task @@ -232,39 +232,39 @@ func UpdateTask(w http.ResponseWriter, r *http.Request) { mod := engine.NewModifier() if req.Description != nil { - mod.SetAttributes["description"] = req.Description + mod.Set("description", req.Description) } if req.Status != nil { - mod.SetAttributes["status"] = req.Status + mod.Set("status", req.Status) } if req.Priority != nil { - mod.SetAttributes["priority"] = req.Priority + mod.Set("priority", req.Priority) } if req.Project != nil { - mod.SetAttributes["project"] = req.Project + mod.Set("project", req.Project) } if req.Due != nil { dueStr := fmt.Sprintf("%d", *req.Due) - mod.SetAttributes["due"] = &dueStr + mod.Set("due", &dueStr) } if req.Scheduled != nil { scheduledStr := fmt.Sprintf("%d", *req.Scheduled) - mod.SetAttributes["scheduled"] = &scheduledStr + mod.Set("scheduled", &scheduledStr) } if req.Wait != nil { waitStr := fmt.Sprintf("%d", *req.Wait) - mod.SetAttributes["wait"] = &waitStr + mod.Set("wait", &waitStr) } if req.Until != nil { untilStr := fmt.Sprintf("%d", *req.Until) - mod.SetAttributes["until"] = &untilStr + mod.Set("until", &untilStr) } if req.Start != nil { @@ -273,7 +273,7 @@ func UpdateTask(w http.ResponseWriter, r *http.Request) { } if req.Recurrence != nil { - mod.SetAttributes["recurrence"] = req.Recurrence + mod.Set("recur", req.Recurrence) } // Apply modifier diff --git a/opal-task/internal/engine/keys.go b/opal-task/internal/engine/keys.go index e890eb9..fa1bc03 100644 --- a/opal-task/internal/engine/keys.go +++ b/opal-task/internal/engine/keys.go @@ -4,14 +4,15 @@ package engine // Used by parseAddArgs (cmd/add.go), ParseFilter, and ParseModifier // to distinguish modifiers from description text. var ValidAttributeKeys = map[string]bool{ - "due": true, - "priority": true, - "project": true, - "recur": true, - "status": true, - "wait": true, - "scheduled": true, - "until": true, + "description": true, + "due": true, + "priority": true, + "project": true, + "recur": true, + "status": true, + "wait": true, + "scheduled": true, + "until": true, } // DateKeys is the subset of ValidAttributeKeys that hold date values. diff --git a/opal-task/internal/engine/modifier.go b/opal-task/internal/engine/modifier.go index c8aab17..b20b71f 100644 --- a/opal-task/internal/engine/modifier.go +++ b/opal-task/internal/engine/modifier.go @@ -23,6 +23,13 @@ func NewModifier() *Modifier { } } +// Set adds an attribute to the modifier, maintaining the SetAttributes and +// AttributeOrder invariant. Pass nil to clear the attribute. +func (m *Modifier) Set(key string, value *string) { + m.SetAttributes[key] = value + m.AttributeOrder = append(m.AttributeOrder, key) +} + // ParseModifier parses command-line args into Modifier. // Only recognized attribute keys (ValidAttributeKeys) are accepted; // unrecognized key:value tokens produce an error. @@ -86,6 +93,14 @@ func (m *Modifier) Apply(task *Task) error { resolvedDates["created"] = task.Created resolvedDates["modified"] = task.Modified + // Safety net: if SetAttributes were populated without AttributeOrder, + // reconstruct order from map keys so updates aren't silently dropped. + if len(m.AttributeOrder) == 0 && len(m.SetAttributes) > 0 { + for key := range m.SetAttributes { + m.AttributeOrder = append(m.AttributeOrder, key) + } + } + // Apply attributes in the order they were specified (important for relative references) dateKeys := DateKeys @@ -134,6 +149,14 @@ func (m *Modifier) ApplyToNew(task *Task) error { resolvedDates["created"] = task.Created } + // Safety net: if SetAttributes were populated without AttributeOrder, + // reconstruct order from map keys so updates aren't silently dropped. + if len(m.AttributeOrder) == 0 && len(m.SetAttributes) > 0 { + for key := range m.SetAttributes { + m.AttributeOrder = append(m.AttributeOrder, key) + } + } + // Apply attributes in the order they were specified (important for relative references) dateKeys := DateKeys @@ -156,9 +179,17 @@ func (m *Modifier) ApplyToNew(task *Task) error { return nil } -// applyNonDateAttribute applies a non-date attribute (priority, project, recur) to a task. +// applyNonDateAttribute applies a non-date attribute to a task. func applyNonDateAttribute(key string, valuePtr *string, task *Task) error { switch key { + case "description": + if valuePtr != nil { + task.Description = *valuePtr + } + case "status": + if valuePtr != nil && len(*valuePtr) > 0 { + task.Status = Status((*valuePtr)[0]) + } case "priority": if valuePtr == nil { task.Priority = PriorityDefault