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 {