Skip to content
Closed
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
7 changes: 7 additions & 0 deletions backend/controllers/add_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) {
priority := requestBody.Priority
dueDate := requestBody.DueDate
start := requestBody.Start
end := requestBody.End

// Validate start/end ordering
if err := validateStartEnd(start, end); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
recur := requestBody.Recur
tags := requestBody.Tags
annotations := requestBody.Annotations
Expand Down
6 changes: 6 additions & 0 deletions backend/controllers/edit_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) {
return
}

// Validate start/end ordering
if err := validateStartEnd(start, end); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

logStore := models.GetLogStore()
job := Job{
Name: "Edit Task",
Expand Down
11 changes: 10 additions & 1 deletion backend/controllers/modify_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) {
priority := requestBody.Priority
status := requestBody.Status
due := requestBody.Due
start := requestBody.Start
end := requestBody.End
tags := requestBody.Tags

if description == "" {
Expand All @@ -58,6 +60,13 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) {
return
}

// Validate start/end ordering if provided
if err := validateStartEnd(start, end); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// requestBody currently doesn't include start field for ModifyTaskRequestBody

// if err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID); err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
Expand All @@ -68,7 +77,7 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) {
Name: "Modify Task",
Execute: func() error {
logStore.AddLog("INFO", fmt.Sprintf("Modifying task ID: %s", taskID), uuid, "Modify Task")
err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID, tags)
err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, end, email, encryptionSecret, taskID, tags)
if err != nil {
logStore.AddLog("ERROR", fmt.Sprintf("Failed to modify task ID %s: %v", taskID, err), uuid, "Modify Task")
return err
Expand Down
63 changes: 63 additions & 0 deletions backend/controllers/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package controllers

import (
"fmt"
"time"
)

var dateLayouts = []string{
time.RFC3339,
"2006-01-02T15:04:05.000Z",
"2006-01-02T15:04:05",
"2006-01-02",
}

// validateStartEnd returns an error if both start and end are provided and end is before start.
func validateStartEnd(start, end string) error {
if start == "" || end == "" {
return nil
}

var sTime, eTime time.Time
var err error

// try multiple layouts for parsing
for _, l := range dateLayouts {
if sTime.IsZero() {
sTime, err = time.Parse(l, start)
if err == nil {
break
}
}
}
if sTime.IsZero() {
// as a last resort try Date constructor via RFC3339 parsing of date-only
if t, err2 := time.Parse("2006-01-02", start); err2 == nil {
sTime = t
}
}

for _, l := range dateLayouts {
if eTime.IsZero() {
eTime, err = time.Parse(l, end)
if err == nil {
break
}
}
}
if eTime.IsZero() {
if t, err2 := time.Parse("2006-01-02", end); err2 == nil {
eTime = t
}
}

// If either failed to parse, skip strict validation
if sTime.IsZero() || eTime.IsZero() {
return nil
}

if eTime.Before(sTime) {
return fmt.Errorf("end must be greater than or equal to start")
}
return nil
}
43 changes: 43 additions & 0 deletions backend/controllers/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package controllers

import (
"strings"
"testing"
)

func TestValidateStartEnd_Table(t *testing.T) {
tests := []struct {
name string
start string
end string
wantErr bool
}{
{name: "equal date-only", start: "2025-01-01", end: "2025-01-01", wantErr: false},
{name: "end after start date-only", start: "2025-01-01", end: "2025-01-02", wantErr: false},
{name: "end before start date-only", start: "2025-01-02", end: "2025-01-01", wantErr: true},
{name: "RFC3339 times valid", start: "2025-01-01T10:00:00Z", end: "2025-01-01T12:00:00Z", wantErr: false},
{name: "RFC3339 times invalid (end before)", start: "2025-01-01T12:00:00Z", end: "2025-01-01T10:00:00Z", wantErr: true},
{name: "millisecond layout valid", start: "2025-01-01T08:00:00.000Z", end: "2025-01-01T09:00:00.000Z", wantErr: false},
{name: "unparsable inputs skip validation", start: "not-a-date", end: "also-not", wantErr: false},
{name: "start unparsable end parseable skip", start: "bad", end: "2025-01-02", wantErr: false},
{name: "end unparsable start parseable skip", start: "2025-01-02", end: "bad", wantErr: false},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateStartEnd(tc.start, tc.end)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error but got nil for start=%q end=%q", tc.start, tc.end)
}
if !strings.Contains(err.Error(), "end must") {
t.Fatalf("unexpected error message: %v", err)
}
} else {
if err != nil {
t.Fatalf("expected no error but got: %v for start=%q end=%q", err, tc.start, tc.end)
}
}
})
}
}
19 changes: 19 additions & 0 deletions backend/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,12 @@
"project": {
"type": "string"
},
"start": {
"type": "string"
},
"end": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
Expand Down Expand Up @@ -476,6 +482,12 @@
"taskid": {
"type": "string"
}
"start": {
"type": "string"
},
"end": {
"type": "string"
}
}
},
"models.ModifyTaskRequestBody": {
Expand Down Expand Up @@ -514,6 +526,13 @@
"taskid": {
"type": "string"
}
,
"start": {
"type": "string"
},
"end": {
"type": "string"
}
}
},
"models.Task": {
Expand Down
12 changes: 12 additions & 0 deletions backend/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ definitions:
type: string
project:
type: string
start:
type: string
end:
type: string
tags:
items:
type: string
Expand Down Expand Up @@ -66,6 +70,10 @@ definitions:
type: array
taskid:
type: string
start:
type: string
end:
type: string
type: object
models.ModifyTaskRequestBody:
properties:
Expand All @@ -91,6 +99,10 @@ definitions:
type: array
taskid:
type: string
start:
type: string
end:
type: string
type: object
models.Task:
properties:
Expand Down
3 changes: 3 additions & 0 deletions backend/models/request_body.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type AddTaskRequestBody struct {
Priority string `json:"priority"`
DueDate *string `json:"due"`
Start string `json:"start"`
End string `json:"end,omitempty"`
Recur string `json:"recur"`
Tags []string `json:"tags"`
Annotations []Annotation `json:"annotations"`
Expand All @@ -24,6 +25,8 @@ type ModifyTaskRequestBody struct {
Priority string `json:"priority"`
Status string `json:"status"`
Due string `json:"due"`
Start string `json:"start"`
End string `json:"end"`
Tags []string `json:"tags"`
}
type EditTaskRequestBody struct {
Expand Down
5 changes: 4 additions & 1 deletion backend/utils/tw/add_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
)

// add task to the user's tw client
func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, start, recur string, tags []string, annotations []models.Annotation) error {
func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, start, end, recur 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 @@ -43,6 +44,8 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p
if start != "" {
cmdArgs = append(cmdArgs, "start:"+start)
}
if end != "" {
cmdArgs = append(cmdArgs, "end:"+end)
// Note: Taskwarrior requires a due date to be set before recur can be set
// Only add recur if dueDate is also provided
if recur != "" && dueDate != "" {
Expand Down
10 changes: 9 additions & 1 deletion backend/utils/tw/modify_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"
)

func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID string, tags []string) error {
func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, end, email, encryptionSecret, taskID string, tags []string) error {
if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil {
fmt.Println("1")
return fmt.Errorf("error deleting Taskwarrior data: %v", err)
Expand Down Expand Up @@ -55,6 +55,14 @@ func ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due,
return fmt.Errorf("failed to edit task due: %v", err)
}

// Handle end date
if end != "" {
escapedEnd := fmt.Sprintf(`end:%s`, strings.ReplaceAll(end, `"`, `\"`))
if err := utils.ExecCommand("task", taskID, "modify", escapedEnd); err != nil {
return fmt.Errorf("failed to edit task end: %v", err)
}
}

// escapedStatus := fmt.Sprintf(`status:%s`, strings.ReplaceAll(status, `"`, `\"`))
if status == "completed" {
utils.ExecCommand("task", taskID, "done", "rc.confirmation=off")
Expand Down
2 changes: 1 addition & 1 deletion backend/utils/tw/taskwarrior_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func TestEditTaskWithMixedTagOperations(t *testing.T) {
}

func TestModifyTaskWithTags(t *testing.T) {
err := ModifyTaskInTaskwarrior("uuid", "description", "project", "H", "pending", "2025-03-03", "email", "encryptionSecret", "taskuuid", []string{"+urgent", "-work", "normal"})
err := ModifyTaskInTaskwarrior("uuid", "description", "project", "H", "pending", "2025-03-03", "", "email", "encryptionSecret", "taskuuid", []string{"+urgent", "-work", "normal"})
if err != nil {
t.Errorf("ModifyTaskInTaskwarrior with tags failed: %v", err)
} else {
Expand Down
6 changes: 3 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/AddTaskDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,20 @@ export const AddTaskdialog = ({
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="end" className="text-right">
End
</Label>
<div className="col-span-3">
<DatePicker
date={newTask.end ? new Date(newTask.end) : undefined}
onDateChange={(date) => {
setNewTask({
...newTask,
end: date ? format(date, 'yyyy-MM-dd') : '',
});
}}
placeholder="Select an end date"
/>
<Label htmlFor="recur" className="text-right">
Recur
</Label>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const Tasks = (
project: '',
due: '',
start: '',
end: '',
recur: '',
tags: [],
annotations: [],
Expand Down Expand Up @@ -309,6 +310,7 @@ export const Tasks = (
priority: task.priority,
due: task.due || undefined,
start: task.start || '',
end: task.end || undefined,
recur: task.recur || '',
tags: task.tags,
annotations: task.annotations,
Expand All @@ -322,6 +324,7 @@ export const Tasks = (
project: '',
due: '',
start: '',
end: '',
recur: '',
tags: [],
annotations: [],
Expand Down
5 changes: 5 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,
start,
end,
recur,
tags,
annotations,
Expand All @@ -56,6 +57,7 @@ export const addTaskToBackend = async ({
priority: string;
due?: string;
start: string;
end?: string;
recur: string;
tags: string[];
annotations: { entry: string; description: string }[];
Expand All @@ -81,6 +83,9 @@ export const addTaskToBackend = async ({
requestBody.start = start;
}

// Only include end if it's provided
if (end !== undefined && end !== '') {
requestBody.end = end;
// Only include recur if it's provided
if (recur !== undefined && recur !== '') {
requestBody.recur = recur;
Expand Down
Loading