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).
78 lines
3.2 KiB
Cheetah
78 lines
3.2 KiB
Cheetah
{{define "dashboard-tiles"}}
|
|
{{if .P.ProjectsCurrent}}
|
|
<section class="dash-tiles">
|
|
{{range .P.ProjectsCurrent}}
|
|
{{template "tile" .}}
|
|
{{end}}
|
|
</section>
|
|
{{else}}
|
|
<p class="empty muted dash-tiles-empty">
|
|
{{if eq .Scope "current"}}
|
|
Nothing current. Pin a project from its detail page, or
|
|
<a href="?scope=all"
|
|
hx-get="?scope=all"
|
|
hx-target="#dashboard-section"
|
|
hx-swap="outerHTML"
|
|
hx-push-url="true">show all active</a>.
|
|
{{else}}
|
|
No projects to show.
|
|
{{end}}
|
|
</p>
|
|
{{end}}
|
|
|
|
{{if .P.ProjectsQuiet}}
|
|
<details class="dash-quiet">
|
|
<summary class="dash-quiet-summary muted">
|
|
Quiet ({{len .P.ProjectsQuiet}}) — older than {{.P.QuietWindowLabel}}{{if .P.QuietStaleCount}} · {{.P.QuietStaleCount}} stale{{end}}
|
|
</summary>
|
|
<section class="dash-tiles dash-tiles-quiet">
|
|
{{range .P.ProjectsQuiet}}
|
|
{{template "tile" .}}
|
|
{{end}}
|
|
</section>
|
|
</details>
|
|
{{end}}
|
|
{{end}}
|
|
|
|
{{define "tile"}}
|
|
{{$path := .Item.PrimaryPath}}
|
|
{{$id := .Item.ID}}
|
|
<article class="tile{{if .Item.Pinned}} tile-pinned{{end}}{{if .Stale}} tile-stale{{end}}" data-item-path="{{$path}}" data-item-id="{{$id}}">
|
|
<header class="tile-head">
|
|
<form class="tile-pin-form"
|
|
hx-post="/dashboard/pin"
|
|
hx-target="#dashboard-section"
|
|
hx-swap="outerHTML">
|
|
<input type="hidden" name="id" value="{{$id}}">
|
|
<input type="hidden" name="pin" value="{{if .Item.Pinned}}false{{else}}true{{end}}">
|
|
<button type="submit" class="tile-pin{{if .Item.Pinned}} pinned{{end}}"
|
|
title="{{if .Item.Pinned}}Unpin{{else}}Pin{{end}}"
|
|
aria-label="{{if .Item.Pinned}}Unpin{{else}}Pin{{end}}">{{if .Item.Pinned}}★{{else}}☆{{end}}</button>
|
|
</form>
|
|
<a class="tile-title" href="/i/{{$path}}">{{.Item.Title}}</a>
|
|
<span class="tile-path muted">{{$path}}</span>
|
|
{{if .IsLive}}<a class="tile-live" href="{{.Item.PublicLiveURL}}" target="_blank" rel="noopener" title="live">live</a>{{end}}
|
|
{{if .Stale}}<span class="tile-stale-flag muted" title="mai-managed · quiet repo · no open work">stale</span>{{end}}
|
|
</header>
|
|
<p class="tile-counts">
|
|
{{if .OpenTasks}}<span class="tile-open"><strong>{{.OpenTasks}}</strong> open</span>{{end}}
|
|
{{if .Overdue}}<span class="tile-overdue"><strong>{{.Overdue}}</strong>!</span>{{end}}
|
|
{{if .OpenIssues}}<span class="tile-issues"><strong>{{.OpenIssues}}</strong> issue{{if ne .OpenIssues 1}}s{{end}}</span>{{end}}
|
|
{{if and (not .OpenTasks) (not .OpenIssues)}}<span class="tile-quiet muted">quiet</span>{{end}}
|
|
</p>
|
|
{{if .NextSignal}}
|
|
<p class="tile-signal" title="{{.NextSignalKind}}">
|
|
<span class="tile-signal-kind muted">{{if eq .NextSignalKind "task"}}•{{else}}◆{{end}}</span>
|
|
<span class="tile-signal-text">{{.NextSignal}}</span>
|
|
</p>
|
|
{{end}}
|
|
<footer class="tile-foot">
|
|
{{if .LastActivityRel}}
|
|
<span class="tile-stamp muted" title="{{.LastActivity.Format "2006-01-02 15:04"}}">{{.LastActivityRel}}</span>
|
|
{{else}}
|
|
<span class="tile-stamp muted">—</span>
|
|
{{end}}
|
|
</footer>
|
|
</article>
|
|
{{end}}
|