Files
projax/web/templates/timeline_section.tmpl
mAi f820fa5830 feat(views): Phase 5j slice C — full URL migration + system views
Per m's Q1 pick (b) (2026-05-29): legacy `/`, `/dashboard`, `/calendar`,
`/timeline`, `/graph` become `/views/{system-slug}`. Old routes
301-redirect to the new ones with chip params preserved; the legacy
?view=<uuid> param from 5i is resolved through the uuid → slug map
when present so old bookmarks land on the right user view.

System views (web/system_views.go):
- SystemView struct (Slug / Name / Icon / URL) — code-resident, never
  rows in projax.views.
- AllSystemViews() returns the canonical five: tree, dashboard,
  calendar, timeline, graph. Display order matches the existing
  sidebar.
- LookupSystemView(slug) returns the matching entry or nil; the
  reserved-slug list in store.IsReservedViewSlug (slice A) is kept
  in sync.
- legacyRedirect(systemSlug) handler 301s with chip-param preservation
  + uuid → slug resolution for any leftover ?view=<uuid>.

Routes (web/server.go):
- GET /views/tree      → handleTree     (was GET /)
- GET /views/dashboard → handleDashboard
- GET /views/timeline  → handleTimeline
- GET /views/calendar  → handleCalendar
- GET /views/graph     → handleGraph
- GET /                → 301 → /views/tree
- GET /dashboard       → 301 → /views/dashboard
- GET /timeline        → 301 → /views/timeline
- GET /calendar        → 301 → /views/calendar
- GET /graph           → 301 → /views/graph
- POST action endpoints (/dashboard/task/*, /dashboard/pin, /admin/*)
  stay where they are — those are RPC-ish, not page renders.

handleTree: dropped the `r.URL.Path != "/"` guard — the only entry
point now is /views/tree, mounted via the new route. Slice F removes
any residual references; this slice keeps the handler reachable.

computeChipCounts grew a `base string` arg so chip URLs anchor on the
caller's route (/views/tree for the system tree, /views/{slug} for
saved views). PageViewTypes recognises both legacy and /views/ keys
during the transition.

Template hrefs / hx-gets bulk-updated to the new URLs:
- layout.tmpl: every sidebar + bottom-nav entry points at
  /views/{system-slug}. Active-state checks updated alongside.
- tree_section.tmpl, tree_card.tmpl, tree_kanban.tmpl: clear-filter
  / clear-all hrefs → /views/tree.
- calendar*.tmpl, timeline_section.tmpl, graph.tmpl,
  dashboard_section.tmpl: every internal nav + filter link points at
  the /views/{slug} surface.
- detail.tmpl, error.tmpl: cancel / back-to-tree → /views/tree.

Test-source updates (per the 5c sharpened rule):
- ~100 test paths bulk-rewritten from /dashboard /calendar /timeline
  /graph (and `/`) to their /views/{slug} counterparts. The
  behaviour-preservation contract holds: status codes + body shapes
  for the rendered pages stay the same; only the URL anchoring the
  test changes.
- layout_test.go: sidebar href assertions updated to /views/{slug}.
- view_type_test.go (Q2 + Q3 follow-up): PageViewTypes lookup table
  updated to use the new route keys.
- 2 deliberate behaviour-change assertions land: TestLegacyRedirects
  expects 301 on the old URLs (was 200); TestTreeRenders fetches
  /views/tree (the new home) instead of /.

Internal go-source URL emissions (dashboard.go, calendar.go,
timeline.go) updated to the new BasePath so chip + refresh URLs round
through /views/{slug} correctly.

New tests:
- TestSystemViewLookup — AllSystemViews shape + LookupSystemView
  round-trip + unknown-slug nil.
- TestLegacyRedirects — every legacy URL 301s to its new home with
  chip params preserved.
- TestLegacyViewUUIDRedirect — old `?view=<uuid>` URLs land on the
  resolved slug per m's Q3 pick.
2026-05-29 11:59:26 +02:00

156 lines
8.4 KiB
Cheetah
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "timeline-section"}}
<section id="timeline-section" class="timeline">
<section class="tagbar" id="timeline-filterbar">
<form id="timeline-filter" class="search"
hx-get="/views/timeline"
hx-target="#timeline-section"
hx-swap="outerHTML"
hx-trigger="change from:select"
hx-push-url="true">
<label>tag&nbsp;
<select name="tag" multiple size="3">
{{range $.Filter.Tags}}<option value="{{.}}" selected>{{.}}</option>{{end}}
</select>
</label>
<label>mgmt&nbsp;
<select name="mgmt" multiple size="4">
{{$selM := .Filter.Management}}
<option value="mai" {{if contains $selM "mai"}}selected{{end}}>mai</option>
<option value="self" {{if contains $selM "self"}}selected{{end}}>self</option>
<option value="external" {{if contains $selM "external"}}selected{{end}}>external</option>
</select>
</label>
<label>has&nbsp;
<select name="has" multiple size="2">
{{$selH := .Filter.HasLinks}}
<option value="caldav-list" {{if contains $selH "caldav-list"}}selected{{end}}>caldav</option>
<option value="gitea-repo" {{if contains $selH "gitea-repo"}}selected{{end}}>gitea</option>
</select>
</label>
<label>kind&nbsp;
<select name="kind" multiple size="4">
{{$selK := .P.Kinds}}
<option value="todo" {{if contains $selK "todo"}}selected{{end}}>todo</option>
<option value="event" {{if contains $selK "event"}}selected{{end}}>event</option>
<option value="doc" {{if contains $selK "doc"}}selected{{end}}>doc</option>
<option value="creation" {{if contains $selK "creation"}}selected{{end}}>creation</option>
</select>
</label>
<label>order&nbsp;
<select name="order">
<option value="desc" {{if eq .P.Order "desc"}}selected{{end}}>newest first</option>
<option value="asc" {{if eq .P.Order "asc"}}selected{{end}}>oldest first</option>
</select>
</label>
{{if .Filter.ProjectPath}}<input type="hidden" name="project" value="{{.Filter.ProjectPath}}">{{end}}
{{if and .Filter.ProjectPath (not .Filter.IncludeDescendants)}}<input type="hidden" name="project_descendants" value="0">{{end}}
{{if .Filter.Active}}<a class="clear" href="/views/timeline">clear filters</a>{{end}}
</form>
{{template "view-project-chip" .}}
<p class="counts muted">
<small>{{.P.TotalRows}} rows · {{.P.From.Format "2006-01-02"}} → {{.P.ToInclusive.Format "2006-01-02"}}</small>
{{if .P.Cached}}<small title="Served from 90s in-memory cache · built {{.P.BuiltAt.Format "15:04:05"}}">· cached</small>{{else}}<small>· fresh</small>{{end}}
</p>
</section>
{{if .P.Days}}
<ol class="spine" data-order="{{.P.Order}}">
{{range .P.Days}}
<li class="spine-day{{if .Sticky}} sticky-{{.Sticky}}{{end}}" data-date="{{.DateKey}}">
<header class="day-header">
{{if .Sticky}}<span class="sticky-pill">{{.Sticky}}</span>{{end}}
<h2><a class="muted" href="/views/timeline?from={{.DateKey}}&amp;to={{.DateKey}}">{{.Label}}</a> <small class="muted">({{len .Rows}})</small></h2>
</header>
<ul class="day-rows">
{{range .Rows}}
{{if eq .Kind "event"}}
<li class="row row-event{{if .FarFuture}} far-future{{end}}" data-uid="{{.Event.UID}}">
<span class="time">{{if .StartLabel}}{{.StartLabel}}{{else}}&nbsp;{{end}}</span>
<span class="kind-badge kind-event" title="event">event</span>
<a class="proj" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
<span class="summary">{{.Event.Summary}}</span>
{{if .Event.Location}}<span class="loc muted">· {{.Event.Location}}</span>{{end}}
{{if .DurationHint}}<span class="duration muted">{{.DurationHint}}</span>{{end}}
{{if .Event.Recurring}}<span class="recurring" title="recurring — only literal DTSTART shown">↻</span>{{end}}
</li>
{{else if eq .Kind "todo"}}
{{$todoCal := .CalendarURL}}
{{$uid := .Todo.UID}}
{{$done := or (eq .Todo.Status "COMPLETED") (eq .Todo.Status "CANCELLED")}}
<li class="row row-todo{{if .FarFuture}} far-future{{end}}{{if $done}} done{{end}}" data-uid="{{$uid}}">
<span class="time">
{{if .Todo.Due}}{{if eq (.Todo.Due.Hour) 0}}{{else}}{{.Todo.Due.Format "15:04"}}{{end}}{{end}}
</span>
<form class="todo-complete inline"
hx-post="/dashboard/task/done"
hx-target="#timeline-section"
hx-swap="outerHTML"
hx-include="this">
<input type="hidden" name="calendar_url" value="{{$todoCal}}">
<input type="hidden" name="uid" value="{{$uid}}">
<button type="submit" class="check" title="{{if $done}}Reopen{{else}}Mark complete{{end}}" aria-label="Toggle">{{if $done}}☑{{else}}☐{{end}}</button>
</form>
<span class="kind-badge kind-todo" title="task">todo</span>
<a class="proj" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
<form class="todo-edit inline"
hx-post="/dashboard/task/edit"
hx-target="#timeline-section"
hx-swap="outerHTML">
<input type="hidden" name="calendar_url" value="{{$todoCal}}">
<input type="hidden" name="uid" value="{{$uid}}">
<input type="text" name="summary" value="{{.Todo.Summary}}" required>
<input type="date" name="due" value="{{if .Todo.Due}}{{.Todo.Due.Format "2006-01-02"}}{{end}}">
<button type="submit" title="Save edits">Save</button>
</form>
<form class="todo-delete inline"
hx-post="/dashboard/task/delete"
hx-target="#timeline-section"
hx-swap="outerHTML"
hx-confirm="Delete this task? This cannot be undone.">
<input type="hidden" name="calendar_url" value="{{$todoCal}}">
<input type="hidden" name="uid" value="{{$uid}}">
<button type="submit" class="x" title="Delete" aria-label="Delete">×</button>
</form>
</li>
{{else if eq .Kind "doc"}}
<li class="row row-doc{{if .FarFuture}} far-future{{end}}" data-link="{{.Link.ID}}">
<span class="time">&nbsp;</span>
<span class="kind-badge kind-doc" title="document">doc</span>
<span class="per">{{.PER}}</span>
<span class="ref-type ref-type-{{.Link.RefType}}">{{.Link.RefType}}</span>
{{if .Link.Note}}<span class="note">{{deref .Link.Note}}</span>{{end}}
<a class="ref-id" href="{{.Link.RefID}}" target="_blank" rel="noopener">{{.Link.RefID}}</a>
<a class="proj muted" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
<form class="link-delete inline"
hx-post="/i/{{.ItemPath}}/links/remove"
hx-target="#timeline-section"
hx-swap="outerHTML"
hx-confirm="Remove this dated link from {{.ItemPath}}?">
<input type="hidden" name="link_id" value="{{.Link.ID}}">
<button type="submit" class="x" title="Remove link" aria-label="Remove">×</button>
</form>
</li>
{{else if eq .Kind "creation"}}
<li class="row row-creation{{if .FarFuture}} far-future{{end}}">
<span class="time">&nbsp;</span>
<span class="kind-badge kind-creation" title="item added">added</span>
<span class="creation-marker muted">added <a class="proj" href="/i/{{.ItemPath}}">{{.ItemPath}}</a> to projax</span>
</li>
{{end}}
{{end}}
</ul>
</li>
{{end}}
</ol>
{{else}}
<p class="empty muted">Nothing on this timeline yet.</p>
{{end}}
</section>
{{end}}