Files
projax/web/templates/tree_section.tmpl
mAi bbc7867a35 feat(views): Phase 5i slice C — kanban view_type with group_by chip strip
m's Q6 pick (2026-05-26): kanban groups the filtered set by `status`
(default) / `area` / `tag` / `management`. Read-only — drag-to-change
is parked. Adds the third view_type render on /tree (alongside list and
card from earlier slices); kanban is now unlocked in PageViewTypes("/").

New web/kanban.go owns BuildKanbanBoard + the per-dimension keyer +
column ordering (status: active/done/archived; management: mai/self/
external/unmanaged; area + tag: alphabetical). Within-column order:
pinned-first → updated_at desc → title.

ParseGroupBy + GroupByChips provide the URL-param hookup and the chip
strip rendered above the board. Multi-tag items appear in every tag
column they belong to (deliberate — the kanban surfaces overlap).

Render:
- handleTree builds the kanban board off the same flatMatchedItems the
  card view consumes; cost is one extra grouping pass, no new DB hits.
- New templates/tree_kanban.tmpl: header chip strip + responsive
  column board (horizontal scroll on overflow). Empty filtered set
  surfaces a friendly nudge.

CSS additions cover the column / card layout; existing chip aesthetics
reused for the group-by toggle.

Test updates:
- view_type_test.go: slice B's "kanban locked on /" assertions tightened
  to "kanban unlocked; calendar + timeline still locked on /" — slice C
  is the unlock event for kanban.
- New kanban_test.go: per-dimension grouping (status, tag, area),
  pinned-first ordering, parser fallback.
- server_test.go: end-to-end render — GET /?view_type=kanban produces
  kanban-board markup + group-by chip strip; forest absent.
2026-05-26 13:47:03 +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}}