Skip to content

Commit af9ab8c

Browse files
authored
feat: add annotations to task creation (#266)
- 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
1 parent 786a6f6 commit af9ab8c

16 files changed

Lines changed: 162 additions & 15 deletions

File tree

backend/controllers/add_task.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) {
4747
priority := requestBody.Priority
4848
dueDate := requestBody.DueDate
4949
tags := requestBody.Tags
50+
annotations := requestBody.Annotations
5051

5152
if description == "" {
5253
http.Error(w, "Description is required, and cannot be empty!", http.StatusBadRequest)
@@ -62,7 +63,7 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) {
6263
Name: "Add Task",
6364
Execute: func() error {
6465
logStore.AddLog("INFO", fmt.Sprintf("Adding task: %s", description), uuid, "Add Task")
65-
err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDateStr, tags)
66+
err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDateStr, tags, annotations)
6667
if err != nil {
6768
logStore.AddLog("ERROR", fmt.Sprintf("Failed to add task: %v", err), uuid, "Add Task")
6869
return err

backend/models/request_body.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package models
22

33
// Request body for task related request handlers
44
type AddTaskRequestBody struct {
5-
Email string `json:"email"`
6-
EncryptionSecret string `json:"encryptionSecret"`
7-
UUID string `json:"UUID"`
8-
Description string `json:"description"`
9-
Project string `json:"project"`
10-
Priority string `json:"priority"`
11-
DueDate *string `json:"due"`
12-
Tags []string `json:"tags"`
5+
Email string `json:"email"`
6+
EncryptionSecret string `json:"encryptionSecret"`
7+
UUID string `json:"UUID"`
8+
Description string `json:"description"`
9+
Project string `json:"project"`
10+
Priority string `json:"priority"`
11+
DueDate *string `json:"due"`
12+
Tags []string `json:"tags"`
13+
Annotations []Annotation `json:"annotations"`
1314
}
1415
type ModifyTaskRequestBody struct {
1516
Email string `json:"email"`

backend/utils/tw/add_task.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
package tw
22

33
import (
4+
"ccsync_backend/models"
45
"ccsync_backend/utils"
6+
"encoding/json"
57
"fmt"
68
"os"
79
"strings"
810
)
911

1012
// add task to the user's tw client
11-
func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate string, tags []string) error {
13+
func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate string, tags []string, annotations []models.Annotation) error {
1214
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
1315
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
1416
}
@@ -53,6 +55,34 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p
5355
return fmt.Errorf("failed to add task: %v\n %v", err, cmdArgs)
5456
}
5557

58+
if len(annotations) > 0 {
59+
output, err := utils.ExecCommandForOutputInDir(tempDir, "task", "export")
60+
if err != nil {
61+
return fmt.Errorf("failed to export tasks: %v", err)
62+
}
63+
64+
var tasks []models.Task
65+
if err := json.Unmarshal(output, &tasks); err != nil {
66+
return fmt.Errorf("failed to parse exported tasks: %v", err)
67+
}
68+
69+
if len(tasks) == 0 {
70+
return fmt.Errorf("no tasks found after creation")
71+
}
72+
73+
lastTask := tasks[len(tasks)-1]
74+
taskID := fmt.Sprintf("%d", lastTask.ID)
75+
76+
for _, annotation := range annotations {
77+
if annotation.Description != "" {
78+
annotateArgs := []string{"rc.confirmation=off", taskID, "annotate", annotation.Description}
79+
if err := utils.ExecCommandInDir(tempDir, "task", annotateArgs...); err != nil {
80+
return fmt.Errorf("failed to add annotation to task %s: %v", taskID, err)
81+
}
82+
}
83+
}
84+
}
85+
5686
// Sync Taskwarrior again
5787
if err := SyncTaskwarrior(tempDir); err != nil {
5888
return err

backend/utils/tw/taskwarrior_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tw
22

33
import (
4+
"ccsync_backend/models"
45
"fmt"
56
"testing"
67
)
@@ -41,7 +42,7 @@ func TestExportTasks(t *testing.T) {
4142
}
4243

4344
func TestAddTaskToTaskwarrior(t *testing.T) {
44-
err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", nil)
45+
err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", nil, []models.Annotation{{Description: "note"}})
4546
if err != nil {
4647
t.Errorf("AddTaskToTaskwarrior failed: %v", err)
4748
} else {
@@ -59,7 +60,7 @@ func TestCompleteTaskInTaskwarrior(t *testing.T) {
5960
}
6061

6162
func TestAddTaskWithTags(t *testing.T) {
62-
err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", []string{"work", "important"})
63+
err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03", []string{"work", "important"}, []models.Annotation{{Description: "note"}})
6364
if err != nil {
6465
t.Errorf("AddTaskToTaskwarrior with tags failed: %v", err)
6566
} else {

frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useState, useEffect } from 'react';
12
import { Badge } from '@/components/ui/badge';
23
import { Button } from '@/components/ui/button';
34
import { DatePicker } from '@/components/ui/date-picker';
@@ -35,6 +36,40 @@ export const AddTaskdialog = ({
3536
setIsCreatingNewProject,
3637
uniqueProjects = [],
3738
}: AddTaskDialogProps) => {
39+
const [annotationInput, setAnnotationInput] = useState('');
40+
41+
useEffect(() => {
42+
if (!isOpen) {
43+
setAnnotationInput('');
44+
}
45+
}, [isOpen]);
46+
47+
const handleAddAnnotation = () => {
48+
if (annotationInput.trim()) {
49+
const newAnnotation = {
50+
entry: new Date().toISOString(),
51+
description: annotationInput.trim(),
52+
};
53+
setNewTask({
54+
...newTask,
55+
annotations: [...newTask.annotations, newAnnotation],
56+
});
57+
setAnnotationInput('');
58+
}
59+
};
60+
61+
const handleRemoveAnnotation = (annotationToRemove: {
62+
entry: string;
63+
description: string;
64+
}) => {
65+
setNewTask({
66+
...newTask,
67+
annotations: newTask.annotations.filter(
68+
(annotation) => annotation !== annotationToRemove
69+
),
70+
});
71+
};
72+
3873
const handleAddTag = () => {
3974
if (tagInput && !newTask.tags.includes(tagInput, 0)) {
4075
setNewTask({ ...newTask, tags: [...newTask.tags, tagInput] });
@@ -228,6 +263,42 @@ export const AddTaskdialog = ({
228263
</div>
229264
)}
230265
</div>
266+
<div className="grid grid-cols-4 items-center gap-4">
267+
<Label htmlFor="annotations" className="text-right">
268+
Annotations
269+
</Label>
270+
<Input
271+
id="annotations"
272+
name="annotations"
273+
placeholder="Add an annotation"
274+
value={annotationInput}
275+
onChange={(e) => setAnnotationInput(e.target.value)}
276+
onKeyDown={(e) => e.key === 'Enter' && handleAddAnnotation()}
277+
className="col-span-3"
278+
/>
279+
</div>
280+
281+
<div className="mt-2">
282+
{newTask.annotations.length > 0 && (
283+
<div className="grid grid-cols-4 items-center">
284+
<div> </div>
285+
<div className="flex flex-wrap gap-2 col-span-3">
286+
{newTask.annotations.map((annotation, index) => (
287+
<Badge key={index}>
288+
<span>{annotation.description}</span>
289+
<button
290+
type="button"
291+
className="ml-2 text-red-500"
292+
onClick={() => handleRemoveAnnotation(annotation)}
293+
>
294+
295+
</button>
296+
</Badge>
297+
))}
298+
</div>
299+
</div>
300+
)}
301+
</div>
231302
</div>
232303
<DialogFooter>
233304
<Button
@@ -240,7 +311,9 @@ export const AddTaskdialog = ({
240311
<Button
241312
className="mb-1"
242313
variant="default"
243-
onClick={() => onSubmit(newTask)}
314+
onClick={() => {
315+
onSubmit(newTask);
316+
}}
244317
>
245318
Add Task
246319
</Button>

frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,20 @@ export const TaskDialog = ({
12271227
</CopyToClipboard>
12281228
</TableCell>
12291229
</TableRow>
1230+
<TableRow>
1231+
<TableCell>Annotations:</TableCell>
1232+
<TableCell>
1233+
{task.annotations && task.annotations.length > 0 ? (
1234+
<span>
1235+
{task.annotations
1236+
.map((ann) => ann.description)
1237+
.join(', ')}
1238+
</span>
1239+
) : (
1240+
<span>No Annotations</span>
1241+
)}
1242+
</TableCell>
1243+
</TableRow>
12301244
</TableBody>
12311245
</Table>
12321246
</DialogDescription>

frontend/src/components/HomeComponents/Tasks/Tasks.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,13 @@ export const Tasks = (
6767
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
6868
const [idSortOrder, setIdSortOrder] = useState<'asc' | 'desc'>('asc');
6969

70-
const [newTask, setNewTask] = useState({
70+
const [newTask, setNewTask] = useState<TaskFormData>({
7171
description: '',
7272
priority: '',
7373
project: '',
7474
due: '',
75-
tags: [] as string[],
75+
tags: [],
76+
annotations: [],
7677
});
7778
const [isCreatingNewProject, setIsCreatingNewProject] = useState(false);
7879
const [isAddTaskOpen, setIsAddTaskOpen] = useState(false);
@@ -306,6 +307,7 @@ export const Tasks = (
306307
priority: task.priority,
307308
due: task.due || undefined,
308309
tags: task.tags,
310+
annotations: task.annotations,
309311
backendURL: url.backendURL,
310312
});
311313

@@ -316,6 +318,7 @@ export const Tasks = (
316318
project: '',
317319
due: '',
318320
tags: [],
321+
annotations: [],
319322
});
320323
setIsAddTaskOpen(false);
321324
} catch (error) {

frontend/src/components/HomeComponents/Tasks/__tests__/AddTaskDialog.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe('AddTaskDialog Component', () => {
5959
project: '',
6060
due: '',
6161
tags: [],
62+
annotations: [],
6263
},
6364
setNewTask: jest.fn(),
6465
tagInput: '',
@@ -219,6 +220,7 @@ describe('AddTaskDialog Component', () => {
219220
project: 'Work',
220221
due: '2024-12-25',
221222
tags: ['urgent'],
223+
annotations: [],
222224
};
223225
render(<AddTaskdialog {...mockProps} />);
224226

frontend/src/components/HomeComponents/Tasks/__tests__/ReportView.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const createMockTask = (
5757
depends,
5858
rtype: 'mockRtype',
5959
recur: 'mockRecur',
60+
annotations: [],
6061
email: 'mockEmail',
6162
};
6263
};

frontend/src/components/HomeComponents/Tasks/__tests__/ReportsView.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('ReportsView', () => {
3030
depends: [],
3131
rtype: '',
3232
recur: '',
33+
annotations: [],
3334
email: 'test@example.com',
3435
...overrides,
3536
});

0 commit comments

Comments
 (0)