m's Q5 pick (2026-05-26): project scope on every Views-supporting page, with descendants exposed as an explicit on/off chip toggle rather than always-on. Slice A ships the smallest standalone piece of the Views system; slices B–E (view_type URL param, kanban, saved-views schema, defaults) follow on the same branch. TreeFilter grows two fields: - ProjectPath: scoped item's primary path; "" = no filter. - IncludeDescendants: default true; flipped via ?project_descendants=0. Matching extends to path-prefix across `it.Paths` when ProjectPath is set; equality-only when IncludeDescendants is off. Multi-parent items pass when ANY of their paths qualifies. Picker is a shared partial (templates/project_chip.tmpl) that every Views-supporting filter strip includes (tree, dashboard, timeline, calendar). Two states: <select> picker when no project is set; active chip with × clear + descendants on/off chip when scoped. Hidden inputs added to each form so non-picker chip clicks preserve the project state. Graph and admin tools are NOT Views consumers (per design.md / docs/plans/views-system.md §5) and stay untouched. Test-source edits (per the 5c sharpened rule): - dashboard_test.go, public_listing_test.go, timeline_test.go: row membership assertions tightened from `Contains(body, slug)` to `Contains(body, href="/i/path")`. The picker now renders every item's primary path inside a <select>, so coarse slug substring matches falsely passed across filtered-out picker options. Behaviour preserved (filtered rows still don't render); the impl-detail assertion moved to the row link. New tests: TestProjectFilterIncludesDescendants, TestProjectFilterDescendantsOff, TestParseTreeFilterProjectFields, TestTreeFilterProjectRoundTrip, TestSetProjectAndToggleHelpers, TestProjectFilterScopesTreeToDescendants (end-to-end via /).
105 lines
4.7 KiB
Cheetah
105 lines
4.7 KiB
Cheetah
{{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}}
|
||
</form>
|
||
|
||
{{template "view-project-chip" .}}
|
||
|
||
{{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>
|
||
|
||
<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>
|
||
</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}}
|