fix: use Modifier.Set() to maintain AttributeOrder invariant in API handlers

API handlers were populating SetAttributes directly without appending to
AttributeOrder. Since Apply() only iterates AttributeOrder, all updates
via PUT/POST were silently dropped — causing edits to revert and tasks
to disappear on reload.

Adds Modifier.Set() helper, safety net in Apply()/ApplyToNew(), adds
description and status to applyNonDateAttribute, and fixes the
recurrence->recur key mismatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 22:42:57 +01:00
parent 201f32d095
commit 393b7a144a
3 changed files with 57 additions and 25 deletions
+9 -8
View File
@@ -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.
+32 -1
View File
@@ -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