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
25 changes: 22 additions & 3 deletions backend/controllers/add_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"os"
)

var GlobalJobQueue *JobQueue
Expand Down Expand Up @@ -62,9 +63,27 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) {
}

// Validate dependencies
if err := utils.ValidateDependencies(depends, ""); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
origin := os.Getenv("CONTAINER_ORIGIN")
existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid)
if err != nil {
if err := utils.ValidateDependencies(depends, ""); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
}
} else {
taskDeps := make([]utils.TaskDependency, len(existingTasks))
for i, task := range existingTasks {
taskDeps[i] = utils.TaskDependency{
UUID: task.UUID,
Depends: task.Depends,
Status: task.Status,
}
}

if err := utils.ValidateCircularDependencies(depends, "", taskDeps); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
}
}
dueDateStr, err := utils.ConvertOptionalISOToTaskwarriorFormat(dueDate)
if err != nil {
Expand Down
25 changes: 22 additions & 3 deletions backend/controllers/edit_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"os"
)

// EditTaskHandler godoc
Expand Down Expand Up @@ -62,9 +63,27 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) {
}

// Validate dependencies
if err := utils.ValidateDependencies(depends, uuid); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
origin := os.Getenv("CONTAINER_ORIGIN")
existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid)
if err != nil {
if err := utils.ValidateDependencies(depends, uuid); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
}
} else {
taskDeps := make([]utils.TaskDependency, len(existingTasks))
for i, task := range existingTasks {
taskDeps[i] = utils.TaskDependency{
UUID: task.UUID,
Depends: task.Depends,
Status: task.Status,
}
}

if err := utils.ValidateCircularDependencies(depends, uuid, taskDeps); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
}
}

logStore := models.GetLogStore()
Expand Down
25 changes: 22 additions & 3 deletions backend/controllers/modify_task.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"os"
)

// ModifyTaskHandler godoc
Expand Down Expand Up @@ -61,9 +62,27 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) {
}

// Validate dependencies
if err := utils.ValidateDependencies(depends, uuid); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
origin := os.Getenv("CONTAINER_ORIGIN")
existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid)
if err != nil {
if err := utils.ValidateDependencies(depends, uuid); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
}
} else {
taskDeps := make([]utils.TaskDependency, len(existingTasks))
for i, task := range existingTasks {
taskDeps[i] = utils.TaskDependency{
UUID: task.UUID,
Depends: task.Depends,
Status: task.Status,
}
}

if err := utils.ValidateCircularDependencies(depends, uuid, taskDeps); err != nil {
http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest)
return
}
}

// if err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID); err != nil {
Expand Down
39 changes: 32 additions & 7 deletions backend/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,45 @@ func Test_ExecCommandForOutputInDir(t *testing.T) {
}
}

func Test_ValidateDependencies_ValidDependencies(t *testing.T) {
depends := []string{"task-uuid-1", "task-uuid-2"}
currentTaskUUID := "current-task-uuid"
err := ValidateDependencies(depends, currentTaskUUID)
assert.NoError(t, err)
}

func Test_ValidateDependencies_EmptyList(t *testing.T) {
depends := []string{}
currentTaskUUID := "current-task-uuid"
err := ValidateDependencies(depends, currentTaskUUID)
assert.NoError(t, err)
}

// Circular Dependency Detection Tests
func Test_detectCycle_NoCycle(t *testing.T) { //A -> B -> C
graph := map[string][]string{
"A": {"B"},
"B": {"C"},
"C": {},
}

hasCycle := detectCycle(graph, "A")
assert.False(t, hasCycle, "Should not detect cycle in linear dependency")
}

func Test_detectCycle_SimpleCycle(t *testing.T) { // A -> B -> A
graph := map[string][]string{
"A": {"B"},
"B": {"A"},
}

hasCycle := detectCycle(graph, "A")
assert.True(t, hasCycle, "Should detect simple cycle A -> B -> A")
}

func Test_detectCycle_ComplexCycle(t *testing.T) { // A -> B -> C -> A
graph := map[string][]string{
"A": {"B"},
"B": {"C"},
"C": {"A"},
}

hasCycle := detectCycle(graph, "A")
assert.True(t, hasCycle, "Should detect complex cycle A -> B -> C -> A")
}
func TestConvertISOToTaskwarriorFormat(t *testing.T) {
tests := []struct {
name string
Expand Down
64 changes: 64 additions & 0 deletions backend/utils/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,67 @@ func ValidateDependencies(depends []string, currentTaskUUID string) error {

return nil
}

type TaskDependency struct {
UUID string `json:"uuid"`
Depends []string `json:"depends"`
Status string `json:"status"`
}

func ValidateCircularDependencies(depends []string, currentTaskUUID string, existingTasks []TaskDependency) error {
if len(depends) == 0 {
return nil
}

dependencyGraph := make(map[string][]string)
for _, task := range existingTasks {
if task.Status == "pending" {
dependencyGraph[task.UUID] = task.Depends
}
}

dependencyGraph[currentTaskUUID] = depends

if hasCycle := detectCycle(dependencyGraph, currentTaskUUID); hasCycle {
return fmt.Errorf("circular dependency detected: adding these dependencies would create a cycle")
}

return nil
}

// (0): unvisited, (1): visiting,(2): visited
func detectCycle(graph map[string][]string, startNode string) bool {
color := make(map[string]int)

for node := range graph {
color[node] = 0
}

for _, deps := range graph {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This n^2 loop needs to be optimized

for _, dep := range deps {
if _, exists := color[dep]; !exists {
color[dep] = 0
}
}
}

return dfsHasCycle(graph, startNode, color)
}

func dfsHasCycle(graph map[string][]string, node string, color map[string]int) bool {
color[node] = 1

for _, dep := range graph[node] {
if color[dep] == 1 {
return true
}
if color[dep] == 0 {
if dfsHasCycle(graph, dep, color) {
return true
}
}
}

color[node] = 2
return false
}
55 changes: 34 additions & 21 deletions frontend/src/components/HomeComponents/Tasks/Tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ export const Tasks = (
return () => clearInterval(interval);
}, []);

// Listen for sync events from WebSocket handler
Copy link
Collaborator

Choose a reason for hiding this comment

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

similarly, please remove trivial comments

useEffect(() => {
const handleSyncTasks = () => {
syncTasksWithTwAndDb();
};

window.addEventListener('syncTasks', handleSyncTasks);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove this UseEffect and event listener architecture, and make use of other Sync based Effects available

return () => window.removeEventListener('syncTasks', handleSyncTasks);
}, [syncTasksWithTwAndDb]);

useEffect(() => {
const fetchTasksForEmail = async () => {
try {
Expand Down Expand Up @@ -397,7 +407,7 @@ export const Tasks = (
annotations,
});

console.log('Task edited successfully!');
// Don't show success message here - wait for WebSocket confirmation
Copy link
Collaborator

Choose a reason for hiding this comment

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

please remove this and other such copilot based comments, they add to the bloat

setIsAddTaskOpen(false);
} catch (error) {
console.error('Failed to edit task:', error);
Expand Down Expand Up @@ -682,28 +692,31 @@ export const Tasks = (
);
};

const handleDependsSaveClick = (task: Task, depends: string[]) => {
task.depends = depends;

const handleDependsSaveClick = async (task: Task, depends: string[]) => {
setUnsyncedTaskUuids((prev) => new Set([...prev, task.uuid]));

handleEditTaskOnBackend(
props.email,
props.encryptionSecret,
props.UUID,
task.description,
task.tags,
task.uuid.toString(),
task.project,
task.start,
task.entry || '',
task.wait || '',
task.end || '',
task.depends,
task.due || '',
task.recur || '',
task.annotations || []
);
try {
console.log('Calling backend...');
await handleEditTaskOnBackend(
props.email,
props.encryptionSecret,
props.UUID,
task.description,
task.tags,
task.uuid.toString(),
task.project,
task.start,
task.entry || '',
task.wait || '',
task.end || '',
depends,
task.due || '',
task.recur || '',
task.annotations || []
);
} catch (error) {
console.error('Failed to update dependencies:', error);
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe just add a new toast here instead of changing it in HomePage

}
};

const handleRecurSaveClick = (task: Task, recur: string) => {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/HomeComponents/Tasks/UseEditTask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const useEditTask = (selectedTask: Task | null) => {
annotationInput: '',
});

// Update edited tags when selected task changes
// Sync all editable fields from selectedTask when it changes
useEffect(() => {
if (selectedTask) {
setState((prev) => ({
Expand All @@ -46,6 +46,7 @@ export const useEditTask = (selectedTask: Task | null) => {
editedRecur: selectedTask.recur || '',
originalRecur: selectedTask.recur || '',
editedAnnotations: selectedTask.annotations || [],
editedDepends: selectedTask.depends || [],
}));
}
}, [selectedTask]);
Expand Down
18 changes: 15 additions & 3 deletions frontend/src/components/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,12 @@ export const HomePage: React.FC = () => {
try {
const data = JSON.parse(event.data);
if (data.status === 'success') {
// Skip refresh for Edit Task to prevent dialog blinking
if (data.job !== 'Edit Task') {
// Use syncTasksWithTwAndDb to update both backend and IndexedDB
// This ensures the Tasks component sees the updated data
if (data.job === 'Edit Task') {
// For Edit Task, we need to trigger a sync to update the UI
window.dispatchEvent(new CustomEvent('syncTasks'));
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this really required? Can we skip this?

} else {
getTasks(userInfo.email, userInfo.encryption_secret, userInfo.uuid);
}

Expand Down Expand Up @@ -128,7 +132,15 @@ export const HomePage: React.FC = () => {
}
} else if (data.status == 'failure') {
console.log(`Failed to ${data.job || 'perform action'}`);
toast.error(`Failed to ${data.job || 'perform action'}`, {

// Show specific message for Edit Task failures
let errorMessage = `Failed to ${data.job || 'perform action'}`;
if (data.job === 'Edit Task') {
errorMessage =
Copy link
Collaborator

@its-me-abhishek its-me-abhishek Dec 30, 2025

Choose a reason for hiding this comment

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

this might not be the case everytime for this Jobs failure, and can be misleading.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You can revert this for now, and leave this as it is. will add a separate toast flow later

'Failed to add dependency: Circular dependency detected';
}

toast.error(errorMessage, {
position: 'bottom-left',
autoClose: 3000,
hideProgressBar: false,
Expand Down
Loading