mirror of
https://github.com/we-promise/sure.git
synced 2026-06-07 19:59:00 +00:00
Unify provider and account card UI and move setup actions to menus (#755)
* feat: add auto-open functionality for collapsible sections and streamline unlinked account handling - Introduce `auto-open` Stimulus controller to auto-expand <details> elements based on URL params. - Update all settings sections and panels to support the new `auto_open_param` for seamless navigation. - Improve unlinked account logic for Coinbase, SimpleFIN, and SnapTrade, ensuring consistent and optimized handling. - Refactor sync warnings and badges for better readability and user experience. - Extend localization for additional menu items, warnings, and setup prompts. * fix: improve error handling and safe HTML usage in Coinbase and settings components - Log warning for unhandled exceptions in Coinbase unlinked account count fallback. - Escape `auto_open_param` in settings section for safe HTML injection. - Clean up URL params in `auto-open` controller after auto-expansion. --------- Co-authored-by: luckyPipewrench <luckypipewrench@proton.me>
This commit is contained in:
@@ -44,9 +44,9 @@ module SettingsHelper
|
||||
}
|
||||
end
|
||||
|
||||
def settings_section(title:, subtitle: nil, collapsible: false, open: true, &block)
|
||||
def settings_section(title:, subtitle: nil, collapsible: false, open: true, auto_open_param: nil, &block)
|
||||
content = capture(&block)
|
||||
render partial: "settings/section", locals: { title: title, subtitle: subtitle, content: content, collapsible: collapsible, open: open }
|
||||
render partial: "settings/section", locals: { title: title, subtitle: subtitle, content: content, collapsible: collapsible, open: open, auto_open_param: auto_open_param }
|
||||
end
|
||||
|
||||
def settings_nav_footer
|
||||
|
||||
29
app/javascript/controllers/auto_open_controller.js
Normal file
29
app/javascript/controllers/auto_open_controller.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Controller } from "@hotwired/stimulus";
|
||||
|
||||
// Connects to data-controller="auto-open"
|
||||
// Auto-opens a <details> element based on URL param
|
||||
// Use data-auto-open-param-value="paramName" to open when ?paramName=1 is in URL
|
||||
export default class extends Controller {
|
||||
static values = { param: String };
|
||||
|
||||
connect() {
|
||||
if (!this.hasParamValue || !this.paramValue) return;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get(this.paramValue) === "1") {
|
||||
this.element.open = true;
|
||||
|
||||
// Clean up the URL param after opening
|
||||
params.delete(this.paramValue);
|
||||
const newUrl = params.toString()
|
||||
? `${window.location.pathname}?${params.toString()}${window.location.hash}`
|
||||
: `${window.location.pathname}${window.location.hash}`;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
|
||||
// Scroll into view after opening
|
||||
requestAnimationFrame(() => {
|
||||
this.element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,26 @@ import { Controller } from "@hotwired/stimulus";
|
||||
// Connects to data-controller="lazy-load"
|
||||
// Used with <details> elements to lazy-load content when expanded
|
||||
// Use data-action="toggle->lazy-load#toggled" on the <details> element
|
||||
// Optional: data-lazy-load-auto-open-param-value="paramName" to auto-open when ?paramName=1 is in URL
|
||||
export default class extends Controller {
|
||||
static targets = ["content", "loading", "frame"];
|
||||
static values = { url: String, loaded: Boolean };
|
||||
static values = { url: String, loaded: Boolean, autoOpenParam: String };
|
||||
|
||||
connect() {
|
||||
// Check if we should auto-open based on URL param
|
||||
if (this.hasAutoOpenParamValue && this.autoOpenParamValue) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get(this.autoOpenParamValue) === "1") {
|
||||
this.element.open = true;
|
||||
// Clean up the URL param after opening
|
||||
params.delete(this.autoOpenParamValue);
|
||||
const newUrl = params.toString()
|
||||
? `${window.location.pathname}?${params.toString()}${window.location.hash}`
|
||||
: `${window.location.pathname}${window.location.hash}`;
|
||||
window.history.replaceState({}, "", newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// If already open on connect (browser restored state), load immediately
|
||||
if (this.element.open && !this.loadedValue) {
|
||||
this.load();
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<%# locals: (coinbase_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(coinbase_item) do %>
|
||||
<%# Compute unlinked count early so it's available for both menu and bottom section %>
|
||||
<% unlinked_count = if defined?(@coinbase_unlinked_count_map) && @coinbase_unlinked_count_map
|
||||
@coinbase_unlinked_count_map[coinbase_item.id] || 0
|
||||
else
|
||||
begin
|
||||
coinbase_item.coinbase_accounts
|
||||
.left_joins(:account_provider)
|
||||
.where(account_providers: { id: nil })
|
||||
.count
|
||||
rescue => e
|
||||
Rails.logger.warn("Coinbase card: unlinked_count fallback failed: #{e.class} - #{e.message}")
|
||||
0
|
||||
end
|
||||
end %>
|
||||
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2 focus-visible:outline-hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -55,15 +70,25 @@
|
||||
href: settings_providers_path,
|
||||
frame: "_top"
|
||||
) %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_coinbase_item_path(coinbase_item)
|
||||
href: sync_coinbase_item_path(coinbase_item),
|
||||
disabled: coinbase_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".import_wallets_menu"),
|
||||
icon: "plus",
|
||||
href: setup_accounts_coinbase_item_path(coinbase_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
@@ -93,20 +118,6 @@
|
||||
provider_item: coinbase_item
|
||||
) %>
|
||||
|
||||
<%# Compute unlinked Coinbase accounts (no AccountProvider link) %>
|
||||
<% unlinked_count = if defined?(@coinbase_unlinked_count_map) && @coinbase_unlinked_count_map
|
||||
@coinbase_unlinked_count_map[coinbase_item.id] || 0
|
||||
else
|
||||
begin
|
||||
coinbase_item.coinbase_accounts
|
||||
.left_joins(:account_provider)
|
||||
.where(account_providers: { id: nil })
|
||||
.count
|
||||
rescue => e
|
||||
0
|
||||
end
|
||||
end %>
|
||||
|
||||
<% if unlinked_count.to_i > 0 && coinbase_item.accounts.empty? %>
|
||||
<%# No accounts imported yet - show prominent setup prompt %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
@@ -120,16 +131,6 @@
|
||||
frame: :modal
|
||||
) %>
|
||||
</div>
|
||||
<% elsif unlinked_count.to_i > 0 %>
|
||||
<%# Some accounts imported, more available - show subtle link %>
|
||||
<div class="pt-2 border-t border-primary">
|
||||
<%= link_to setup_accounts_coinbase_item_path(coinbase_item),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "flex items-center gap-2 text-sm text-secondary hover:text-primary transition-colors" do %>
|
||||
<%= icon "plus", size: "sm" %>
|
||||
<span><%= t(".more_wallets_available", count: unlinked_count) %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif coinbase_item.accounts.empty? && coinbase_item.coinbase_accounts.none? %>
|
||||
<%# No coinbase_accounts at all - waiting for sync %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<%# locals: (title:, subtitle: nil, content:, collapsible: false, open: true) %>
|
||||
<%# locals: (title:, subtitle: nil, content:, collapsible: false, open: true, auto_open_param: nil) %>
|
||||
<% if collapsible %>
|
||||
<details <%= "open" if open %> class="group bg-container shadow-border-xs rounded-xl p-4">
|
||||
<details <%= "open" if open %>
|
||||
class="group bg-container shadow-border-xs rounded-xl p-4"
|
||||
<%= "data-controller=\"auto-open\" data-auto-open-param-value=\"#{h(auto_open_param)}\"".html_safe if auto_open_param.present? %>>
|
||||
<summary class="flex items-center justify-between gap-2 cursor-pointer rounded-lg list-none [&::-webkit-details-marker]:hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "text-secondary group-open:transform group-open:rotate-90 transition-transform" %>
|
||||
|
||||
@@ -54,31 +54,26 @@
|
||||
<div class="border-t border-primary pt-4 mt-4">
|
||||
<% if items&.any? %>
|
||||
<% item = items.first %>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<% if item.user_registered? %>
|
||||
<div class="w-2 h-2 bg-success rounded-full"></div>
|
||||
<p class="text-sm text-secondary">
|
||||
<%= t("providers.snaptrade.status_connected", count: item.snaptrade_accounts.count) %>
|
||||
<% if item.unlinked_accounts_count > 0 %>
|
||||
<span class="text-warning">(<%= t("providers.snaptrade.needs_setup", count: item.unlinked_accounts_count) %>)</span>
|
||||
<% end %>
|
||||
</p>
|
||||
<% else %>
|
||||
<div class="w-2 h-2 bg-warning rounded-full"></div>
|
||||
<p class="text-sm text-secondary"><%= t("providers.snaptrade.status_needs_registration") %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if item.user_registered? %>
|
||||
<details class="group mt-4"
|
||||
<details class="group"
|
||||
data-controller="lazy-load"
|
||||
data-action="toggle->lazy-load#toggled"
|
||||
data-lazy-load-url-value="<%= connections_snaptrade_item_path(item) %>">
|
||||
<summary class="flex items-center justify-end gap-1 cursor-pointer text-sm text-secondary hover:text-primary list-none [&::-webkit-details-marker]:hidden">
|
||||
<%= t("providers.snaptrade.manage_connections") %>
|
||||
<%= icon "chevron-right", class: "w-3 h-3 transition-transform group-open:rotate-90" %>
|
||||
data-lazy-load-url-value="<%= connections_snaptrade_item_path(item) %>"
|
||||
data-lazy-load-auto-open-param-value="manage">
|
||||
<summary class="flex items-center justify-between cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-success rounded-full"></div>
|
||||
<p class="text-sm text-secondary">
|
||||
<%= t("providers.snaptrade.status_connected", count: item.snaptrade_accounts.count) %>
|
||||
<% if item.unlinked_accounts_count > 0 %>
|
||||
<span class="text-warning">(<%= t("providers.snaptrade.needs_setup", count: item.unlinked_accounts_count) %>)</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<span class="flex items-center gap-1 text-sm text-secondary hover:text-primary">
|
||||
<%= t("providers.snaptrade.manage_connections") %>
|
||||
<%= icon "chevron-right", class: "w-3 h-3 transition-transform group-open:rotate-90" %>
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="mt-3 space-y-3" data-lazy-load-target="content">
|
||||
@@ -86,7 +81,6 @@
|
||||
<%= t("providers.snaptrade.connection_limit_info") %>
|
||||
</p>
|
||||
|
||||
<%# Loading state - replaced by fetched content %>
|
||||
<div data-lazy-load-target="loading" class="flex items-center gap-2 text-sm text-secondary py-2">
|
||||
<%= icon "loader-2", class: "w-4 h-4 animate-spin" %>
|
||||
<%= t("providers.snaptrade.loading_connections") %>
|
||||
@@ -96,6 +90,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-warning rounded-full"></div>
|
||||
<p class="text-sm text-secondary"><%= t("providers.snaptrade.status_needs_registration") %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</turbo-frame>
|
||||
<% end %>
|
||||
|
||||
<%= settings_section title: "SnapTrade (beta)", collapsible: true, open: false do %>
|
||||
<%= settings_section title: "SnapTrade (beta)", collapsible: true, open: false, auto_open_param: "manage" do %>
|
||||
<turbo-frame id="snaptrade-providers-panel">
|
||||
<%= render "settings/providers/snaptrade_panel" %>
|
||||
</turbo-frame>
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
<%# locals: (simplefin_item:) %>
|
||||
|
||||
<%= tag.div id: dom_id(simplefin_item) do %>
|
||||
<%# Compute unlinked count early so it's available for both menu and bottom section %>
|
||||
<% unlinked_count = if defined?(@simplefin_unlinked_count_map) && @simplefin_unlinked_count_map
|
||||
@simplefin_unlinked_count_map[simplefin_item.id] || 0
|
||||
else
|
||||
begin
|
||||
simplefin_item.simplefin_accounts
|
||||
.left_joins(:account, :account_provider)
|
||||
.where(accounts: { id: nil }, account_providers: { id: nil })
|
||||
.count
|
||||
rescue => e
|
||||
Rails.logger.warn("SimpleFin card: unlinked_count fallback failed: #{e.class} - #{e.message}")
|
||||
0
|
||||
end
|
||||
end %>
|
||||
|
||||
<details open class="group bg-container p-4 shadow-border-xs rounded-xl">
|
||||
<summary class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -16,31 +31,9 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Compute unlinked count early for badge display %>
|
||||
<% header_unlinked_count = if defined?(@simplefin_unlinked_count_map) && @simplefin_unlinked_count_map
|
||||
@simplefin_unlinked_count_map[simplefin_item.id] || 0
|
||||
else
|
||||
begin
|
||||
simplefin_item.simplefin_accounts
|
||||
.left_joins(:account, :account_provider)
|
||||
.where(accounts: { id: nil }, account_providers: { id: nil })
|
||||
.count
|
||||
rescue => e
|
||||
0
|
||||
end
|
||||
end %>
|
||||
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p simplefin_item.name, class: "font-medium text-primary" %>
|
||||
<% if header_unlinked_count.to_i > 0 %>
|
||||
<%= link_to setup_accounts_simplefin_item_path(simplefin_item),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning hover:bg-warning/20 transition-colors" do %>
|
||||
<%= icon "alert-circle", size: "xs" %>
|
||||
<span><%= header_unlinked_count %> <%= header_unlinked_count == 1 ? "account" : "accounts" %> need setup</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if simplefin_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
@@ -49,25 +42,25 @@
|
||||
<p class="text-xs text-secondary">
|
||||
<%= simplefin_item.institution_summary %>
|
||||
</p>
|
||||
<%# Extra inline badges from latest sync stats %>
|
||||
<%# Extra inline badges from latest sync stats - only show warnings %>
|
||||
<% stats = (@simplefin_sync_stats_map || {})[simplefin_item.id] || {} %>
|
||||
<% if stats.present? %>
|
||||
<% has_warnings = stats["accounts_skipped"].to_i > 0 ||
|
||||
stats["rate_limited"].present? ||
|
||||
stats["rate_limited_at"].present? ||
|
||||
stats["total_errors"].to_i > 0 ||
|
||||
(stats["errors"].is_a?(Array) && stats["errors"].any?) %>
|
||||
<% if has_warnings %>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<% if stats["unlinked_accounts"].to_i > 0 %>
|
||||
<%= render DS::Tooltip.new(text: "Accounts need setup", icon: "link-2", size: "sm") %>
|
||||
<span class="text-xs text-secondary">Unlinked: <%= stats["unlinked_accounts"].to_i %></span>
|
||||
<% end %>
|
||||
|
||||
<% if stats["accounts_skipped"].to_i > 0 %>
|
||||
<%= render DS::Tooltip.new(text: "Some accounts were skipped due to errors during sync", icon: "alert-triangle", size: "sm", color: "warning") %>
|
||||
<span class="text-xs text-warning">Skipped: <%= stats["accounts_skipped"].to_i %></span>
|
||||
<%= render DS::Tooltip.new(text: t(".accounts_skipped_tooltip"), icon: "alert-triangle", size: "sm", color: "warning") %>
|
||||
<span class="text-xs text-warning"><%= t(".accounts_skipped_label", count: stats["accounts_skipped"].to_i) %></span>
|
||||
<% end %>
|
||||
|
||||
<% if stats["rate_limited"].present? || stats["rate_limited_at"].present? %>
|
||||
<% ts = stats["rate_limited_at"] %>
|
||||
<% ago = (ts.present? ? (begin; time_ago_in_words(Time.parse(ts)); rescue StandardError; nil; end) : nil) %>
|
||||
<%= render DS::Tooltip.new(
|
||||
text: (ago ? "Rate limited (" + ago + " ago)" : "Rate limited recently"),
|
||||
text: (ago ? t(".rate_limited_ago", time: ago) : t(".rate_limited_recently")),
|
||||
icon: "clock",
|
||||
size: "sm",
|
||||
color: "warning"
|
||||
@@ -80,10 +73,6 @@
|
||||
<%= render DS::Tooltip.new(text: tooltip_text, icon: "alert-octagon", size: "sm", color: "warning") %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if stats["total_accounts"].to_i > 0 %>
|
||||
<span class="text-xs text-secondary">Total: <%= stats["total_accounts"].to_i %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -163,15 +152,25 @@
|
||||
href: edit_simplefin_item_path(simplefin_item),
|
||||
frame: "modal"
|
||||
) %>
|
||||
<% elsif Rails.env.development? %>
|
||||
<% else %>
|
||||
<%= icon(
|
||||
"refresh-cw",
|
||||
as_button: true,
|
||||
href: sync_simplefin_item_path(simplefin_item)
|
||||
href: sync_simplefin_item_path(simplefin_item),
|
||||
disabled: simplefin_item.syncing?
|
||||
) %>
|
||||
<% end %>
|
||||
|
||||
<%= render DS::Menu.new do |menu| %>
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts_menu"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_simplefin_item_path(simplefin_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
@@ -205,23 +204,8 @@
|
||||
institutions_count: simplefin_item.connected_institutions.size
|
||||
) %>
|
||||
|
||||
<%# Compute unlinked SimpleFin accounts (no legacy account and no AccountProvider link)
|
||||
# Prefer controller-provided map; fallback to a local query so the card stays accurate after Turbo broadcasts %>
|
||||
<% unlinked_count = if defined?(@simplefin_unlinked_count_map) && @simplefin_unlinked_count_map
|
||||
@simplefin_unlinked_count_map[simplefin_item.id] || 0
|
||||
else
|
||||
begin
|
||||
simplefin_item.simplefin_accounts
|
||||
.left_joins(:account, :account_provider)
|
||||
.where(accounts: { id: nil }, account_providers: { id: nil })
|
||||
.count
|
||||
rescue => e
|
||||
Rails.logger.warn("SimpleFin card: unlinked_count fallback failed: #{e.class} - #{e.message}")
|
||||
0
|
||||
end
|
||||
end %>
|
||||
|
||||
<% if unlinked_count.to_i > 0 %>
|
||||
<% if unlinked_count.to_i > 0 && simplefin_item.accounts.empty? %>
|
||||
<%# No accounts imported yet - show prominent setup prompt %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-primary font-medium text-sm"><%= t(".setup_needed") %></p>
|
||||
<p class="text-secondary text-sm"><%= t(".setup_description") %></p>
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<%= icon "chevron-right", class: "group-open:transform group-open:rotate-90" %>
|
||||
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-primary/10 rounded-full">
|
||||
<div class="flex items-center justify-center h-8 w-8 bg-success/10 rounded-full">
|
||||
<% if snaptrade_item.logo.attached? %>
|
||||
<%= image_tag snaptrade_item.logo, class: "rounded-full h-full w-full", loading: "lazy" %>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= tag.p snaptrade_item.name.first.upcase, class: "text-primary text-xs font-medium" %>
|
||||
<%= tag.p snaptrade_item.name.first.upcase, class: "text-success text-xs font-medium" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -21,14 +21,6 @@
|
||||
<div class="pl-1 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<%= tag.p snaptrade_item.name, class: "font-medium text-primary" %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<%= link_to setup_accounts_snaptrade_item_path(snaptrade_item),
|
||||
data: { turbo_frame: :modal },
|
||||
class: "inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-warning/10 text-warning hover:bg-warning/20 transition-colors" do %>
|
||||
<%= icon "alert-circle", size: "xs" %>
|
||||
<span><%= t(".accounts_need_setup", count: unlinked_count) %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if snaptrade_item.scheduled_for_deletion? %>
|
||||
<p class="text-destructive text-sm animate-pulse"><%= t(".deletion_in_progress") %></p>
|
||||
<% end %>
|
||||
@@ -89,6 +81,21 @@
|
||||
icon: "plus",
|
||||
href: connect_snaptrade_item_path(snaptrade_item)
|
||||
) %>
|
||||
<% if unlinked_count > 0 %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".setup_accounts_menu"),
|
||||
icon: "settings",
|
||||
href: setup_accounts_snaptrade_item_path(snaptrade_item),
|
||||
frame: :modal
|
||||
) %>
|
||||
<% end %>
|
||||
<% menu.with_item(
|
||||
variant: "link",
|
||||
text: t(".manage_connections"),
|
||||
icon: "cable",
|
||||
href: settings_providers_path(manage: "1")
|
||||
) %>
|
||||
<% menu.with_item(
|
||||
variant: "button",
|
||||
text: t(".delete"),
|
||||
@@ -105,13 +112,6 @@
|
||||
<div class="space-y-4 mt-4">
|
||||
<% if snaptrade_item.accounts.any? %>
|
||||
<%= render "accounts/index/account_groups", accounts: snaptrade_item.accounts %>
|
||||
<div class="flex justify-end pt-2">
|
||||
<%= link_to connect_snaptrade_item_path(snaptrade_item),
|
||||
class: "text-sm text-secondary hover:text-primary flex items-center gap-1 transition-colors" do %>
|
||||
<%= icon "plus", size: "sm" %>
|
||||
<span><%= t(".add_another_brokerage") %></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%# Sync summary (collapsible) - using shared ProviderSyncSummary component %>
|
||||
@@ -124,7 +124,8 @@
|
||||
activities_pending: activities_pending
|
||||
) %>
|
||||
|
||||
<% if unlinked_count > 0 %>
|
||||
<% if unlinked_count > 0 && snaptrade_item.accounts.empty? %>
|
||||
<%# No accounts imported yet - show prominent setup prompt %>
|
||||
<div class="p-4 flex flex-col gap-3 items-center justify-center">
|
||||
<p class="text-primary font-medium text-sm"><%= t(".setup_needed") %></p>
|
||||
<p class="text-secondary text-sm"><%= t(".setup_description") %></p>
|
||||
|
||||
@@ -47,6 +47,7 @@ en:
|
||||
setup_needed: Wallets ready to import
|
||||
setup_description: Select which Coinbase wallets you want to track.
|
||||
setup_action: Import Wallets
|
||||
import_wallets_menu: Import Wallets
|
||||
more_wallets_available:
|
||||
one: "%{count} more wallet available to import"
|
||||
other: "%{count} more wallets available to import"
|
||||
|
||||
@@ -65,6 +65,14 @@ en:
|
||||
setup_needed: New accounts ready to set up
|
||||
setup_description: Choose account types for your newly imported SimpleFIN accounts.
|
||||
setup_action: Set Up New Accounts
|
||||
setup_accounts_menu: Set Up Accounts
|
||||
more_accounts_available:
|
||||
one: "%{count} more account available to set up"
|
||||
other: "%{count} more accounts available to set up"
|
||||
accounts_skipped_tooltip: "Some accounts were skipped due to errors during sync"
|
||||
accounts_skipped_label: "Skipped: %{count}"
|
||||
rate_limited_ago: "Rate limited (%{time} ago)"
|
||||
rate_limited_recently: "Rate limited recently"
|
||||
status: Last synced %{timestamp} ago
|
||||
status_never: Never synced
|
||||
status_with_summary: "Last synced %{timestamp} ago • %{summary}"
|
||||
|
||||
@@ -94,6 +94,11 @@ en:
|
||||
setup_needed: "Accounts need setup"
|
||||
setup_description: "Some accounts from SnapTrade need to be linked to Sure accounts."
|
||||
setup_action: "Setup Accounts"
|
||||
setup_accounts_menu: "Set Up Accounts"
|
||||
manage_connections: "Manage Connections"
|
||||
more_accounts_available:
|
||||
one: "%{count} more account available to set up"
|
||||
other: "%{count} more accounts available to set up"
|
||||
no_accounts_title: "No accounts discovered"
|
||||
no_accounts_description: "Connect a brokerage to import your investment accounts."
|
||||
|
||||
|
||||
Reference in New Issue
Block a user