Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions backend/controllers/edit_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,33 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) {
job := Job{
Name: "Edit Task",
Execute: func() error {
logStore.AddLog("INFO", fmt.Sprintf("Editing task ID: %s", taskUUID), uuid, "Edit Task")
err := tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskUUID, tags, project, start, entry, wait, end, depends, due, recur, annotations)
logStore.AddLog("INFO", fmt.Sprintf("Editing task UUID: %s", taskUUID), uuid, "Edit Task")

// Construct parameters struct for batched command execution
params := models.EditTaskParams{
UUID: uuid,
TaskUUID: taskUUID,
Email: email,
EncryptionSecret: encryptionSecret,
Description: description,
Tags: tags,
Project: project,
Start: start,
Entry: entry,
Wait: wait,
End: end,
Depends: depends,
Due: due,
Recur: recur,
Annotations: annotations,
}

err := tw.EditTaskInTaskwarrior(params)
if err != nil {
logStore.AddLog("ERROR", fmt.Sprintf("Failed to edit task ID %s: %v", taskUUID, err), uuid, "Edit Task")
logStore.AddLog("ERROR", fmt.Sprintf("Failed to edit task UUID %s: %v", taskUUID, err), uuid, "Edit Task")
return err
}
logStore.AddLog("INFO", fmt.Sprintf("Successfully edited task ID: %s", taskUUID), uuid, "Edit Task")
logStore.AddLog("INFO", fmt.Sprintf("Successfully edited task UUID: %s", taskUUID), uuid, "Edit Task")
return nil
},
}
Expand Down
20 changes: 20 additions & 0 deletions backend/models/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ type EditTaskRequestBody struct {
Recur string `json:"recur"`
Annotations []Annotation `json:"annotations"`
}

// EditTaskParams encapsulates all parameters needed to edit a task in Taskwarrior
// This struct is used to reduce parameter bloat and improve maintainability
type EditTaskParams struct {
UUID string // User's UUID for Taskwarrior sync
TaskUUID string // Task's permanent UUID (used instead of volatile taskID)
Email string // User's email for temp directory naming
EncryptionSecret string // Encryption secret for Taskwarrior sync
Description string // Task description
Tags []string // Tags to add/remove (prefix with +/-)
Project string // Project name
Start string // Start date
Entry string // Entry date
Wait string // Wait date
End string // End date
Depends []string // Task dependencies (UUIDs)
Due string // Due date
Recur string // Recurrence pattern
Annotations []Annotation // Task annotations
}
type CompleteTaskRequestBody struct {
Email string `json:"email"`
EncryptionSecret string `json:"encryptionSecret"`
Expand Down
158 changes: 76 additions & 82 deletions backend/utils/tw/edit_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,145 +9,139 @@ import (
"strings"
)

func EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID string, tags []string, project string, start string, entry string, wait string, end string, depends []string, due string, recur string, annotations []models.Annotation) error {
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
}
tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", email))
// EditTaskInTaskwarrior edits a task in Taskwarrior using batched command execution
// This function uses a single 'task modify' command for better performance instead of
// spawning multiple shell processes for each field modification.
func EditTaskInTaskwarrior(params models.EditTaskParams) error {
// Create isolated temporary directory for this operation
tempDir, err := os.MkdirTemp("", utils.SafeTempDirPrefix("taskwarrior-", params.Email))
if err != nil {
return fmt.Errorf("failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tempDir)

// Configure Taskwarrior with user's encryption and sync settings
origin := os.Getenv("CONTAINER_ORIGIN")
if err := SetTaskwarriorConfig(tempDir, encryptionSecret, origin, uuid); err != nil {
if err := SetTaskwarriorConfig(tempDir, params.EncryptionSecret, origin, params.UUID); err != nil {
return err
}

// Sync to get latest task data
if err := SyncTaskwarrior(tempDir); err != nil {
return err
}

// Escape the double quotes in the description and format it
if err := utils.ExecCommand("task", taskID, "modify", description); err != nil {
return fmt.Errorf("failed to edit task: %v", err)
}
// Build batched modify command arguments
// Format: task <uuid> modify <arg1> <arg2> ... <argN>
modifyArgs := []string{params.TaskUUID, "modify"}

// Handle project
if project != "" {
if err := utils.ExecCommand("task", taskID, "modify", "project:"+project); err != nil {
return fmt.Errorf("failed to set project %s: %v", project, err)
}
// Add description if provided
if params.Description != "" {
modifyArgs = append(modifyArgs, params.Description)
}

// Handle wait date
if wait != "" {
// Convert `2025-11-29` -> `2025-11-29T00:00:00`
formattedWait := wait + "T00:00:00"

if err := utils.ExecCommand("task", taskID, "modify", "wait:"+formattedWait); err != nil {
return fmt.Errorf("failed to set wait date %s: %v", formattedWait, err)
}
// Add project if provided
if params.Project != "" {
modifyArgs = append(modifyArgs, "project:"+params.Project)
}

// Handle tags
if len(tags) > 0 {
for _, tag := range tags {
if strings.HasPrefix(tag, "+") {
// Add tag
tagValue := strings.TrimPrefix(tag, "+")
if err := utils.ExecCommand("task", taskID, "modify", "+"+tagValue); err != nil {
return fmt.Errorf("failed to add tag %s: %v", tagValue, err)
}
} else if strings.HasPrefix(tag, "-") {
// Remove tag
tagValue := strings.TrimPrefix(tag, "-")
if err := utils.ExecCommand("task", taskID, "modify", "-"+tagValue); err != nil {
return fmt.Errorf("failed to remove tag %s: %v", tagValue, err)
}
} else {
// Add tag without prefix
if err := utils.ExecCommand("task", taskID, "modify", "+"+tag); err != nil {
return fmt.Errorf("failed to add tag %s: %v", tag, err)
}
}
}
// Add wait date if provided
// Convert date format from YYYY-MM-DD to YYYY-MM-DDTHH:MM:SS
if params.Wait != "" {
formattedWait := params.Wait + "T00:00:00"
modifyArgs = append(modifyArgs, "wait:"+formattedWait)
}

// Handle start date
if start != "" {
if err := utils.ExecCommand("task", taskID, "modify", "start:"+start); err != nil {
return fmt.Errorf("failed to set start date %s: %v", start, err)
}
// Add start date if provided
if params.Start != "" {
modifyArgs = append(modifyArgs, "start:"+params.Start)
}

// Handle entry date
if entry != "" {
if err := utils.ExecCommand("task", taskID, "modify", "entry:"+entry); err != nil {
return fmt.Errorf("failed to set entry date %s: %v", entry, err)
}
// Add entry date if provided
if params.Entry != "" {
modifyArgs = append(modifyArgs, "entry:"+params.Entry)
}

// Handle end date
if end != "" {
if err := utils.ExecCommand("task", taskID, "modify", "end:"+end); err != nil {
return fmt.Errorf("failed to set end date %s: %v", end, err)
}
// Add end date if provided
if params.End != "" {
modifyArgs = append(modifyArgs, "end:"+params.End)
}

// Handle depends - always set to ensure clearing works
dependsStr := strings.Join(depends, ",")
if err := utils.ExecCommand("task", taskID, "modify", "depends:"+dependsStr); err != nil {
return fmt.Errorf("failed to set depends %s: %v", dependsStr, err)
// Add dependencies
// Always set to ensure clearing works (empty string clears dependencies)
dependsStr := strings.Join(params.Depends, ",")
modifyArgs = append(modifyArgs, "depends:"+dependsStr)

// Add due date if provided
// Convert date format from YYYY-MM-DD to YYYY-MM-DDTHH:MM:SS
if params.Due != "" {
formattedDue := params.Due + "T00:00:00"
modifyArgs = append(modifyArgs, "due:"+formattedDue)
}

// Handle due date
if due != "" {
// Convert `2025-11-29` -> `2025-11-29T00:00:00`
formattedDue := due + "T00:00:00"
// Add recurrence pattern if provided
// This will automatically set the rtype field in Taskwarrior
if params.Recur != "" {
modifyArgs = append(modifyArgs, "recur:"+params.Recur)
}

if err := utils.ExecCommand("task", taskID, "modify", "due:"+formattedDue); err != nil {
return fmt.Errorf("failed to set due date %s: %v", formattedDue, err)
// Add tags (supports +tag to add, -tag to remove)
// Tags are processed individually as they have special prefix syntax
for _, tag := range params.Tags {
if strings.HasPrefix(tag, "+") {
// Add tag (already has + prefix)
modifyArgs = append(modifyArgs, tag)
} else if strings.HasPrefix(tag, "-") {
// Remove tag (already has - prefix)
modifyArgs = append(modifyArgs, tag)
} else {
// Add tag without prefix
modifyArgs = append(modifyArgs, "+"+tag)
}
}

// Handle recur - this will automatically set rtype field
if recur != "" {
if err := utils.ExecCommand("task", taskID, "modify", "recur:"+recur); err != nil {
return fmt.Errorf("failed to set recur %s: %v", recur, err)
}
// Execute the batched modify command
// This single command replaces 10+ individual shell process spawns
if err := utils.ExecCommand("task", modifyArgs...); err != nil {
return fmt.Errorf("failed to edit task: %v", err)
}

// Handle annotations
if len(annotations) >= 0 {
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", taskID, "export")
// Handle annotations separately as they require individual commands
// Annotations cannot be batched with the modify command
if len(params.Annotations) >= 0 {
// First, remove all existing annotations
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", params.TaskUUID, "export")
if err == nil {
var tasks []map[string]interface{}
if err := json.Unmarshal(output, &tasks); err == nil && len(tasks) > 0 {
if existingAnnotations, ok := tasks[0]["annotations"].([]interface{}); ok {
// Remove each existing annotation
for _, ann := range existingAnnotations {
if annMap, ok := ann.(map[string]interface{}); ok {
if desc, ok := annMap["description"].(string); ok {
utils.ExecCommand("task", taskID, "denotate", desc)
// Ignore errors on denotate as annotation might not exist
utils.ExecCommand("task", params.TaskUUID, "denotate", desc)
}
}
}
}
}
}

for _, annotation := range annotations {
// Add new annotations
for _, annotation := range params.Annotations {
if annotation.Description != "" {
if err := utils.ExecCommand("task", taskID, "annotate", annotation.Description); err != nil {
if err := utils.ExecCommand("task", params.TaskUUID, "annotate", annotation.Description); err != nil {
return fmt.Errorf("failed to add annotation %s: %v", annotation.Description, err)
}
}
}
}

// Sync Taskwarrior again
// Sync changes back to server
if err := SyncTaskwarrior(tempDir); err != nil {
return err
}

return nil
}