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;