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