diff --git a/PROGRESS.md b/PROGRESS.md index 129a87a..35f73ac 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -21,7 +21,7 @@ - Transactional emails (order confirmation, shipping notification) - Demo content polished and ready for production -**Tier 1 MVP complete.** CI pipeline done. Hosting & deployment done (including observability). PageSpeed CI done (99-100 mobile, 97+ desktop). Usability fixes 15/18 done (remaining 3 are now tracked as features below). +**Tier 1 MVP complete.** CI pipeline done. Hosting & deployment done (including observability). PageSpeed CI done (99-100 mobile, 97+ desktop). Usability fixes 16/18 done (remaining 2 are now tracked as features below). ## Task list @@ -60,9 +60,9 @@ Plans: [admin-redesign.md](docs/plans/admin-redesign.md) | [setup-wizard.md](doc **Total remaining: ~27-33 hours across ~12 sessions** -## Usability fixes (15/18 done) +## Usability fixes (16/18 done) -Issues from hands-on testing of the deployed prod site (Feb 2025). 15 of 18 complete. The remaining 3 are tracked as features in the task list above (#5 search, #16 variant refinement, #18 shipping costs). +Issues from hands-on testing of the deployed prod site (Feb 2025). 16 of 18 complete. The remaining 2 are tracked as features in the task list above (#16 variant refinement, #18 shipping costs). ## Roadmap @@ -296,9 +296,13 @@ All shop pages now have LiveView integration tests (612 total): - [x] Index auto-rebuilds after each provider sync - [x] 18 search tests, 744 total -**Follow-ups:** +**Follow-ups (all complete):** - [x] Wire shop LiveViews to direct DB queries (PreviewData removed from all shop pages, cart, error page) -- [ ] Keyboard navigation in search modal (up/down arrows, Enter to navigate) +- [x] Search modal keyboard nav (arrow keys, Enter, Escape, Cmd+K shortcut) +- [x] Full ARIA combobox pattern (role=combobox, listbox, option, aria-selected) +- [x] SearchModal JS hook, `<.link navigate>` for client-side nav, 150ms debounce +- [x] search.ex: transaction safety on reindex, public `remove_product/1` +- [x] 10 new integration tests, 755 total ### Page Editor **Status:** Future (Tier 4) @@ -313,6 +317,7 @@ See: [docs/plans/page-builder.md](docs/plans/page-builder.md) for design | Feature | Commit | Notes | |---------|--------|-------| +| DB wiring + search UX | 57c3ba0 | Shop LiveViews use DB queries, search keyboard nav, ARIA, 755 tests | | FTS5 search + products refactor | 037cd16 | FTS5 index, BM25 ranking, search modal, denormalized fields, Product struct usage, 744 tests | | PageSpeed CI | 516d0d0 | `mix lighthouse` task, prod asset build, gzip, 99-100 mobile scores | | Observability | eaa4bbb | LiveDashboard in prod, ErrorTracker, JSON logging, Oban/LV metrics, os_mon | diff --git a/docs/plans/admin-redesign.md b/docs/plans/admin-redesign.md index 15adce0..1a82ebf 100644 --- a/docs/plans/admin-redesign.md +++ b/docs/plans/admin-redesign.md @@ -1,6 +1,6 @@ # Plan: Admin area redesign -Status: Pending +Status: Complete ## Overview diff --git a/docs/plans/products-context.md b/docs/plans/products-context.md index 0df2eea..05f1a0a 100644 --- a/docs/plans/products-context.md +++ b/docs/plans/products-context.md @@ -1,6 +1,6 @@ # Plan: Products Context with Provider Integration -> **Status:** Phase 1 Complete (c5c06d9) - See [PROGRESS.md](../../PROGRESS.md) for current status. +> **Status:** Complete (c5c06d9, 037cd16, 57c3ba0) - See [PROGRESS.md](../../PROGRESS.md) for current status. ## Goal Build a Products context that syncs products from external POD providers (Printify first), stores them locally, and enables order submission for fulfillment. diff --git a/docs/plans/search.md b/docs/plans/search.md index 31fea03..de83308 100644 --- a/docs/plans/search.md +++ b/docs/plans/search.md @@ -1,6 +1,6 @@ # Plan: Implement product search in search modal -Status: Pending (after usability fixes) +Status: Complete (037cd16, 57c3ba0) ## Overview diff --git a/docs/plans/setup-wizard.md b/docs/plans/setup-wizard.md index 7be776e..c070dc6 100644 --- a/docs/plans/setup-wizard.md +++ b/docs/plans/setup-wizard.md @@ -1,6 +1,6 @@ # Plan: Setup wizard and "go live" gate -Status: Pending +Status: Complete ## Overview diff --git a/lib/simpleshop_theme/search.ex b/lib/simpleshop_theme/search.ex index 02b5ac8..fa400a5 100644 --- a/lib/simpleshop_theme/search.ex +++ b/lib/simpleshop_theme/search.ex @@ -27,7 +27,11 @@ defmodule SimpleshopTheme.Search do [] else fts_query = build_fts_query(query) - search_fts(fts_query) + + case search_fts(fts_query) do + [] -> search_like(query) + results -> results + end end end @@ -135,6 +139,26 @@ defmodule SimpleshopTheme.Search do end end + # Substring fallback when FTS5 prefix matching returns nothing + defp search_like(query) do + pattern = "%#{sanitize_like(query)}%" + + Product + |> where([p], p.visible == true and p.status == "active") + |> where([p], like(p.title, ^pattern) or like(p.category, ^pattern)) + |> order_by([p], p.title) + |> limit(20) + |> preload(^@listing_preloads) + |> Repo.all() + end + + defp sanitize_like(input) do + input + |> String.replace("\\", "\\\\") + |> String.replace("%", "\\%") + |> String.replace("_", "\\_") + end + defp insert_into_index(%Product{} = product) do Repo.query!( "INSERT INTO products_search_map (product_id) VALUES (?1)", diff --git a/test/simpleshop_theme/search_test.exs b/test/simpleshop_theme/search_test.exs index d0550aa..799a770 100644 --- a/test/simpleshop_theme/search_test.exs +++ b/test/simpleshop_theme/search_test.exs @@ -169,18 +169,24 @@ defmodule SimpleshopTheme.SearchTest do describe "index_product/1" do test "indexes a single product", %{ocean: ocean} do - # Clear and rebuild without ocean + # Clear FTS index SimpleshopTheme.Repo.query!("DELETE FROM products_search_map") SimpleshopTheme.Repo.query!("DELETE FROM products_search") - assert Search.search("ocean") == [] + # Verify FTS index is empty + %{rows: rows} = SimpleshopTheme.Repo.query!("SELECT COUNT(*) FROM products_search_map") + assert rows == [[0]] # Index just ocean ocean = SimpleshopTheme.Repo.preload(ocean, [:variants]) Search.index_product(ocean) + # Verify ocean is now in the FTS index + %{rows: [[count]]} = SimpleshopTheme.Repo.query!("SELECT COUNT(*) FROM products_search_map") + assert count == 1 + results = Search.search("ocean") - assert length(results) == 1 + assert length(results) >= 1 assert hd(results).id == ocean.id end @@ -197,13 +203,41 @@ defmodule SimpleshopTheme.SearchTest do end end + describe "LIKE fallback" do + test "finds substring matches that FTS5 prefix misses", %{ocean: ocean} do + # "ebook" is in the middle of "Notebook" — FTS5 prefix won't match + results = Search.search("ebook") + assert Enum.any?(results, &(&1.id == ocean.id)) + end + + test "falls back to category substring match", %{forest: forest} do + # "ppar" is a substring of "Apparel" — FTS5 prefix won't match + results = Search.search("pparel") + assert Enum.any?(results, &(&1.id == forest.id)) + end + end + describe "remove_product/1" do test "removes a product from the index", %{ocean: ocean} do - assert Search.search("ocean") != [] + # Verify ocean is in the FTS index + %{rows: [[rowid]]} = + SimpleshopTheme.Repo.query!( + "SELECT rowid FROM products_search_map WHERE product_id = ?1", + [ocean.id] + ) + + assert rowid Search.remove_product(ocean.id) - assert Search.search("ocean") == [] + # Verify it's gone from the FTS index + %{rows: rows} = + SimpleshopTheme.Repo.query!( + "SELECT rowid FROM products_search_map WHERE product_id = ?1", + [ocean.id] + ) + + assert rows == [] end test "is a no-op for unindexed product" do