Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/controllers/add_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
17 changes: 9 additions & 8 deletions backend/models/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
32 changes: 31 additions & 1 deletion backend/utils/tw/add_task.go
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation logic retrieves the actual task ID after creation instead of using newest filter, which ensures reliable annotation attachment to the correct task in Taskwarrior.

Original file line number Diff line number Diff line change
@@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions backend/utils/tw/taskwarrior_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tw

import (
"ccsync_backend/models"
"fmt"
"testing"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
75 changes: 74 additions & 1 deletion frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -35,6 +36,40 @@ export const AddTaskdialog = ({
setIsCreatingNewProject,
uniqueProjects = [],
}: AddTaskDialogProps) => {
const [annotationInput, setAnnotationInput] = useState('');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems this takes only one Annotation as an input, please fix this as there can be many annotations!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated , task can have multiple annotations like tags


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] });
Expand Down Expand Up @@ -228,6 +263,42 @@ export const AddTaskdialog = ({
</div>
)}
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="annotations" className="text-right">
Annotations
</Label>
<Input
id="annotations"
name="annotations"
placeholder="Add an annotation"
value={annotationInput}
onChange={(e) => setAnnotationInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAddAnnotation()}
className="col-span-3"
/>
</div>

<div className="mt-2">
{newTask.annotations.length > 0 && (
<div className="grid grid-cols-4 items-center">
<div> </div>
<div className="flex flex-wrap gap-2 col-span-3">
{newTask.annotations.map((annotation, index) => (
<Badge key={index}>
<span>{annotation.description}</span>
<button
type="button"
className="ml-2 text-red-500"
onClick={() => handleRemoveAnnotation(annotation)}
>
</button>
</Badge>
))}
</div>
</div>
)}
</div>
</div>
<DialogFooter>
<Button
Expand All @@ -240,7 +311,9 @@ export const AddTaskdialog = ({
<Button
className="mb-1"
variant="default"
onClick={() => onSubmit(newTask)}
onClick={() => {
onSubmit(newTask);
}}
>
Add Task
</Button>
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/TaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,20 @@ export const TaskDialog = ({
</CopyToClipboard>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Annotations:</TableCell>
<TableCell>
{task.annotations && task.annotations.length > 0 ? (
<span>
{task.annotations
.map((ann) => ann.description)
.join(', ')}
</span>
) : (
<span>No Annotations</span>
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</DialogDescription>
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<TaskFormData>({
description: '',
priority: '',
project: '',
due: '',
tags: [] as string[],
tags: [],
annotations: [],
});
const [isCreatingNewProject, setIsCreatingNewProject] = useState(false);
const [isAddTaskOpen, setIsAddTaskOpen] = useState(false);
Expand Down Expand Up @@ -306,6 +307,7 @@ export const Tasks = (
priority: task.priority,
due: task.due || undefined,
tags: task.tags,
annotations: task.annotations,
backendURL: url.backendURL,
});

Expand All @@ -316,6 +318,7 @@ export const Tasks = (
project: '',
due: '',
tags: [],
annotations: [],
});
setIsAddTaskOpen(false);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ describe('AddTaskDialog Component', () => {
project: '',
due: '',
tags: [],
annotations: [],
},
setNewTask: jest.fn(),
tagInput: '',
Expand Down Expand Up @@ -219,6 +220,7 @@ describe('AddTaskDialog Component', () => {
project: 'Work',
due: '2024-12-25',
tags: ['urgent'],
annotations: [],
};
render(<AddTaskdialog {...mockProps} />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const createMockTask = (
depends,
rtype: 'mockRtype',
recur: 'mockRecur',
annotations: [],
email: 'mockEmail',
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('ReportsView', () => {
depends: [],
rtype: '',
recur: '',
annotations: [],
email: 'test@example.com',
...overrides,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('TaskDialog Component', () => {
depends: [],
recur: '',
rtype: '',
annotations: [],
};

const mockAllTasks: Task[] = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('useEditTask Hook', () => {
depends: [],
rtype: '',
recur: '',
annotations: [],
email: 'test@example.com',
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const createTask = (
depends: [],
rtype: '',
recur: '',
annotations: [],
});

describe('sortTasks', () => {
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const addTaskToBackend = async ({
priority,
due,
tags,
annotations,
backendURL,
}: {
email: string;
Expand All @@ -53,6 +54,7 @@ export const addTaskToBackend = async ({
priority: string;
due?: string;
tags: string[];
annotations: { entry: string; description: string }[];
backendURL: string;
}) => {
const requestBody: any = {
Expand All @@ -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),
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/utils/__tests__/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('Task interface', () => {
depends: ['123e4567', '123e4567'],
rtype: 'any',
recur: 'none',
annotations: [],
email: 'test@example.com',
};

Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export interface CopyButtonProps {
label: string;
}

export interface Annotation {
entry: string;
description: string;
}

export interface Task {
id: number;
description: string;
Expand All @@ -33,6 +38,7 @@ export interface Task {
depends: string[];
rtype: string;
recur: string;
annotations: Annotation[];
email: string;
}

Expand Down Expand Up @@ -94,6 +100,7 @@ export interface TaskFormData {
project: string;
due: string;
tags: string[];
annotations: Annotation[];
}

export interface AddTaskDialogProps {
Expand Down
Loading