Merge branch 'mai/kahn/phase-5j-views-redesign' (phase 5j slice E: sidebar Views section with user views)
This commit is contained in:
@@ -1039,6 +1039,17 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, dat
|
||||
if _, set := data["Path"]; !set {
|
||||
data["Path"] = r.URL.Path
|
||||
}
|
||||
// Phase 5j slice E: layout's "Views" sidebar section lists every
|
||||
// user view. Lookup is one indexed query per render — at m's scale
|
||||
// (≤30 saved views) the cost is negligible and dwarfed by the
|
||||
// dashboard/timeline aggregation cards. The login page bypasses the
|
||||
// layout entirely so we don't fetch for it; stub servers without a
|
||||
// configured store also skip cleanly.
|
||||
if _, set := data["UserViews"]; !set && name != "login" && s.Store != nil {
|
||||
if uv, err := s.Store.ListViews(r.Context()); err == nil {
|
||||
data["UserViews"] = uv
|
||||
}
|
||||
}
|
||||
entry := "layout"
|
||||
switch name {
|
||||
case "login":
|
||||
|
||||
@@ -1211,6 +1211,15 @@ html[data-sidebar-collapsed="true"] .projax-sidebar .brand-label {
|
||||
border-left: 2px solid var(--accent);
|
||||
padding-left: 14px;
|
||||
}
|
||||
/* Phase 5j slice E — Views sub-section: user-view entries sit below the
|
||||
main nav items, slightly indented + smaller, so the system rows stay
|
||||
visually anchored. The Views section header (the "Views" main entry)
|
||||
is unchanged; this just styles the per-saved-view rows. */
|
||||
.projax-sidebar .sidebar-user-views { display: flex; flex-direction: column; gap: 2px; padding: 4px 0; }
|
||||
.projax-sidebar .nav-item-user-view { font-size: 0.92em; padding-left: 24px; }
|
||||
.projax-sidebar .nav-item-user-view.active { padding-left: 22px; }
|
||||
.projax-sidebar .user-view-icon { width: 1em; text-align: center; }
|
||||
.projax-sidebar .nav-item-new-view { color: var(--muted); }
|
||||
.projax-sidebar .nav-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
|
||||
@@ -65,6 +65,40 @@ func TestLegacyRedirects(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSidebarListsUserViews — slice E: every chrome-bearing page renders
|
||||
// the saved-view list under the main nav. Each entry links to
|
||||
// /views/{slug} with the name as the label. Active state fires when the
|
||||
// current URL matches.
|
||||
func TestSidebarListsUserViews(t *testing.T) {
|
||||
srv, pool := mustServer(t)
|
||||
defer pool.Close()
|
||||
h := srv.Routes()
|
||||
ctx := context.Background()
|
||||
stamp := strings.ReplaceAll(time.Now().UTC().Format("150405.000"), ".", "")
|
||||
slug := "p5j-e-sidebar-" + stamp
|
||||
defer pool.Exec(context.Background(), `DELETE FROM projax.views WHERE slug = $1`, slug)
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO projax.views (slug, name, filter_json)
|
||||
VALUES ($1, 'P5jE Sidebar', '{"view_type":"list"}'::jsonb)`, slug); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
_, body := get(t, h, "/views/tree")
|
||||
if !strings.Contains(body, `href="/views/`+slug+`"`) {
|
||||
t.Error("sidebar should list saved view as /views/<slug>")
|
||||
}
|
||||
if !strings.Contains(body, "P5jE Sidebar") {
|
||||
t.Error("sidebar should show saved view's display name")
|
||||
}
|
||||
if !strings.Contains(body, `href="/views/new"`) {
|
||||
t.Error("sidebar Views section should include a + New view link")
|
||||
}
|
||||
// Active state when the URL matches.
|
||||
_, onView := get(t, h, "/views/"+slug)
|
||||
if !strings.Contains(onView, `class="nav-item nav-item-user-view active"`) {
|
||||
t.Error("user-view nav-item should carry .active when its URL is current")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLegacyViewUUIDRedirect — when a legacy URL carries the 5i overlay
|
||||
// `?view=<uuid>` param, the redirect resolves the uuid to the current
|
||||
// slug (per m's Q3 pick), so old bookmarks land on the right user view.
|
||||
|
||||
@@ -89,6 +89,24 @@
|
||||
</svg>
|
||||
<span class="nav-label">Views</span>
|
||||
</a>
|
||||
{{if .UserViews}}
|
||||
<div class="sidebar-user-views" aria-label="Saved views">
|
||||
{{range .UserViews}}
|
||||
<a href="/views/{{.Slug}}"
|
||||
class="nav-item nav-item-user-view{{if eq $path (printf "/views/%s" .Slug)}} active{{end}}"
|
||||
title="{{.Name}}">
|
||||
<span class="nav-icon user-view-icon" aria-hidden="true">
|
||||
{{if and .Icon (ne (deref .Icon) "")}}◆{{else}}◆{{end}}
|
||||
</span>
|
||||
<span class="nav-label">{{.Name}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
<a href="/views/new" class="nav-item nav-item-user-view nav-item-new-view" title="New view">
|
||||
<span class="nav-icon" aria-hidden="true">+</span>
|
||||
<span class="nav-label">New view</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<a href="/admin" class="nav-item{{if eq $path "/admin"}} active{{end}}" title="Admin">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
|
||||
Reference in New Issue
Block a user