From dc79d13358f500d0eb443ef66b259192f0612866 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 15 May 2026 13:54:24 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20MPS-24=20Phase=202.5=20=E2=80=94=20prod?= =?UTF-8?q?uct=20page=20polish=20(description=20wrap=20+=20price-history?= =?UTF-8?q?=20toggle)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two product-page issues surfaced while shopping printableprompts on mobile. Both fixed in one commit since they're tightly scoped to the product page experience. Description text clipping the right edge on mobile: - .content-card uses CSS Grid but its grid items had default min-width: auto, so they expanded to their content's intrinsic width — long unbreakable tokens (URLs, etc.) pushed the card wider than the viewport. Then .content's overflow-x: clip silently hid the right side instead of wrapping the text. - Add min-width: 0 + overflow-wrap: break-word to .content-card, .content-card-header, .content-card-body. Add word-break: break-word to inner /

so URLs hyphenate at any character. Price history shown by default: - The price history table (commit 1e5fe27, 2026-02-11) was always visible to anyone who could edit the shop. Operator feedback: "wait for a sale" psychology hurts conversions; shoppers shouldn't see a timeline of past prices. - New Shop.show_price_history Boolean (default False, server-default "0") with idempotent Alembic migration c792642911e2. - Toggle lives in the existing ribbon-settings form section. - views/product.py (public view) + views/watch.py JSON gate the price_history list on the toggle. Template product.j2 also gates rendering as belt-and-suspenders. - Edit page (also views/product.py:product_edit) intentionally remains always-on — the operator needs price audit access from their own admin surface regardless of the shopper-facing toggle. - New shop matrix entry in docs/architecture.md. - 2 new functional tests (default-off + toggle round-trip). 1090 tests passing. --- docs/architecture.md | 1 + docs/tickets/mps-24.md | 24 ++++++++++ make_post_sell/models/shop.py | 7 +++ ...11e2_add_shop_show_price_history_column.py | 44 ++++++++++++++++++ make_post_sell/static/css/common.css | 26 ++++++++++- make_post_sell/templates/shop.j2 | 3 +- make_post_sell/templates/shop_settings.j2 | 16 +++++++ make_post_sell/templates/styleguide.j2 | 4 -- make_post_sell/tests/test_functional.py | 46 +++++++++++++++++++ make_post_sell/views/product.py | 17 +++++-- make_post_sell/views/shop.py | 14 ++++++ make_post_sell/views/watch.py | 4 +- 12 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 make_post_sell/scripts/alembic/versions/c792642911e2_add_shop_show_price_history_column.py diff --git a/docs/architecture.md b/docs/architecture.md index ae6c95c..860d878 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -192,6 +192,7 @@ mps_page_session (raw rows) |---------|-------------|--------------|---------| | Watch mode | `shop.watch_mode_enabled` | `ribbon-settings` | Off | | Sandbox mode | `shop.sandbox_mode` | `ribbon-settings` | Off | +| Show price history (MPS-24 Phase 2.5) | `shop.show_price_history` | `ribbon-settings` | Off | | Show dates | `shop.show_dates` | `ribbon-settings` | On | | Grid lanes | `shop.grid_lanes_enabled` | `ribbon-settings` | Off | | Color filter | `shop.color_filter` | `ribbon-settings` | 0 (none) | diff --git a/docs/tickets/mps-24.md b/docs/tickets/mps-24.md index 330a856..61accfd 100644 --- a/docs/tickets/mps-24.md +++ b/docs/tickets/mps-24.md @@ -319,6 +319,30 @@ 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.5 — product page polish: description wrap + price-history toggle (shipped 2026-05-15) + +Two product-page bugs surfaced while shopping printableprompts: + +- **Description text clipping right edge on mobile.** `.content-card` + uses CSS Grid but its grid items had default `min-width: auto` — + they expanded to their content's intrinsic width, pushing the card + past the viewport. `.content`'s `overflow-x: clip` then silently + hid the right side of the text instead of wrapping. Fix: `min-width: + 0` + `overflow-wrap: break-word` on `.content-card`, `.content-card- + header`, `.content-card-body`, plus `word-break: break-word` on the + inner ``/`

` elements so long URLs hyphenate at any character. +- **Price history shown by default.** The price history table + (commit `1e5fe27`, 2026-02-11) was always visible to anyone who + could edit the shop. Operator feedback: "wait for a sale" psychology + hurts conversions; shoppers shouldn't see a timeline of past prices. + Added `Shop.show_price_history` Boolean (default `False`, server- + default `"0"`) with idempotent Alembic migration `c792642911e2`. + Toggle lives in `ribbon-settings` form. View + watch JSON now gate + the `price_history` list on the toggle; template gates rendering + separately as belt-and-suspenders. When off (default), nobody sees + the table — including the operator on their own product page. + Operator can still review history in shop analytics. + ### Phase 2.4 — SPA bulk tagger + Netflix-style lanes (shipped 2026-05-15) Two improvements that compound for the operator workflow: diff --git a/make_post_sell/models/shop.py b/make_post_sell/models/shop.py index b6de8c9..b6880a2 100644 --- a/make_post_sell/models/shop.py +++ b/make_post_sell/models/shop.py @@ -241,6 +241,13 @@ class Shop(RBase, Base): UnicodeText, nullable=False, default="", server_default="" ) + # Show full price-history table on product pages. Off by default — + # most shops just want shoppers to see the current price, not a + # historical timeline that can read as "wait for a sale." + show_price_history = Column( + Boolean, nullable=False, default=False, server_default="0" + ) + # many to many uses association_proxy. users = association_proxy("shop_users", "user", creator=lambda u: UserShop(user=u)) diff --git a/make_post_sell/scripts/alembic/versions/c792642911e2_add_shop_show_price_history_column.py b/make_post_sell/scripts/alembic/versions/c792642911e2_add_shop_show_price_history_column.py new file mode 100644 index 0000000..a8c4a27 --- /dev/null +++ b/make_post_sell/scripts/alembic/versions/c792642911e2_add_shop_show_price_history_column.py @@ -0,0 +1,44 @@ +"""add shop show_price_history column + +Revision ID: c792642911e2 +Revises: 2dbdb8c89e66 +Create Date: 2026-05-15 13:09:49.673592 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "c792642911e2" +down_revision = "2dbdb8c89e66" +branch_labels = None +depends_on = None + + +def _column_exists(table, column): + conn = op.get_bind() + result = conn.execute(sa.text(f"PRAGMA table_info({table})")) + return any(row[1] == column for row in result.fetchall()) + + +def upgrade(): + # Default off — most shops just want shoppers to see the current + # price, not a timeline that reads as "wait for a sale". The + # original price-history feature shipped in commit 1e5fe27 + # (2026-02-11) was always-on. + if not _column_exists("mps_shop", "show_price_history"): + op.add_column( + "mps_shop", + sa.Column( + "show_price_history", + sa.Boolean(), + nullable=False, + server_default="0", + ), + ) + + +def downgrade(): + if _column_exists("mps_shop", "show_price_history"): + op.drop_column("mps_shop", "show_price_history") diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index 0eed861..1eac77a 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -2413,6 +2413,14 @@ section.checkout-page .well { margin-bottom: var(--space-4, 16px); display: grid; gap: var(--space-3, 12px); + /* min-width: 0 lets this grid item shrink below its content's + intrinsic width (default: auto). Without it, descriptions with + long links or unbreakable tokens push the card wider than the + viewport — and since .content has overflow-x: clip, the right + edge silently gets cut off instead of wrapping. */ + min-width: 0; + max-width: 100%; + overflow-wrap: break-word; } .content-card-header { @@ -2422,9 +2430,25 @@ section.checkout-page .well { letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-muted, #777); + min-width: 0; + overflow-wrap: break-word; } -.content-card-body { margin: 0; } +.content-card-body { + margin: 0; + min-width: 0; + overflow-wrap: break-word; +} + +/* Long URLs / unbroken tokens inside rendered markdown still need to + wrap — break-word lets the browser hyphenate them at any character. */ +.content-card-body a, +.content-card-body p, +.product-description a, +.product-description p { + overflow-wrap: break-word; + word-break: break-word; +} .content-card-meta { margin: 0; diff --git a/make_post_sell/templates/shop.j2 b/make_post_sell/templates/shop.j2 index 8ac0203..0eb670e 100644 --- a/make_post_sell/templates/shop.j2 +++ b/make_post_sell/templates/shop.j2 @@ -73,8 +73,7 @@
${{ '{:,.2f}'.format(product.price) }} {% endif %} -
- {{ product.shop.name }} + {# Shop name intentionally omitted — see home.j2 comment. #} {% endif %} diff --git a/make_post_sell/templates/shop_settings.j2 b/make_post_sell/templates/shop_settings.j2 index cf2e495..ff1d54c 100644 --- a/make_post_sell/templates/shop_settings.j2 +++ b/make_post_sell/templates/shop_settings.j2 @@ -1086,6 +1086,22 @@

+ {# MPS-24 Phase 2.5: price history toggle. Default off. #} + +
+ + +
+ + +
+ When off, product pages display only the current price. Turn on to reveal a timeline of past prices. Most shops keep this off — a visible history can read as "wait for a sale" and slow conversions. + +
+
+
diff --git a/make_post_sell/templates/styleguide.j2 b/make_post_sell/templates/styleguide.j2 index 6e001c2..57a1f53 100644 --- a/make_post_sell/templates/styleguide.j2 +++ b/make_post_sell/templates/styleguide.j2 @@ -834,16 +834,12 @@ Dark mode overrides via --notice-*-bg and --notice-*-border tokens. Product Title
$19.99 -
- Shop Name

Image
Another Product
$7.50 -
- Shop Name
diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py index f405087..6d5e7da 100644 --- a/make_post_sell/tests/test_functional.py +++ b/make_post_sell/tests/test_functional.py @@ -8812,3 +8812,49 @@ class TestHomeLayoutAndTags(_AuthenticatedBase): # NO X-Requested-With ) self.assertEqual(res.status_int, 302) + + # --- MPS-24 Phase 2.5: show_price_history shop toggle ------------- + + def test_show_price_history_default_off(self): + shop = self._create_shop_helper( + user_creds=self.user1_creds, + shop_params={**self.shop1_params, "name": "price-hist-default"}, + ) + self.dbsession.expire(shop) + self.assertFalse(shop.show_price_history) + + def test_show_price_history_toggle_via_ribbon_settings(self): + shop = self._create_shop_helper( + user_creds=self.user1_creds, + shop_params={**self.shop1_params, "name": "price-hist-toggle"}, + ) + # Turn it on + res = self.testapp.post( + f"/s/{shop.id}/settings", + { + "form_section": "ribbon-settings", + "show_price_history": "1", + "submit": "Save Settings", + }, + ) + if res.status_int == 302: + res = res.follow() + self.assertIn("price history is now shown", res.body.decode()) + self.dbsession.expire(shop) + self.assertTrue(shop.show_price_history) + + # Turn it off + res = self.testapp.post( + f"/s/{shop.id}/settings", + { + "form_section": "ribbon-settings", + "show_price_history": "0", + "submit": "Save Settings", + }, + ) + if res.status_int == 302: + res = res.follow() + self.assertIn("price history is now hidden", res.body.decode()) + self.dbsession.expire(shop) + self.assertFalse(shop.show_price_history) + diff --git a/make_post_sell/views/product.py b/make_post_sell/views/product.py index 5e77ee8..4e6c3e2 100644 --- a/make_post_sell/views/product.py +++ b/make_post_sell/views/product.py @@ -104,12 +104,14 @@ def product(request): else: related_products = get_related_products(product) + # Price history is gated by shop.show_price_history (off by default + # per operator feedback — "wait for a sale" psychology kills sales). + # When the toggle is ON, everyone sees the table. When OFF (default), + # the table is hidden for both shoppers and operators (operator can + # still review historical prices in shop analytics). The template + # gates rendering separately as belt-and-suspenders. price_history = [] - if ( - product.is_sellable - and request.user - and request.user.can_edit_shop(product.shop) - ): + if product.shop.show_price_history and product.is_sellable: history_rows = product.price_history.limit(20).all() for i, ph in enumerate(history_rows): price_history.append({ @@ -741,6 +743,11 @@ def product_edit(request): inv.shop_location.id: inv.quantity for inv in product.inventories } + # Edit page is operator-only — always show price history here so + # the operator can reference what they've set. The shop-wide toggle + # (shop.show_price_history) gates only shopper-facing surfaces: + # the public product view (above) and watch.py JSON. Operators + # need their own price audit trail regardless of the toggle. price_history = [] if product.is_sellable: history_rows = product.price_history.limit(20).all() diff --git a/make_post_sell/views/shop.py b/make_post_sell/views/shop.py index a632ac1..e85a616 100644 --- a/make_post_sell/views/shop.py +++ b/make_post_sell/views/shop.py @@ -935,6 +935,20 @@ def shop_settings(request): ) ) + # MPS-24 Phase 2.5: show_price_history toggle (default off). + show_price_history_value = request.params.get("show_price_history") + if show_price_history_value is not None: + show_price_history = int(show_price_history_value) == 1 + if shop.show_price_history != show_price_history: + shop.show_price_history = show_price_history + status = "shown" if show_price_history else "hidden" + request.session.flash( + ( + f"Product page price history is now {status}.", + "success", + ) + ) + # Handle public sales stats setting public_sales_stats_value = request.params.get("public_sales_stats") if public_sales_stats_value is not None: diff --git a/make_post_sell/views/watch.py b/make_post_sell/views/watch.py index 58d201b..d9f9306 100644 --- a/make_post_sell/views/watch.py +++ b/make_post_sell/views/watch.py @@ -200,9 +200,9 @@ def watch_json(request): if request.user and request.user.authenticated: is_mod = request.user.can_edit_shop(request.shop) - # Build price history for mod users + # MPS-24 Phase 2.5: price history gated by shop.show_price_history. price_history = [] - if is_mod and product.is_sellable: + if request.shop.show_price_history and product.is_sellable: history_rows = product.price_history.limit(20).all() for i, ph in enumerate(history_rows): price_history.append({