vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Add a Projects link to the global bar
cd71b776a0d2446a3b432e88289f683ad2c17fbd · Keenan Nemetz · 13d ago · parent 13cff33f
Add a Projects link to the global bar The bar offered Activity (and Accounts to an instance admin) and nothing that said "the repository list". The landing page was reachable only through the emblem, which links there without saying so, and a mark is not a label: a text destination is what makes the bar navigable. Projects points at the landing page and marks itself current wherever the repository list is the context — the landing page and a repository page, which is the same global destination pageSection already assigns to "repos". The glyph is Lucide's grid-2x2 rather than the layout-dashboard already used for the two "Overview" subnavs, so the global bar does not reuse a tab's icon.
server/nav_test.go +48
| @@ -0,0 +1,48 @@ | |||
| 1 | package server_test | ||
| 2 | |||
| 3 | // The global bar: which destinations it offers, and which one it says you are on. | ||
| 4 | |||
| 5 | import ( | ||
| 6 | "net/http" | ||
| 7 | "strings" | ||
| 8 | "testing" | ||
| 9 | ) | ||
| 10 | |||
| 11 | // TestProjectsIsTheWayHome: the bar needs a text destination for the landing page. | ||
| 12 | // The emblem links there too, but a mark is not a label, and "Projects" is what | ||
| 13 | // the landing page is a list of. It marks itself current wherever the repository | ||
| 14 | // list is the context: the landing page and a repository page alike. | ||
| 15 | func TestProjectsIsTheWayHome(t *testing.T) { | ||
| 16 | t.Parallel() | ||
| 17 | f := newFixture(t) | ||
| 18 | f.publish(f.repo) | ||
| 19 | |||
| 20 | for _, path := range []string{"/", "/r/" + f.repo} { | ||
| 21 | code, body := f.get(path) | ||
| 22 | if code != http.StatusOK { | ||
| 23 | t.Fatalf("%s = %d", path, code) | ||
| 24 | } | ||
| 25 | if !strings.Contains(body, ">Projects</a>") { | ||
| 26 | t.Fatalf("%s has no Projects link:\n%s", path, body) | ||
| 27 | } | ||
| 28 | if !strings.Contains(body, `class="active" href="/" aria-current="page"`) { | ||
| 29 | t.Fatalf("%s does not mark Projects current:\n%s", path, body) | ||
| 30 | } | ||
| 31 | if !strings.Contains(body, `class="icon"`) { | ||
| 32 | t.Fatalf("%s renders the Projects link without an icon", path) | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | // Activity is a different destination, so Projects stops being current — and | ||
| 37 | // Activity takes over, which is what makes the bar say where you are. | ||
| 38 | code, body := f.get("/activity") | ||
| 39 | if code != http.StatusOK { | ||
| 40 | t.Fatalf("/activity = %d", code) | ||
| 41 | } | ||
| 42 | if strings.Contains(body, `class="active" href="/"`) { | ||
| 43 | t.Fatalf("/activity marks Projects current:\n%s", body) | ||
| 44 | } | ||
| 45 | if !strings.Contains(body, `class="active" href="/activity"`) { | ||
| 46 | t.Fatalf("/activity does not mark Activity current:\n%s", body) | ||
| 47 | } | ||
| 48 | } | ||