feat(beta-gating): beta_gated_nav_item helper auto-marks gated entries

Wraps the conditional + dot wiring into a single call so adding a new
beta nav entry doesn't require remembering to set `beta: true` by hand
or duplicating the `beta_features_enabled?` check. Naming mirrors the
existing `BetaGateable` concern.
This commit is contained in:
Guillem Arias
2026-05-18 20:43:02 +02:00
parent 7b4cee60aa
commit af647a9cfc
3 changed files with 15 additions and 5 deletions

View File

@@ -59,6 +59,16 @@ module ApplicationHelper
current_page?(path) || (request.path.start_with?(path) && path != "/")
end
# Wraps a nav-item hash so a single call performs both halves of a
# beta-gated entry: returns `nil` for users without the flag (so the
# entry never reaches the rendered nav), and stamps `beta: true` on
# the hash for users with the flag (so the partial paints the violet
# dot on the icon). Use inside an `Array#compact` nav-items list.
def beta_gated_nav_item(item)
return nil unless beta_features_enabled?
item.merge(beta: true)
end
# Wrapper around I18n.l to support custom date formats
def format_date(object, format = :default, options = {})
date = object.to_date

View File

@@ -11,7 +11,7 @@ else
{ name: t(".nav.transactions"), path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
{ name: t(".nav.reports"), path: reports_path, icon: "chart-bar", icon_custom: false, active: page_active?(reports_path) },
{ name: t(".nav.budgets"), path: budgets_path, icon: "map", icon_custom: false, active: page_active?(budgets_path) },
(beta_features_enabled? ? { name: t(".nav.goals"), path: goals_path, icon: "piggy-bank", icon_custom: false, active: page_active?(goals_path), beta: true } : nil),
beta_gated_nav_item({ name: t(".nav.goals"), path: goals_path, icon: "piggy-bank", icon_custom: false, active: page_active?(goals_path) }),
{ name: t(".nav.assistant"), path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
].compact
end %>

View File

@@ -61,18 +61,18 @@ Same pattern works for dashboard widgets, scoreboard cards, anything that surfac
The desktop sidebar rail and the mobile bottom nav both render from `app/views/layouts/shared/_nav_item.html.erb`. The partial accepts an optional `beta:` local — when true, it overlays a violet dot-only pill on the icon so opted-in users can tell at a glance that the rail entry leads to a beta surface.
Build the nav-item hash conditionally inside the `beta_features_enabled?` branch and set `beta: true` on it. The compact form using `Array#compact` keeps the array clean:
Use the `beta_gated_nav_item` helper to wrap the entry. It returns `nil` for non-beta users (so the entry never enters the nav, once `Array#compact` runs) and stamps `beta: true` for opted-in users (so the partial paints the dot). One call, both halves of the gate:
```erb
<% mobile_nav_items = [
{ name: t(".nav.home"), path: root_path, icon: "pie-chart", icon_custom: false, active: page_active?(root_path) },
{ name: t(".nav.transactions"), path: transactions_path, icon: "credit-card", icon_custom: false, active: page_active?(transactions_path) },
(beta_features_enabled? ? { name: t(".nav.goals"), path: goals_path, icon: "piggy-bank", icon_custom: false, active: page_active?(goals_path), beta: true } : nil),
beta_gated_nav_item({ name: t(".nav.goals"), path: goals_path, icon: "piggy-bank", icon_custom: false, active: page_active?(goals_path) }),
{ name: t(".nav.assistant"), path: chats_path, icon: "icon-assistant", icon_custom: true, active: page_active?(chats_path), mobile_only: true }
].compact %>
```
Two things happen from this single change: non-beta users never see the entry (the `nil` gets compacted out) and beta users see the entry with the dot marker (the partial reads `beta:` and renders the pill). You don't need to touch `_nav_item.html.erb` itself.
You don't need to touch `_nav_item.html.erb` or set `beta: true` by hand. Adding a new beta nav entry is one helper call wrapped around the same hash you'd write anyway.
## Marking the feature in the UI
@@ -131,7 +131,7 @@ When a feature moves from beta to general availability, removing the gate is a s
1. Drop the `before_action :require_beta_features!` line from the controller.
2. Unwrap the `if beta_features_enabled?` blocks in views.
3. Drop the `DS::Pill` markers from headers and section titles, and drop the `beta: true` flag from the nav-item hash.
3. Drop the `DS::Pill` markers from headers and section titles, and unwrap the `beta_gated_nav_item(...)` call back into a plain nav-item hash.
4. Delete the controller / view tests that exercise the redirect.
Grep for `require_beta_features!` and `beta_features_enabled?` near your feature to confirm nothing's left behind.