Phase 5h slice 4 — adds the star button on each tile that flips Pinned on the projax item via POST /dashboard/pin. Backend: - store.SetPinned(ids, pinned bool) — minimal-write helper that mirrors SetPublic, only touching the pinned column. - web/dashboard_pin.go — handleDashboardPin parses id + pin from form, calls SetPinned, invalidates the entire dashboard cache (pin affects sort order across every view/scope/filter combo), then re-renders by delegating to handleDashboard so HTMX receives the updated #dashboard-section HTML. - Route: POST /dashboard/pin (sibling of /dashboard/task/*). Frontend: - Tile template now leads with a <form class="tile-pin-form"> that POSTs id + the inverted pin state. Button glyph is ☆ when unpinned, ★ when pinned; aria-label flips accordingly. - HTMX swaps the entire #dashboard-section so the tile moves to the pinned-first position (or back to alphabetical) without a full reload. - CSS: .tile-pin (transparent button, muted color, accent on hover); .tile-pin.pinned for the filled-star state. Test helper: server_test.go gains a post() helper paired with the existing get() — form-encoded POSTs for writeback tests. Tests (dashboard_pin_test.go): - TestDashboardPinTogglesItem — POST pin=true flips the row, and the re-render shows the .tile-pinned class on the tile <article>. - TestDashboardPinUnpinsItem — POST pin=false on a pinned row unpins. - TestDashboardPinRequiresID — missing id returns 400. - TestDashboardPinInvalidatesCache — primes with unpinned cache, POSTs pin, asserts the next GET reflects the pinned class (proving the prior cache entry was busted).
47 lines
1.3 KiB
Go
47 lines
1.3 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// handleDashboardPin flips the Pinned boolean on a single item from the
|
|
// Tiles view's star toggle, then re-renders the dashboard section so
|
|
// the tile moves to the pinned-first slot (or back to alphabetical when
|
|
// unpinned). POST body carries:
|
|
//
|
|
// id — required uuid of the item
|
|
// pin — "true" / "false" / "1" / "0"
|
|
//
|
|
// The handler invalidates ALL dashboard cache entries on success
|
|
// because Pinned affects sort order across every (view, scope, filter)
|
|
// combination; a per-key invalidation would race with siblings.
|
|
func (s *Server) handleDashboardPin(w http.ResponseWriter, r *http.Request) {
|
|
if err := r.ParseForm(); err != nil {
|
|
s.fail(w, r, err)
|
|
return
|
|
}
|
|
id := strings.TrimSpace(r.FormValue("id"))
|
|
if id == "" {
|
|
http.Error(w, "id required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
pinned := parseFormBool(r.FormValue("pin"))
|
|
if err := s.Store.SetPinned(r.Context(), []string{id}, pinned); err != nil {
|
|
s.fail(w, r, err)
|
|
return
|
|
}
|
|
s.dashboard.InvalidateAll()
|
|
s.handleDashboard(w, r)
|
|
}
|
|
|
|
// parseFormBool reads the common shapes ("true"/"1"/"on" → true; "false"/"0"/"" → false).
|
|
func parseFormBool(raw string) bool {
|
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
|
case "true", "1", "on", "yes":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|