fix: MPS-24 Phase 2.8 — bulk tagger AJAX tag-focus + real drag-to-reorder

Operator (printableprompts.com, 481 products) reported the bulk tagger
'still refreshing the whole screen' and 'dragging tags doesn't work'
after 2.7. Two real defects the 2.7 static audit missed:

1. Tag-focus was a full-page navigation: clicking a tag chip is
   <a href=?focus=slug>, and the view loaded+rendered ALL products on
   EVERY GET. On a 481-product catalog every tag click reloaded a
   multi-MB page. The forms were AJAX; the dominant workflow was not.
2. Drag-to-reorder never existed: shop_tags.j2 shipped draggable=true +
   a handle + help text, but tag_bulk.js had ZERO drag handlers.

Fix:
- shop.py:shop_tags — all_products loads only when focus_tag or
  show_suggestions (bare GET is light). New AJAX branch: is_ajax +
  ?focus=slug -> JSON {focus, products:[{id,title,url,attached}]}.
- shop_tags.j2 — stable [data-focus-section] (always in DOM, hidden
  until focused); ?focus= chips carry data-tag-focus-link. No-JS
  unchanged (real navigation, server renders the section).
- tag_bulk.js — wireFocusLinks() intercepts chip clicks, fetchFocus()
  + renderFocus() swap the list in place, active-chip + history
  pushState/popstate, real-navigation fallback. wireDragAndDrop()
  HTML5 DnD -> persistOrder() POSTs action=set_order&tag_slugs=…
  (view already supported it) + re-syncs up/down disabled states.
  .tag-list-dragging CSS added.
- Tests: TestProductTagsSpa +4 (ajax focus json, unknown-slug null,
  set_order persists positions, bare GET no catalog). 1128 passed.

Docs: mps-24.md Phase 2.8, architecture.md, design-system.md, CLAUDE.md.
Deferred: AJAX 'Suggest categories' link (occasional click, not hot path).
This commit is contained in:
russell@unturf.com 2026-05-16 08:31:03 -04:00
parent f591620424
commit 155f7ff66f
No known key found for this signature in database
9 changed files with 439 additions and 10 deletions

View file

@ -222,6 +222,7 @@ mps_page_session (raw rows)
| Facet nav on shop home (MPS-24 Phase 2.6b) | `_facet_nav.j2` macros + `home.j2` / `shop.j2` wrappers | Desktop sidebar + mobile `<details>` accordion when `shop.home_layout >= 1` | Opt-in via home_layout |
| Mobile SERP rows under each lane (MPS-24 Phase 2.6b) | `home.j2` / `shop.j2` `.tag-lane-rows` markup + CSS visibility swap | Horizontal tiles ≥800px; vertical SERP rows with 6-sentence excerpts <800px | On for layout 2 |
| Per-product SPA tag chips (MPS-24 Phase 2.7) | `product_tags` view (`/p/{id}/tags`) + `static/js/product_tags.js` + `views/__init__.py:is_ajax()` | Chip add/remove on product edit, AJAX persist, no full page reload; no-JS keeps the comma `tags` field on the main form | Always on (JS-enhanced; no-JS fallback) |
| Bulk tagger AJAX focus + DnD (MPS-24 Phase 2.8) | `shop.py:shop_tags` AJAX `?focus=` branch + `tag_bulk.js` `wireFocusLinks`/`wireDragAndDrop` | Tag chip click swaps product list in place (no reload); HTML5 drag-to-reorder POSTs `set_order`; bare GET no longer loads the whole catalog | Always on (JS-enhanced; no-JS = real navigation) |
## Ticket Index
@ -251,7 +252,7 @@ mps_page_session (raw rows)
| [MPS-21](tickets/mps-21.md) | Make-an-Offer Mode | Complete |
| [MPS-22](tickets/mps-22.md) | Kill-Switch Feature Flags — Karaoke + Torrent Off by Default | Complete |
| [MPS-23](tickets/mps-23.md) | Consolidated Transactional Sender Identity + Shop Contact Email | Open |
| [MPS-24](tickets/mps-24.md) | Shop home page overhaul + product categorization (tags + chips + lanes + auto-suggest + per-product SPA tag chips) | In progress (Phases 1 + 2 + 2.6 + 2.7 landed) |
| [MPS-24](tickets/mps-24.md) | Shop home page overhaul + product categorization (tags + chips + lanes + auto-suggest + per-product SPA tag chips + bulk-tagger AJAX/DnD) | In progress (Phases 1 + 2 + 2.6 + 2.7 + 2.8 landed) |
## Related Docs

View file

@ -289,6 +289,9 @@ All components are documented with live examples at `/styleguide`. The styleguid
| `.tag-chip-add` | `product_edit.j2` | Add-a-tag row, grid `1fr auto`, stacks to one column under 600px |
| `.tag-chip-flash` | `product_edit.j2` | Toast region reusing `.tag-flash-toast` / `.tag-flash-{success,error,info}` |
| `[data-product-tags]` / `[data-product-tags-url]` | `product_edit.j2` | JS hooks for `product_tags.js`: container + the `/p/{id}/tags` endpoint URL |
| `[data-focus-section]` / `[data-focus-heading]` / `[data-focus-list]` | `shop_tags.j2` (Phase 2.8) | Stable bulk-tagger focus container — always in the DOM, `hidden` until a tag is focused; `tag_bulk.js` swaps the product list in place instead of a full reload |
| `[data-tag-focus-link]` | `shop_tags.j2` (Phase 2.8) | Tag chip in the All-tags list; `tag_bulk.js` intercepts the click and fetches `?focus=<slug>` as JSON |
| `.tag-list-dragging` / `.tag-list-drop-target` | `shop_tags.j2` (Phase 2.8) | Dragged row (dimmed) + active drop position during HTML5 drag-to-reorder; `tag_bulk.js` POSTs `set_order` on drop |
## CSS Conventions

View file

@ -319,6 +319,50 @@ picker is Phase 2).
| `tests/test_models.py` | `TestTagSuggestPureFunctions` — 11 unit tests over tokenize / stem / cluster |
| `tests/test_functional.py` | `test_suggest_clusters_renders_candidates`, `test_apply_suggestion_creates_tag_and_attaches_products`, `test_dismiss_suggestion_adds_to_stopwords`, `test_apply_suggestion_rejects_empty_input` |
### Phase 2.8 — bulk tagger: AJAX tag-focus + real drag-to-reorder + page-weight fix (shipped 2026-05-16)
Operator (printableprompts.com, **481 products**) reported the bulk
tagger "still refreshing the whole screen" and "dragging tags doesn't
work" after 2.7. Two real defects the 2.7 static audit missed:
1. **Tag-focus was a full-page navigation.** Clicking a tag chip is
`<a href="?focus=slug">` — a real reload. On a 481-product catalog
the view loaded + rendered *every* product on *every* GET, so each
tag click reloaded a multi-MB page. The forms were AJAX; the
dominant workflow action (focus a tag → assign products) was not.
2. **Drag-to-reorder never existed.** `shop_tags.j2` shipped
`draggable="true"`, a ≡ handle, and "drag rows when JS is enabled"
help text, but `tag_bulk.js` had **zero** drag handlers — only the
↑/↓ buttons worked. The affordance lied.
Fix:
- **View** (`shop.py:shop_tags`): `all_products` now loads only when
`focus_tag or show_suggestions` (bare GET is light). New AJAX branch:
`is_ajax + ?focus=<slug>` → JSON `{focus:{name,slug},
products:[{id,title,url,attached}]}`.
- **Template** (`shop_tags.j2`): focus section is now a stable
`[data-focus-section]` (always in DOM, `hidden` until focused);
`?focus=` chips carry `data-tag-focus-link` + `data-tag-slug`.
No-JS unchanged: the link is a real navigation, server still renders
the section.
- **JS** (`tag_bulk.js`): `wireFocusLinks()` intercepts chip clicks →
`fetchFocus()``renderFocus()` swaps the product list in place,
updates the active chip, `history.pushState` (back/forward via
`popstate`), graceful real-navigation fallback. `wireDragAndDrop()`
implements HTML5 DnD on the tag rows → `persistOrder()` POSTs
`action=set_order&tag_slugs=…` (view already supported it) and
re-syncs ↑/↓ disabled states. `.tag-list-dragging` CSS added.
Tests (`test_functional.py::TestProductTagsSpa`):
`test_ajax_focus_returns_product_list_json`,
`test_ajax_focus_unknown_slug_returns_null_focus`,
`test_ajax_set_order_persists_tag_positions`,
`test_bulk_tagger_bare_get_renders_without_products`.
Deferred (occasional click, not the hot path): AJAX-ifying the
"Suggest categories" link — still a full navigation by design.
### Phase 2.7 — per-product SPA tag chips on product edit (shipped 2026-05-16)
Operator report: adding/removing a tag on the product edit page