diff --git a/web/server.go b/web/server.go index b5fa3aa..1595472 100644 --- a/web/server.go +++ b/web/server.go @@ -1008,6 +1008,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": diff --git a/web/static/style.css b/web/static/style.css index 7f96439..2fd7898 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -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; diff --git a/web/system_views_test.go b/web/system_views_test.go index 6f4a738..d236336 100644 --- a/web/system_views_test.go +++ b/web/system_views_test.go @@ -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/") + } + 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=` param, the redirect resolves the uuid to the current // slug (per m's Q3 pick), so old bookmarks land on the right user view. diff --git a/web/templates/layout.tmpl b/web/templates/layout.tmpl index ee78ebf..b153f70 100644 --- a/web/templates/layout.tmpl +++ b/web/templates/layout.tmpl @@ -89,6 +89,24 @@ Views + {{if .UserViews}} + + {{end}}