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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Turn the user directory into a sortable table

4739664b94c4321b83a2650a3a2486ec090aed64 · Keenan Nemetz · 8d ago · parent c14588ef

Turn the user directory into a sortable table

The list became a table with the same counts, and the column headers
order it: user, repositories, issues, changes, reviews, joining date.
Counts start largest-first and the name A to Z; clicking the active
header flips it. The active column carries aria-sort and an arrow.

The search, the status chips (everyone, active, no longer active), the
order and the page all live in the URL and are carried into every link,
so a view can be bookmarked and paging cannot change the question. It is
all server-side, so it works without JavaScript.

Sorting by a count needs every matched account's summary before the page
is cut, so the index now answers for the whole directory in one grouped
query — ActorSummaries — instead of a lookup for a page of names. That
also drops the per-request name list, so a large roster cannot run into
SQLite's bound-variable limit.

Retired accounts are marked in their row and can be filtered to rather
than living in a separate group, and a roster larger than one page is
covered by a test.

server/web/templates/people.html +46−48

@@ -7,63 +7,61 @@
7 7 </div>
8 8 </div>
9 9
10 <form class="filters" method="get" action="/u">
10 {{/* The search, the status and the order are one row: the chips are links that
11 keep the search, and the form carries the status and order so a search does
12 not reset them. Everything is server-side, so a view can be linked. */}}
13 <form class="filters" method="get" action="/u" role="search">
14 <div class="chips">
15 {{range .Filters}}<a class="chip{{if .Active}} active{{end}}" href="{{.Href}}">{{.Label}}</a>{{end}}
16 </div>
17 {{if .Status}}<input type="hidden" name="status" value="{{.Status}}">{{end}}
18 {{if .Sort}}<input type="hidden" name="sort" value="{{.Sort}}"><input type="hidden" name="dir" value="{{.Dir}}">{{end}}
11 19 <div class="filters-search">
12 <input type="search" name="q" value="{{.Query}}" placeholder="Find a name" aria-label="Find a name" autocomplete="off">
13 <button class="secondary" type="submit">{{icon "search"}}Search</button>
20 <input type="search" name="q" value="{{.Query}}" placeholder="Search names" aria-label="Search names" autocomplete="off">
21 <button type="submit">{{icon "search"}}Search</button>
22 {{if .Filtered}}<a class="button secondary" href="/u">{{icon "x"}}Clear</a>{{end}}
14 23 </div>
15 {{if .Query}}<div class="filters-search"><a class="button secondary" href="/u">{{icon "x"}}Clear</a></div>{{end}}
16 24 </form>
17 25
18 {{if .Query}}<p class="muted">{{.Total}} {{if eq .Total 1}}person matches{{else}}people match{{end}} “{{.Query}}”.</p>{{end}}
19
20 {{if or .Rows .Retired}}
21 <p class="muted">Counts only include repositories you can read.</p>
22 {{end}}
23
24 26 {{if .Rows}}
25 <ul class="people-list">
26 {{range .Rows}}
27 <li class="people-row">
28 <div class="people-body">
29 <a class="people-name" href="/u/{{.Name}}">{{.Name}}</a>
30 <span class="people-summary">{{if .Summary.Repos}}{{.Summary.Repos}} {{if eq .Summary.Repos 1}}repository{{else}}repositories{{end}}{{if .Summary.Issues}} · {{.Summary.Issues}} {{if eq .Summary.Issues 1}}issue{{else}}issues{{end}}{{end}}{{if .Summary.Changes}} · {{.Summary.Changes}} {{if eq .Summary.Changes 1}}change{{else}}changes{{end}}{{end}}{{if .Summary.Reviews}} · {{.Summary.Reviews}} {{if eq .Summary.Reviews 1}}review{{else}}reviews{{end}}{{end}}{{else}}Nothing you can see yet{{end}}</span>
31 </div>
32 {{if and $.InstanceAdmin (ne .Role "none")}}<span class="label">{{.Role}}</span>{{end}}
33 <span class="muted people-joined">joined {{.Joined.Format "Jan 2006"}}</span>
34 </li>
35 {{end}}
36 </ul>
37 {{end}}
38
39 {{if .Retired}}
40 <div class="section-head"><h2>No longer active</h2></div>
41 <ul class="people-list people-retired">
42 {{range .Retired}}
43 <li class="people-row">
44 <div class="people-body">
45 <a class="people-name" href="/u/{{.Name}}">{{.Name}}</a>
46 <span class="people-summary">This account is no longer active; its history is kept.</span>
47 </div>
48 <span class="muted people-joined">joined {{.Joined.Format "Jan 2006"}}</span>
49 </li>
50 {{end}}
51 </ul>
52 {{end}}
53
54 {{if not (or .Rows .Retired)}}
27 <p class="muted user-scope">Counts only include repositories you can read.</p>
28 <div class="panel">
29 <table class="user-list">
30 <thead>
31 <tr>
32 {{range .Columns}}
33 <th{{if .Numeric}} class="count"{{end}}{{if .Active}} aria-sort="{{if .Desc}}descending{{else}}ascending{{end}}"{{end}}><a href="{{.Href}}">{{.Label}}{{if .Active}}<span class="sort-mark" aria-hidden="true">{{if .Desc}}↓{{else}}↑{{end}}</span>{{end}}</a></th>
34 {{end}}
35 {{if .ShowRole}}<th>Role</th>{{end}}
36 </tr>
37 </thead>
38 <tbody>
39 {{range .Rows}}
40 <tr>
41 <td><a class="people-name" href="/u/{{.Name}}">{{.Name}}</a>{{if not .Active}} <span class="state retired">retired</span>{{end}}</td>
42 <td class="count">{{.Summary.Repos}}</td>
43 <td class="count">{{.Summary.Issues}}</td>
44 <td class="count">{{.Summary.Changes}}</td>
45 <td class="count">{{.Summary.Reviews}}</td>
46 <td class="muted nowrap">{{.Joined.Format "Jan 2006"}}</td>
47 {{if $.ShowRole}}<td>{{if ne .Role "none"}}<span class="label">{{.Role}}</span>{{else}}<span class="muted">—</span>{{end}}</td>{{end}}
48 </tr>
49 {{end}}
50 </tbody>
51 </table>
52 </div>
53 {{template "pager" .Pager}}
54 {{else if .Filtered}}
55 <div class="empty-state">
56 <p>No one matches this view.</p>
57 {{if .Query}}<p class="muted">No account name contains “{{.Query}}”.</p>{{else}}<p class="muted">No accounts have that status.</p>{{end}}
58 <p><a class="button secondary" href="/u">{{icon "x"}}Show everyone</a></p>
59 </div>
60 {{else}}
55 61 <div class="empty-state">
56 {{if .Filtered}}
57 <p>No one matches “{{.Query}}”.</p>
58 <p class="muted">Search looks at account names only.</p>
59 <p><a class="button secondary" href="/u">{{icon "x"}}Clear the search</a></p>
60 {{else}}
61 62 <p>No accounts yet.</p>
62 63 <p class="muted">An instance admin creates the first one.</p>
63 {{end}}
64 64 </div>
65 65 {{end}}
66
67 {{template "pager" .Pager}}
68 66 {{template "footer" .}}
69 67 {{end}}