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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user