mirror of
https://github.com/we-promise/sure.git
synced 2026-05-29 23:39:03 +00:00
* refactor: rename beta features gate to preview features Renames the opt-in gate introduced in PR #1829 from "beta" to "preview". Same shape (per-user JSONB toggle, `before_action` concern, marker pill) just retitled so the surface speaks the language Sure uses elsewhere ("preview" reads as in-progress, "beta" had baggage with provider maturity copy and external testing programs). Renames: - BetaGateable -> PreviewGateable - require_beta_features! -> require_preview_features! - beta_features_enabled? -> preview_features_enabled? - preferences["beta_features_enabled"] -> preferences["preview_features_enabled"] - DS::Pill default label "Beta" -> "Preview" - Settings -> Preferences toggle copy "beta features" -> "preview features" - config/locales/views/beta/ -> config/locales/views/preview/ - docs/llm-guides/gating-a-beta-feature.md -> gating-a-preview-feature.md Includes a data migration that copies any existing `beta_features_enabled` JSONB key into `preview_features_enabled` so early opt-ins survive the rename, then removes the old key. The migration is fully reversible. Provider maturity copy ("maturity.beta = Beta" under Settings -> Bank sync) is intentionally untouched - that's a separate concept describing a provider's integration stability, not Sure's feature gate. * review: apply CodeRabbit findings on PR #1837 - Settings::PreferencesController#update now routes the `preview_features_enabled` input through strong params and casts via ActiveModel::Type::Boolean instead of reading raw params and string- comparing to "1". Matches Sure's controller convention for permitted params and avoids stringly-typed boolean handling. - Rename migration now wraps the destination JSONB key write in COALESCE so a row that somehow ends up with both keys keeps the destination value instead of having it overwritten by the source. Up and down paths get the same defensive shape. * 📝 CodeRabbit Chat: Implement requested code changes * 📝 CodeRabbit Chat: Implement requested code changes * fix: restore all missing translation keys; rename beta→preview label * fix: restore all missing sections (appearances, debugs, llm_usages, providers, etc.); rename beta→preview * fix: restore missing keys (member_removal_failed, confirm_delete, etc.); add preview section * fix(i18n/ca): use 'està en vista prèvia' instead of 'és una vista prèvia' * fix(i18n/ca): use 'en desenvolupament'; drop article in preview title * fix(i18n/es): use 'en desarrollo' instead of 'en progreso' * fix(i18n/ca): use 'funcions experimentals' instead of 'vista prèvia' * fix(i18n/es): use 'funciones experimentales' instead of 'vista previa' * fix(i18n/ca): use 'funcions experimentals' in preferences.show.preview * fix(i18n/es): use 'funciones experimentales' in preferences.show.preview * fix(i18n/ca): use 'Experimental' pill label instead of 'Vista prèvia' * fix(i18n/es): use 'Experimental' pill label instead of 'Vista previa' --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
235 lines
12 KiB
Plaintext
235 lines
12 KiB
Plaintext
<%= content_for :page_title, t(".page_title") %>
|
|
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
|
|
<div>
|
|
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
|
<%= form.hidden_field :redirect_to, value: "preferences" %>
|
|
<%= form.select :locale,
|
|
language_options,
|
|
{ label: t(".language"), include_blank: t(".language_auto") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<%= form.fields_for :family do |family_form| %>
|
|
<%= family_form.select :timezone,
|
|
timezone_options,
|
|
{ label: t(".timezone") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<%= family_form.select :date_format,
|
|
Family::DATE_FORMATS,
|
|
{ label: t(".date_format") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<%= form.select :default_account_order,
|
|
AccountOrder.all.map { |order| [ order.label, order.key ] },
|
|
{ label: t(".default_account_order") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<%= family_form.select :country,
|
|
country_options,
|
|
{ label: t(".country") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<%= family_form.select :month_start_day,
|
|
(1..28).map { |day| [localized_ordinal(day), day] },
|
|
{ label: t(".month_start_day"), hint: t(".month_start_day_hint") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<% if @user.family.uses_custom_month_start? %>
|
|
<div class="text-sm text-warning bg-warning/10 p-3 rounded-lg">
|
|
<%= t(".month_start_day_warning") %>
|
|
</div>
|
|
<% end %>
|
|
<p class="text-xs italic pl-2 text-secondary"><%= t(".translations_notice") %></p>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<% if Current.user.admin? %>
|
|
<%= settings_section title: t(".currencies_title", moniker: family_moniker), subtitle: t(".currencies_subtitle", moniker: family_moniker_downcase) do %>
|
|
<% selected_count_translations = t(".selected_currencies_count") %>
|
|
<div class="space-y-4"
|
|
data-controller="currency-preferences"
|
|
data-currency-preferences-base-currency-value="<%= @user.family.primary_currency_code %>"
|
|
data-currency-preferences-locale-value="<%= I18n.locale %>"
|
|
data-currency-preferences-selected-count-translations-value="<%= selected_count_translations.to_json %>">
|
|
<% additional_preview_currencies = @user.family.secondary_enabled_currency_objects.first(6) %>
|
|
<% additional_currency_count = @user.family.secondary_enabled_currency_objects.count %>
|
|
<div class="rounded-xl border border-secondary p-4 bg-container-inset">
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
<div>
|
|
<p class="text-xs font-medium uppercase tracking-wide text-secondary"><%= t(".base_currency_label") %></p>
|
|
<div class="mt-2">
|
|
<p class="text-sm font-medium text-primary"><%= @user.family.primary_currency_code %></p>
|
|
<p class="text-sm text-secondary"><%= currency_label(@user.family.primary_currency_code) %></p>
|
|
</div>
|
|
</div>
|
|
<div class="md:border-l md:border-secondary md:pl-4">
|
|
<p class="text-xs font-medium uppercase tracking-wide text-secondary"><%= t(".additional_currencies_label") %></p>
|
|
<% if additional_preview_currencies.any? %>
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
<% additional_preview_currencies.each do |currency| %>
|
|
<span class="inline-flex items-center rounded-full bg-surface-inset px-2.5 py-1 text-xs font-medium text-primary">
|
|
<%= currency.iso_code %>
|
|
</span>
|
|
<% end %>
|
|
<% if additional_currency_count > additional_preview_currencies.count %>
|
|
<span class="inline-flex items-center rounded-full bg-surface-inset px-2.5 py-1 text-xs font-medium text-secondary">
|
|
<%= t(".currencies_more", count: additional_currency_count - additional_preview_currencies.count) %>
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<p class="mt-2 text-sm text-secondary"><%= t(".no_additional_currencies") %></p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end">
|
|
<%= render DS::Button.new(
|
|
text: t(".manage_currencies"),
|
|
type: :button,
|
|
class: "md:w-auto w-full justify-center",
|
|
data: { action: "currency-preferences#open" }
|
|
) %>
|
|
</div>
|
|
<%= render DS::Dialog.new(
|
|
id: "currency-preferences-dialog",
|
|
auto_open: false,
|
|
disable_frame: true,
|
|
width: "md",
|
|
data: { currency_preferences_target: "dialog" }
|
|
) do |dialog| %>
|
|
<% dialog.with_header(title: t(".manage_currencies"), subtitle: t(".manage_currencies_subtitle")) %>
|
|
<% dialog.with_body do %>
|
|
<% primary_currency_code = @user.family.primary_currency_code %>
|
|
<% selected_currency_codes = @user.family.enabled_currency_codes %>
|
|
<% base_currency_rows, other_currency_rows = Money::Currency.as_options.partition { |currency| currency.iso_code == primary_currency_code } %>
|
|
<% currency_rows = base_currency_rows + other_currency_rows %>
|
|
<%= styled_form_with model: @user, url: user_path(@user), class: "space-y-4", data: { action: "turbo:submit-end->currency-preferences#handleSubmitEnd" } do |form| %>
|
|
<%= form.hidden_field :redirect_to, value: "preferences" %>
|
|
<%= hidden_field_tag "user[family_attributes][id]", @user.family.id %>
|
|
<%= hidden_field_tag "user[family_attributes][enabled_currencies][]", primary_currency_code, id: nil %>
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div class="flex flex-wrap gap-2">
|
|
<%= render DS::Button.new(
|
|
text: t(".select_all_currencies"),
|
|
type: :button,
|
|
variant: :ghost,
|
|
size: :sm,
|
|
data: { action: "currency-preferences#selectAll" }
|
|
) %>
|
|
<%= render DS::Button.new(
|
|
text: t(".select_base_only"),
|
|
type: :button,
|
|
variant: :ghost,
|
|
size: :sm,
|
|
data: { action: "currency-preferences#selectBaseOnly" }
|
|
) %>
|
|
</div>
|
|
<p class="text-sm font-medium text-secondary"
|
|
data-currency-preferences-target="selectedCount"
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-atomic="true"></p>
|
|
</div>
|
|
<div data-controller="list-filter" class="space-y-3">
|
|
<div class="relative">
|
|
<input type="search"
|
|
autocomplete="off"
|
|
placeholder="<%= t(".currency_search_placeholder") %>"
|
|
aria-label="<%= t(".currency_search_placeholder") %>"
|
|
data-list-filter-target="input"
|
|
data-action="input->list-filter#filter"
|
|
class="block w-full border border-secondary rounded-md py-2.5 pl-10 pr-3 bg-container focus:ring-gray-500 sm:text-sm">
|
|
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
<%= icon("search", class: "text-secondary") %>
|
|
</div>
|
|
</div>
|
|
<div data-list-filter-target="list" class="max-h-80 overflow-auto rounded-xl border border-secondary divide-y divide-alpha-black-100 theme-dark:divide-alpha-white-100">
|
|
<p class="hidden px-4 py-3 text-sm text-secondary" data-list-filter-target="emptyMessage">
|
|
<%= t(".no_matching_currencies") %>
|
|
</p>
|
|
<% currency_rows.each do |currency| %>
|
|
<% checked = selected_currency_codes.include?(currency.iso_code) %>
|
|
<% base_currency = currency.iso_code == primary_currency_code %>
|
|
<label class="<%= class_names(
|
|
"filterable-item flex items-start gap-3 px-4 py-3 cursor-pointer transition-colors hover:bg-surface-inset",
|
|
"bg-surface-inset": base_currency,
|
|
"cursor-default": base_currency
|
|
) %>"
|
|
data-filter-name="<%= "#{currency.iso_code} #{currency.name}" %>">
|
|
<%= check_box_tag "user[family_attributes][enabled_currencies][]",
|
|
currency.iso_code,
|
|
checked,
|
|
disabled: base_currency,
|
|
class: "checkbox checkbox--light mt-0.5",
|
|
data: {
|
|
currency_preferences_target: "checkbox",
|
|
action: "change->currency-preferences#updateSelectedCount"
|
|
} %>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-start justify-between gap-3">
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium text-primary"><%= currency.iso_code %></span>
|
|
<% if base_currency %>
|
|
<span class="inline-flex items-center rounded-full bg-surface px-2 py-0.5 text-[11px] font-medium text-secondary">
|
|
<%= t(".base_currency_badge") %>
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
<p class="mt-0.5 text-sm text-secondary"><%= currency.name %></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end gap-2">
|
|
<%= render DS::Button.new(text: t("shared.cancel"), type: :button, variant: :ghost, data: { action: "DS--dialog#close" }) %>
|
|
<%= render DS::Button.new(text: t(".save_currencies")) %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<%= settings_section title: t(".sharing_title", moniker: family_moniker), subtitle: t(".sharing_subtitle", moniker: family_moniker_downcase) do %>
|
|
<div>
|
|
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
|
|
<%= form.hidden_field :redirect_to, value: "preferences" %>
|
|
<%= form.fields_for :family do |family_form| %>
|
|
<%= family_form.select :default_account_sharing,
|
|
[
|
|
[t(".sharing_shared"), "shared"],
|
|
[t(".sharing_private"), "private"]
|
|
],
|
|
{ label: t(".sharing_default_label") },
|
|
{ data: { auto_submit_form_target: "auto" } } %>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<% end %>
|
|
|
|
<%# Preview features toggle — visible to all users, not just admins. Lives at
|
|
the bottom of Preferences as a standalone card (no section header) so the
|
|
toggle row is the entire surface. Posts directly to
|
|
settings#preferences#update via the Settings::PreferencesController,
|
|
matching the auto-submit pattern used on the Appearance page. %>
|
|
<section class="bg-container shadow-border-xs rounded-xl p-4">
|
|
<%= form_with url: settings_preferences_path, method: :patch,
|
|
data: { controller: "auto-submit-form" } do |f| %>
|
|
<%# Wrapping the row in <label for=…> makes the title + description
|
|
themselves a click target for the toggle, so the cursor-pointer
|
|
affordance on the container is honest. %>
|
|
<label for="user_preview_features_enabled" class="flex cursor-pointer items-center gap-4 justify-between">
|
|
<div class="text-sm space-y-1">
|
|
<h4 class="text-primary"><%= t(".preview.title") %></h4>
|
|
<p class="text-secondary"><%= t(".preview.description") %></p>
|
|
</div>
|
|
<%= render DS::Toggle.new(
|
|
id: "user_preview_features_enabled",
|
|
name: "user[preview_features_enabled]",
|
|
checked: @user.preview_features_enabled?,
|
|
data: { auto_submit_form_target: "auto" }
|
|
) %>
|
|
</label>
|
|
<% end %>
|
|
</section>
|