From ddd98665cc300b7bc84f9c53e0f027625be1dd81 Mon Sep 17 00:00:00 2001 From: Rajat yadav Date: Thu, 27 Nov 2025 20:45:17 +0530 Subject: [PATCH 1/4] feat: make recur field editable Added recur field editing functionality. Users can set task recurrence (daily, weekly, monthly, etc.) and Taskwarrior automatically updates the rtype field. Both recur and rtype fields are now visible in task details. --- backend/controllers/edit_task.go | 3 +- backend/models/request_body.go | 1 + backend/utils/tw/edit_task.go | 9 +- backend/utils/tw/taskwarrior_test.go | 8 +- .../components/HomeComponents/Tasks/Tasks.tsx | 154 ++++++++++++++---- .../components/HomeComponents/Tasks/hooks.ts | 3 + 6 files changed, 142 insertions(+), 36 deletions(-) diff --git a/backend/controllers/edit_task.go b/backend/controllers/edit_task.go index c7ca747d..8ef0e5b6 100644 --- a/backend/controllers/edit_task.go +++ b/backend/controllers/edit_task.go @@ -52,6 +52,7 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { end := requestBody.End depends := requestBody.Depends due := requestBody.Due + recur := requestBody.Recur if taskID == "" { http.Error(w, "taskID is required", http.StatusBadRequest) @@ -63,7 +64,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, depends, due) + err := tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID, tags, project, start, entry, wait, end, depends, due, recur) 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 8e1d97ea..7f281c12 100644 --- a/backend/models/request_body.go +++ b/backend/models/request_body.go @@ -37,6 +37,7 @@ type EditTaskRequestBody struct { End string `json:"end"` Depends []string `json:"depends"` Due string `json:"due"` + Recur string `json:"recur"` } type CompleteTaskRequestBody struct { Email string `json:"email"` diff --git a/backend/utils/tw/edit_task.go b/backend/utils/tw/edit_task.go index fb7667f9..bc287aec 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, depends []string, due string) error { +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) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { return fmt.Errorf("error deleting Taskwarrior data: %v", err) } @@ -112,6 +112,13 @@ func EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID st } } + // 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) + } + } + // 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 c8be55fb..be6ff700 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", nil, "2025-12-01T18: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, "2025-12-01T18:30:00.000Z", "") 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", nil, "2025-12-01T18: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, "2025-12-01T18:30:00.000Z", "") 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", nil, "2025-12-01T18: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, "2025-12-01T18:30:00.000Z", "") 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", nil, "2025-12-01T18: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, "2025-12-01T18:30:00.000Z", "") 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 d5b0df33..19aff6ed 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -136,6 +136,8 @@ export const Tasks = ( const [editedDepends, setEditedDepends] = useState([]); const [dependsDropdownOpen, setDependsDropdownOpen] = useState(false); const [dependsSearchTerm, setDependsSearchTerm] = useState(''); + const [isEditingRecur, setIsEditingRecur] = useState(false); + const [editedRecur, setEditedRecur] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [debouncedTerm, setDebouncedTerm] = useState(''); const [lastSyncTime, setLastSyncTime] = useState(null); @@ -379,7 +381,8 @@ export const Tasks = ( wait: string, end: string, depends: string[], - due: string + due: string, + recur: string ) { try { await editTaskOnBackend({ @@ -397,6 +400,7 @@ export const Tasks = ( end, depends, due, + recur, }); console.log('Task edited successfully!'); @@ -444,7 +448,8 @@ export const Tasks = ( task.wait || '', task.end || '', task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditing(false); }; @@ -464,7 +469,8 @@ export const Tasks = ( task.wait || '', task.end || '', task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingProject(false); }; @@ -485,7 +491,8 @@ export const Tasks = ( task.wait, task.end || '', task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingWaitDate(false); @@ -507,7 +514,8 @@ export const Tasks = ( task.wait || '', task.end || '', task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingStartDate(false); @@ -529,7 +537,8 @@ export const Tasks = ( task.wait, task.end, task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingEntryDate(false); @@ -551,7 +560,8 @@ export const Tasks = ( task.wait, task.end, task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingEndDate(false); @@ -573,7 +583,8 @@ export const Tasks = ( task.wait, task.end, task.depends || [], - task.due + task.due, + task.recur || '' ); setIsEditingDueDate(false); @@ -595,13 +606,37 @@ export const Tasks = ( task.wait || '', task.end || '', task.depends, - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingDepends(false); setDependsDropdownOpen(false); }; + const handleRecurSaveClick = (task: Task) => { + task.recur = editedRecur; + + 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 || [], + task.due || '', + task.recur + ); + + setIsEditingRecur(false); + }; + const handleAddDependency = (uuid: string) => { if (!editedDepends.includes(uuid)) { setEditedDepends([...editedDepends, uuid]); @@ -637,6 +672,8 @@ export const Tasks = ( setEditedDepends([]); setDependsDropdownOpen(false); setDependsSearchTerm(''); + setIsEditingRecur(false); + setEditedRecur(''); } else { setSelectedTask(task); setEditedDescription(task?.description || ''); @@ -762,7 +799,8 @@ export const Tasks = ( task.wait || '', task.end || '', task.depends || [], - task.due || '' + task.due || '', + task.recur || '' ); setIsEditingTags(false); @@ -1195,8 +1233,7 @@ export const Tasks = ( id="sync-task" variant="outline" onClick={() => ( - props.setIsLoading(true), - syncTasksWithTwAndDb() + props.setIsLoading(true), syncTasksWithTwAndDb() )} > Sync @@ -1313,18 +1350,18 @@ export const Tasks = ( task.status === 'deleted' ? 'destructive' : task.status === 'completed' - ? 'default' - : 'secondary' + ? 'default' + : 'secondary' } > {task.status === 'pending' && isOverdue(task.due) ? 'O' : task.status === 'completed' - ? 'C' - : task.status === 'deleted' - ? 'D' - : 'P'} + ? 'C' + : task.status === 'deleted' + ? 'D' + : 'P'} @@ -1937,14 +1974,6 @@ export const Tasks = ( )} - - Recur: - {task.recur} - - - RType: - {task.rtype} - Priority: @@ -1998,10 +2027,10 @@ export const Tasks = ( ? task.priority === 'H' ? 'High (H)' : task.priority === 'M' - ? 'Medium (M)' - : task.priority === 'L' - ? 'Low (L)' - : task.priority + ? 'Medium (M)' + : task.priority === 'L' + ? 'Low (L)' + : task.priority : 'None'} + + + ) : ( +
+ + {task.recur || 'None'} + + +
+ )} +
+
+ + RType: + + {task.rtype || 'None'} + {!task.rtype && ( + + (Auto-set by recur) + + )} + + Urgency: {task.urgency} diff --git a/frontend/src/components/HomeComponents/Tasks/hooks.ts b/frontend/src/components/HomeComponents/Tasks/hooks.ts index f0931f96..d0c0d93d 100644 --- a/frontend/src/components/HomeComponents/Tasks/hooks.ts +++ b/frontend/src/components/HomeComponents/Tasks/hooks.ts @@ -95,6 +95,7 @@ export const editTaskOnBackend = async ({ end, depends, due, + recur, }: { email: string; encryptionSecret: string; @@ -110,6 +111,7 @@ export const editTaskOnBackend = async ({ end: string; depends: string[]; due: string; + recur: string; }) => { const response = await fetch(`${backendURL}edit-task`, { method: 'POST', @@ -127,6 +129,7 @@ export const editTaskOnBackend = async ({ end, depends, due, + recur, }), headers: { 'Content-Type': 'application/json', From 2dd0b954bafc3dc7eff2b7ab96b18105c9b10874 Mon Sep 17 00:00:00 2001 From: Rajat yadav Date: Sat, 29 Nov 2025 21:51:35 +0530 Subject: [PATCH 2/4] chore: fix prettier formatting --- .../components/HomeComponents/Tasks/Tasks.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 19aff6ed..455d6c96 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -1233,7 +1233,8 @@ export const Tasks = ( id="sync-task" variant="outline" onClick={() => ( - props.setIsLoading(true), syncTasksWithTwAndDb() + props.setIsLoading(true), + syncTasksWithTwAndDb() )} > Sync @@ -1350,18 +1351,18 @@ export const Tasks = ( task.status === 'deleted' ? 'destructive' : task.status === 'completed' - ? 'default' - : 'secondary' + ? 'default' + : 'secondary' } > {task.status === 'pending' && isOverdue(task.due) ? 'O' : task.status === 'completed' - ? 'C' - : task.status === 'deleted' - ? 'D' - : 'P'} + ? 'C' + : task.status === 'deleted' + ? 'D' + : 'P'} @@ -2027,10 +2028,10 @@ export const Tasks = ( ? task.priority === 'H' ? 'High (H)' : task.priority === 'M' - ? 'Medium (M)' - : task.priority === 'L' - ? 'Low (L)' - : task.priority + ? 'Medium (M)' + : task.priority === 'L' + ? 'Low (L)' + : task.priority : 'None'}