Files
projax/web/templates/bulk_section.tmpl
mAi 0bea9c1ba4 feat(phase 4f): per-item timeline_exclude flag (hide noise from /timeline)
m's stated use case: home VTODOs (shopping list) shouldn't pollute the
chronological /timeline by default, but they should stay visible on the
home detail page itself. This adds an item-level switch with four kinds
and a URL override to peek at everything when wanted.

## Schema (migration 0015)
- timeline_exclude text[] NOT NULL DEFAULT '{}'
- items_timeline_exclude_idx GIN
- items_unified view rebuilt to surface the new column
- Behaviour-neutral: empty array = unchanged from today. m flips the
  toggle himself via /admin/bulk or the detail-page form.

## Aggregation
- web/timeline.go: pre-compute the per-kind keep-list via keepFor(kind)
  before fanning out — items with the kind in their exclude array are
  dropped entirely (no CalDAV call wasted on excluded sources). Doc and
  creation rows check the per-item flag inline. `?include_excluded=1`
  (URL) and `include_excluded:true` (MCP arg) override the filter.
- store.Item.ExcludesTimelineKind(kind) helper accepts either singular
  ("todo") or plural ("todos") to bridge the kind-constant / persisted-
  value naming choice — see comment for the why.

## UI
- /i/{path} grows a "Timeline behaviour" collapsible section with four
  checkboxes (todos / events / docs / creation) and helper text. Open by
  default when any kind is excluded, so m can see at a glance what's
  hidden for this item.
- /admin/bulk gains a "timeline todos" select with "Exclude from timeline"
  and "Re-include on timeline" — the other three kinds stay editable
  per-item only per the task brief (most common use case is just todos).

## MCP
- update_item accepts timeline_exclude as a partial-update field with an
  enum-restricted whitelist; unknown values dropped silently.
- itemView always emits timeline_exclude (defaults to []) so consumers
  can render the toggle state without a second round-trip.

## Tests
- Migration + GIN index landed
- Item with timeline_exclude=['todos'] hides the VTODO from /timeline
- ?include_excluded=1 brings it back
- Bulk action toggles the array idempotently in both directions
- Detail page renders all 4 checkbox affordances

## docs/design.md
§12 gains a "Per-item exclusion" subsection documenting semantics, the
URL override, the bulk action, and the "detail page still shows everything"
invariant.

## Out of scope (per task brief)
- Per-tag exclusion (per-item is clearer)
- Per-day exclusion (overkill)
- Dashboard exclusion (m only flagged timeline; dashboard's "today" view
  should still show shopping today if it's due today)
- Auto-seeding home with timeline_exclude=['todos'] (m runs once himself
  via /admin/bulk after the deploy — schema change stays behaviour-neutral)
2026-05-17 19:28:49 +02:00

171 lines
6.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 "bulk-section"}}
<section id="bulk-section" class="bulk-section">
<section class="tagbar" id="bulk-filterbar">
<form id="bulk-filter" class="search"
hx-get="/admin/bulk"
hx-target="#bulk-section"
hx-swap="outerHTML"
hx-trigger="keyup changed delay:200ms from:input[name=q], change from:select, change from:input[type=hidden], change from:input[name=show-archived]"
hx-push-url="true">
<input type="search" name="q" value="{{.Filter.Q}}" placeholder="search title, slug, content…" autocomplete="off">
<label>tag&nbsp;
<select name="tag" multiple size="3">
{{$selTags := .Filter.Tags}}
{{range .AllTags}}<option value="{{.}}" {{if contains $selTags .}}selected{{end}}>{{.}}</option>{{end}}
</select>
</label>
<label>mgmt&nbsp;
<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>
<option value="unmanaged" {{if contains $selM "unmanaged"}}selected{{end}}>unmanaged</option>
</select>
</label>
<label>status&nbsp;
<select name="status" multiple size="3">
{{$selS := .Filter.Status}}
<option value="active" {{if contains $selS "active"}}selected{{end}}>active</option>
<option value="done" {{if contains $selS "done"}}selected{{end}}>done</option>
<option value="archived" {{if contains $selS "archived"}}selected{{end}}>archived</option>
</select>
</label>
<label class="checkbox">
<input type="checkbox" name="show-archived" value="1" {{if .Filter.ShowArchived}}checked{{end}}>
show archived
</label>
</form>
<p class="counts"><strong>{{.Matched}}</strong> / <strong>{{.Total}}</strong> items match</p>
</section>
{{with .Banner}}<p class="banner warn">{{.}}</p>{{end}}
<form id="bulk-actions"
method="post"
action="/admin/bulk/apply"
hx-post="/admin/bulk/apply"
hx-target="#bulk-section"
hx-swap="outerHTML"
hx-include="#bulk-filter">
<fieldset class="actions">
<legend>Bulk action <small>(applies to all checked rows)</small></legend>
<div class="action-row">
<label>+ tag <input name="add_tag" placeholder="tag-slug"></label>
<label> tag <input name="remove_tag" placeholder="tag-slug"></label>
<label>set management
<select name="set_mgmt">
<option value="">—</option>
<option value="mai">mai</option>
<option value="self">self</option>
<option value="external">external</option>
<option value="clear">clear</option>
</select>
</label>
<label>set status
<select name="set_status">
<option value="">—</option>
<option value="active">active</option>
<option value="done">done</option>
<option value="archived">archived</option>
</select>
</label>
<label>public-listing
<select name="set_public">
<option value="">—</option>
<option value="make_public">Make public</option>
<option value="make_private">Make private</option>
</select>
</label>
<label>timeline todos
<select name="timeline_todos">
<option value="">—</option>
<option value="exclude">Exclude from timeline</option>
<option value="include">Re-include on timeline</option>
</select>
</label>
<button type="submit">Apply</button>
</div>
</fieldset>
<table class="bulk">
<thead>
<tr>
<th><input type="checkbox" id="bulk-all" onclick="document.querySelectorAll('input[name=ids]').forEach(c=>c.checked=this.checked)"></th>
<th>slug</th>
<th>primary path</th>
<th>tags</th>
<th>mgmt</th>
<th>status</th>
</tr>
</thead>
<tbody>
{{range .Rows}}
<tr id="bulk-row-{{.ID}}">
<td><input type="checkbox" name="ids" value="{{.ID}}"></td>
<td><a href="/i/{{.PrimaryPath}}">{{.Slug}}</a></td>
<td><small class="muted">{{.PrimaryPath}}</small></td>
<td class="cell-tags" id="cell-tags-{{.ID}}">
{{template "bulk-chip-tags" .}}
</td>
<td class="cell-mgmt" id="cell-mgmt-{{.ID}}">
{{template "bulk-chip-mgmt" .}}
</td>
<td><span class="status status-{{.Status}}">{{.Status}}</span></td>
</tr>
{{else}}
<tr><td colspan="6"><em>No items match. Loosen the filters.</em></td></tr>
{{end}}
</tbody>
</table>
</form>
</section>
{{end}}
{{define "bulk-chip-tags"}}
{{range .Tags}}
<span class="tag">{{.}}
<button class="chip-x"
hx-post="/admin/bulk/chip"
hx-vals='{"id":"{{$.ID}}","op":"remove","kind":"tag","value":"{{.}}"}'
hx-target="#cell-tags-{{$.ID}}"
hx-swap="innerHTML"
type="button"
title="remove tag">×</button>
</span>
{{end}}
{{/* No nested <form> — HTML forbids it. The input fires hx-post on
Enter; the +-button is a click trigger on the same endpoint. Both
carry id/op/kind via hx-vals; the input's own value field rides
along as name="value". */}}
<span class="chip-add">
<input name="value" placeholder="+tag" size="6"
id="chip-add-value-{{.ID}}"
hx-post="/admin/bulk/chip"
hx-target="#cell-tags-{{.ID}}"
hx-swap="innerHTML"
hx-trigger="keyup[key=='Enter']"
hx-vals='{"id":"{{.ID}}","op":"add","kind":"tag"}'>
<button type="button"
class="chip-add-btn"
hx-post="/admin/bulk/chip"
hx-target="#cell-tags-{{.ID}}"
hx-swap="innerHTML"
hx-include="#chip-add-value-{{.ID}}"
hx-vals='{"id":"{{.ID}}","op":"add","kind":"tag"}'
title="add tag">+</button>
</span>
{{end}}
{{define "bulk-chip-mgmt"}}
{{range .Management}}<span class="mgmt mgmt-{{.}}">{{.}}</span>{{end}}
{{if not .Management}}<span class="muted">unmanaged</span>{{end}}
{{end}}