From df46c1861ba7437348b442d4f7763913932e8b01 Mon Sep 17 00:00:00 2001 From: Vasist10 <155972527+Vasist10@users.noreply.github.com> Date: Sun, 21 Dec 2025 23:33:12 +0530 Subject: [PATCH 1/4] feat: add task dependency support --- backend/controllers/add_task.go | 10 +- backend/controllers/edit_task.go | 7 + backend/controllers/modify_task.go | 10 +- backend/models/request_body.go | 2 + backend/utils/tw/add_task.go | 7 +- backend/utils/tw/modify_task.go | 10 +- backend/utils/validation.go | 21 +++ .../HomeComponents/Tasks/AddTaskDialog.tsx | 126 ++++++++++++++++++ .../components/HomeComponents/Tasks/Tasks.tsx | 35 +++++ .../components/HomeComponents/Tasks/hooks.ts | 6 + frontend/src/components/utils/types.ts | 2 + 11 files changed, 232 insertions(+), 4 deletions(-) create mode 100644 backend/utils/validation.go diff --git a/backend/controllers/add_task.go b/backend/controllers/add_task.go index 4dfa465e..191ca671 100644 --- a/backend/controllers/add_task.go +++ b/backend/controllers/add_task.go @@ -2,6 +2,7 @@ package controllers import ( "ccsync_backend/models" + "ccsync_backend/utils" "ccsync_backend/utils/tw" "encoding/json" "fmt" @@ -49,11 +50,18 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { start := requestBody.Start tags := requestBody.Tags annotations := requestBody.Annotations + depends := requestBody.Depends if description == "" { http.Error(w, "Description is required, and cannot be empty!", http.StatusBadRequest) return } + + // Validate dependencies + if err := utils.ValidateDependencies(depends, ""); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } var dueDateStr string if dueDate != nil && *dueDate != "" { dueDateStr = *dueDate @@ -64,7 +72,7 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { Name: "Add Task", Execute: func() error { logStore.AddLog("INFO", fmt.Sprintf("Adding task: %s", description), uuid, "Add Task") - err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDateStr, start, tags, annotations) + err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDateStr, start, tags, annotations, depends) if err != nil { logStore.AddLog("ERROR", fmt.Sprintf("Failed to add task: %v", err), uuid, "Add Task") return err diff --git a/backend/controllers/edit_task.go b/backend/controllers/edit_task.go index 8ef0e5b6..cbbdc346 100644 --- a/backend/controllers/edit_task.go +++ b/backend/controllers/edit_task.go @@ -2,6 +2,7 @@ package controllers import ( "ccsync_backend/models" + "ccsync_backend/utils" "ccsync_backend/utils/tw" "encoding/json" "fmt" @@ -59,6 +60,12 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { return } + // Validate dependencies + if err := utils.ValidateDependencies(depends, uuid); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } + logStore := models.GetLogStore() job := Job{ Name: "Edit Task", diff --git a/backend/controllers/modify_task.go b/backend/controllers/modify_task.go index 573d97a4..e4eb2acd 100644 --- a/backend/controllers/modify_task.go +++ b/backend/controllers/modify_task.go @@ -2,6 +2,7 @@ package controllers import ( "ccsync_backend/models" + "ccsync_backend/utils" "ccsync_backend/utils/tw" "encoding/json" "fmt" @@ -48,6 +49,7 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) { status := requestBody.Status due := requestBody.Due tags := requestBody.Tags + depends := requestBody.Depends if description == "" { http.Error(w, "Description is required, and cannot be empty!", http.StatusBadRequest) @@ -58,6 +60,12 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) { return } + // Validate dependencies + if err := utils.ValidateDependencies(depends, uuid); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } + // if err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID); err != nil { // http.Error(w, err.Error(), http.StatusInternalServerError) // return @@ -68,7 +76,7 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) { Name: "Modify Task", Execute: func() error { logStore.AddLog("INFO", fmt.Sprintf("Modifying task ID: %s", taskID), uuid, "Modify Task") - err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID, tags) + err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID, tags, depends) if err != nil { logStore.AddLog("ERROR", fmt.Sprintf("Failed to modify task ID %s: %v", taskID, err), uuid, "Modify Task") return err diff --git a/backend/models/request_body.go b/backend/models/request_body.go index 30f7dddb..4c28e55a 100644 --- a/backend/models/request_body.go +++ b/backend/models/request_body.go @@ -12,6 +12,7 @@ type AddTaskRequestBody struct { Start string `json:"start"` Tags []string `json:"tags"` Annotations []Annotation `json:"annotations"` + Depends []string `json:"depends"` } type ModifyTaskRequestBody struct { Email string `json:"email"` @@ -24,6 +25,7 @@ type ModifyTaskRequestBody struct { Status string `json:"status"` Due string `json:"due"` Tags []string `json:"tags"` + Depends []string `json:"depends"` } type EditTaskRequestBody struct { Email string `json:"email"` diff --git a/backend/utils/tw/add_task.go b/backend/utils/tw/add_task.go index 56178fb9..618b6027 100644 --- a/backend/utils/tw/add_task.go +++ b/backend/utils/tw/add_task.go @@ -10,7 +10,7 @@ import ( ) // add task to the user's tw client -func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, start string, tags []string, annotations []models.Annotation) error { +func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, start string, tags []string, annotations []models.Annotation, depends []string) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { return fmt.Errorf("error deleting Taskwarrior data: %v", err) } @@ -43,6 +43,11 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p if start != "" { cmdArgs = append(cmdArgs, "start:"+start) } + // Add dependencies to the task + if len(depends) > 0 { + dependsStr := strings.Join(depends, ",") + cmdArgs = append(cmdArgs, "depends:"+dependsStr) + } // Add tags to the task if len(tags) > 0 { for _, tag := range tags { diff --git a/backend/utils/tw/modify_task.go b/backend/utils/tw/modify_task.go index 286a435e..ba23b4bb 100644 --- a/backend/utils/tw/modify_task.go +++ b/backend/utils/tw/modify_task.go @@ -7,7 +7,7 @@ import ( "strings" ) -func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID string, tags []string) error { +func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID string, tags []string, depends []string) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { fmt.Println("1") return fmt.Errorf("error deleting Taskwarrior data: %v", err) @@ -55,6 +55,14 @@ func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, return fmt.Errorf("failed to edit task due: %v", err) } + // Handle dependencies + if len(depends) > 0 { + dependsStr := strings.Join(depends, ",") + if err := utils.ExecCommand("task", taskID, "modify", "depends:"+dependsStr); err != nil { + return fmt.Errorf("failed to set dependencies %s: %v", dependsStr, err) + } + } + // escapedStatus := fmt.Sprintf(`status:%s`, strings.ReplaceAll(status, `"`, `\"`)) if status == "completed" { utils.ExecCommand("task", taskID, "done", "rc.confirmation=off") diff --git a/backend/utils/validation.go b/backend/utils/validation.go new file mode 100644 index 00000000..a439e055 --- /dev/null +++ b/backend/utils/validation.go @@ -0,0 +1,21 @@ +package utils + +import ( + "fmt" +) + +// ValidateDependencies validates dependencies +func ValidateDependencies(depends []string, currentTaskUUID string) error { + if len(depends) == 0 { + return nil + } + + // check for self-dependency + for _, dep := range depends { + if dep == currentTaskUUID { + return fmt.Errorf("task cannot depend on itself: %s", dep) + } + } + + return nil +} diff --git a/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx b/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx index 7ea37277..094ecafb 100644 --- a/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx +++ b/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx @@ -35,6 +35,7 @@ export const AddTaskdialog = ({ isCreatingNewProject, setIsCreatingNewProject, uniqueProjects = [], + allTasks = [], // Add this prop }: AddTaskDialogProps) => { const [annotationInput, setAnnotationInput] = useState(''); @@ -322,6 +323,131 @@ export const AddTaskdialog = ({ )} +
+ +
+ + + {/* Display selected dependencies */} + {newTask.depends.length > 0 && ( +
+ {newTask.depends.map((taskUuid) => { + const dependentTask = allTasks.find( + (t) => t.uuid === taskUuid + ); + return ( + + + #{dependentTask?.id || '?'}{' '} + {dependentTask?.description?.substring(0, 20) || + taskUuid.substring(0, 8)} + {dependentTask?.description && + dependentTask.description.length > 20 && + '...'} + + + + ); + })} +
+ )} +
+