m's bug (verbatim from /views): "we cant edit views yet. and the filters on custom views dont seem to work. No apply button and no instant apply" Two distinct gaps, both surgically fixed. ## Gap 1 — edit UI missing Slice D shipped POST /views/<id> (update) but no GET form to drive it. The index page had delete + redirect-open links only. Fix: - New handleViewEdit serves GET /views/<id>/edit with the form pre-filled from the persisted row. - New templates/view_edit.tmpl mirrors the create form, selecting the current values on each <select>, populating each <input value="">. - filterJSONToQuery rebuilds the URL-query representation of filter_json so the `filter_query` text input round-trips on edit. - /views index row gets an "edit" link next to delete. - Route registered before the catch-all GET /views/ so the more specific pattern wins. handleViewRedirect also defensively forwards /edit suffix in case routing falls through. ## Gap 2 — URL chips clobbered by saved-view filter applySavedView did `*filter = filterFromJSONPayload(payload)` — wholesale replace. URL chip params parsed earlier in handleTree were thrown away. Compounded by chip URLs not preserving `?view=<id>`, so even if the overlay had worked, chip clicks would have stripped the saved view. Fix: - TreeFilter grows a `ViewID` field that round-trips through ParseTreeFilter + QueryString. Not a "filter dimension" in the matching sense (Matches ignores it); just a URL anchor that every chip URL emits forward. - applySavedView builds the saved filter, then overlayURLFields() selectively replaces any dimension the user set via URL chip on top (q/tag/mgmt/status/has/show-archived/public/project/project_descendants). - view_type: URL wins when explicitly set, saved value otherwise. - Drift is transient — URL bookmarkable as a "narrowed saved view" without auto-saving back to the row. To persist, user opens /edit. ## Tests - TestViewEditFlow — GET /<id>/edit pre-fills name + filter_query; POST /<id> updates name + view_type + filter_json round-trip in DB. - TestSavedViewPageFilterApply — seed two items + an empty saved view; /?view=<id> shows both; /?view=<id>&tag=work shows only the work one. Also asserts chip URLs contain view=<id> so navigation stays in the saved view. Out of scope (per brief): - No schema changes. - No view sharing / multi-user. - HTMX modal save UI deferred — the existing inline edit page is the surgical fix m's bug actually needs.
71 lines
2.8 KiB
Cheetah
71 lines
2.8 KiB
Cheetah
{{define "content"}}
|
|
<h1>Views</h1>
|
|
|
|
<p class="muted">Saved bundles of (filter + view_type + sort + group_by). Page-agnostic — open one to render the saved set on the matching page.</p>
|
|
|
|
<section class="views-list">
|
|
{{if .Views}}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>★</th><th>Name</th><th>Type</th><th>Default for</th><th>Group by</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Views}}
|
|
<tr>
|
|
<td>{{if .Pinned}}★{{end}}</td>
|
|
<td><a href="/views/{{.ID}}">{{.Name}}</a>{{if .Description}}<br><small class="muted">{{.Description}}</small>{{end}}</td>
|
|
<td>{{.ViewType}}</td>
|
|
<td>{{if .IsDefaultFor}}{{deref .IsDefaultFor}}{{else}}<span class="muted">—</span>{{end}}</td>
|
|
<td>{{if .GroupBy}}{{deref .GroupBy}}{{else}}<span class="muted">—</span>{{end}}</td>
|
|
<td>
|
|
<a href="/views/{{.ID}}/edit">edit</a>
|
|
<form method="post" action="/views/{{.ID}}/delete" style="display:inline">
|
|
<button type="submit" class="link-button" onclick="return confirm('Delete view {{.Name}}?')">delete</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
{{else}}
|
|
<p class="empty muted"><em>No saved views yet. Create one with the form below or via the "Save view…" link on any Views-supporting page.</em></p>
|
|
{{end}}
|
|
</section>
|
|
|
|
<section class="views-create">
|
|
<h2>New view</h2>
|
|
<form method="post" action="/views">
|
|
<label>Name <input type="text" name="name" required maxlength="80"></label>
|
|
<label>Description <input type="text" name="description" maxlength="200"></label>
|
|
<label>View type
|
|
<select name="view_type" required>
|
|
{{range .AllViewTypes}}<option value="{{.}}">{{.}}</option>{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Default for
|
|
<select name="is_default_for">
|
|
{{range .DefaultForOptions}}<option value="{{.}}">{{if eq . ""}}—{{else}}{{.}}{{end}}</option>{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Group by
|
|
<select name="group_by">
|
|
{{range .GroupByOptions}}<option value="{{.}}">{{if eq . ""}}—{{else}}{{.}}{{end}}</option>{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Sort field <input type="text" name="sort_field" placeholder="title / updated_at / start_time" maxlength="40"></label>
|
|
<label>Sort dir
|
|
<select name="sort_dir">
|
|
{{range .SortDirOptions}}<option value="{{.}}">{{if eq . ""}}—{{else}}{{.}}{{end}}</option>{{end}}
|
|
</select>
|
|
</label>
|
|
<label><input type="checkbox" name="pinned" value="1"> Pinned</label>
|
|
<label>Filter (URL query form, e.g. <code>tag=work&mgmt=mai</code>)
|
|
<input type="text" name="filter_query" placeholder="tag=work&mgmt=mai" value="{{.Prefill.filter}}">
|
|
</label>
|
|
<button type="submit">Create view</button>
|
|
</form>
|
|
</section>
|
|
{{end}}
|