Files
projax/web/templates/view_editor.tmpl
mAi e305f0e0ae feat(views): Phase 5j slice B — paliad-shape route family + render
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.
2026-05-29 11:47:33 +02:00

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}}