Offer a spelling on every list, not just the global search
Some checks failed
CI / backend (push) Failing after 7m15s
CI / frontend (push) Has been cancelled
CI / migrations-mysql (push) Has been cancelled
CI / naming (push) Has been cancelled

Correction only helped on the global search page, and people search from the
list they are already on. Extending it turned out to be a shape question rather
than a volume one: thirty routes take a search parameter across a dozen files,
so carrying a suggestion in each of their responses is a large change today and
one more thing every future plugin author has to remember.

So the suggestion moved to ITS OWN ENDPOINT, /api/search/suggest, which any page
can call after rendering no rows. A page that has not adopted it shows nothing,
which is exactly what it showed before - nothing breaks by omission.

The plumbing lives in useListQuery, which already owned the search term, so a
list needs three lines: take `suggestion` and `reportCount` from the composable,
call reportCount(rows.length) after a fetch, and drop SearchSuggestion into the
empty state it already has. A list that never calls reportCount never offers a
suggestion.

Wired: global search, machines, printers, PCs, network devices, measuring tools,
knowledge base, vendors. NOT wired, deliberately: the type and reference lists
(machine types, PC types, VLANs, subnets, operating systems and the rest), which
are small controlled vocabularies nobody typo-searches, and USB, whose empty
state has a different shape and wants doing by hand rather than by pattern.

TRAP FOUND WHILE WIRING IT, and left commented in every page: applying a
suggestion by calling setSearch alone updates the box and the URL and does NOT
reload the list. setSearch only syncs the URL, and the watcher that would reload
is suppressed because search.value already holds the new term - the same trap
the global search page documents in performSearch. Each page calls its own load
function directly.

The composable guards a stale answer arriving after a newer search was typed,
never offers back the word that was typed, and swallows its own errors: a search
that found nothing is already the answer, and failing to improve on it is not
worth an error in front of anyone.

The route-parity gate caught the new endpoint being served without an entry in
docs/api-inventory.json, which is hand-written on purpose; added, and the spec
regenerated from it (283 paths, 418 operations). That regeneration also carries
the openapi version to 0.12.0, left over from the release.
This commit is contained in:
cproudlock
2026-08-21 11:14:56 -04:00
parent 1abb6430f5
commit 5001aedcf9
15 changed files with 330 additions and 9 deletions

View File

@@ -160,3 +160,32 @@ def test_a_mistyped_asset_number_is_not_corrected_to_a_real_one(
data = resp.get_json()['data']
assert data['results'] == []
assert 'suggestion' not in data
def test_the_suggest_endpoint_serves_any_page(client, db, auth_headers):
"""The shared endpoint every list uses, rather than a suggestion key added
to thirty search routes."""
from shopdb.core.models import Vendor
from shopdb.extensions import cache
db.session.add(Vendor(vendor='Keyence'))
db.session.commit()
cache.delete('search_vocabulary')
resp = client.get('/api/search/suggest?q=keyecne', headers=auth_headers)
assert resp.status_code == 200
assert resp.get_json()['data']['suggestion'] == 'keyence'
def test_the_suggest_endpoint_says_null_rather_than_guessing(client, db,
auth_headers):
from shopdb.extensions import cache
cache.delete('search_vocabulary')
resp = client.get('/api/search/suggest?q=helicopter', headers=auth_headers)
assert resp.get_json()['data']['suggestion'] is None
def test_the_suggest_endpoint_ignores_a_too_short_query(client, auth_headers):
resp = client.get('/api/search/suggest?q=c', headers=auth_headers)
assert resp.status_code == 200
assert resp.get_json()['data']['suggestion'] is None