diff options
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | internal/handlers/handlers.go | 40 | ||||
| -rw-r--r-- | internal/handlers/handlers_test.go | 108 | ||||
| -rw-r--r-- | migrations/017_add_bugs_resolved_at.sql | 2 | ||||
| -rw-r--r-- | web/templates/partials/bugs.html | 12 | ||||
| -rw-r--r-- | web/templates/partials/lists-options.html | 4 | ||||
| -rw-r--r-- | web/templates/partials/task-detail.html | 12 |
7 files changed, 147 insertions, 33 deletions
@@ -16,7 +16,7 @@ css: ## Build CSS once css-watch: ## Watch CSS for changes npm run dev -build: css ## Build Go binary with CSS +build: ## Build Go binary go build -o dashboard cmd/dashboard/main.go run: css ## Build CSS and run server diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 4988038..39e67c9 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -835,12 +835,7 @@ func (h *Handler) HandleGetListsOptions(w http.ResponseWriter, r *http.Request) return } - w.Header().Set("Content-Type", "text/html") - for _, list := range lists { - _, _ = fmt.Fprintf(w, `<option value="%s">%s</option>`, - template.HTMLEscapeString(list.ID), - template.HTMLEscapeString(list.Name)) - } + HTMLResponse(w, h.renderer, "lists-options", lists) } // HandleGetBugs returns the list of reported bugs @@ -850,19 +845,7 @@ func (h *Handler) HandleGetBugs(w http.ResponseWriter, r *http.Request) { JSONError(w, http.StatusInternalServerError, "Failed to fetch bugs", err) return } - - w.Header().Set("Content-Type", "text/html") - if len(bugs) == 0 { - _, _ = fmt.Fprint(w, `<p class="text-gray-500 text-sm">No bugs reported yet.</p>`) - return - } - - for _, bug := range bugs { - _, _ = fmt.Fprintf(w, `<div class="text-sm border-b border-gray-100 py-2"> - <p class="text-gray-900">%s</p> - <p class="text-gray-400 text-xs">%s</p> - </div>`, template.HTMLEscapeString(bug.Description), bug.CreatedAt.Format("Jan 2, 3:04 PM")) - } + HTMLResponse(w, h.renderer, "bugs", bugs) } // HandleReportBug saves a new bug report @@ -919,19 +902,12 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) { } } - html := fmt.Sprintf(` - <div class="p-4"> - <h3 class="font-semibold text-gray-900 mb-3">%s</h3> - <form hx-post="/tasks/update" hx-swap="none" hx-on::after-request="if(event.detail.successful) { closeTaskModal(); htmx.trigger(document.body, 'refresh-tasks'); }"> - <input type="hidden" name="id" value="%s"> - <input type="hidden" name="source" value="%s"> - <label class="block text-sm font-medium text-gray-700 mb-1">Description</label> - <textarea name="description" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-32">%s</textarea> - <button type="submit" class="w-full bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg text-sm font-medium">Save</button> - </form> - </div> - `, template.HTMLEscapeString(title), template.HTMLEscapeString(id), template.HTMLEscapeString(source), template.HTMLEscapeString(description)) - HTMLString(w, html) + HTMLResponse(w, h.renderer, "task-detail", struct { + Title string + ID string + Source string + Description string + }{title, id, source, description}) } // HandleUpdateTask updates a task description diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go index 470ea5c..9a2287f 100644 --- a/internal/handlers/handlers_test.go +++ b/internal/handlers/handlers_test.go @@ -2827,6 +2827,114 @@ func TestHandleSettingsPage_IncludesSyncLog(t *testing.T) { } } +// ============================================================================= +// HandleGetBugs template tests +// ============================================================================= + +// TestHandleGetBugs_RendersTemplate verifies that HandleGetBugs uses the renderer +// with the "bugs" template and passes the bug list as data. +func TestHandleGetBugs_RendersTemplate(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + _ = h.store.SaveBug("test bug description") + + req := httptest.NewRequest("GET", "/bugs", nil) + w := httptest.NewRecorder() + h.HandleGetBugs(w, req) + + mr := h.renderer.(*MockRenderer) + var found bool + for _, call := range mr.Calls { + if call.Name == "bugs" { + found = true + break + } + } + if !found { + t.Error("Expected renderer to be called with 'bugs' template") + } +} + +// TestHandleGetBugs_NoBugs_RendersTemplate verifies that HandleGetBugs uses the +// renderer even when there are no bugs. +func TestHandleGetBugs_NoBugs_RendersTemplate(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + req := httptest.NewRequest("GET", "/bugs", nil) + w := httptest.NewRecorder() + h.HandleGetBugs(w, req) + + mr := h.renderer.(*MockRenderer) + var found bool + for _, call := range mr.Calls { + if call.Name == "bugs" { + found = true + break + } + } + if !found { + t.Error("Expected renderer to be called with 'bugs' template even when no bugs") + } +} + +// ============================================================================= +// HandleGetTaskDetail template tests +// ============================================================================= + +// TestHandleGetTaskDetail_RendersTemplate verifies that HandleGetTaskDetail uses +// the renderer with the "task-detail" template. +func TestHandleGetTaskDetail_RendersTemplate(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=todoist", nil) + w := httptest.NewRecorder() + h.HandleGetTaskDetail(w, req) + + mr := h.renderer.(*MockRenderer) + var found bool + for _, call := range mr.Calls { + if call.Name == "task-detail" { + found = true + break + } + } + if !found { + t.Error("Expected renderer to be called with 'task-detail' template") + } +} + +// ============================================================================= +// HandleGetListsOptions template tests +// ============================================================================= + +// TestHandleGetListsOptions_RendersTemplate verifies that HandleGetListsOptions uses +// the renderer with the "lists-options" template. +func TestHandleGetListsOptions_RendersTemplate(t *testing.T) { + h, cleanup := setupTestHandler(t) + defer cleanup() + + h.trelloClient = &mockTrelloClient{boards: []models.Board{}} + + req := httptest.NewRequest("GET", "/trello/lists?board_id=board1", nil) + w := httptest.NewRecorder() + h.HandleGetListsOptions(w, req) + + mr := h.renderer.(*MockRenderer) + var found bool + for _, call := range mr.Calls { + if call.Name == "lists-options" { + found = true + break + } + } + if !found { + t.Error("Expected renderer to be called with 'lists-options' template") + } +} + // TestHandleSyncSources_AddsLogEntry verifies that syncing sources creates a sync log entry. func TestHandleSyncSources_AddsLogEntry(t *testing.T) { h, cleanup := setupTestHandler(t) diff --git a/migrations/017_add_bugs_resolved_at.sql b/migrations/017_add_bugs_resolved_at.sql new file mode 100644 index 0000000..836b1d2 --- /dev/null +++ b/migrations/017_add_bugs_resolved_at.sql @@ -0,0 +1,2 @@ +-- Add resolved_at column to bugs table for tracking resolution status +ALTER TABLE bugs ADD COLUMN resolved_at DATETIME; diff --git a/web/templates/partials/bugs.html b/web/templates/partials/bugs.html new file mode 100644 index 0000000..cb5ba25 --- /dev/null +++ b/web/templates/partials/bugs.html @@ -0,0 +1,12 @@ +{{define "bugs"}} +{{if .}} +{{range .}} +<div class="text-sm border-b border-gray-100 py-2"> + <p class="text-gray-900">{{.Description}}</p> + <p class="text-gray-400 text-xs">{{.CreatedAt.Format "Jan 2, 3:04 PM"}}</p> +</div> +{{end}} +{{else}} +<p class="text-gray-500 text-sm">No bugs reported yet.</p> +{{end}} +{{end}} diff --git a/web/templates/partials/lists-options.html b/web/templates/partials/lists-options.html new file mode 100644 index 0000000..f08fdcf --- /dev/null +++ b/web/templates/partials/lists-options.html @@ -0,0 +1,4 @@ +{{define "lists-options"}} +{{range .}}<option value="{{.ID}}">{{.Name}}</option> +{{end}} +{{end}} diff --git a/web/templates/partials/task-detail.html b/web/templates/partials/task-detail.html new file mode 100644 index 0000000..ace604f --- /dev/null +++ b/web/templates/partials/task-detail.html @@ -0,0 +1,12 @@ +{{define "task-detail"}} +<div class="p-4"> + <h3 class="font-semibold text-gray-900 mb-3">{{.Title}}</h3> + <form hx-post="/tasks/update" hx-swap="none" hx-on::after-request="if(event.detail.successful) { closeTaskModal(); htmx.trigger(document.body, 'refresh-tasks'); }"> + <input type="hidden" name="id" value="{{.ID}}"> + <input type="hidden" name="source" value="{{.Source}}"> + <label class="block text-sm font-medium text-gray-700 mb-1">Description</label> + <textarea name="description" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm mb-3 h-32">{{.Description}}</textarea> + <button type="submit" class="w-full bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg text-sm font-medium">Save</button> + </form> +</div> +{{end}} |
