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 · 7d 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/static/vole.css +10−18
| @@ -2888,26 +2888,18 @@ textarea:focus-visible { | |||
| 2888 | 2888 | .profile-repo { gap: 4px; } | |
| 2889 | 2889 | .profile-repo-meta { display: flex; flex-wrap: wrap; gap: 4px 10px; margin: 0; color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); } | |
| 2890 | 2890 | ||
| 2891 | /* One row per account. A list rather than a table: a row says what someone has | ||
| 2892 | been working on in a sentence, and a name is the only thing worth a column. */ | ||
| 2893 | .people-list { list-style: none; margin: 10px 0 0; padding: 0; } | ||
| 2894 | .people-row { | ||
| 2895 | display: flex; | ||
| 2896 | align-items: center; | ||
| 2897 | gap: 11px; | ||
| 2898 | padding: 10px 2px; | ||
| 2899 | border-bottom: 1px solid var(--border); | ||
| 2900 | min-width: 0; | ||
| 2901 | } | ||
| 2902 | .people-row:last-child { border-bottom: 0; } | ||
| 2903 | .people-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; } | ||
| 2891 | /* The user directory is a table: one row per account, sortable headers, and | ||
| 2892 | counts right-aligned so a column can be scanned. */ | ||
| 2904 | 2893 | .people-name { font-weight: 600; } | |
| 2905 | .people-summary { color: var(--fg-muted); font-size: calc(12.65px * var(--font-scale)); } | ||
| 2906 | .people-joined { font-size: calc(12.65px * var(--font-scale)); white-space: nowrap; } | ||
| 2907 | .people-retired .people-name { color: var(--fg-muted); } | ||
| 2908 | .people-row .label { flex: 0 0 auto; } | ||
| 2894 | .user-scope { margin: 0 0 10px; font-size: calc(12.65px * var(--font-scale)); } | ||
| 2895 | .user-list th a { color: inherit; } | ||
| 2896 | .user-list th a:hover { color: var(--accent); text-decoration: none; } | ||
| 2897 | .user-list .sort-mark { margin-left: 4px; color: var(--accent); } | ||
| 2898 | .user-list th.count, | ||
| 2899 | .user-list td.count { text-align: right; font-variant-numeric: tabular-nums; } | ||
| 2900 | .user-list td.count { color: var(--fg-muted); } | ||
| 2901 | .user-list .state { margin-left: 4px; vertical-align: 1px; } | ||
| 2909 | 2902 | ||
| 2910 | 2903 | @media (max-width: 720px) { | |
| 2911 | .people-joined { display: none; } | ||
| 2912 | 2904 | .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } | |
| 2913 | 2905 | } | |