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 /).
59 lines
2.7 KiB
Cheetah
59 lines
2.7 KiB
Cheetah
{{/*
|
||
Phase 5i Slice A — shared project-scope chip + picker.
|
||
|
||
Rendered inside the tagbar on every Views-supporting page (tree, dashboard,
|
||
timeline, calendar). Two visual states:
|
||
|
||
ProjectPath == "" → <select> picker with all items by primary path
|
||
ProjectPath != "" → active chip with × clear + descendants on/off toggle
|
||
|
||
Each caller passes a top-level page-data map with:
|
||
Filter — TreeFilter
|
||
Projects — []ParentOption (sorted by path)
|
||
BasePath — page route, e.g. "/", "/dashboard", "/timeline", "/calendar"
|
||
ProjectChipTarget — HTMX target selector, e.g. "#tree-section"
|
||
|
||
m's Q5 pick (2026-05-26): descendants toggle is on by default but exposed
|
||
explicitly on the chip, not always-on.
|
||
*/}}
|
||
{{define "view-project-chip"}}
|
||
<div class="chip-row project-chip-row">
|
||
<span class="muted">project:</span>
|
||
{{if .Filter.ProjectPath}}
|
||
{{$cleared := (.Filter.SetProject "").URLOn .BasePath}}
|
||
{{$toggled := .Filter.ToggleIncludeDescendants.URLOn .BasePath}}
|
||
<span class="proj-chip chip-on" title="Scoped to {{.Filter.ProjectPath}}">
|
||
<span class="proj-name">{{.Filter.ProjectPath}}</span>
|
||
<a class="proj-clear"
|
||
href="{{$cleared}}"
|
||
hx-get="{{$cleared}}" hx-target="{{.ProjectChipTarget}}" hx-swap="outerHTML" hx-push-url="true"
|
||
title="Clear project scope">×</a>
|
||
</span>
|
||
<a class="proj-desc-chip {{if .Filter.IncludeDescendants}}chip-on{{end}}"
|
||
href="{{$toggled}}"
|
||
hx-get="{{$toggled}}" hx-target="{{.ProjectChipTarget}}" hx-swap="outerHTML" hx-push-url="true"
|
||
title="Include descendants of {{.Filter.ProjectPath}} in scope">
|
||
descendants {{if .Filter.IncludeDescendants}}on{{else}}off{{end}}
|
||
</a>
|
||
{{else}}
|
||
<form class="proj-picker"
|
||
hx-get="{{.BasePath}}"
|
||
hx-target="{{.ProjectChipTarget}}"
|
||
hx-swap="outerHTML"
|
||
hx-trigger="change from:select"
|
||
hx-push-url="true">
|
||
{{if .Filter.Q}}<input type="hidden" name="q" value="{{.Filter.Q}}">{{end}}
|
||
{{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}}
|
||
<select name="project" autocomplete="off">
|
||
<option value="">— any —</option>
|
||
{{range .Projects}}<option value="{{.Path}}">{{.Path}}</option>{{end}}
|
||
</select>
|
||
</form>
|
||
{{end}}
|
||
</div>
|
||
{{end}}
|