feat(series): multi-field search, sort-direction toggle, apply placement
Toolbar feedback round: the series search box now matches whatever the main search matches for a group — series NAME, any book's AUTHOR, and any book's TITLE inside the series (WHERE EXISTS per group-row, so counts stay true); the separate author input is gone from the toolbar (the author= param stays live for the JSON API and deep links). The sort control becomes a base-key select (Date added/Name/Author/Books/ Progress/Last read) plus a chevron direction toggle that flips and resubmits; sort/dir compose through ResolveSeriesSort (full registry keys keep working for back-compat, unknown base keys fall to recent). The search/apply button moved next to the search box as requested, and default URLs stay clean (sort/dir only emitted once the user chooses).
This commit is contained in:
1 parent
14285ff314
commit
96c1228805
11 files changed
+504
-149
No files matched your search
@@ -164,13 +164,49 @@ func TestSeriesSortAndFilter(t *testing.T) {
|
||||
assert.Equal(t, []string{"Probe Zebra Series", "Probe Mango Series", "Probe Apple Series"}, renderedNames("&sort=bogus-key"))
|
||||
})
|
||||
|
||||
t.Run("base sort + direction toggle composes registry keys", func(t *testing.T) {
|
||||
// The toolbar sends base keys + dir; defaults apply when dir is
|
||||
// absent, and both directions of every base key are reachable.
|
||||
cases := []struct {
|
||||
query string
|
||||
want []string
|
||||
}{
|
||||
{"sort=name", []string{"Apple", "Mango", "Zebra"}}, // name asc default
|
||||
{"sort=name&dir=desc", []string{"Zebra", "Mango", "Apple"}}, // toggle flips
|
||||
{"sort=books&dir=asc", []string{"Apple", "Zebra", "Mango"}}, // books desc default, flipped
|
||||
{"sort=progress&dir=asc", []string{"Apple", "Zebra", "Mango"}}, // progress desc default, flipped
|
||||
{"sort=author", []string{"Mango", "Apple", "Zebra"}}, // author asc default
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := renderedNames("&" + tc.query)
|
||||
require.Len(t, got, 3, tc.query)
|
||||
for i, want := range tc.want {
|
||||
assert.Contains(t, got[i], want, "%s: position %d", tc.query, i)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("name query filters", func(t *testing.T) {
|
||||
assert.Equal(t, []string{"Probe Mango Series"}, renderedNames("&q=Mango"))
|
||||
// ILIKE is case-insensitive and substring-shaped.
|
||||
assert.Equal(t, []string{"Probe Mango Series"}, renderedNames("&q=mango"))
|
||||
})
|
||||
|
||||
t.Run("author query filters on any book's author", func(t *testing.T) {
|
||||
t.Run("multi-field query matches authors and book titles", func(t *testing.T) {
|
||||
// Author of Mango is "Alpha One" — the query no longer needs the
|
||||
// separate author box.
|
||||
assert.Equal(t, []string{"Probe Mango Series"}, renderedNames("&q=alpha+one"))
|
||||
// "Zebra Book 2" is a book TITLE inside the Zebra series: the
|
||||
// series must surface for a title match too.
|
||||
assert.Equal(t, []string{"Probe Zebra Series"}, renderedNames("&q=zebra+book+2"))
|
||||
// Nothing matches anywhere.
|
||||
assert.Empty(t, renderedNames("&q=nobody-nowhere"))
|
||||
})
|
||||
|
||||
t.Run("author param (API back-compat) filters on any book's author", func(t *testing.T) {
|
||||
// The toolbar no longer renders a separate author box (the
|
||||
// multi-field q covers authors), but the param stays live for
|
||||
// the JSON API and deep links.
|
||||
assert.Equal(t, []string{"Probe Mango Series"}, renderedNames("&author=alpha"))
|
||||
assert.Empty(t, renderedNames("&author=nobody"))
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user