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/issues.go +31
| @@ -4,8 +4,10 @@ import ( | |||
| 4 | 4 | "bytes" | |
| 5 | 5 | "html/template" | |
| 6 | 6 | "net/http" | |
| 7 | "net/url" | ||
| 7 | 8 | "regexp" | |
| 8 | 9 | "sort" | |
| 10 | "strconv" | ||
| 9 | 11 | "strings" | |
| 10 | 12 | ||
| 11 | 13 | "github.com/microcosm-cc/bluemonday" | |
| @@ -112,6 +114,13 @@ func (w *Web) Issues(rw http.ResponseWriter, r *http.Request) { | |||
| 112 | 114 | } | |
| 113 | 115 | return rows[i].ID > rows[j].ID | |
| 114 | 116 | }) | |
| 117 | page, _ := strconv.Atoi(r.URL.Query().Get("page")) | ||
| 118 | pg := newPager(len(rows), 25, page, func(n int) string { | ||
| 119 | return issueListURL(m.Name, state, filter.Query, filter.Label, filter.Assignee, n) | ||
| 120 | }) | ||
| 121 | if pg.Total > 0 { | ||
| 122 | rows = rows[(pg.Page-1)*25 : pg.To] | ||
| 123 | } | ||
| 115 | 124 | labels, err := w.Index.Labels(m.ID) | |
| 116 | 125 | if err != nil { | |
| 117 | 126 | labels = nil | |
| @@ -134,6 +143,7 @@ func (w *Web) Issues(rw http.ResponseWriter, r *http.Request) { | |||
| 134 | 143 | Closed int | |
| 135 | 144 | Filtered bool | |
| 136 | 145 | CanDiscuss bool | |
| 146 | Pager pager | ||
| 137 | 147 | }{ | |
| 138 | 148 | Page: w.page(r, m.Name+" · issues"), | |
| 139 | 149 | Name: m.Name, | |
| @@ -147,6 +157,7 @@ func (w *Web) Issues(rw http.ResponseWriter, r *http.Request) { | |||
| 147 | 157 | Closed: len(all) - open, | |
| 148 | 158 | Filtered: filter.Label != "" || filter.Assignee != "" || filter.Query != "" || (state != "open" && len(all) > 0), | |
| 149 | 159 | CanDiscuss: acl != nil && w.canDiscuss(r, acl), | |
| 160 | Pager: pg, | ||
| 150 | 161 | } | |
| 151 | 162 | if htmxRequest(r) { | |
| 152 | 163 | w.render(rw, "issue_list", view, 0) | |
| @@ -155,6 +166,26 @@ func (w *Web) Issues(rw http.ResponseWriter, r *http.Request) { | |||
| 155 | 166 | w.render(rw, "issues", view, 0) | |
| 156 | 167 | } | |
| 157 | 168 | ||
| 169 | func issueListURL(name, state, query, label, assignee string, page int) string { | ||
| 170 | v := url.Values{} | ||
| 171 | if state != "" { | ||
| 172 | v.Set("state", state) | ||
| 173 | } | ||
| 174 | if query != "" { | ||
| 175 | v.Set("q", query) | ||
| 176 | } | ||
| 177 | if label != "" { | ||
| 178 | v.Set("label", label) | ||
| 179 | } | ||
| 180 | if assignee != "" { | ||
| 181 | v.Set("assignee", assignee) | ||
| 182 | } | ||
| 183 | if page > 1 { | ||
| 184 | v.Set("page", strconv.Itoa(page)) | ||
| 185 | } | ||
| 186 | return "/r/" + name + "/issues?" + v.Encode() | ||
| 187 | } | ||
| 188 | |||
| 158 | 189 | // issueCommentView is a comment with its body already rendered through the | |
| 159 | 190 | // markdown boundary. | |
| 160 | 191 | type issueCommentView struct { | |