From 448b1fbb36c7563a541fcd2dc618094c1e7b60ba Mon Sep 17 00:00:00 2001 From: Rajat yadav Date: Wed, 19 Nov 2025 23:42:28 +0530 Subject: [PATCH] feat: make depends field editable and clickable Changed depends from string to array to support multiple dependencies. Added UI to add/remove dependencies with search dropdown. Made dependency badges clickable to navigate between related tasks. Fixed layout so search box appears below dependency list. --- backend/controllers/edit_task.go | 3 +- backend/models/request_body.go | 1 + backend/utils/tw/edit_task.go | 10 +- backend/utils/tw/taskwarrior_test.go | 8 +- .../components/HomeComponents/Tasks/Tasks.tsx | 254 +++++++++++++++++- .../Tasks/__tests__/tasks-utils.test.ts | 2 +- .../components/HomeComponents/Tasks/hooks.ts | 3 + 7 files changed, 265 insertions(+), 16 deletions(-) diff --git a/backend/controllers/edit_task.go b/backend/controllers/edit_task.go index c0122ab3..8647ee04 100644 --- a/backend/controllers/edit_task.go +++ b/backend/controllers/edit_task.go @@ -50,6 +50,7 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { entry := requestBody.Entry wait := requestBody.Wait end := requestBody.End + depends := requestBody.Depends if taskID == "" { http.Error(w, "taskID is required", http.StatusBadRequest) @@ -61,7 +62,7 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { Name: "Edit Task", Execute: func() error { logStore.AddLog("INFO", fmt.Sprintf("Editing task ID: %s", taskID), uuid, "Edit Task") - err := tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID, tags, project, start, entry, wait, end) + err := tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID, tags, project, start, entry, wait, end, depends) if err != nil { logStore.AddLog("ERROR", fmt.Sprintf("Failed to edit task ID %s: %v", taskID, err), uuid, "Edit Task") return err diff --git a/backend/models/request_body.go b/backend/models/request_body.go index 2744249d..c6f1ceba 100644 --- a/backend/models/request_body.go +++ b/backend/models/request_body.go @@ -35,6 +35,7 @@ type EditTaskRequestBody struct { Entry string `json:"entry"` Wait string `json:"wait"` End string `json:"end"` + Depends []string `json:"depends"` } type CompleteTaskRequestBody struct { Email string `json:"email"` diff --git a/backend/utils/tw/edit_task.go b/backend/utils/tw/edit_task.go index 1e40cfad..63c74aa8 100644 --- a/backend/utils/tw/edit_task.go +++ b/backend/utils/tw/edit_task.go @@ -7,7 +7,7 @@ import ( "strings" ) -func EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID string, tags []string, project string, start string, entry string, wait string, end string) error { +func EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID string, tags []string, project string, start string, entry string, wait string, end string, depends []string) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { return fmt.Errorf("error deleting Taskwarrior data: %v", err) } @@ -94,6 +94,14 @@ func EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID st } } + // Handle depends + 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 depends %s: %v", dependsStr, err) + } + } + // Sync Taskwarrior again if err := SyncTaskwarrior(tempDir); err != nil { return err diff --git a/backend/utils/tw/taskwarrior_test.go b/backend/utils/tw/taskwarrior_test.go index cf55f6a6..fe8b0b63 100644 --- a/backend/utils/tw/taskwarrior_test.go +++ b/backend/utils/tw/taskwarrior_test.go @@ -23,7 +23,7 @@ func TestSyncTaskwarrior(t *testing.T) { } func TestEditTaskInATaskwarrior(t *testing.T) { - err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", nil, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z") + err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", nil, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z", nil) if err != nil { t.Errorf("EditTaskInTaskwarrior() failed: %v", err) } else { @@ -68,7 +68,7 @@ func TestAddTaskWithTags(t *testing.T) { } func TestEditTaskWithTagAddition(t *testing.T) { - err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", []string{"+urgent", "+important"}, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z") + err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", []string{"+urgent", "+important"}, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z", nil) if err != nil { t.Errorf("EditTaskInTaskwarrior with tag addition failed: %v", err) } else { @@ -77,7 +77,7 @@ func TestEditTaskWithTagAddition(t *testing.T) { } func TestEditTaskWithTagRemoval(t *testing.T) { - err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", []string{"-work", "-lowpriority"}, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z") + err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", []string{"-work", "-lowpriority"}, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z", nil) if err != nil { t.Errorf("EditTaskInTaskwarrior with tag removal failed: %v", err) } else { @@ -86,7 +86,7 @@ func TestEditTaskWithTagRemoval(t *testing.T) { } func TestEditTaskWithMixedTagOperations(t *testing.T) { - err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", []string{"+urgent", "-work", "normal"}, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z") + err := EditTaskInTaskwarrior("uuid", "description", "email", "encryptionSecret", "taskuuid", []string{"+urgent", "-work", "normal"}, "project", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-29T18:30:00.000Z", "2025-11-30T18:30:00.000Z", nil) if err != nil { t.Errorf("EditTaskInTaskwarrior with mixed tag operations failed: %v", err) } else { diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 2ab48ed5..8a8af3e2 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -126,6 +126,10 @@ export const Tasks = ( const [editedEntryDate, setEditedEntryDate] = useState(''); const [isEditingEndDate, setIsEditingEndDate] = useState(false); const [editedEndDate, setEditedEndDate] = useState(''); + const [isEditingDepends, setIsEditingDepends] = useState(false); + const [editedDepends, setEditedDepends] = useState([]); + const [dependsDropdownOpen, setDependsDropdownOpen] = useState(false); + const [dependsSearchTerm, setDependsSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [lastSyncTime, setLastSyncTime] = useState(null); @@ -351,7 +355,8 @@ export const Tasks = ( start: string, entry: string, wait: string, - end: string + end: string, + depends: string[] ) { try { await editTaskOnBackend({ @@ -367,6 +372,7 @@ export const Tasks = ( entry, wait, end, + depends, }); console.log('Task edited successfully!'); @@ -413,7 +419,8 @@ export const Tasks = ( task.start, task.entry || '', task.wait || '', - task.end || '' + task.end || '', + task.depends || [] ); setIsEditing(false); }; @@ -431,7 +438,8 @@ export const Tasks = ( task.start, task.entry || '', task.wait || '', - task.end || '' + task.end || '', + task.depends || [] ); setIsEditingProject(false); }; @@ -450,7 +458,8 @@ export const Tasks = ( task.start, task.entry || '', task.wait, - task.end || '' + task.end || '', + task.depends || [] ); setIsEditingWaitDate(false); @@ -470,7 +479,8 @@ export const Tasks = ( task.start, task.entry || '', task.wait || '', - task.end || '' + task.end || '', + task.depends || [] ); setIsEditingStartDate(false); @@ -490,7 +500,8 @@ export const Tasks = ( task.start, task.entry, task.wait, - task.end + task.end, + task.depends || [] ); setIsEditingEntryDate(false); @@ -510,12 +521,45 @@ export const Tasks = ( task.start, task.entry, task.wait, - task.end + task.end, + task.depends || [] ); setIsEditingEndDate(false); }; + const handleDependsSaveClick = (task: Task) => { + task.depends = editedDepends; + + handleEditTaskOnBackend( + props.email, + props.encryptionSecret, + props.UUID, + task.description, + task.tags, + task.id.toString(), + task.project, + task.start, + task.entry || '', + task.wait || '', + task.end || '', + task.depends + ); + + setIsEditingDepends(false); + setDependsDropdownOpen(false); + }; + + const handleAddDependency = (uuid: string) => { + if (!editedDepends.includes(uuid)) { + setEditedDepends([...editedDepends, uuid]); + } + }; + + const handleRemoveDependency = (uuid: string) => { + setEditedDepends(editedDepends.filter((dep) => dep !== uuid)); + }; + const handleCancelClick = () => { setIsEditing(false); }; @@ -535,6 +579,10 @@ export const Tasks = ( setEditedEntryDate(''); setIsEditingEndDate(false); setEditedEndDate(''); + setIsEditingDepends(false); + setEditedDepends([]); + setDependsDropdownOpen(false); + setDependsSearchTerm(''); } else { setSelectedTask(task); setEditedDescription(task?.description || ''); @@ -627,7 +675,8 @@ export const Tasks = ( task.start, task.entry || '', task.wait || '', - task.end || '' + task.end || '', + task.depends || [] ); setIsEditingTags(false); // Exit editing mode @@ -1016,6 +1065,9 @@ export const Tasks = ( ) : ( currentTasks.map((task: Task, index: number) => ( handleDialogOpenChange(_isDialogOpen, task) } @@ -1414,7 +1466,191 @@ export const Tasks = ( Depends: - {task.depends} + + {!isEditingDepends ? ( +
+ {(task.depends || []).map( + (depUuid) => { + const depTask = tasks.find( + (t) => t.uuid === depUuid + ); + return ( + { + if (depTask) { + setIsDialogOpen( + false + ); + setTimeout(() => { + setSelectedTask( + depTask + ); + setIsDialogOpen( + true + ); + }, 100); + } + }} + > + {depTask?.description || + depUuid.substring(0, 8)} + + ); + } + )} + +
+ ) : ( +
+
+ {editedDepends.map( + (depUuid) => { + const depTask = tasks.find( + (t) => t.uuid === depUuid + ); + return ( + + + {depTask?.description || + depUuid.substring( + 0, + 8 + )} + + + + ); + } + )} +
+
+
+ + {dependsDropdownOpen && ( +
+ + setDependsSearchTerm( + e.target.value + ) + } + className="m-2 w-[calc(100%-1rem)]" + /> + {tasks + .filter( + (t) => + t.uuid !== + task.uuid && + t.status === + 'pending' && + !editedDepends.includes( + t.uuid + ) && + t.description + .toLowerCase() + .includes( + dependsSearchTerm.toLowerCase() + ) + ) + .map((t) => ( +
{ + handleAddDependency( + t.uuid + ); + setDependsSearchTerm( + '' + ); + }} + > + + + {t.description} + +
+ ))} +
+ )} +
+ + +
+
+ )} +
Recur: diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts index 7ac2d540..59438ea9 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts @@ -36,7 +36,7 @@ const createTask = ( email: '', start: '', wait: '', - depends: [''], + depends: [], rtype: '', recur: '', }); diff --git a/frontend/src/components/HomeComponents/Tasks/hooks.ts b/frontend/src/components/HomeComponents/Tasks/hooks.ts index 06601fae..11034d51 100644 --- a/frontend/src/components/HomeComponents/Tasks/hooks.ts +++ b/frontend/src/components/HomeComponents/Tasks/hooks.ts @@ -93,6 +93,7 @@ export const editTaskOnBackend = async ({ entry, wait, end, + depends, }: { email: string; encryptionSecret: string; @@ -106,6 +107,7 @@ export const editTaskOnBackend = async ({ entry: string; wait: string; end: string; + depends: string[]; }) => { const response = await fetch(`${backendURL}edit-task`, { method: 'POST', @@ -121,6 +123,7 @@ export const editTaskOnBackend = async ({ entry, wait, end, + depends, }), headers: { 'Content-Type': 'application/json',