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.
Put each repository tab's filters and search on one row
6c98a2711993b1fec51af785e9ad28ced0518bf3 · Keenan Nemetz · 9d ago · parent 20442b59
Put each repository tab's filters and search on one row The filter controls had grown into two rows on the pages that also had state chips: the chips sat above, the search and dropdowns below. Where the search sat also depended on the page, so moving from issues to changes to code changed the order of the same controls. Every filter row now reads left to right: chips and labelled dropdowns on the left, the search group on the right, with the clear action in the same right group on pages that have no text search. The chips moved inside the form on issues, changes and activity so a search keeps the chosen state or kind, and an htmx swap replaces the whole row with the values the server rendered. The row is one `.filters` element with a `.filters-search` group, so code search and the global search page use the same shape rather than a second one. On a phone the row stacks and the search stays on one line with its button.
server/ui_test.go +59
| @@ -847,3 +847,62 @@ func TestPasskeyScriptIsAStaticAsset(t *testing.T) { | |||
| 847 | 847 | t.Fatal("the page shell does not load the passkey script from /static/") | |
| 848 | 848 | } | |
| 849 | 849 | } | |
| 850 | |||
| 851 | // The filter row reads left to right on every surface: the chips and labelled | ||
| 852 | // dropdowns first, the search group last and against the right edge. A page with | ||
| 853 | // nothing to search puts its clear action in the same slot. | ||
| 854 | func TestFilterRowPutsFiltersBeforeSearch(t *testing.T) { | ||
| 855 | t.Parallel() | ||
| 856 | f := newFixture(t) | ||
| 857 | f.publish(f.repo) | ||
| 858 | work := workRepo(t, map[string]string{"README.md": "# row\n"}) | ||
| 859 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | ||
| 860 | t.Fatalf("push: %s", errs) | ||
| 861 | } | ||
| 862 | issues := f.issuesRepo(t, map[string]string{ | ||
| 863 | "issues/01M5JR2JK29KKMAA1T9MDNTPF1/issue.md": issueFile("Filter row", "keenan", "open", | ||
| 864 | "2026-10-22T21:00:00Z", []string{"bug"}, "body\n", "wilbur"), | ||
| 865 | }) | ||
| 866 | if _, errs, err := f.git(issues, "push", f.sshURL(f.repo), "HEAD:refs/heads/vole"); err != nil { | ||
| 867 | t.Fatalf("issues push: %s", errs) | ||
| 868 | } | ||
| 869 | |||
| 870 | for _, row := range []struct { | ||
| 871 | path string | ||
| 872 | chips bool | ||
| 873 | }{ | ||
| 874 | {"/r/" + f.repo + "/issues", true}, | ||
| 875 | {"/r/" + f.repo + "/requests", true}, | ||
| 876 | {"/r/" + f.repo + "/activity?actor=keenan", true}, | ||
| 877 | {"/r/" + f.repo + "/code", false}, | ||
| 878 | {"/r/" + f.repo + "/search", false}, | ||
| 879 | {"/search", false}, | ||
| 880 | } { | ||
| 881 | code, body := f.get(row.path) | ||
| 882 | if code != 200 { | ||
| 883 | t.Fatalf("%s = %d", row.path, code) | ||
| 884 | } | ||
| 885 | at := strings.Index(body, `class="filters"`) | ||
| 886 | if at < 0 { | ||
| 887 | t.Fatalf("%s has no filter row", row.path) | ||
| 888 | } | ||
| 889 | line := body[at:] | ||
| 890 | search := strings.Index(line, `class="filters-search"`) | ||
| 891 | if search < 0 { | ||
| 892 | t.Fatalf("%s has no right-aligned search group:\n%s", row.path, body) | ||
| 893 | } | ||
| 894 | if !strings.Contains(line[search:], `aria-label="Search`) && | ||
| 895 | !strings.Contains(line[search:], "Search</button>") && | ||
| 896 | !strings.Contains(line[search:], ">Clear</a>") { | ||
| 897 | t.Fatalf("%s's right group holds no search or clear action:\n%s", row.path, body) | ||
| 898 | } | ||
| 899 | for _, left := range []string{`class="chips"`, `<span class="filter-label">`} { | ||
| 900 | if i := strings.Index(line, left); i >= 0 && i > search { | ||
| 901 | t.Fatalf("%s puts %q after the search group:\n%s", row.path, left, body) | ||
| 902 | } | ||
| 903 | } | ||
| 904 | if row.chips && !strings.Contains(line[:search], `class="chips"`) { | ||
| 905 | t.Fatalf("%s does not keep its chips in the filter row:\n%s", row.path, body) | ||
| 906 | } | ||
| 907 | } | ||
| 908 | } | ||