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.

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 }