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 · 10d 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/web/templates/issues.html +13−9

@@ -16,16 +16,16 @@
16 16
17 17 {{define "issue_list"}}
18 18 <div id="issue-list">
19 <div class="chips" hx-boost="true" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
20 <a class="chip{{if eq .State "open"}} active{{end}}" href="/r/{{.Name}}/issues?state=open{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Open <span class="chip-count">{{.Open}}</span></a>
21 <a class="chip{{if eq .State "closed"}} active{{end}}" href="/r/{{.Name}}/issues?state=closed{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Closed <span class="chip-count">{{.Closed}}</span></a>
22 <a class="chip{{if eq .State "all"}} active{{end}}" href="/r/{{.Name}}/issues?state=all{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">All <span class="chip-count">{{.Total}}</span></a>
23 </div>
24
19 {{/* One row: the state chips and the dropdowns on the left, the search field on
20 the right. The form wraps the chips so a search submits the state too, and
21 so a swap replaces the row with the values the server rendered. */}}
25 22 <form method="get" action="/r/{{.Name}}/issues" class="filters" role="search" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
23 <div class="chips" hx-boost="true" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">
24 <a class="chip{{if eq .State "open"}} active{{end}}" href="/r/{{.Name}}/issues?state=open{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Open <span class="chip-count">{{.Open}}</span></a>
25 <a class="chip{{if eq .State "closed"}} active{{end}}" href="/r/{{.Name}}/issues?state=closed{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">Closed <span class="chip-count">{{.Closed}}</span></a>
26 <a class="chip{{if eq .State "all"}} active{{end}}" href="/r/{{.Name}}/issues?state=all{{if .Filter.Query}}&amp;q={{urlquery .Filter.Query}}{{end}}{{if .Filter.Label}}&amp;label={{urlquery .Filter.Label}}{{end}}{{if .Filter.Assignee}}&amp;assignee={{urlquery .Filter.Assignee}}{{end}}">All <span class="chip-count">{{.Total}}</span></a>
27 </div>
26 28 <input type="hidden" name="state" value="{{.State}}">
27 <input name="q" type="search" placeholder="Search title and body" aria-label="Search title and body" value="{{.Filter.Query}}">
28 <button type="submit">{{icon "search"}}Search</button>
29 29 {{if .Labels}}
30 30 <label class="filter-field">
31 31 <span class="filter-label">Label</span>
@@ -44,7 +44,11 @@
44 44 </select>
45 45 </label>
46 46 {{end}}
47 {{if .Filtered}}<a class="button secondary" href="/r/{{.Name}}/issues" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">{{icon "x"}}Clear</a>{{end}}
47 <div class="filters-search">
48 <input name="q" type="search" placeholder="Search title and body" aria-label="Search title and body" value="{{.Filter.Query}}">
49 <button type="submit">{{icon "search"}}Search</button>
50 {{if .Filtered}}<a class="button secondary" href="/r/{{.Name}}/issues" hx-get="/r/{{.Name}}/issues" hx-target="#issue-list" hx-swap="outerHTML" hx-push-url="true">{{icon "x"}}Clear</a>{{end}}
51 </div>
48 52 </form>
49 53
50 54 {{if .Issues}}