Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 repository search, list pagination, and theme/a11y polish

5cab7b18548e5263c01549cb61415fd7777d1758 · Keenan Nemetz · 15d ago · parent 0b26547e

Add repository search, list pagination, and theme/a11y polish

The chrome and the index page search repository names and descriptions after the same read filter, so a private repository cannot appear. Issues and changes paginate at 25 rows with filters and HTMX navigation preserved.

Polish from the UI plan: a light token block follows prefers-color-scheme, a skip link and focus-visible outlines help keyboard users, panels scroll horizontally on narrow screens, and the empty index/search states explain what is visible. Also fixes a flaky key assertion in the admin users test.

server/web/requests.go +24

@@ -4,6 +4,7 @@ import (
4 4 "fmt"
5 5 "html/template"
6 6 "net/http"
7 "net/url"
7 8 "sort"
8 9 "strconv"
9 10 "strings"
@@ -118,6 +119,13 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
118 119 }
119 120 return rows[i].ID > rows[j].ID
120 121 })
122 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
123 pg := newPager(len(rows), 25, page, func(n int) string {
124 return requestListURL(m.Name, state, query, n)
125 })
126 if pg.Total > 0 {
127 rows = rows[(pg.Page-1)*25 : pg.To]
128 }
121 129 form := w.requestForm(r, m)
122 130
123 131 acl, _ := m.ACL()
@@ -140,12 +148,14 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
140 148 Base string
141 149 BaseBranches []string
142 150 HeadBranches []string
151 Pager pager
143 152 }{
144 153 Page: w.page(r, m.Name+" · changes"), Name: m.Name, Rows: rows, State: state,
145 154 Query: query, Open: open, Total: len(list), Closed: len(list) - open,
146 155 Required: required, Filtered: query != "" || (state != "open" && len(list) > 0),
147 156 CanOpen: form.CanOpen, Base: form.Base,
148 157 BaseBranches: form.BaseBranches, HeadBranches: form.HeadBranches,
158 Pager: pg,
149 159 }
150 160 if htmxRequest(r) {
151 161 w.render(rw, "change_list", view, 0)
@@ -154,6 +164,20 @@ func (w *Web) Requests(rw http.ResponseWriter, r *http.Request) {
154 164 w.render(rw, "requests", view, 0)
155 165 }
156 166
167 func requestListURL(name, state, query string, page int) string {
168 v := url.Values{}
169 if state != "" {
170 v.Set("state", state)
171 }
172 if query != "" {
173 v.Set("q", query)
174 }
175 if page > 1 {
176 v.Set("page", strconv.Itoa(page))
177 }
178 return "/r/" + name + "/requests?" + v.Encode()
179 }
180
157 181 // NewChangePage serves the dedicated new-change form. The list page only links
158 182 // here when the viewer can open a request, and the handler checks again because
159 183 // a link is not an authorization.