Files
sure/app/views/settings/providers/show.html.erb
Guillem Arias d037412b8d feat(settings/providers): replace Add another provider CTA with a search + kind filter
Per the design review, the "Add another provider · Browse providers"
card was a redirect to content one scroll-tick away. A search input
plus kind chips lets users self-segment the catalog and is the right
tool once it grows beyond the four to twelve providers we ship today.

- New providers_filter Stimulus controller — case-insensitive free
  text search across name/region/kind, plus a chip group with
  All / Banks / Crypto / Investment that toggle visibility via
  Tailwind's `hidden` class.
- _search_filters partial: search box (count-pluralized placeholder)
  + chip group, ARIA-labelled and aria-pressed for the chips.
- ProviderCard exposes filter_data (target + name/region/kind data
  attrs) so the controller can match without re-rendering.
- Lunchflow's `kind` was "Lunch" — switched to "Bank" so it falls
  under the Banks chip alongside its actual offering (it aggregates
  banks).
- Drops the add_provider_cta partial and its locale entries; adds
  search_filters.* and an empty_filter message.
2026-05-09 11:33:13 +02:00

102 lines
5.0 KiB
Plaintext

<%= content_for :page_title, t("settings.providers.bank_sync.page_title") %>
<div class="space-y-4">
<% if @encryption_error %>
<div class="p-4 rounded-lg bg-destructive/10 border border-destructive/20">
<div class="flex items-start gap-3">
<%= icon("triangle-alert", class: "w-5 h-5 text-destructive shrink-0 mt-0.5") %>
<div>
<h3 class="font-medium text-primary"><%= t("settings.providers.encryption_error.title") %></h3>
<p class="text-secondary text-sm mt-1"><%= t("settings.providers.encryption_error.message") %></p>
</div>
</div>
</div>
<% else %>
<div class="flex items-start justify-between gap-4">
<p class="text-secondary"><%= t("settings.providers.bank_sync.lede") %></p>
<% if @connected.any? || @needs_attention.any? %>
<% sync_all_disabled = Current.family.last_sync_all_attempted_at.present? && Current.family.last_sync_all_attempted_at > 30.seconds.ago %>
<%= button_to sync_all_settings_providers_path,
method: :post,
disabled: sync_all_disabled,
title: sync_all_disabled ? t("settings.providers.sync_all_recently") : nil,
class: "inline-flex items-center gap-2 shrink-0 whitespace-nowrap px-3 py-1.5 text-sm font-medium text-secondary hover:text-primary border border-secondary rounded-lg hover:border-primary transition-colors disabled:opacity-50 disabled:cursor-not-allowed" do %>
<%= icon "refresh-cw", class: "w-4 h-4" %>
<%= t("settings.providers.sync_all") %>
<% end %>
<% end %>
</div>
<% all_connections = @needs_attention + @connected %>
<% if all_connections.any? %>
<%= render "settings/providers/group_heading",
title: t("settings.providers.groups.your_connections"),
count: all_connections.size %>
<% end %>
<% all_connections.each do |entry| %>
<% auto_open = [ :warn, :err ].include?(entry[:summary][:status]) || all_connections.size == 1 %>
<% sync_action = entry[:partial].present? ? render("settings/providers/sync_button", provider_key: entry[:provider_key], last_synced_at: entry[:summary][:last_synced_at]) : nil %>
<% maturity_label_key = Settings::ProviderCard::MATURITY_LABELS[entry[:maturity]] %>
<% maturity_label = maturity_label_key ? t(maturity_label_key) : nil %>
<% maturity_badge = maturity_label ? content_tag(:span, maturity_label, class: "text-xs font-medium px-1.5 py-0.5 rounded-full bg-alpha-black-50 text-secondary") : nil %>
<% status_pill = render("settings/providers/status_pill", status: entry[:summary][:status]) %>
<%= settings_section title: entry[:title],
collapsible: true,
open: auto_open,
auto_open_param: entry[:auto_open_param],
status: status_pill,
meta: entry[:summary][:meta],
actions: sync_action,
badge: maturity_badge do %>
<% if entry[:configuration] %>
<%= render "settings/providers/provider_form", configuration: entry[:configuration] %>
<% else %>
<turbo-frame id="<%= entry[:turbo_id] %>-providers-panel">
<%= render "settings/providers/#{entry[:partial]}" %>
</turbo-frame>
<% end %>
<% end %>
<% end %>
<% if @available.any? %>
<div data-controller="providers-filter">
<%= render "settings/providers/search_filters", count: @available.size %>
<%= render "settings/providers/group_heading",
title: t("settings.providers.groups.available"),
count: @available.size,
anchor: "available" %>
<p data-providers-filter-target="empty" class="hidden text-sm text-secondary px-1 py-2">
<%= t("settings.providers.empty_filter") %>
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<% @available.each do |entry| %>
<% meta = Provider::Metadata.for(entry[:provider_key]) %>
<%= render Settings::ProviderCard.new(
provider_key: entry[:provider_key],
name: entry[:title],
tagline: t("settings.providers.taglines.#{entry[:provider_key]}", default: nil),
region: meta[:region],
kind: meta[:kind],
tier: meta[:tier],
maturity: meta[:maturity] || :stable,
logo_bg: meta[:logo_bg],
logo_text: meta[:logo_text]
) %>
<% end %>
</div>
</div>
<% else %>
<%= render "settings/providers/group_heading",
title: t("settings.providers.groups.available"),
count: 0,
anchor: "available" %>
<p class="text-sm text-secondary px-1 py-2"><%= t("settings.providers.groups.empty_available") %></p>
<% end %>
<% end %>
</div>