From f40007e542140b0a4be476f1bd793e3ab6b9b111 Mon Sep 17 00:00:00 2001 From: Rajat yadav Date: Wed, 24 Dec 2025 03:32:00 +0530 Subject: [PATCH] Make annotations field editable Added full annotations editing functionality following the same pattern as tags editing. Users can now add, edit, and remove task annotations through the task details dialog. Backend handles proper annotation replacement using TaskWarrior's denotate/annotate commands. --- backend/controllers/edit_task.go | 3 +- backend/models/request_body.go | 29 ++-- backend/utils/tw/edit_task.go | 31 ++++- backend/utils/tw/taskwarrior_test.go | 12 +- .../HomeComponents/Tasks/TaskDialog.tsx | 131 +++++++++++++++++- .../components/HomeComponents/Tasks/Tasks.tsx | 58 ++++++-- .../HomeComponents/Tasks/UseEditTask.tsx | 7 + .../Tasks/__tests__/TaskDialog.test.tsx | 4 + .../Tasks/__tests__/UseEditTask.test.ts | 3 + .../components/HomeComponents/Tasks/hooks.ts | 3 + frontend/src/components/utils/types.ts | 4 + 11 files changed, 244 insertions(+), 41 deletions(-) diff --git a/backend/controllers/edit_task.go b/backend/controllers/edit_task.go index 8ef0e5b6..49d0a843 100644 --- a/backend/controllers/edit_task.go +++ b/backend/controllers/edit_task.go @@ -53,6 +53,7 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { depends := requestBody.Depends due := requestBody.Due recur := requestBody.Recur + annotations := requestBody.Annotations if taskID == "" { http.Error(w, "taskID is required", http.StatusBadRequest) @@ -64,7 +65,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, recur) + err := tw.EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID, tags, project, start, entry, wait, end, depends, due, recur, annotations) 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 b12d84d9..514da0e5 100644 --- a/backend/models/request_body.go +++ b/backend/models/request_body.go @@ -29,20 +29,21 @@ type ModifyTaskRequestBody struct { Tags []string `json:"tags"` } type EditTaskRequestBody struct { - Email string `json:"email"` - EncryptionSecret string `json:"encryptionSecret"` - UUID string `json:"UUID"` - TaskID string `json:"taskid"` - Description string `json:"description"` - Tags []string `json:"tags"` - Project string `json:"project"` - Start string `json:"start"` - Entry string `json:"entry"` - Wait string `json:"wait"` - End string `json:"end"` - Depends []string `json:"depends"` - Due string `json:"due"` - Recur string `json:"recur"` + Email string `json:"email"` + EncryptionSecret string `json:"encryptionSecret"` + UUID string `json:"UUID"` + TaskID string `json:"taskid"` + Description string `json:"description"` + Tags []string `json:"tags"` + Project string `json:"project"` + Start string `json:"start"` + Entry string `json:"entry"` + Wait string `json:"wait"` + End string `json:"end"` + Depends []string `json:"depends"` + Due string `json:"due"` + Recur string `json:"recur"` + Annotations []Annotation `json:"annotations"` } type CompleteTaskRequestBody struct { Email string `json:"email"` diff --git a/backend/utils/tw/edit_task.go b/backend/utils/tw/edit_task.go index bc287aec..5849eabe 100644 --- a/backend/utils/tw/edit_task.go +++ b/backend/utils/tw/edit_task.go @@ -1,13 +1,15 @@ package tw import ( + "ccsync_backend/models" "ccsync_backend/utils" + "encoding/json" "fmt" "os" "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, recur 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, annotations []models.Annotation) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { return fmt.Errorf("error deleting Taskwarrior data: %v", err) } @@ -119,6 +121,33 @@ func EditTaskInTaskwarrior(uuid, description, email, encryptionSecret, taskID st } } + // Handle annotations + if len(annotations) >= 0 { + output, err := utils.ExecCommandForOutputInDir(tempDir, "task", taskID, "export") + if err == nil { + var tasks []map[string]interface{} + if err := json.Unmarshal(output, &tasks); err == nil && len(tasks) > 0 { + if existingAnnotations, ok := tasks[0]["annotations"].([]interface{}); ok { + for _, ann := range existingAnnotations { + if annMap, ok := ann.(map[string]interface{}); ok { + if desc, ok := annMap["description"].(string); ok { + utils.ExecCommand("task", taskID, "denotate", desc) + } + } + } + } + } + } + + for _, annotation := range annotations { + if annotation.Description != "" { + if err := utils.ExecCommand("task", taskID, "annotate", annotation.Description); err != nil { + return fmt.Errorf("failed to add annotation %s: %v", annotation.Description, 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 aad8cff6..0891d0f0 100644 --- a/backend/utils/tw/taskwarrior_test.go +++ b/backend/utils/tw/taskwarrior_test.go @@ -24,7 +24,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", "weekly") + 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", "weekly", []models.Annotation{{Description: "test annotation"}}) if err != nil { t.Errorf("EditTaskInTaskwarrior() failed: %v", err) } else { @@ -51,7 +51,7 @@ func TestAddTaskToTaskwarrior(t *testing.T) { } func TestAddTaskToTaskwarriorWithWaitDate(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03", "2025-03-04", "2025-03-04", "2025-03-04", "", nil, nil) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03", "2025-03-04", "2025-03-04", "2025-03-04", "", nil, []models.Annotation{}) if err != nil { t.Errorf("AddTaskToTaskwarrior with wait date failed: %v", err) } else { @@ -78,7 +78,7 @@ func TestAddTaskWithTags(t *testing.T) { } func TestAddTaskToTaskwarriorWithWaitDateWithTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{"work", "important"}, nil) + err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{"work", "important"}, []models.Annotation{}) if err != nil { t.Errorf("AddTaskToTaskwarrior with wait date failed: %v", err) } else { @@ -87,7 +87,7 @@ func TestAddTaskToTaskwarriorWithWaitDateWithTags(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", "daily") + 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", "daily", []models.Annotation{}) if err != nil { t.Errorf("EditTaskInTaskwarrior with tag addition failed: %v", err) } else { @@ -96,7 +96,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", "monthly") + 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", "monthly", []models.Annotation{}) if err != nil { t.Errorf("EditTaskInTaskwarrior with tag removal failed: %v", err) } else { @@ -105,7 +105,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", "yearly") + 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", "yearly", []models.Annotation{}) if err != nil { t.Errorf("EditTaskInTaskwarrior with mixed tag operations failed: %v", err) } else { diff --git a/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx b/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx index 1f912274..0d5b64c8 100644 --- a/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx +++ b/frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx @@ -56,6 +56,7 @@ export const TaskDialog = ({ onSaveDueDate, onSaveDepends, onSaveRecur, + onSaveAnnotations, onMarkComplete, onMarkDeleted, isOverdue, @@ -1230,14 +1231,130 @@ export const TaskDialog = ({ Annotations: - {task.annotations && task.annotations.length > 0 ? ( - - {task.annotations - .map((ann) => ann.description) - .join(', ')} - + {editState.isEditingAnnotations ? ( +
+
+ { + onUpdateState({ + annotationInput: e.target.value, + }); + }} + placeholder="Add an annotation (press enter to add)" + className="flex-grow mr-2" + onKeyDown={(e) => { + if ( + e.key === 'Enter' && + editState.annotationInput.trim() + ) { + const newAnnotation = { + entry: new Date().toISOString(), + description: editState.annotationInput.trim(), + }; + onUpdateState({ + editedAnnotations: [ + ...editState.editedAnnotations, + newAnnotation, + ], + annotationInput: '', + }); + } + }} + /> + + +
+
+ {editState.editedAnnotations != null && + editState.editedAnnotations.length > 0 && ( +
+
+ {editState.editedAnnotations.map( + (annotation, index) => ( + + {annotation.description} + + + ) + )} +
+
+ )} +
+
) : ( - No Annotations +
+ {task.annotations && task.annotations.length >= 1 ? ( + task.annotations.map((annotation, index) => ( + + {annotation.description} + + )) + ) : ( + No Annotations + )} + +
)}
diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 73b78c56..a9ccea22 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback, useRef } from 'react'; import { useEditTask } from './UseEditTask'; -import { Task } from '../../utils/types'; +import { Task, Annotation } from '../../utils/types'; import { ReportsView } from './ReportsView'; import Fuse from 'fuse.js'; import { useHotkeys } from '@/components/utils/use-hotkeys'; @@ -352,7 +352,8 @@ export const Tasks = ( end: string, depends: string[], due: string, - recur: string + recur: string, + annotations: Annotation[] ) { try { await editTaskOnBackend({ @@ -371,6 +372,7 @@ export const Tasks = ( depends, due, recur, + annotations, }); console.log('Task edited successfully!'); @@ -438,7 +440,8 @@ export const Tasks = ( task.end || '', task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -458,7 +461,8 @@ export const Tasks = ( task.end || '', task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -479,7 +483,8 @@ export const Tasks = ( task.end || '', task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -500,7 +505,8 @@ export const Tasks = ( task.end || '', task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -521,7 +527,8 @@ export const Tasks = ( task.end, task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -542,7 +549,8 @@ export const Tasks = ( task.end, task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -563,7 +571,8 @@ export const Tasks = ( task.end, task.depends || [], task.due, - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -584,7 +593,8 @@ export const Tasks = ( task.end || '', task.depends, task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] ); }; @@ -615,7 +625,8 @@ export const Tasks = ( task.end || '', task.depends || [], task.due || '', - task.recur + task.recur, + task.annotations || [] ); }; @@ -714,7 +725,29 @@ export const Tasks = ( task.end || '', task.depends || [], task.due || '', - task.recur || '' + task.recur || '', + task.annotations || [] + ); + }; + + const handleSaveAnnotations = (task: Task, annotations: Annotation[]) => { + task.annotations = annotations; + 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 || '', + task.annotations ); }; @@ -1028,6 +1061,7 @@ export const Tasks = ( onSaveDueDate={handleDueDateSaveClick} onSaveDepends={handleDependsSaveClick} onSaveRecur={handleRecurSaveClick} + onSaveAnnotations={handleSaveAnnotations} onMarkComplete={handleMarkComplete} onMarkDeleted={handleMarkDelete} isOverdue={isOverdue} diff --git a/frontend/src/components/HomeComponents/Tasks/UseEditTask.tsx b/frontend/src/components/HomeComponents/Tasks/UseEditTask.tsx index 0a339bc4..7e4bbf40 100644 --- a/frontend/src/components/HomeComponents/Tasks/UseEditTask.tsx +++ b/frontend/src/components/HomeComponents/Tasks/UseEditTask.tsx @@ -29,6 +29,9 @@ export const useEditTask = (selectedTask: Task | null) => { isEditingRecur: false, editedRecur: '', originalRecur: '', + isEditingAnnotations: false, + editedAnnotations: [], + annotationInput: '', }); // Update edited tags when selected task changes @@ -42,6 +45,7 @@ export const useEditTask = (selectedTask: Task | null) => { editedProject: selectedTask.project || '', editedRecur: selectedTask.recur || '', originalRecur: selectedTask.recur || '', + editedAnnotations: selectedTask.annotations || [], })); } }, [selectedTask]); @@ -74,6 +78,9 @@ export const useEditTask = (selectedTask: Task | null) => { isEditingRecur: false, editedRecur: '', originalRecur: '', + isEditingAnnotations: false, + editedAnnotations: [], + annotationInput: '', }); }; diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx index 08a4e297..05847084 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/TaskDialog.test.tsx @@ -70,6 +70,9 @@ describe('TaskDialog Component', () => { isEditingRecur: false, editedRecur: '', originalRecur: '', + isEditingAnnotations: false, + editedAnnotations: [], + annotationInput: '', }; const defaultProps = { @@ -93,6 +96,7 @@ describe('TaskDialog Component', () => { onSaveDueDate: jest.fn(), onSaveDepends: jest.fn(), onSaveRecur: jest.fn(), + onSaveAnnotations: jest.fn(), onMarkComplete: jest.fn(), onMarkDeleted: jest.fn(), isOverdue: jest.fn(() => false), diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts index ab0b014d..efe2459e 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/UseEditTask.test.ts @@ -55,6 +55,9 @@ describe('useEditTask Hook', () => { isEditingRecur: false, editedRecur: '', originalRecur: '', + isEditingAnnotations: false, + editedAnnotations: [], + annotationInput: '', }); }); diff --git a/frontend/src/components/HomeComponents/Tasks/hooks.ts b/frontend/src/components/HomeComponents/Tasks/hooks.ts index dc5f1431..e12d021b 100644 --- a/frontend/src/components/HomeComponents/Tasks/hooks.ts +++ b/frontend/src/components/HomeComponents/Tasks/hooks.ts @@ -133,6 +133,7 @@ export const editTaskOnBackend = async ({ depends, due, recur, + annotations, }: { email: string; encryptionSecret: string; @@ -149,6 +150,7 @@ export const editTaskOnBackend = async ({ depends: string[]; due: string; recur: string; + annotations: { entry: string; description: string }[]; }) => { const response = await fetch(`${backendURL}edit-task`, { method: 'POST', @@ -167,6 +169,7 @@ export const editTaskOnBackend = async ({ depends, due, recur, + annotations, }), headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/components/utils/types.ts b/frontend/src/components/utils/types.ts index 5f187778..2b4597ec 100644 --- a/frontend/src/components/utils/types.ts +++ b/frontend/src/components/utils/types.ts @@ -92,6 +92,9 @@ export interface EditTaskState { isEditingRecur: boolean; editedRecur: string; originalRecur: string; + isEditingAnnotations: boolean; + editedAnnotations: Annotation[]; + annotationInput: string; } export interface TaskFormData { @@ -141,6 +144,7 @@ export interface EditTaskDialogProps { onSaveDueDate: (task: Task, date: string) => void; onSaveDepends: (task: Task, depends: string[]) => void; onSaveRecur: (task: Task, recur: string) => void; + onSaveAnnotations: (task: Task, annotations: Annotation[]) => void; onMarkComplete: (uuid: string) => void; onMarkDeleted: (uuid: string) => void; isOverdue: (due?: string) => boolean;