feat(search): extended search, sort options, live tag filter, result count #183

Merged
marcel merged 26 commits from feat/issue-180-extended-search-sort into main 2026-04-06 19:18:12 +02:00

26 Commits

Author SHA1 Message Date
Marcel
a863f8baad docs(search): explain void sort/dir ESLint workaround in SearchFilterBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:50:52 +02:00
Marcel
1f86e6e238 fix(a11y): bump result count text to text-base (16px) for senior readability
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:50:00 +02:00
Marcel
c82bd61ad4 feat(a11y): fix SortDropdown accessibility — label, aria-label i18n, chevron
- Add sr-only <label> for the sort <select> (WCAG 1.3.1)
- Replace hardcoded German aria-label with Paraglide sort_dir_asc/desc keys
- Add custom SVG chevron overlay to restore visual dropdown indicator
  (appearance-none had removed the native browser arrow)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:49:06 +02:00
Marcel
56f7282a9d test(search): add empty-receivers edge case for RECEIVER sort
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:45:01 +02:00
Marcel
110024245d docs(search): document in-memory sort tradeoff and total=size() limitation
Add TODO comment explaining why SENDER/RECEIVER sort is in-memory
(JPA INNER JOIN drops null-sender docs) and note that pagination
will require a DB COUNT query in DocumentSearchResult.of().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:41:17 +02:00
Marcel
972048d57d fix(search): treat null sender.lastName as empty in sort key
A sender with lastName=null produced sort key "null Bob" which sorted
before names starting with lowercase letters (n < s, t, u, v...).
Now returns "" for null lastName, which the comparator places at end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:39:30 +02:00
Marcel
1c1ab0c72a feat(search): reject invalid dir parameter with 400
Previously any value other than ASC/DESC silently defaulted to
DESC with no feedback. Now returns 400 Bad Request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:34:38 +02:00
Marcel
6ac3f6b176 refactor(search): remove dead SENDER case from resolveSort switch
SENDER and RECEIVER are handled by in-memory sort before resolveSort
is called, making those switch cases unreachable. Removed and added
a comment making the invariant explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:31:39 +02:00
Marcel
12023513b2 refactor(search): move DocumentSort from model/ to dto/
DocumentSort is a query parameter enum, not a JPA entity.
Placing it in model/ violated the layer boundary — model/ should
contain only domain entities.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:29:35 +02:00
Marcel
79250fb705 fix(ui): fix SortDropdown height alignment — appearance-none + items-stretch
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:57:35 +02:00
Marcel
fc3496abb6 fix(ui): align SortDropdown styling with SearchFilterBar button style
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:55:55 +02:00
Marcel
0e13fd194b feat(search): show spinner in search input while navigation is in-flight
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:52:37 +02:00
Marcel
023b6ddb49 fix(search): tagQ alone now triggers search mode; selecting chip clears tagQ
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
- isDashboard was ignoring tagQ so typing in tag filter showed dashboard
- addTag now calls onTextInput('') to clear tagQ when a chip is selected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 15:46:54 +02:00
Marcel
bc397048b7 fix(search): use in-memory sort for SENDER to include documents with null sender
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
INNER JOIN from Sort.by("sender.lastName") was excluding docs without a sender.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:15:03 +02:00
Marcel
07dbe152e2 feat(search): show result count and term-aware empty state in DocumentList
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:03:16 +02:00
Marcel
78fdb01ec1 feat(search): wire sort/dir/tagQ state into page.svelte and URL params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:58:53 +02:00
Marcel
769937e03d feat(search): read sort/dir/tagQ from URL and unwrap DocumentSearchResult envelope
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:53:54 +02:00
Marcel
4fe10e1316 feat(search): add sort/dir/tagQ props to SearchFilterBar with SortDropdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:50:45 +02:00
Marcel
eeb78c98ec feat(search): add onTextInput callback to TagInput for live tag filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:42:08 +02:00
Marcel
aeed6e0dac feat(search): add SortDropdown component with direction toggle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:39:26 +02:00
Marcel
3f8f3cd938 feat(i18n): add sort, result count, and empty-state translation keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:35:46 +02:00
Marcel
2c0748d60e feat(utils): add debounce utility with full test coverage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:23:14 +02:00
Marcel
d1ad4d834c chore: regenerate API types with search envelope and new sort/tagQ params
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:21:41 +02:00
Marcel
879435c8d9 feat(search): wrap search response in { documents, total } envelope
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:17:08 +02:00
Marcel
c2b5008c66 feat(search): add sort param (DATE/TITLE/SENDER/RECEIVER/UPLOAD_DATE) and tagQ filter
- DocumentSort enum validated by Spring MVC (400 for unknown values)
- SENDER sort uses Spring Data Sort on sender.lastName/firstName
- RECEIVER sort uses in-memory sort by first receiver alphabetically
- UPLOAD_DATE sort uses createdAt; default sort is DATE DESC
- tagQ param wired to hasTagPartial specification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:13:06 +02:00
Marcel
beca2d463a feat(search): extend hasText to match sender/receiver/tag names, add hasTagPartial
- hasText now JOINs sender (LEFT JOIN) and uses EXISTS subqueries for
  receivers and tags to avoid duplicate rows
- hasTagPartial added for live debounced tag text filter (ILIKE partial match)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 13:07:39 +02:00