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 &&
+ '...'}
+
+
+
+ );
+ })}
+
+ )}
+
+