Conversation
There was a problem hiding this comment.
Pull request overview
Adds tag autocomplete for the entry form by introducing a tag search API endpoint and wiring up client-side dropdown behavior/styling.
Changes:
- Add
GET /api/tags/search?q=to return matching tags (scoped to the authenticated user) as an HTML fragment. - Add vanilla JS autocomplete behavior to the entry form tags input (debounced fetch + keyboard/click interactions).
- Add CSS styling for the autocomplete dropdown; add integration tests for the new endpoint.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/routes/tags.rs |
Adds /api/tags/search route + handler that queries user-scoped tags and renders HTML buttons. |
templates/entries/form.html |
Wraps tags input and adds JS to fetch/render suggestions and support keyboard/click selection. |
static/style.css |
Adds dropdown and item styling for tag autocomplete UI. |
tests/tags.rs |
Adds integration tests covering auth, matching, empty results, and user scoping for tag search. |
docs/plans/2026-02-17-tag-autocomplete.md |
Documents the implementation plan and test plan for the feature. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let prefix = params.q.unwrap_or_default().trim().to_lowercase(); | ||
| if prefix.is_empty() { | ||
| return Ok(Html(String::new())); | ||
| } | ||
|
|
||
| let pattern = format!("{}%", prefix); | ||
| let tags: Vec<(String,)> = sqlx::query_as( |
There was a problem hiding this comment.
The LIKE pattern is built directly from user input, so % and _ in q act as wildcards (e.g., q=% returns arbitrary tags) and the behavior is no longer strict prefix matching. Escape %/_ in the prefix (and use ... LIKE ? ESCAPE '\\'), or switch to a safer prefix predicate if available for the chosen DB/collation.
| fetch('/api/tags/search?q=' + encodeURIComponent(segment.toLowerCase())) | ||
| .then(function(r) { return r.text(); }) | ||
| .then(function(html) { | ||
| var existing = getExistingTags(); |
There was a problem hiding this comment.
The fetch chain doesn’t handle non-OK responses or redirects (e.g., an expired session will redirect to /login, and the login HTML may be rendered inside the dropdown). Check r.ok/r.redirected (or r.status === 401 if you change the API behavior) and hide the dropdown / trigger a navigation to login instead of blindly rendering r.text().
| debounceTimer = setTimeout(function() { | ||
| fetch('/api/tags/search?q=' + encodeURIComponent(segment.toLowerCase())) | ||
| .then(function(r) { return r.text(); }) | ||
| .then(function(html) { |
There was a problem hiding this comment.
There’s a race condition between in-flight requests: a slower response for an older segment can arrive after a newer one and overwrite the dropdown with stale suggestions. Consider using AbortController to cancel the previous request, or track an incrementing request id and ignore responses that aren’t the latest.
Summary
GET /api/tags/search?q=endpoint that returns matching tags as HTML buttons, scoped to the authenticated userTest Plan