Restores the /views URL family in the paliad shape m asked for:
GET /views → MRU 302 or onboarding shell
GET /views/{slug} → render saved view as its own page
GET /views/new → editor blank
GET /views/{slug}/edit → editor existing
POST /views → create
POST /views/{slug} → update
POST /views/{slug}/delete → delete
POST /views/reorder → drag-reorder hook (used in slice G)
Render path:
- handleViewRender resolves the slug against user views (slice C adds
system views), touches last_used_at fire-and-forget so the next /views
landing 302s here, then dispatches the same view_type renderers the
tree page uses (list / card / kanban). filter_json is decoded into a
TreeFilter + view_type + group_by; URL chip params overlay the saved
filter so chips narrow the view further without losing the saved
baseline. calendar / timeline view_types fall back to list in slice B;
slice D wires their dedicated templates.
Editor path:
- handleViewEditor renders templates/view_editor.tmpl, a minimal form
for slice B (slice D adds the live chip strip, slug auto-derivation,
and the icon registry). Pre-fills every persisted field on edit.
Templates:
- views_landing.tmpl — index card list + "+ new view" link.
- view_render.tmpl — header (name + slug + edit/delete) + tree-section
partial. Bundled with tree_section / tree_card / tree_kanban /
project_chip so the rendered view shares the dispatch chain.
- view_editor.tmpl — form for create + edit.
Encoding:
- encodeFilterToJSON canonicalises (filter_query, view_type) into the
filter_json shape. view_type lives INSIDE the JSON per m's Q2 pick.
- decodeViewSpec is the inverse — slice C's system-view code reuses it
to convert SystemView definitions into the same shape.
- overlayURLOntoSavedFilter mirrors the 5i fix-shift pattern: URL chip
values selectively override the saved baseline (q / tag / mgmt /
status / has / show-archived / public / project / project_descendants).
Error mapping:
- writeViewError translates the typed store errors (ErrViewSlugFormat /
Reserved / Taken / NotFound) into 400 / 409 with human-readable
banners. handlers map ErrViewNotFound to 404 directly.
Tests (HTTP integration):
- TestViewsLandingOnboarding — empty store → shell with "+ New view".
- TestViewsLandingMRURedirects — touched view triggers 302 to it.
- TestViewRenderShowsSavedView — name + slug + view_type=card grid.
- TestViewRender404OnUnknownSlug — unknown slug 404s, no silent
fall-back to tree.
- TestViewCreateAndDelete — POST /views creates; reserved slug 400s;
POST /views/<slug>/delete removes the row.
- TestSavedViewFilterOverlay — ?tag=work narrows the saved view; URL
chip values overlay the persisted filter.
54 lines
2.4 KiB
Cheetah
54 lines
2.4 KiB
Cheetah
{{define "content"}}
|
|
<h1>{{if .View}}Edit {{.View.Name}}{{else}}New view{{end}}</h1>
|
|
<p class="muted"><a href="/views">← back to views</a></p>
|
|
|
|
<form class="view-editor"
|
|
method="post"
|
|
action="{{if .View}}/views/{{.View.Slug}}{{else}}/views{{end}}">
|
|
<label>Name <input type="text" name="name" required maxlength="80" value="{{if .View}}{{.View.Name}}{{end}}"></label>
|
|
<label>Slug
|
|
<input type="text" name="slug" required maxlength="63"
|
|
pattern="^[a-z0-9][a-z0-9-]{0,62}$"
|
|
value="{{if .View}}{{.View.Slug}}{{end}}">
|
|
<small class="muted">lowercase letters, digits, dashes. No reserved system slugs.</small>
|
|
</label>
|
|
<label>Icon
|
|
<select name="icon">
|
|
{{$cur := ""}}
|
|
{{if and .View .View.Icon}}{{$cur = deref .View.Icon}}{{end}}
|
|
<option value="">— folder (default)</option>
|
|
<option value="clock" {{if eq $cur "clock"}}selected{{end}}>clock</option>
|
|
<option value="star" {{if eq $cur "star"}}selected{{end}}>star</option>
|
|
<option value="tag" {{if eq $cur "tag"}}selected{{end}}>tag</option>
|
|
<option value="inbox" {{if eq $cur "inbox"}}selected{{end}}>inbox</option>
|
|
<option value="box" {{if eq $cur "box"}}selected{{end}}>box</option>
|
|
<option value="file-text" {{if eq $cur "file-text"}}selected{{end}}>file-text</option>
|
|
</select>
|
|
</label>
|
|
<fieldset class="view-type-radios">
|
|
<legend>View type</legend>
|
|
{{range .ViewTypes}}
|
|
<label><input type="radio" name="view_type" value="{{.}}" {{if eq . $.CurrentVT}}checked{{end}}> {{.}}</label>
|
|
{{end}}
|
|
</fieldset>
|
|
<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" 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="show_count" value="1"
|
|
{{if and .View .View.ShowCount}}checked{{end}}> Show row-count badge in sidebar</label>
|
|
<label>Filter (URL query form)
|
|
<input type="text" name="filter_query" placeholder="tag=work&mgmt=mai" value="{{.FilterQuery}}">
|
|
</label>
|
|
<button type="submit">{{if .View}}Save changes{{else}}Create view{{end}}</button>
|
|
<a class="muted" href="/views">cancel</a>
|
|
</form>
|
|
{{end}}
|