- migration 0012: one-shot populate empty tags from each item's area-roots (so chips on /?tag=work etc. actually filter the 40+ mai-backfilled rows) - migration 0013: cleanup 12 orphan item_links + BEFORE-UPDATE trigger that cascades soft-delete to item_links going forward — closes the data drift that made TestItemsUnifiedSurfacesMaiPointer fail since 3c - /admin/bulk page: flat filter+checkbox list with one-tx Apply for add/ remove tag, set management, set status. Per-row inline chip add/remove via /admin/bulk/chip. Reuses tree_filter URL params 1:1. - design.md §3.2 + §4.1 updated; tag+management section notes 0012 - bulk + tag-backfill + soft-delete-cascade tests cover the new surface
144 lines
5.3 KiB
Cheetah
144 lines
5.3 KiB
Cheetah
{{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
|
||
<select name="tag" multiple size="3">
|
||
{{$selTags := .Filter.Tags}}
|
||
{{range .AllTags}}<option value="{{.}}" {{if contains $selTags .}}selected{{end}}>{{.}}</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>
|
||
<option value="unmanaged" {{if contains $selM "unmanaged"}}selected{{end}}>unmanaged</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label>status
|
||
<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>
|
||
|
||
<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>
|
||
<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}}
|
||
<form class="chip-add" onsubmit="return false"
|
||
hx-post="/admin/bulk/chip"
|
||
hx-target="#cell-tags-{{.ID}}"
|
||
hx-swap="innerHTML"
|
||
hx-trigger="submit">
|
||
<input type="hidden" name="id" value="{{.ID}}">
|
||
<input type="hidden" name="op" value="add">
|
||
<input type="hidden" name="kind" value="tag">
|
||
<input name="value" placeholder="+tag" size="6">
|
||
</form>
|
||
{{end}}
|
||
|
||
{{define "bulk-chip-mgmt"}}
|
||
{{range .Management}}<span class="mgmt mgmt-{{.}}">{{.}}</span>{{end}}
|
||
{{if not .Management}}<span class="muted">unmanaged</span>{{end}}
|
||
{{end}}
|