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/web.go +61−4

@@ -48,7 +48,7 @@ type Web struct {
48 48 // index, queue, LFS namespace, and locks. The web owns the form and ACL check.
49 49 RenameRepo func(oldName, newName string) error
50 50 DeleteRepo func(name string) error
51 Logger func(format string, args ...any)
51 Logger func(format string, args ...any)
52 52
53 53 // sem bounds compressed archive streams; see releases.go.
54 54 sem chan struct{}
@@ -159,6 +159,9 @@ type Page struct {
159 159 // InstanceAdmin reports whether the viewer may use the instance-wide admin
160 160 // surface. The global bar uses it to show Admin.
161 161 InstanceAdmin bool
162 // Query is the repository search on the index page. The global search box
163 // echoes it; other pages leave it empty.
164 Query string
162 165 }
163 166
164 167 // canRead reports whether the viewer may see the repository. An anonymous
@@ -222,20 +225,31 @@ func (w *Web) IndexPage(rw http.ResponseWriter, r *http.Request) {
222 225 w.serverError(rw, r, "index unavailable: "+err.Error())
223 226 return
224 227 }
228 query := strings.TrimSpace(r.URL.Query().Get("q"))
229 needle := strings.ToLower(query)
225 230 var visible []index.Entry
226 231 for _, e := range entries {
227 232 m, err := w.Repos.Open(e.Name)
228 233 if err != nil {
229 234 continue
230 235 }
231 if w.canRead(r, m) {
232 visible = append(visible, e)
236 if !w.canRead(r, m) {
237 continue
238 }
239 if needle != "" &&
240 !strings.Contains(strings.ToLower(e.Name), needle) &&
241 !strings.Contains(strings.ToLower(e.Description), needle) {
242 continue
233 243 }
244 visible = append(visible, e)
234 245 }
246 page := w.page(r, "Repositories")
247 page.Query = query
235 248 w.render(rw, "index", struct {
236 249 Page
237 250 Repos []index.Entry
238 }{w.page(r, "Repositories"), visible}, 0)
251 Query string
252 }{page, visible, query}, 0)
239 253 }
240 254
241 255 // open resolves a repository for the viewer, answering 404 when it is missing
@@ -562,3 +576,46 @@ func isMarkdown(path string) bool {
562 576 }
563 577 return false
564 578 }
579
580 // pager is the shared shape for a list split into pages. urlFor builds the URL
581 // for a page number so the handler keeps control of which filters survive.
582 type pager struct {
583 Page int
584 Pages int
585 Total int
586 From int
587 To int
588 PrevURL string
589 NextURL string
590 }
591
592 func newPager(total, per, page int, urlFor func(int) string) pager {
593 if per < 1 {
594 per = 1
595 }
596 pages := (total + per - 1) / per
597 if pages < 1 {
598 pages = 1
599 }
600 if page < 1 {
601 page = 1
602 }
603 if page > pages {
604 page = pages
605 }
606 p := pager{Page: page, Pages: pages, Total: total}
607 if total > 0 {
608 p.From = (page-1)*per + 1
609 p.To = page * per
610 if p.To > total {
611 p.To = total
612 }
613 }
614 if page > 1 {
615 p.PrevURL = urlFor(page - 1)
616 }
617 if page < pages {
618 p.NextURL = urlFor(page + 1)
619 }
620 return p
621 }