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 /).
156 lines
8.4 KiB
Cheetah
156 lines
8.4 KiB
Cheetah
{{define "timeline-section"}}
|
||
<section id="timeline-section" class="timeline">
|
||
|
||
<section class="tagbar" id="timeline-filterbar">
|
||
<form id="timeline-filter" class="search"
|
||
hx-get="/timeline"
|
||
hx-target="#timeline-section"
|
||
hx-swap="outerHTML"
|
||
hx-trigger="change from:select"
|
||
hx-push-url="true">
|
||
<label>tag
|
||
<select name="tag" multiple size="3">
|
||
{{range $.Filter.Tags}}<option value="{{.}}" selected>{{.}}</option>{{end}}
|
||
</select>
|
||
</label>
|
||
<label>mgmt
|
||
<select name="mgmt" multiple size="4">
|
||
{{$selM := .Filter.Management}}
|
||
<option value="mai" {{if contains $selM "mai"}}selected{{end}}>mai</option>
|
||
<option value="self" {{if contains $selM "self"}}selected{{end}}>self</option>
|
||
<option value="external" {{if contains $selM "external"}}selected{{end}}>external</option>
|
||
</select>
|
||
</label>
|
||
<label>has
|
||
<select name="has" multiple size="2">
|
||
{{$selH := .Filter.HasLinks}}
|
||
<option value="caldav-list" {{if contains $selH "caldav-list"}}selected{{end}}>caldav</option>
|
||
<option value="gitea-repo" {{if contains $selH "gitea-repo"}}selected{{end}}>gitea</option>
|
||
</select>
|
||
</label>
|
||
<label>kind
|
||
<select name="kind" multiple size="4">
|
||
{{$selK := .P.Kinds}}
|
||
<option value="todo" {{if contains $selK "todo"}}selected{{end}}>todo</option>
|
||
<option value="event" {{if contains $selK "event"}}selected{{end}}>event</option>
|
||
<option value="doc" {{if contains $selK "doc"}}selected{{end}}>doc</option>
|
||
<option value="creation" {{if contains $selK "creation"}}selected{{end}}>creation</option>
|
||
</select>
|
||
</label>
|
||
<label>order
|
||
<select name="order">
|
||
<option value="desc" {{if eq .P.Order "desc"}}selected{{end}}>newest first</option>
|
||
<option value="asc" {{if eq .P.Order "asc"}}selected{{end}}>oldest first</option>
|
||
</select>
|
||
</label>
|
||
{{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 .Filter.Active}}<a class="clear" href="/timeline">clear filters</a>{{end}}
|
||
</form>
|
||
|
||
{{template "view-project-chip" .}}
|
||
|
||
<p class="counts muted">
|
||
<small>{{.P.TotalRows}} rows · {{.P.From.Format "2006-01-02"}} → {{.P.ToInclusive.Format "2006-01-02"}}</small>
|
||
{{if .P.Cached}}<small title="Served from 90s in-memory cache · built {{.P.BuiltAt.Format "15:04:05"}}">· cached</small>{{else}}<small>· fresh</small>{{end}}
|
||
</p>
|
||
</section>
|
||
|
||
{{if .P.Days}}
|
||
<ol class="spine" data-order="{{.P.Order}}">
|
||
{{range .P.Days}}
|
||
<li class="spine-day{{if .Sticky}} sticky-{{.Sticky}}{{end}}" data-date="{{.DateKey}}">
|
||
<header class="day-header">
|
||
{{if .Sticky}}<span class="sticky-pill">{{.Sticky}}</span>{{end}}
|
||
<h2><a class="muted" href="/timeline?from={{.DateKey}}&to={{.DateKey}}">{{.Label}}</a> <small class="muted">({{len .Rows}})</small></h2>
|
||
</header>
|
||
<ul class="day-rows">
|
||
{{range .Rows}}
|
||
{{if eq .Kind "event"}}
|
||
<li class="row row-event{{if .FarFuture}} far-future{{end}}" data-uid="{{.Event.UID}}">
|
||
<span class="time">{{if .StartLabel}}{{.StartLabel}}{{else}} {{end}}</span>
|
||
<span class="kind-badge kind-event" title="event">event</span>
|
||
<a class="proj" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
|
||
<span class="summary">{{.Event.Summary}}</span>
|
||
{{if .Event.Location}}<span class="loc muted">· {{.Event.Location}}</span>{{end}}
|
||
{{if .DurationHint}}<span class="duration muted">{{.DurationHint}}</span>{{end}}
|
||
{{if .Event.Recurring}}<span class="recurring" title="recurring — only literal DTSTART shown">↻</span>{{end}}
|
||
</li>
|
||
{{else if eq .Kind "todo"}}
|
||
{{$todoCal := .CalendarURL}}
|
||
{{$uid := .Todo.UID}}
|
||
{{$done := or (eq .Todo.Status "COMPLETED") (eq .Todo.Status "CANCELLED")}}
|
||
<li class="row row-todo{{if .FarFuture}} far-future{{end}}{{if $done}} done{{end}}" data-uid="{{$uid}}">
|
||
<span class="time">
|
||
{{if .Todo.Due}}{{if eq (.Todo.Due.Hour) 0}}{{else}}{{.Todo.Due.Format "15:04"}}{{end}}{{end}}
|
||
</span>
|
||
<form class="todo-complete inline"
|
||
hx-post="/dashboard/task/done"
|
||
hx-target="#timeline-section"
|
||
hx-swap="outerHTML"
|
||
hx-include="this">
|
||
<input type="hidden" name="calendar_url" value="{{$todoCal}}">
|
||
<input type="hidden" name="uid" value="{{$uid}}">
|
||
<button type="submit" class="check" title="{{if $done}}Reopen{{else}}Mark complete{{end}}" aria-label="Toggle">{{if $done}}☑{{else}}☐{{end}}</button>
|
||
</form>
|
||
<span class="kind-badge kind-todo" title="task">todo</span>
|
||
<a class="proj" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
|
||
|
||
<form class="todo-edit inline"
|
||
hx-post="/dashboard/task/edit"
|
||
hx-target="#timeline-section"
|
||
hx-swap="outerHTML">
|
||
<input type="hidden" name="calendar_url" value="{{$todoCal}}">
|
||
<input type="hidden" name="uid" value="{{$uid}}">
|
||
<input type="text" name="summary" value="{{.Todo.Summary}}" required>
|
||
<input type="date" name="due" value="{{if .Todo.Due}}{{.Todo.Due.Format "2006-01-02"}}{{end}}">
|
||
<button type="submit" title="Save edits">Save</button>
|
||
</form>
|
||
|
||
<form class="todo-delete inline"
|
||
hx-post="/dashboard/task/delete"
|
||
hx-target="#timeline-section"
|
||
hx-swap="outerHTML"
|
||
hx-confirm="Delete this task? This cannot be undone.">
|
||
<input type="hidden" name="calendar_url" value="{{$todoCal}}">
|
||
<input type="hidden" name="uid" value="{{$uid}}">
|
||
<button type="submit" class="x" title="Delete" aria-label="Delete">×</button>
|
||
</form>
|
||
</li>
|
||
{{else if eq .Kind "doc"}}
|
||
<li class="row row-doc{{if .FarFuture}} far-future{{end}}" data-link="{{.Link.ID}}">
|
||
<span class="time"> </span>
|
||
<span class="kind-badge kind-doc" title="document">doc</span>
|
||
<span class="per">{{.PER}}</span>
|
||
<span class="ref-type ref-type-{{.Link.RefType}}">{{.Link.RefType}}</span>
|
||
{{if .Link.Note}}<span class="note">{{deref .Link.Note}}</span>{{end}}
|
||
<a class="ref-id" href="{{.Link.RefID}}" target="_blank" rel="noopener">{{.Link.RefID}}</a>
|
||
<a class="proj muted" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
|
||
|
||
<form class="link-delete inline"
|
||
hx-post="/i/{{.ItemPath}}/links/remove"
|
||
hx-target="#timeline-section"
|
||
hx-swap="outerHTML"
|
||
hx-confirm="Remove this dated link from {{.ItemPath}}?">
|
||
<input type="hidden" name="link_id" value="{{.Link.ID}}">
|
||
<button type="submit" class="x" title="Remove link" aria-label="Remove">×</button>
|
||
</form>
|
||
</li>
|
||
{{else if eq .Kind "creation"}}
|
||
<li class="row row-creation{{if .FarFuture}} far-future{{end}}">
|
||
<span class="time"> </span>
|
||
<span class="kind-badge kind-creation" title="item added">added</span>
|
||
<span class="creation-marker muted">added <a class="proj" href="/i/{{.ItemPath}}">{{.ItemPath}}</a> to projax</span>
|
||
</li>
|
||
{{end}}
|
||
{{end}}
|
||
</ul>
|
||
</li>
|
||
{{end}}
|
||
</ol>
|
||
{{else}}
|
||
<p class="empty muted">Nothing on this timeline yet.</p>
|
||
{{end}}
|
||
</section>
|
||
{{end}}
|