From 1ac343f76745c8dabb3d853af801b69cc86d2a41 Mon Sep 17 00:00:00 2001 From: Rajat yadav Date: Tue, 9 Dec 2025 20:10:58 +0530 Subject: [PATCH] feat: add annotations to task creation - Add annotations field to AddTaskRequestBody and frontend forms - Implement annotation processing using task export for reliable task ID - Fix stale state bug in handleAddTask function --- backend/controllers/add_task.go | 3 +- backend/models/request_body.go | 17 +++-- backend/utils/tw/add_task.go | 32 +++++++- backend/utils/tw/taskwarrior_test.go | 5 +- .../HomeComponents/Tasks/AddTaskDialog.tsx | 75 ++++++++++++++++++- .../HomeComponents/Tasks/TaskDialog.tsx | 14 ++++ .../components/HomeComponents/Tasks/Tasks.tsx | 7 +- .../Tasks/__tests__/AddTaskDialog.test.tsx | 2 + .../Tasks/__tests__/ReportView.test.tsx | 1 + .../Tasks/__tests__/ReportsView.test.tsx | 1 + .../Tasks/__tests__/TaskDialog.test.tsx | 1 + .../Tasks/__tests__/UseEditTask.test.ts | 1 + .../Tasks/__tests__/tasks-utils.test.ts | 1 + .../components/HomeComponents/Tasks/hooks.ts | 9 +++ .../components/utils/__tests__/types.test.ts | 1 + frontend/src/components/utils/types.ts | 7 ++ 16 files changed, 162 insertions(+), 15 deletions(-) diff --git a/backend/controllers/add_task.go b/backend/controllers/add_task.go index b4ec8c7e..0bd367ef 100644 --- a/backend/controllers/add_task.go +++ b/backend/controllers/add_task.go @@ -47,6 +47,7 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { priority := requestBody.Priority dueDate := requestBody.DueDate tags := requestBody.Tags + annotations := requestBody.Annotations if description == "" { http.Error(w, "Description is required, and cannot be empty!", http.StatusBadRequest) @@ -62,7 +63,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, tags) + err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDateStr, tags, annotations) if err != nil { logStore.AddLog("ERROR", fmt.Sprintf("Failed to add task: %v", err), uuid, "Add Task") return err diff --git a/backend/models/request_body.go b/backend/models/request_body.go index 32290070..f62b684a 100644 --- a/backend/models/request_body.go +++ b/backend/models/request_body.go @@ -2,14 +2,15 @@ package models // Request body for task related request handlers type AddTaskRequestBody struct { - Email string `json:"email"` - EncryptionSecret string `json:"encryptionSecret"` - UUID string `json:"UUID"` - Description string `json:"description"` - Project string `json:"project"` - Priority string `json:"priority"` - DueDate *string `json:"due"` - Tags []string `json:"tags"` + Email string `json:"email"` + EncryptionSecret string `json:"encryptionSecret"` + UUID string `json:"UUID"` + Description string `json:"description"` + Project string `json:"project"` + Priority string `json:"priority"` + DueDate *string `json:"due"` + Tags []string `json:"tags"` + Annotations []Annotation `json:"annotations"` } type ModifyTaskRequestBody struct { Email string `json:"email"` diff --git a/backend/utils/tw/add_task.go b/backend/utils/tw/add_task.go index 16105f7b..b4f644a8 100644 --- a/backend/utils/tw/add_task.go +++ b/backend/utils/tw/add_task.go @@ -1,14 +1,16 @@ package tw import ( + "ccsync_backend/models" "ccsync_backend/utils" + "encoding/json" "fmt" "os" "strings" ) // add task to the user's tw client -func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate string, tags []string) error { +func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate string, tags []string, annotations []models.Annotation) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { return fmt.Errorf("error deleting Taskwarrior data: %v", err) } @@ -53,6 +55,34 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p return fmt.Errorf("failed to add task: %v\n %v", err, cmdArgs) } + if len(annotations) > 0 { + output, err := utils.ExecCommandForOutputInDir(tempDir, "task", "export") + if err != nil { + return fmt.Errorf("failed to export tasks: %v", err) + } + + var tasks []models.Task + if err := json.Unmarshal(output, &tasks); err != nil { + return fmt.Errorf("failed to parse exported tasks: %v", err) + } + + if len(tasks) == 0 { + return fmt.Errorf("no tasks found after creation") + } + + lastTask := tasks[len(tasks)-1] + taskID := fmt.Sprintf("%d", lastTask.ID) + + for _, annotation := range annotations { + if annotation.Description != "" { + annotateArgs := []string{"rc.confirmation=off", taskID, "annotate", annotation.Description} + if err := utils.ExecCommandInDir(tempDir, "task", annotateArgs...); err != nil { + return fmt.Errorf("failed to add annotation to task %s: %v", taskID, 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 cf35a627..efeba382 100644 --- a/backend/utils/tw/taskwarrior_test.go +++ b/backend/utils/tw/taskwarrior_test.go @@ -1,6 +1,7 @@ package tw import ( + "ccsync_backend/models" "fmt" "testing" ) @@ -41,7 +42,7 @@ func TestExportTasks(t *testing.T) { } func TestAddTaskToTaskwarrior(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", nil) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", nil, []models.Annotation{{Description: "note"}}) if err != nil { t.Errorf("AddTaskToTaskwarrior failed: %v", err) } else { @@ -59,7 +60,7 @@ func TestCompleteTaskInTaskwarrior(t *testing.T) { } func TestAddTaskWithTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", []string{"work", "important"}) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", []string{"work", "important"}, []models.Annotation{{Description: "note"}}) if err != nil { t.Errorf("AddTaskToTaskwarrior with tags failed: %v", err) } else { diff --git a/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx b/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx index ff2d47ef..209270cd 100644 --- a/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx +++ b/frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { DatePicker } from '@/components/ui/date-picker'; @@ -35,6 +36,40 @@ export const AddTaskdialog = ({ setIsCreatingNewProject, uniqueProjects = [], }: AddTaskDialogProps) => { + const [annotationInput, setAnnotationInput] = useState(''); + + useEffect(() => { + if (!isOpen) { + setAnnotationInput(''); + } + }, [isOpen]); + + const handleAddAnnotation = () => { + if (annotationInput.trim()) { + const newAnnotation = { + entry: new Date().toISOString(), + description: annotationInput.trim(), + }; + setNewTask({ + ...newTask, + annotations: [...newTask.annotations, newAnnotation], + }); + setAnnotationInput(''); + } + }; + + const handleRemoveAnnotation = (annotationToRemove: { + entry: string; + description: string; + }) => { + setNewTask({ + ...newTask, + annotations: newTask.annotations.filter( + (annotation) => annotation !== annotationToRemove + ), + }); + }; + const handleAddTag = () => { if (tagInput && !newTask.tags.includes(tagInput, 0)) { setNewTask({ ...newTask, tags: [...newTask.tags, tagInput] }); @@ -228,6 +263,42 @@ export const AddTaskdialog = ({ )} +
+ + setAnnotationInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleAddAnnotation()} + className="col-span-3" + /> +
+ +
+ {newTask.annotations.length > 0 && ( +
+
+
+ {newTask.annotations.map((annotation, index) => ( + + {annotation.description} + + + ))} +
+
+ )} +
diff --git a/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx b/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx index a85c4702..1f912274 100644 --- a/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx +++ b/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx @@ -1227,6 +1227,20 @@ export const TaskDialog = ({ + + Annotations: + + {task.annotations && task.annotations.length > 0 ? ( + + {task.annotations + .map((ann) => ann.description) + .join(', ')} + + ) : ( + No Annotations + )} + + diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 3de109d2..4eb179a2 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -67,12 +67,13 @@ export const Tasks = ( const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); const [idSortOrder, setIdSortOrder] = useState<'asc' | 'desc'>('asc'); - const [newTask, setNewTask] = useState({ + const [newTask, setNewTask] = useState({ description: '', priority: '', project: '', due: '', - tags: [] as string[], + tags: [], + annotations: [], }); const [isCreatingNewProject, setIsCreatingNewProject] = useState(false); const [isAddTaskOpen, setIsAddTaskOpen] = useState(false); @@ -306,6 +307,7 @@ export const Tasks = ( priority: task.priority, due: task.due || undefined, tags: task.tags, + annotations: task.annotations, backendURL: url.backendURL, }); @@ -316,6 +318,7 @@ export const Tasks = ( project: '', due: '', tags: [], + annotations: [], }); setIsAddTaskOpen(false); } catch (error) { diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/AddTaskDialog.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/AddTaskDialog.test.tsx index 9e114f07..c46b4c31 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/AddTaskDialog.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/AddTaskDialog.test.tsx @@ -59,6 +59,7 @@ describe('AddTaskDialog Component', () => { project: '', due: '', tags: [], + annotations: [], }, setNewTask: jest.fn(), tagInput: '', @@ -219,6 +220,7 @@ describe('AddTaskDialog Component', () => { project: 'Work', due: '2024-12-25', tags: ['urgent'], + annotations: [], }; render(); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx index 6b87a4de..f895bd34 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx @@ -57,6 +57,7 @@ const createMockTask = ( depends, rtype: 'mockRtype', recur: 'mockRecur', + annotations: [], email: 'mockEmail', }; }; diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx index c5e85428..495a51dc 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx @@ -30,6 +30,7 @@ describe('ReportsView', () => { depends: [], rtype: '', recur: '', + annotations: [], email: 'test@example.com', ...overrides, }); diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx index 8961e98e..08a4e297 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx @@ -29,6 +29,7 @@ describe('TaskDialog Component', () => { depends: [], recur: '', rtype: '', + annotations: [], }; const mockAllTasks: Task[] = [ diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts index 605c64dc..ab0b014d 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts @@ -21,6 +21,7 @@ describe('useEditTask Hook', () => { depends: [], rtype: '', recur: '', + annotations: [], email: 'test@example.com', }; 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 59438ea9..666d698f 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts @@ -39,6 +39,7 @@ const createTask = ( depends: [], rtype: '', recur: '', + annotations: [], }); describe('sortTasks', () => { diff --git a/frontend/src/components/HomeComponents/Tasks/hooks.ts b/frontend/src/components/HomeComponents/Tasks/hooks.ts index 8b9c6191..8429415b 100644 --- a/frontend/src/components/HomeComponents/Tasks/hooks.ts +++ b/frontend/src/components/HomeComponents/Tasks/hooks.ts @@ -43,6 +43,7 @@ export const addTaskToBackend = async ({ priority, due, tags, + annotations, backendURL, }: { email: string; @@ -53,6 +54,7 @@ export const addTaskToBackend = async ({ priority: string; due?: string; tags: string[]; + annotations: { entry: string; description: string }[]; backendURL: string; }) => { const requestBody: any = { @@ -69,6 +71,13 @@ export const addTaskToBackend = async ({ if (due !== undefined && due !== '') { requestBody.due = due; } + + // Add annotations to request body, filtering out empty descriptions + requestBody.annotations = annotations.filter( + (annotation) => + annotation.description && annotation.description.trim() !== '' + ); + const response = await fetch(`${backendURL}add-task`, { method: 'POST', body: JSON.stringify(requestBody), diff --git a/frontend/src/components/utils/__tests__/types.test.ts b/frontend/src/components/utils/__tests__/types.test.ts index bc39f0fe..0052d655 100644 --- a/frontend/src/components/utils/__tests__/types.test.ts +++ b/frontend/src/components/utils/__tests__/types.test.ts @@ -54,6 +54,7 @@ describe('Task interface', () => { depends: ['123e4567', '123e4567'], rtype: 'any', recur: 'none', + annotations: [], email: 'test@example.com', }; diff --git a/frontend/src/components/utils/types.ts b/frontend/src/components/utils/types.ts index 7555700b..c16b2c1a 100644 --- a/frontend/src/components/utils/types.ts +++ b/frontend/src/components/utils/types.ts @@ -15,6 +15,11 @@ export interface CopyButtonProps { label: string; } +export interface Annotation { + entry: string; + description: string; +} + export interface Task { id: number; description: string; @@ -33,6 +38,7 @@ export interface Task { depends: string[]; rtype: string; recur: string; + annotations: Annotation[]; email: string; } @@ -94,6 +100,7 @@ export interface TaskFormData { project: string; due: string; tags: string[]; + annotations: Annotation[]; } export interface AddTaskDialogProps {