From f5916204243ea31a67bdda7887e3c60cc149bb8c Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sat, 16 May 2026 07:58:25 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20MPS-24=20Phase=202.7=20=E2=80=94=20per-?= =?UTF-8?q?product=20SPA=20tag=20chips=20on=20product=20edit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator report: adding/removing a tag on the product edit page refreshed the whole screen. Tags lived only as a comma-separated inside the big product form, so any tag change needed a full Save Settings POST + page reload. - New route/view: product_tags -> /p/{id}/tags (before product_slug catch-all), @shop_editor_required + @trial_active_required. action=add (get_or_create_tag + attach) / action=remove (detach). AJAX (X-Requested-With) -> JSON, no reload; plain POST -> 302 back to edit (no-JS still works). Rebuilds discovery ring like product_edit. - Shared is_ajax() in views/__init__.py (single source of truth; shop.py:_is_ajax delegates — bulk tagger behaviour unchanged). - product_edit.j2: comma field kept as no-JS path; js-only chip editor added. product_tags.js reveals chips, demotes raw input to hidden, keeps it in lock-step so a later full Save is a no-op. - .tag-chip-removable family in common.css (tokens-only, Grid-only, always-visible remove button) + /styleguide#tagchips. - Harden tag_bulk.js: init() binds the delegated submit listener unconditionally (no early-return that could strand the bulk-tagger SPA into full reloads). - Tests: unit (slug dedupe invariant), integration (TestProductTagAddRemoveIntegration), functional (TestProductTagsSpa incl. bulk-tagger-AJAX-returns-JSON regression guard). 1124 passed. Docs: architecture.md, design-system.md, CLAUDE.md, mps-24.md. --- CLAUDE.md | 13 +- docs/architecture.md | 3 +- docs/design-system.md | 5 + docs/tickets/mps-24.md | 45 +++++ make_post_sell/routes.py | 3 + make_post_sell/static/css/common.css | 101 +++++++++++ make_post_sell/static/js/product_tags.js | 215 +++++++++++++++++++++++ make_post_sell/static/js/tag_bulk.js | 12 +- make_post_sell/templates/product_edit.j2 | 60 +++++-- make_post_sell/templates/styleguide.j2 | 44 +++++ make_post_sell/tests/test_functional.py | 143 +++++++++++++++ make_post_sell/tests/test_integration.py | 81 +++++++++ make_post_sell/tests/test_models.py | 15 ++ make_post_sell/views/__init__.py | 11 ++ make_post_sell/views/product.py | 102 ++++++++++- make_post_sell/views/shop.py | 4 +- 16 files changed, 838 insertions(+), 19 deletions(-) create mode 100644 make_post_sell/static/js/product_tags.js diff --git a/CLAUDE.md b/CLAUDE.md index b7b7687..e169088 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -478,7 +478,18 @@ Routes (registered before `shop_slug` catch-all): - `/s/{shop_id}/tag/{slug}` — public tag detail page (works without JS) Tag input on product edit: comma-separated `tags` field on -`product_edit.j2`; server slugifies, dedupes, upserts. +`product_edit.j2`; server slugifies, dedupes, upserts. **Phase 2.7**: +that field is now the no-JS fallback. With JS, `static/js/product_tags.js` +turns it into a chip editor and POSTs each add/remove to the +`product_tags` route — `/p/{product_id}/tags` (registered before the +`product_slug` catch-all), `@shop_editor_required` + `@trial_active_required`, +`action=add|remove`. AJAX (`X-Requested-With`) → JSON, no full page +reload; plain POST → 302 back to product edit. The JS keeps the hidden +`tags` field in lock-step with the chips so a later full "Save Settings" +is a no-op, never a stale revert. Capability-driven split helper: +`views/__init__.py:is_ajax(request)` (single source of truth; +`shop.py:_is_ajax` delegates to it). Removable chip component: +`.tag-chip-removable` family in `common.css` + `/styleguide#tagchips`. Home rendering: `views/shop.py:_build_home_layout_context()` builds `home_chips` (list of `Tag`), `home_lanes` (list of diff --git a/docs/architecture.md b/docs/architecture.md index 4b7bd98..8b84646 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -221,6 +221,7 @@ mps_page_session (raw rows) | 6-sentence SERP excerpt (MPS-24 Phase 2.6) | `Product.excerpt_sentences(n=6, max_chars=1500)` via shared `_strip_markdown()` helper | `shop_tag.j2` row description | Always on | | Facet nav on shop home (MPS-24 Phase 2.6b) | `_facet_nav.j2` macros + `home.j2` / `shop.j2` wrappers | Desktop sidebar + mobile `
` 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) | ## Ticket Index @@ -250,7 +251,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) | In progress (Phases 1 + 2 landed) | +| [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) | ## Related Docs diff --git a/docs/design-system.md b/docs/design-system.md index 5ff4d3e..40cb5b8 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -284,6 +284,11 @@ All components are documented with live examples at `/styleguide`. The styleguid | `[data-tag-slugs]` | `.serp-item` | Space-separated tag slugs the item carries; consumed by `tag_filter.js` | | `.tag-suggest-list` / `.tag-suggest-item` | `shop_tags.j2` (Phase 2) | Suggested-cluster well rendered when operator clicks "Suggest categories from titles + descriptions"; per-cluster grid with label, sample titles, Apply / Dismiss actions | | `.tag-suggest-actions` | `shop_tags.j2` | Apply / Dismiss button row, `grid-auto-flow: column` | +| `.product-tag-chips` / `.tag-chip-list` | `product_edit.j2` (Phase 2.7) | Per-product SPA tag editor container; `js-only`, revealed by `product_tags.js`. Chips wrap as inline-level pills (the `