mirror of
https://github.com/we-promise/sure.git
synced 2026-05-21 19:44:55 +00:00
feat(settings/providers): group providers into Connected and Available
Partition the provider list in the controller into @connected_providers and @available_providers based on provider_summary status, and render each group under its own heading with a count. Auto-open the section when only one provider is connected. Adds an empty-state line when nothing is connected yet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -119,19 +119,32 @@ class Settings::ProvidersController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# Hardcoded family-scoped panels — provider connections are managed through
|
||||
# their own models (SimplefinItem, LunchflowItem, etc.) rather than global
|
||||
# settings, so they need custom UI per-provider for connection management,
|
||||
# status display, and sync actions. The configuration registry excludes
|
||||
# them (see prepare_show_context).
|
||||
FAMILY_PANELS = [
|
||||
{ key: "lunchflow", title: "Lunch Flow", turbo_id: "lunchflow", partial: "lunchflow_panel" },
|
||||
{ key: "simplefin", title: "SimpleFIN", turbo_id: "simplefin", partial: "simplefin_panel" },
|
||||
{ key: "enable_banking", title: "Enable Banking (beta)", turbo_id: "enable_banking", partial: "enable_banking_panel" },
|
||||
{ key: "coinstats", title: "CoinStats (beta)", turbo_id: "coinstats", partial: "coinstats_panel" },
|
||||
{ key: "mercury", title: "Mercury (beta)", turbo_id: "mercury", partial: "mercury_panel" },
|
||||
{ key: "coinbase", title: "Coinbase (beta)", turbo_id: "coinbase", partial: "coinbase_panel" },
|
||||
{ key: "binance", title: "Binance (beta)", turbo_id: "binance", partial: "binance_panel" },
|
||||
{ key: "snaptrade", title: "SnapTrade (beta)", turbo_id: "snaptrade", partial: "snaptrade_panel", auto_open: "manage" },
|
||||
{ key: "indexa_capital", title: "Indexa Capital (alpha)", turbo_id: "indexa_capital", partial: "indexa_capital_panel" },
|
||||
{ key: "sophtron", title: "Sophtron (alpha)", turbo_id: "sophtron", partial: "sophtron_panel" }
|
||||
].freeze
|
||||
|
||||
FAMILY_PANEL_KEYS = FAMILY_PANELS.map { |p| p[:key] }.freeze
|
||||
|
||||
# Prepares instance vars needed by the show view and partials
|
||||
def prepare_show_context
|
||||
# Load all provider configurations (exclude SimpleFin and Lunchflow, which have their own family-specific panels below)
|
||||
# Load all provider configurations (exclude family-scoped panels, which have their own UI below)
|
||||
Provider::Factory.ensure_adapters_loaded
|
||||
@provider_configurations = Provider::ConfigurationRegistry.all.reject do |config|
|
||||
config.provider_key.to_s.casecmp("simplefin").zero? || config.provider_key.to_s.casecmp("lunchflow").zero? || \
|
||||
config.provider_key.to_s.casecmp("enable_banking").zero? || \
|
||||
config.provider_key.to_s.casecmp("sophtron").zero? || \
|
||||
config.provider_key.to_s.casecmp("coinstats").zero? || \
|
||||
config.provider_key.to_s.casecmp("mercury").zero? || \
|
||||
config.provider_key.to_s.casecmp("coinbase").zero? || \
|
||||
config.provider_key.to_s.casecmp("snaptrade").zero? || \
|
||||
config.provider_key.to_s.casecmp("indexa_capital").zero?
|
||||
FAMILY_PANEL_KEYS.any? { |key| config.provider_key.to_s.casecmp(key).zero? }
|
||||
end
|
||||
|
||||
# Providers page only needs to know whether any SimpleFin/Lunchflow connections exist with valid credentials
|
||||
@@ -145,5 +158,36 @@ class Settings::ProvidersController < ApplicationController
|
||||
@coinbase_items = Current.family.coinbase_items.ordered # Coinbase panel needs name and sync info for status display
|
||||
@snaptrade_items = Current.family.snaptrade_items.includes(:snaptrade_accounts).ordered
|
||||
@indexa_capital_items = Current.family.indexa_capital_items.ordered.select(:id)
|
||||
|
||||
entries = build_provider_entries
|
||||
@connected_providers, @available_providers = entries.partition { |entry| entry[:summary][:status] == :ok }
|
||||
end
|
||||
|
||||
# Builds a unified list of provider entries (registry-driven configurations
|
||||
# and hardcoded family panels) with pre-computed status, sorted
|
||||
# alphabetically by display title. Each entry carries enough data for the
|
||||
# view to render either a provider_form or a family panel partial.
|
||||
def build_provider_entries
|
||||
configuration_entries = @provider_configurations.map do |config|
|
||||
{
|
||||
provider_key: config.provider_key.to_s,
|
||||
title: config.provider_key.to_s.titleize,
|
||||
configuration: config,
|
||||
summary: view_context.provider_summary(config.provider_key)
|
||||
}
|
||||
end
|
||||
|
||||
family_entries = FAMILY_PANELS.map do |panel|
|
||||
{
|
||||
provider_key: panel[:key],
|
||||
title: panel[:title],
|
||||
turbo_id: panel[:turbo_id],
|
||||
partial: panel[:partial],
|
||||
auto_open_param: panel[:auto_open],
|
||||
summary: view_context.provider_summary(panel[:key])
|
||||
}
|
||||
end
|
||||
|
||||
(configuration_entries + family_entries).sort_by { |entry| entry[:title].downcase }
|
||||
end
|
||||
end
|
||||
|
||||
12
app/views/settings/providers/_group_heading.html.erb
Normal file
12
app/views/settings/providers/_group_heading.html.erb
Normal file
@@ -0,0 +1,12 @@
|
||||
<%# locals: (title:, count: nil, description: nil) %>
|
||||
<div class="flex items-baseline justify-between gap-3 mt-2 mb-1 px-1">
|
||||
<h2 class="text-sm font-medium text-primary flex items-baseline gap-2">
|
||||
<%= title %>
|
||||
<% if count %>
|
||||
<span class="text-subdued font-normal tabular-nums">· <%= count %></span>
|
||||
<% end %>
|
||||
</h2>
|
||||
<% if description.present? %>
|
||||
<p class="text-xs text-secondary"><%= description %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -20,47 +20,51 @@
|
||||
<% end %>
|
||||
|
||||
<% unless @encryption_error %>
|
||||
<% @provider_configurations.each do |config| %>
|
||||
<% summary = provider_summary(config.provider_key) %>
|
||||
<%= settings_section title: config.provider_key.titleize, collapsible: true, open: false, status: summary[:status], meta: summary[:meta] do %>
|
||||
<%= render "settings/providers/provider_form", configuration: config %>
|
||||
<%= render "settings/providers/group_heading",
|
||||
title: t("settings.providers.groups.connected"),
|
||||
count: @connected_providers.size %>
|
||||
|
||||
<% if @connected_providers.empty? %>
|
||||
<p class="text-sm text-secondary px-1 py-2"><%= t("settings.providers.groups.empty_connected") %></p>
|
||||
<% else %>
|
||||
<%# Auto-open the only-one connected case so the relevant section is immediately visible. %>
|
||||
<% auto_open_only = @connected_providers.size == 1 %>
|
||||
<% @connected_providers.each do |entry| %>
|
||||
<%= settings_section title: entry[:title],
|
||||
collapsible: true,
|
||||
open: auto_open_only,
|
||||
auto_open_param: entry[:auto_open_param],
|
||||
status: entry[:summary][:status],
|
||||
meta: entry[:summary][:meta] 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 %>
|
||||
<% end %>
|
||||
|
||||
<%# Providers below are hardcoded because they manage Family-scoped connections %>
|
||||
<%# (via their own models like SimplefinItem, LunchflowItem, etc.) rather than global settings. %>
|
||||
<%# They require custom UI for connection management, status display, and sync actions. %>
|
||||
<%# The controller excludes them from @provider_configurations (see prepare_show_context). %>
|
||||
<%= render "settings/providers/group_heading",
|
||||
title: t("settings.providers.groups.available"),
|
||||
count: @available_providers.size %>
|
||||
|
||||
<%
|
||||
family_panels = [
|
||||
{ key: "lunchflow", title: "Lunch Flow", turbo_id: "lunchflow", partial: "lunchflow_panel" },
|
||||
{ key: "simplefin", title: "SimpleFIN", turbo_id: "simplefin", partial: "simplefin_panel" },
|
||||
{ key: "enable_banking", title: "Enable Banking (beta)", turbo_id: "enable_banking", partial: "enable_banking_panel" },
|
||||
{ key: "coinstats", title: "CoinStats (beta)", turbo_id: "coinstats", partial: "coinstats_panel" },
|
||||
{ key: "mercury", title: "Mercury (beta)", turbo_id: "mercury", partial: "mercury_panel" },
|
||||
{ key: "coinbase", title: "Coinbase (beta)", turbo_id: "coinbase", partial: "coinbase_panel" },
|
||||
{ key: "binance", title: "Binance (beta)", turbo_id: "binance", partial: "binance_panel" },
|
||||
{ key: "snaptrade", title: "SnapTrade (beta)", turbo_id: "snaptrade", partial: "snaptrade_panel", auto_open: "manage" },
|
||||
{ key: "indexa_capital", title: "Indexa Capital (alpha)", turbo_id: "indexa_capital", partial: "indexa_capital_panel" },
|
||||
{ key: "sophtron", title: "Sophtron (alpha)", turbo_id: "sophtron", partial: "sophtron_panel" },
|
||||
]
|
||||
|
||||
status_order = { ok: 0, warn: 1, err: 2, off: 3 }
|
||||
sorted_panels = family_panels.sort_by { |p| status_order[provider_summary(p[:key])[:status]] || 3 }
|
||||
%>
|
||||
|
||||
<% sorted_panels.each do |panel| %>
|
||||
<% summary = provider_summary(panel[:key]) %>
|
||||
<%= settings_section title: panel[:title],
|
||||
<% @available_providers.each do |entry| %>
|
||||
<%= settings_section title: entry[:title],
|
||||
collapsible: true,
|
||||
open: false,
|
||||
auto_open_param: panel[:auto_open],
|
||||
status: summary[:status],
|
||||
meta: summary[:meta] do %>
|
||||
<turbo-frame id="<%= panel[:turbo_id] %>-providers-panel">
|
||||
<%= render "settings/providers/#{panel[:partial]}" %>
|
||||
</turbo-frame>
|
||||
auto_open_param: entry[:auto_open_param],
|
||||
status: entry[:summary][:status],
|
||||
meta: entry[:summary][:meta] 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 %>
|
||||
<% end %>
|
||||
|
||||
@@ -191,6 +191,10 @@ en:
|
||||
warn: Action needed
|
||||
err: Error
|
||||
off: Not configured
|
||||
groups:
|
||||
connected: Connected
|
||||
available: Available
|
||||
empty_connected: Nothing connected yet — pick a provider below to get started.
|
||||
show:
|
||||
coinbase_title: Coinbase
|
||||
encryption_error:
|
||||
|
||||
@@ -48,4 +48,24 @@ class Settings::ProvidersTest < ApplicationSystemTestCase
|
||||
assert details[:open], "Section should open when clicked"
|
||||
details.assert_text "Setup Token"
|
||||
end
|
||||
|
||||
test "groups providers into Connected and Available with counts" do
|
||||
SimplefinItem.create!(family: @family, name: "Test SimpleFIN", access_url: "https://bridge.simplefin.org/simplefin/access")
|
||||
|
||||
visit settings_providers_path
|
||||
|
||||
connected_heading = find("h2", text: /\AConnected/)
|
||||
assert_match(/· 1\z/, connected_heading.text)
|
||||
|
||||
available_heading = find("h2", text: /\AAvailable/)
|
||||
|
||||
connected_y = connected_heading.native.location.y
|
||||
available_y = available_heading.native.location.y
|
||||
simplefin_y = find("details", text: "SimpleFIN").native.location.y
|
||||
binance_y = find("details", text: "Binance").native.location.y
|
||||
|
||||
assert connected_y < simplefin_y, "Connected heading should appear above SimpleFIN section"
|
||||
assert simplefin_y < available_y, "SimpleFIN should appear above Available heading"
|
||||
assert available_y < binance_y, "Available heading should appear above Binance section"
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user