Files
projax/web/templates/tree_section.tmpl
mAi 173d7ddbb2 feat(views): Phase 5j slice A — paliad-shape schema redesign
Hard-replaces the 5i projax.views table per m's Q10 pick (2026-05-29):
no real data to preserve after a few hours, and the shape changes are
big enough that a clean recreate beats a 6-step ALTER.

Schema (migration 0017_views_redesign.sql):
- id (uuid), slug (text, format-CHECK'd, UNIQUE), name, icon,
  filter_json (jsonb — INCLUDES view_type per m's Q2), sort_field,
  sort_dir, group_by, sort_order, show_count, last_used_at,
  created_at, updated_at.
- DROPPED: pinned, is_default_for, view_type column. m's Q9 picked
  MRU (last_used_at) over per-page-default; Q2 placed view_type
  inside filter_json so the JSON owns the canonical render spec.
- Constraints: slug regex, sort_dir enum. NO view_type CHECK — the
  JSON-shape validator owns it now.
- Indexes: slug UNIQUE, (sort_order, name), (last_used_at DESC).
- updated_at trigger reused; projax_admin ownership preserved.

Store (store/views.go rewrite):
- View struct: Slug as the user-facing key; uuid kept on ID for the
  legacy `?view=<uuid>` 302-redirect path that lands in slice C.
- ListViews ordered by sort_order, name (matches sidebar).
- GetView(slug) + GetViewByID(uuid). MostRecentView() drives the
  /views landing redirect (slice B).
- TouchView(slug) bumps last_used_at fire-and-forget.
- ReorderViews([]slugs) wires the column for slice G's drag UI.
- CreateView server-assigns sort_order = MAX+1 inside the tx.
- UpdateView replaces every writeable field; renames are supported.
- Validation: slug format regex + reserved-list rejection +
  filter_json JSON well-formed check before round-trip.
- ErrViewNotFound / ErrViewSlugTaken / ErrViewSlugReserved /
  ErrViewSlugFormat surface to handlers as the typed error set.

Cleanup of the 5i overlay (drops what the new shape obsoletes):
- web/views.go: gutted to a stub. applySavedView, applyDefaultView,
  overlayURLFields, filterQueryToJSON, filterJSONToQuery,
  filterFromJSONPayload, anySliceToStrings + every old handler
  (handleViewsIndex, handleViewCreate, handleViewWrite, handleViewEdit,
  handleViewRedirect, handleViewDelete) deleted.
- web/server.go: dropped the /views route registrations and the
  applySavedView + applyDefaultView calls in handleTree.
  DefaultBanner data-map field removed.
- web/tree_filter.go: TreeFilter.ViewID field removed; ParseTreeFilter
  and QueryString stop reading/emitting ?view=.
- web/templates/views.tmpl and view_edit.tmpl deleted.
- web/templates/tree_section.tmpl: default-banner block deleted.
- web/views_test.go: deleted (every test was against the 5i shape).

Between slice A and slice B, /views/* URLs return 404 by design.
Slice B reintroduces the route family in paliad-shape:
  GET /views          → MRU landing
  GET /views/{slug}   → render
  GET /views/new      → editor
  GET /views/{slug}/edit → editor
  POST /views, /views/{slug}, /views/{slug}/delete → CRUD

Tests (store/views_test.go, new):
- TestViewSlugCRUD — create / get-by-slug / get-by-id / rename /
  delete round-trip, including rename-leaves-old-slug-gone.
- TestViewSlugFormatRejected — uppercase, underscore, leading dash,
  length-cap, empty all surface ErrViewSlugFormat.
- TestViewReservedSlugRejected — tree/dashboard/calendar/timeline/graph
  and friends all reject with ErrViewSlugReserved.
- TestViewSlugCollision — duplicate slug surfaces ErrViewSlugTaken.
- TestViewMRU — TouchView + MostRecentView ordering against a
  controlled pair of slugs (resilient to other suites' touched views).
- TestViewReorder — ReorderViews rewrites sort_order ascending.

Web tests stay green (the 5i overlay tests are gone, the rest don't
touch the views shape).
2026-05-29 11:41:28 +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="/">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="/">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}}