Files
projax/web/templates/tree_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

122 lines
5.5 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 "tree-section"}}
<section id="tree-section" class="tree-section">
<p class="counts">
<strong>{{.Matched}}</strong> / <strong>{{.Total}}</strong> items match
{{if .OrphanN}} · <strong>{{.OrphanN}}</strong> unclassified mai-managed roots <a href="/admin/classify">→ classify</a>{{end}}
{{if .Filter.Active}} · <a class="clear" href="/views/tree">clear filters</a>{{end}}
</p>
<section class="tagbar" id="tree-filterbar">
<form class="search" hx-get="/" hx-target="#tree-section" hx-swap="outerHTML"
hx-trigger="keyup changed delay:200ms from:input[name=q], change from:input[type=hidden]"
hx-push-url="true">
<input type="search" name="q" value="{{.Filter.Q}}" placeholder="search title, slug, content…" autocomplete="off">
{{if .Filter.Tags}}<input type="hidden" name="tag" value="{{join "," .Filter.Tags}}">{{end}}
{{if .Filter.Management}}<input type="hidden" name="mgmt" value="{{join "," .Filter.Management}}">{{end}}
{{if ne (join "," .Filter.Status) "active"}}<input type="hidden" name="status" value="{{join "," .Filter.Status}}">{{end}}
{{if .Filter.HasLinks}}<input type="hidden" name="has" value="{{join "," .Filter.HasLinks}}">{{end}}
{{if .Filter.ShowArchived}}<input type="hidden" name="show-archived" value="1">{{end}}
{{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 ne .ViewType "list"}}<input type="hidden" name="view_type" value="{{.ViewType}}">{{end}}
</form>
{{template "view-project-chip" .}}
<div class="chip-row view-type-chip-row">
<span class="muted">view:</span>
{{range .ViewTypeChips}}
<a class="view-type-chip{{if .Active}} chip-on{{end}}{{if .Locked}} chip-locked{{end}}"
href="{{.URL}}"
hx-get="{{.URL}}" hx-target="#tree-section" hx-swap="outerHTML" hx-push-url="true"
{{if .Locked}}title="{{.Label}} view lands in a future slice; clicks fall back to the default."{{end}}>{{.Label}}</a>
{{end}}
</div>
{{if .AllTags}}
<div class="chip-row">
<span class="muted">tag:</span>
{{range .Counts.Tags}}
<a class="tag {{if .Active}}tag-on{{end}}"
href="{{.URL}}"
hx-get="{{.URL}}" hx-target="#tree-section" hx-swap="outerHTML" hx-push-url="true">{{.Label}} <small>({{.Count}})</small></a>
{{end}}
</div>
{{end}}
<div class="chip-row">
<span class="muted">mgmt:</span>
{{range .Counts.Management}}
<a class="mgmt-chip {{if .Active}}chip-on{{end}}"
href="{{.URL}}"
hx-get="{{.URL}}" hx-target="#tree-section" hx-swap="outerHTML" hx-push-url="true">{{.Label}} <small>({{.Count}})</small></a>
{{end}}
</div>
<div class="chip-row">
<span class="muted">status:</span>
{{range .Counts.Status}}
<a class="status-chip {{if .Active}}chip-on{{end}}"
href="{{.URL}}"
hx-get="{{.URL}}" hx-target="#tree-section" hx-swap="outerHTML" hx-push-url="true">{{.Label}} <small>({{.Count}})</small></a>
{{end}}
<a class="status-chip {{if .Filter.ShowArchived}}chip-on{{end}}"
href="{{.Counts.ShowArchived.URL}}"
hx-get="{{.Counts.ShowArchived.URL}}" hx-target="#tree-section" hx-swap="outerHTML" hx-push-url="true"
title="When off, archived rows hide even when status=archived is selected">show archived <small>({{.Counts.ShowArchived.Count}})</small></a>
</div>
<div class="chip-row">
<span class="muted">has:</span>
{{range .Counts.Has}}
<a class="has-chip {{if .Active}}chip-on{{end}}"
href="{{.URL}}"
hx-get="{{.URL}}" hx-target="#tree-section" hx-swap="outerHTML" hx-push-url="true">{{.Label}} <small>({{.Count}})</small></a>
{{end}}
</div>
</section>
{{if eq .ViewType "card"}}
{{template "tree-card" .}}
{{else if eq .ViewType "kanban"}}
{{template "tree-kanban" .}}
{{else}}
<section class="tree">
<ul class="forest">
{{range .Roots}}
<li class="node root">
<a href="/i/{{.Item.PrimaryPath}}">{{.Item.Title}}</a>
<span class="slug">{{.Item.PrimaryPath}}</span>
{{range .Item.Management}}<span class="mgmt mgmt-{{.}}">{{.}}</span>{{end}}
{{range .Item.Tags}}<span class="tag">{{.}}</span>{{end}}
<a class="add" href="/new?parent={{.Item.PrimaryPath}}">+</a>
{{template "children" .}}
</li>
{{else}}
<li class="empty"><em>No items match. Try fewer filters or <a href="/views/tree">clear all</a>.</em></li>
{{end}}
</ul>
</section>
{{end}}
</section>
{{end}}
{{define "children"}}
{{if .Children}}
<ul>
{{range .Children}}
<li class="node project">
<a href="/i/{{.Item.PrimaryPath}}">{{.Item.Title}}</a>
<span class="slug">{{.Item.PrimaryPath}}</span>
<span class="status status-{{.Item.Status}}">{{.Item.Status}}</span>
{{if gt (len .Item.Paths) 1}}<span class="muted multi-parent" title="appears under multiple parents">×{{len .Item.Paths}}</span>{{end}}
{{range .Item.Management}}<span class="mgmt mgmt-{{.}}">{{.}}</span>{{end}}
{{range .Item.Tags}}<span class="tag">{{.}}</span>{{end}}
<a class="add" href="/new?parent={{.Item.PrimaryPath}}">+</a>
{{template "children" .}}
</li>
{{end}}
</ul>
{{end}}
{{end}}