summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-08-13 08:38:29 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-08-13 08:38:29 +0000
commita03d7673e7adf9a575c7272b2b528a74b230535e (patch)
treea4ac635d92eade4df117db2a4fbdfe1eff13d677 /internal
parent2509dde6aa372a505b186657706f4d21bd391807 (diff)
Fix blank task-detail modal for Google Tasks
loadTaskDetailData/HandleUpdateTask only had cases for "trello" and "doot" sources, so opening a gtask from the Tasks or Timeline tab rendered an empty title/description. Reuses the existing findGoogleTask cache lookup for both. Renamed the API client's UpdateTaskNotes to UpdateTask(title, notes) so the web modal can save an edited title too, not just description; the widget's description-only edit popup now just round-trips the task's existing title unchanged.
Diffstat (limited to 'internal')
-rw-r--r--internal/api/google_tasks.go11
-rw-r--r--internal/api/interfaces.go2
-rw-r--r--internal/handlers/handlers.go22
-rw-r--r--internal/handlers/handlers_test.go56
-rw-r--r--internal/handlers/widget.go2
-rw-r--r--internal/handlers/widget_test.go8
6 files changed, 94 insertions, 7 deletions
diff --git a/internal/api/google_tasks.go b/internal/api/google_tasks.go
index 1f9aefb..644f124 100644
--- a/internal/api/google_tasks.go
+++ b/internal/api/google_tasks.go
@@ -174,14 +174,19 @@ func (c *GoogleTasksClient) CompleteTask(ctx context.Context, listID, taskID str
return nil
}
-// UpdateTaskNotes updates a task's notes (description)
-func (c *GoogleTasksClient) UpdateTaskNotes(ctx context.Context, listID, taskID, notes string) error {
+// UpdateTask updates a task's title and notes (description). An empty
+// title is omitted from the PATCH body by the underlying client library
+// (omitempty), not sent as a blank -- so callers that don't support editing
+// title (e.g. the widget's description-only edit popup) can pass the
+// task's existing title back unchanged, or "" to leave it untouched.
+func (c *GoogleTasksClient) UpdateTask(ctx context.Context, listID, taskID, title, notes string) error {
task := &tasks.Task{
+ Title: title,
Notes: notes,
}
_, err := c.srv.Tasks.Patch(listID, taskID, task).Context(ctx).Do()
if err != nil {
- return fmt.Errorf("failed to update task notes: %v", err)
+ return fmt.Errorf("failed to update task: %v", err)
}
return nil
}
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
index 183f3f0..e764130 100644
--- a/internal/api/interfaces.go
+++ b/internal/api/interfaces.go
@@ -38,7 +38,7 @@ type GoogleTasksAPI interface {
GetTasksByDateRange(ctx context.Context, start, end time.Time) ([]models.GoogleTask, error)
CompleteTask(ctx context.Context, listID, taskID string) error
UncompleteTask(ctx context.Context, listID, taskID string) error
- UpdateTaskNotes(ctx context.Context, listID, taskID, notes string) error
+ UpdateTask(ctx context.Context, listID, taskID, title, notes string) error
GetTaskLists(ctx context.Context) ([]models.TaskListInfo, error)
SetTaskListID(id string)
}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 30aaf67..7220474 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -832,6 +832,10 @@ func (h *Handler) loadTaskDetailData(id, source string) taskDetailData {
data.RecurrenceInterval = task.RecurrenceInterval
data.RecurrenceWeekdays = task.RecurrenceWeekdays
}
+ case "gtasks":
+ if t, ok := h.findGoogleTask(id); ok {
+ data.Title, data.Description = t.Title, t.Notes
+ }
}
return data
}
@@ -975,6 +979,24 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) {
updates["name"] = title
}
err = h.trelloClient.UpdateCard(r.Context(), id, updates)
+ case "gtasks":
+ if h.googleTasksClient == nil {
+ JSONError(w, http.StatusServiceUnavailable, "Google Tasks not configured", nil)
+ return
+ }
+ t, ok := h.findGoogleTask(id)
+ if !ok {
+ JSONError(w, http.StatusNotFound, "task not found", nil)
+ return
+ }
+ saveTitle := title
+ if saveTitle == "" {
+ saveTitle = t.Title
+ }
+ err = h.googleTasksClient.UpdateTask(r.Context(), t.ListID, id, saveTitle, description)
+ if err == nil {
+ _ = h.store.InvalidateCache(store.CacheKeyGoogleTasks)
+ }
default:
JSONError(w, http.StatusBadRequest, "Unknown source", nil)
return
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index b618b71..8900f66 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -1965,6 +1965,62 @@ func TestHandleUpdateTask_DootSource_NoTitle_LeavesContentUntouched(t *testing.T
}
}
+func TestHandleGetTaskDetail_GtasksSource_LoadsRealTaskFields(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.SaveGoogleTasks([]models.GoogleTask{
+ {ID: "g1", Title: "Renew passport", Notes: "bring photo", ListID: "list-a", UpdatedAt: time.Now()},
+ }); err != nil {
+ t.Fatal(err)
+ }
+
+ req := httptest.NewRequest("GET", "/tasks/detail?id=g1&source=gtasks", nil)
+ w := httptest.NewRecorder()
+ h.HandleGetTaskDetail(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String())
+ }
+ mock := h.renderer.(*MockRenderer)
+ data, ok := mock.Calls[len(mock.Calls)-1].Data.(taskDetailData)
+ if !ok {
+ t.Fatalf("unexpected data type %T", mock.Calls[len(mock.Calls)-1].Data)
+ }
+ if data.Title != "Renew passport" || data.Description != "bring photo" {
+ t.Errorf("Title/Description = %q/%q, want %q/%q", data.Title, data.Description, "Renew passport", "bring photo")
+ }
+ if data.IsDoot {
+ t.Error("IsDoot = true, want false for a Google Task")
+ }
+}
+
+func TestHandleUpdateTask_GtasksSource_SavesTitleAndNotes(t *testing.T) {
+ h, cleanup := setupTestHandler(t)
+ defer cleanup()
+
+ if err := h.store.SaveGoogleTasks([]models.GoogleTask{
+ {ID: "g1", Title: "Old title", Notes: "Old notes", ListID: "list-a", UpdatedAt: time.Now()},
+ }); err != nil {
+ t.Fatal(err)
+ }
+ mock := &mockGoogleTasksClient{}
+ h.googleTasksClient = mock
+
+ req := httptest.NewRequest("POST", "/tasks/update", strings.NewReader("id=g1&source=gtasks&title=New+title&description=New+notes"))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("HX-Request", "true")
+ w := httptest.NewRecorder()
+ h.HandleUpdateTask(w, req)
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String())
+ }
+ if mock.notesListID != "list-a" || mock.notesTaskID != "g1" || mock.notesTitle != "New title" || mock.notes != "New notes" {
+ t.Errorf("unexpected update call: listID=%q taskID=%q title=%q notes=%q", mock.notesListID, mock.notesTaskID, mock.notesTitle, mock.notes)
+ }
+}
+
func TestHandleDeleteTask_DootSource_RemovesTask(t *testing.T) {
h, cleanup := setupTestHandler(t)
defer cleanup()
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
index 1f6911b..7557da1 100644
--- a/internal/handlers/widget.go
+++ b/internal/handlers/widget.go
@@ -367,7 +367,7 @@ func (h *Handler) HandleWidgetUpdate(w http.ResponseWriter, r *http.Request) {
http.Error(w, "task not found", http.StatusNotFound)
return
}
- if err := h.googleTasksClient.UpdateTaskNotes(r.Context(), t.ListID, req.ID, req.Description); err != nil {
+ if err := h.googleTasksClient.UpdateTask(r.Context(), t.ListID, req.ID, t.Title, req.Description); err != nil {
http.Error(w, "failed to update task", http.StatusInternalServerError)
return
}
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index 2bb92bc..fcbba52 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -17,6 +17,7 @@ import (
type mockGoogleTasksClient struct {
completedListID, completedTaskID string
notesListID, notesTaskID, notes string
+ notesTitle string
}
func (m *mockGoogleTasksClient) GetTasks(ctx context.Context) ([]models.GoogleTask, error) {
@@ -32,8 +33,8 @@ func (m *mockGoogleTasksClient) CompleteTask(ctx context.Context, listID, taskID
func (m *mockGoogleTasksClient) UncompleteTask(ctx context.Context, listID, taskID string) error {
return nil
}
-func (m *mockGoogleTasksClient) UpdateTaskNotes(ctx context.Context, listID, taskID, notes string) error {
- m.notesListID, m.notesTaskID, m.notes = listID, taskID, notes
+func (m *mockGoogleTasksClient) UpdateTask(ctx context.Context, listID, taskID, title, notes string) error {
+ m.notesListID, m.notesTaskID, m.notesTitle, m.notes = listID, taskID, title, notes
return nil
}
func (m *mockGoogleTasksClient) GetTaskLists(ctx context.Context) ([]models.TaskListInfo, error) {
@@ -678,6 +679,9 @@ func TestHandleWidgetUpdate_GoogleTask(t *testing.T) {
if mock.notesListID != "list-a" || mock.notesTaskID != "g1" || mock.notes != "bring photo and $170" {
t.Errorf("unexpected update call: listID=%q taskID=%q notes=%q", mock.notesListID, mock.notesTaskID, mock.notes)
}
+ if mock.notesTitle != "Renew passport" {
+ t.Errorf("title = %q, want unchanged %q (widget's edit popup has no title field)", mock.notesTitle, "Renew passport")
+ }
}
func seedTestCard(t *testing.T, db *store.Store) {