perf(transactions): preload new form options (#2189)

* perf(transactions): preload new form options

* refactor(transactions): reuse new form account scope
This commit is contained in:
ghost
2026-06-06 09:11:01 -06:00
committed by GitHub
parent a461ff97bb
commit a0f5e56668
4 changed files with 96 additions and 10 deletions

View File

@@ -9,9 +9,7 @@ class TransactionsController < ApplicationController
prefill_params_from_duplicate!
super
apply_duplicate_attributes!
@income_categories = Current.family.categories.incomes.alphabetically
@expense_categories = Current.family.categories.expenses.alphabetically
@categories = Current.family.categories.alphabetically
set_new_transaction_form_options
end
def index
@@ -117,6 +115,7 @@ class TransactionsController < ApplicationController
format.turbo_stream { stream_redirect_back_or_to(account_path(@entry.account)) }
end
else
set_new_transaction_form_options
render :new, status: :unprocessable_entity
end
end
@@ -489,6 +488,21 @@ class TransactionsController < ApplicationController
set_entry
end
def set_new_transaction_form_options
accessible_accounts_scope = accessible_accounts
@account_currencies = accessible_accounts_scope.pluck(:id, :currency).to_h
@manual_accounts = accessible_accounts_scope
.manual
.active
.alphabetically
.includes(:account_providers, logo_attachment: :blob)
.to_a
@categories = Current.family.categories.alphabetically.to_a
@merchants = Current.family.available_merchants_for(Current.user).alphabetically.to_a
@tags = Current.family.tags.alphabetically.to_a
end
# Filters entry_params based on the user's permission on the account.
# read_write users can only annotate (category, tags, notes, merchant).
# read_only users cannot update anything.

View File

@@ -1,7 +1,6 @@
<%# locals: (entry:, categories:) %>
<%# locals: (entry:, account_currencies:, manual_accounts:, categories:, merchants:, tags:) %>
<% account_currencies = Current.family.accounts.map { |a| [a.id, a.currency] }.to_h.to_json %>
<%= styled_form_with model: entry, url: transactions_path, class: "space-y-4", data: { controller: "transaction-form", transaction_form_exchange_rate_url_value: exchange_rate_path, transaction_form_account_currencies_value: account_currencies } do |f| %>
<%= styled_form_with model: entry, url: transactions_path, class: "space-y-4", data: { controller: "transaction-form", transaction_form_exchange_rate_url_value: exchange_rate_path, transaction_form_account_currencies_value: account_currencies.to_json } do |f| %>
<% if entry.errors.any? %>
<%= render "shared/form_errors", model: entry %>
<% end %>
@@ -19,7 +18,7 @@
<% if @entry.account_id %>
<%= f.hidden_field :account_id, data: { transaction_form_target: "account" } %>
<% else %>
<%= f.collection_select :account_id, accessible_accounts.manual.active.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account"), selected: Current.user.default_account_for_transactions&.id, variant: :logo }, required: true, class: "form-field__input text-ellipsis", data: { transaction_form_target: "account", action: "change->transaction-form#checkCurrencyDifference" } %>
<%= f.collection_select :account_id, manual_accounts, :id, :name, { prompt: t(".account_prompt"), label: t(".account"), selected: Current.user.default_account_for_transactions&.id, variant: :logo }, required: true, class: "form-field__input text-ellipsis", data: { transaction_form_target: "account", action: "change->transaction-form#checkCurrencyDifference" } %>
<% end %>
<%= f.money_field :amount,
@@ -87,7 +86,7 @@
<section class="space-y-2">
<%= f.fields_for :entryable do |ef| %>
<%= ef.collection_select :merchant_id,
Current.family.available_merchants_for(Current.user).alphabetically,
merchants,
:id, :name,
{ include_blank: t(".none"),
label: t(".merchant_label"),
@@ -96,7 +95,7 @@
menu_placement: :auto } %>
<%= render DS::TagSelect.new(
form: ef,
tags: Current.family.tags.alphabetically,
tags: tags,
selected_ids: ef.object.tag_ids
) %>
<% end %>

View File

@@ -1,6 +1,12 @@
<%= render DS::Dialog.new(scrollable: false, content_class: "lg:max-h-none lg:overflow-y-auto") do |dialog| %>
<% dialog.with_header(title: t(".new_transaction")) %>
<% dialog.with_body do %>
<%= render "form", entry: @entry, categories: @categories %>
<%= render "form",
entry: @entry,
account_currencies: @account_currencies,
manual_accounts: @manual_accounts,
categories: @categories,
merchants: @merchants,
tags: @tags %>
<% end %>
<% end %>

View File

@@ -462,6 +462,56 @@ end
end
end
test "new preloads transaction form option data" do
family = families(:empty)
user = users(:empty)
sign_in user
manual_account_ids = []
4.times do |idx|
account = family.accounts.create!(
name: "Manual Account #{idx}",
balance: 0,
currency: "USD",
accountable: Depository.new
)
assert Account.manual.active.exists?(id: account.id), "Account should be included in the manual active scope"
manual_account_ids << account.id
family.categories.create!(
name: "Category #{idx}",
color: "#000000",
lucide_icon: "shapes"
)
family.merchants.create!(name: "Merchant #{idx}")
family.tags.create!(name: "Tag #{idx}")
end
inaccessible_account = families(:dylan_family).accounts.create!(
name: "Other Family Account",
balance: 0,
currency: "EUR",
accountable: Depository.new
)
queries = capture_sql_queries { get new_transaction_url }
assert_response :success
assert_select "input[name='entry[account_id]']"
assert_select "input[name='entry[entryable_attributes][category_id]']"
assert_select "input[name='entry[entryable_attributes][merchant_id]']"
assert_select "form[data-transaction-form-account-currencies-value]" do |forms|
account_currencies = JSON.parse(forms.first["data-transaction-form-account-currencies-value"])
manual_account_ids.each do |account_id|
assert_equal "USD", account_currencies[account_id.to_s]
end
assert_nil account_currencies[inaccessible_account.id.to_s]
end
assert_empty queries.grep(/FROM "account_providers" WHERE "account_providers"\."account_id" =/)
assert_operator queries.grep(/FROM "active_storage_attachments" WHERE "active_storage_attachments"\."record_id" =/).size, :<=, 1
assert_operator queries.grep(/SELECT "categories"\.\* FROM "categories" WHERE "categories"\."family_id" =/).size, :<=, 1
end
test "unlock clears import_locked flag" do
family = families(:empty)
sign_in users(:empty)
@@ -634,4 +684,21 @@ end
created_entry = Entry.order(:created_at).last
assert_nil created_entry.transaction.extra["exchange_rate"]
end
private
def capture_sql_queries
queries = []
callback = lambda do |_name, _started, _finished, _unique_id, payload|
next if payload[:cached]
next if %w[SCHEMA TRANSACTION].include?(payload[:name])
queries << payload[:sql].squish
end
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
yield
end
queries
end
end