1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00

Preserve transaction filters and transaction focus across page visits (#1733)

* Preserve transaction filters across page visits

* Preserve params when per_page is updated

* Autofocus selected transactions

* Lint fixes

* Fix syntax error

* Fix filter clearing

* Update e2e tests for new UI

* Consolidate focus behavior into concern

* Lint fixes
This commit is contained in:
Zach Gollwitzer 2025-01-30 14:12:01 -05:00 committed by GitHub
parent 0b17976256
commit 282c05345d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 310 additions and 243 deletions

View file

@ -1,26 +0,0 @@
class Account::EntriesController < ApplicationController
layout :with_sidebar
before_action :set_account
def index
@q = search_params
@pagy, @entries = pagy(entries_scope.search(@q).reverse_chronological, limit: params[:per_page] || "10")
end
private
def set_account
@account = Current.family.accounts.find(params[:account_id])
end
def entries_scope
scope = Current.family.entries
scope = scope.where(account: @account) if @account
scope
end
def search_params
params.fetch(:q, {})
.permit(:search)
end
end

View file

@ -2,6 +2,8 @@ module AccountableResource
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
include ScrollFocusable
layout :with_sidebar layout :with_sidebar
before_action :set_account, only: [ :show, :edit, :update, :destroy ] before_action :set_account, only: [ :show, :edit, :update, :destroy ]
before_action :set_link_token, only: :new before_action :set_link_token, only: :new
@ -22,6 +24,12 @@ module AccountableResource
end end
def show def show
@q = params.fetch(:q, {}).permit(:search)
entries = @account.entries.search(@q).reverse_chronological
set_focused_record(entries, params[:focused_record_id])
@pagy, @entries = pagy(entries, limit: params[:per_page] || "10", params: ->(params) { params.except(:focused_record_id) })
end end
def edit def edit

View file

@ -0,0 +1,21 @@
module ScrollFocusable
extend ActiveSupport::Concern
def set_focused_record(record_scope, record_id, default_per_page: 10)
return unless record_id.present?
@focused_record = record_scope.find_by(id: record_id)
record_index = record_scope.pluck(:id).index(record_id)
return unless record_index
page_of_focused_record = (record_index / (params[:per_page]&.to_i || default_per_page)) + 1
if params[:page]&.to_i != page_of_focused_record
(
redirect_to(url_for(page: page_of_focused_record, focused_record_id: record_id))
)
end
end
end

View file

@ -1,10 +1,21 @@
class TransactionsController < ApplicationController class TransactionsController < ApplicationController
include ScrollFocusable
layout :with_sidebar layout :with_sidebar
before_action :store_params!, only: :index
def index def index
@q = search_params @q = search_params
search_query = Current.family.transactions.search(@q).reverse_chronological search_query = Current.family.transactions.search(@q).reverse_chronological
@pagy, @transaction_entries = pagy(search_query, limit: params[:per_page] || "50")
set_focused_record(search_query, params[:focused_record_id], default_per_page: 50)
@pagy, @transaction_entries = pagy(
search_query,
limit: params[:per_page].presence || default_params[:per_page],
params: ->(params) { params.except(:focused_record_id) }
)
totals_query = search_query.incomes_and_expenses totals_query = search_query.incomes_and_expenses
family_currency = Current.family.currency family_currency = Current.family.currency
@ -18,13 +29,76 @@ class TransactionsController < ApplicationController
} }
end end
def clear_filter
updated_params = stored_params.deep_dup
q_params = updated_params["q"] || {}
param_key = params[:param_key]
param_value = params[:param_value]
if q_params[param_key].is_a?(Array)
q_params[param_key].delete(param_value)
q_params.delete(param_key) if q_params[param_key].empty?
else
q_params.delete(param_key)
end
updated_params["q"] = q_params.presence
Current.session.update!(prev_transaction_page_params: updated_params)
redirect_to transactions_path(updated_params)
end
private private
def search_params def search_params
params.fetch(:q, {}) cleaned_params = params.fetch(:q, {})
.permit( .permit(
:start_date, :end_date, :search, :amount, :start_date, :end_date, :search, :amount,
:amount_operator, accounts: [], account_ids: [], :amount_operator, accounts: [], account_ids: [],
categories: [], merchants: [], types: [], tags: [] categories: [], merchants: [], types: [], tags: []
) )
.to_h
.compact_blank
cleaned_params.delete(:amount_operator) unless cleaned_params[:amount].present?
cleaned_params
end
def store_params!
if should_restore_params?
params_to_restore = {}
params_to_restore[:q] = stored_params["q"].presence || default_params[:q]
params_to_restore[:page] = stored_params["page"].presence || default_params[:page]
params_to_restore[:per_page] = stored_params["per_page"].presence || default_params[:per_page]
redirect_to transactions_path(params_to_restore)
else
Current.session.update!(
prev_transaction_page_params: {
q: search_params,
page: params[:page],
per_page: params[:per_page]
}
)
end
end
def should_restore_params?
request.query_parameters.blank? && (stored_params["q"].present? || stored_params["page"].present? || stored_params["per_page"].present?)
end
def stored_params
Current.session.prev_transaction_page_params
end
def default_params
{
q: {},
page: 1,
per_page: 50
}
end end
end end

View file

@ -176,24 +176,4 @@ module ApplicationHelper
cookies[:admin] == "true" cookies[:admin] == "true"
end end
def custom_pagy_url_for(pagy, page, current_path: nil)
if current_path.blank?
pagy_url_for(pagy, page)
else
uri = URI.parse(current_path)
params = URI.decode_www_form(uri.query || "").to_h
# Delete existing page param if it exists
params.delete("page")
# Add new page param unless it's page 1
params["page"] = page unless page == 1
if params.empty?
uri.path
else
"#{uri.path}?#{URI.encode_www_form(params)}"
end
end
end
end end

View file

@ -18,21 +18,4 @@ module TransactionsHelper
def get_default_transaction_search_filter def get_default_transaction_search_filter
transaction_search_filters[0] transaction_search_filters[0]
end end
def transactions_path_without_param(param_key, param_value)
updated_params = request.query_parameters.deep_dup
q_params = updated_params[:q] || {}
current_value = q_params[param_key]
if current_value.is_a?(Array)
q_params[param_key] = current_value - [ param_value ]
else
q_params.delete(param_key)
end
updated_params[:q] = q_params
transactions_path(updated_params)
end
end end

View file

@ -0,0 +1,21 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="focus-record"
export default class extends Controller {
static values = {
id: String,
};
connect() {
const element = document.getElementById(this.idValue);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
// Remove the focused_record_id parameter from URL
const url = new URL(window.location);
url.searchParams.delete("focused_record_id");
window.history.replaceState({}, "", url);
}
}
}

View file

@ -0,0 +1,20 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="selectable-link"
export default class extends Controller {
connect() {
this.element.addEventListener("change", this.handleChange.bind(this));
}
disconnect() {
this.element.removeEventListener("change", this.handleChange.bind(this));
}
handleChange(event) {
const paramName = this.element.name;
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set(paramName, event.target.value);
Turbo.visit(currentUrl.toString());
}
}

View file

@ -1,93 +0,0 @@
<%= turbo_frame_tag dom_id(@account, "entries") do %>
<div class="bg-white p-5 border border-alpha-black-25 rounded-xl shadow-xs">
<div class="flex items-center justify-between mb-4">
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
<% unless @account.plaid_account_id.present? %>
<div data-controller="menu" data-testid="activity-menu">
<button class="btn btn--secondary flex items-center gap-2" data-menu-target="button">
<%= lucide_icon("plus", class: "w-4 h-4") %>
<%= tag.span t(".new") %>
</button>
<div data-menu-target="content" class="z-10 hidden bg-white rounded-lg border border-alpha-black-25 shadow-xs p-1">
<%= link_to new_account_valuation_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
<%= tag.span t(".new_balance"), class: "text-sm" %>
<% end %>
<% unless @account.crypto? %>
<%= link_to @account.investment? ? new_account_trade_path(account_id: @account.id) : new_account_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("credit-card", class: "text-gray-500 w-5 h-5") %>
<%= tag.span t(".new_transaction"), class: "text-sm" %>
<% end %>
<% end %>
</div>
</div>
<% end %>
</div>
<div>
<%= form_with url: account_entries_path,
id: "entries-search",
scope: :q,
method: :get,
data: { controller: "auto-submit-form" } do |form| %>
<div class="flex gap-2 mb-4">
<div class="grow">
<div class="flex items-center px-3 py-2 gap-2 border border-gray-200 rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900">
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500") %>
<%= hidden_field_tag :account_id, @account.id %>
<%= form.search_field :search,
placeholder: "Search entries by name",
value: @q[:search],
class: "form-field__input placeholder:text-sm placeholder:text-gray-500",
"data-auto-submit-form-target": "auto" %>
</div>
</div>
</div>
<% end %>
</div>
<% if @entries.empty? %>
<p class="text-gray-500 text-sm p-4"><%= t(".no_entries") %></p>
<% else %>
<%= tag.div id: dom_id(@account, "entries_bulk_select"),
data: {
controller: "bulk-select",
bulk_select_singular_label_value: t(".entry"),
bulk_select_plural_label_value: t(".entries")
} do %>
<div id="entry-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
<%= render "account/entries/selection_bar" %>
</div>
<div class="grid bg-gray-25 rounded-xl grid-cols-12 items-center uppercase text-xs font-medium text-gray-500 px-5 py-3 mb-4">
<div class="pl-0.5 col-span-8 flex items-center gap-4">
<%= check_box_tag "selection_entry",
class: "maybe-checkbox maybe-checkbox--light",
data: { action: "bulk-select#togglePageSelection" } %>
<p><%= t(".date") %></p>
</div>
<%= tag.p t(".amount"), class: "col-span-2 justify-self-end" %>
<%= tag.p t(".balance"), class: "col-span-2 justify-self-end" %>
</div>
<div>
<div class="rounded-tl-lg rounded-tr-lg bg-white border-alpha-black-25 shadow-xs">
<div class="space-y-4">
<% calculator = Account::BalanceTrendCalculator.for(@entries) %>
<%= entries_by_date(@entries) do |entries| %>
<% entries.each do |entry| %>
<%= render entry, balance_trend: calculator&.trend_for(entry) %>
<% end %>
<% end %>
</div>
</div>
<div class="p-4 bg-white rounded-bl-lg rounded-br-lg">
<%= render "pagination", pagy: @pagy, current_path: account_path(@account, page: params[:page], tab: params[:tab]) %>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>

View file

@ -1,7 +1,7 @@
<%# locals: (entry:, selectable: true, balance_trend: nil) %> <%# locals: (entry:, selectable: true, balance_trend: nil) %>
<% transaction, account = entry.account_transaction, entry.account %> <% transaction, account = entry.account_transaction, entry.account %>
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4"> <div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4 <%= @focused_record == entry ? "border border-gray-900 rounded-lg" : "" %>">
<div class="pr-10 flex items-center gap-4 <%= balance_trend ? "col-span-6" : "col-span-8" %>"> <div class="pr-10 flex items-center gap-4 <%= balance_trend ? "col-span-6" : "col-span-8" %>">
<% if selectable %> <% if selectable %>
<%= check_box_tag dom_id(entry, "selection"), <%= check_box_tag dom_id(entry, "selection"),
@ -45,7 +45,7 @@
<% if entry.account_transaction.transfer? %> <% if entry.account_transaction.transfer? %>
<%= render "transfers/account_links", transfer: entry.account_transaction.transfer, is_inflow: entry.account_transaction.transfer_as_inflow.present? %> <%= render "transfers/account_links", transfer: entry.account_transaction.transfer, is_inflow: entry.account_transaction.transfer_as_inflow.present? %>
<% else %> <% else %>
<%= link_to entry.account.name, account_path(entry.account, tab: "transactions"), data: { turbo_frame: "_top" }, class: "hover:underline" %> <%= link_to entry.account.name, account_path(entry.account, tab: "transactions", focused_record_id: entry.id), data: { turbo_frame: "_top" }, class: "hover:underline" %>
<% end %> <% end %>
</div> </div>
</div> </div>

View file

@ -1,5 +1,95 @@
<%# locals: (account:) %> <%# locals: (account:) %>
<%= turbo_frame_tag dom_id(account, :entries), src: account_entries_path(account_id: account.id, page: params[:page], tab: params[:tab]) do %> <%= turbo_frame_tag dom_id(account, "entries") do %>
<%= render "account/entries/loading" %> <div class="bg-white p-5 border border-alpha-black-25 rounded-xl shadow-xs" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
<div class="flex items-center justify-between mb-4">
<%= tag.h2 t(".title"), class: "font-medium text-lg" %>
<% unless @account.plaid_account_id.present? %>
<div data-controller="menu" data-testid="activity-menu">
<button class="btn btn--secondary flex items-center gap-2" data-menu-target="button">
<%= lucide_icon("plus", class: "w-4 h-4") %>
<%= tag.span t(".new") %>
</button>
<div data-menu-target="content" class="z-10 hidden bg-white rounded-lg border border-alpha-black-25 shadow-xs p-1">
<%= link_to new_account_valuation_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("circle-dollar-sign", class: "text-gray-500 w-5 h-5") %>
<%= tag.span t(".new_balance"), class: "text-sm" %>
<% end %>
<% unless @account.crypto? %>
<%= link_to @account.investment? ? new_account_trade_path(account_id: @account.id) : new_account_transaction_path(account_id: @account.id), data: { turbo_frame: :modal }, class: "block p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2" do %>
<%= lucide_icon("credit-card", class: "text-gray-500 w-5 h-5") %>
<%= tag.span t(".new_transaction"), class: "text-sm" %>
<% end %>
<% end %>
</div>
</div>
<% end %>
</div>
<div>
<%= form_with url: account_path(account),
id: "entries-search",
scope: :q,
method: :get,
data: { controller: "auto-submit-form" } do |form| %>
<div class="flex gap-2 mb-4">
<div class="grow">
<div class="flex items-center px-3 py-2 gap-2 border border-gray-200 rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900">
<%= lucide_icon("search", class: "w-5 h-5 text-gray-500") %>
<%= hidden_field_tag :account_id, @account.id %>
<%= form.search_field :search,
placeholder: "Search entries by name",
value: @q[:search],
class: "form-field__input placeholder:text-sm placeholder:text-gray-500",
"data-auto-submit-form-target": "auto" %>
</div>
</div>
</div>
<% end %>
</div>
<% if @entries.empty? %>
<p class="text-gray-500 text-sm p-4"><%= t(".no_entries") %></p>
<% else %>
<%= tag.div id: dom_id(@account, "entries_bulk_select"),
data: {
controller: "bulk-select",
bulk_select_singular_label_value: t(".entry"),
bulk_select_plural_label_value: t(".entries")
} do %>
<div id="entry-selection-bar" data-bulk-select-target="selectionBar" class="flex justify-center hidden">
<%= render "account/entries/selection_bar" %>
</div>
<div class="grid bg-gray-25 rounded-xl grid-cols-12 items-center uppercase text-xs font-medium text-gray-500 px-5 py-3 mb-4">
<div class="pl-0.5 col-span-8 flex items-center gap-4">
<%= check_box_tag "selection_entry",
class: "maybe-checkbox maybe-checkbox--light",
data: { action: "bulk-select#togglePageSelection" } %>
<p><%= t(".date") %></p>
</div>
<%= tag.p t(".amount"), class: "col-span-2 justify-self-end" %>
<%= tag.p t(".balance"), class: "col-span-2 justify-self-end" %>
</div>
<div>
<div class="rounded-tl-lg rounded-tr-lg bg-white border-alpha-black-25 shadow-xs">
<div class="space-y-4">
<% calculator = Account::BalanceTrendCalculator.for(@entries) %>
<%= entries_by_date(@entries) do |entries| %>
<% entries.each do |entry| %>
<%= render entry, balance_trend: calculator&.trend_for(entry) %>
<% end %>
<% end %>
</div>
</div>
<div class="p-4 bg-white rounded-bl-lg rounded-br-lg">
<%= render "pagination", pagy: @pagy %>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %> <% end %>

View file

@ -1,11 +1,12 @@
<%# locals: (pagy:, current_path: nil) %> <%# locals: (pagy:) %>
<nav class="flex w-full items-center justify-between"> <nav class="flex w-full items-center justify-between">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div> <div>
<% if pagy.prev %> <% if pagy.prev %>
<%= link_to custom_pagy_url_for(pagy, pagy.prev, current_path: current_path), <%= link_to pagy_url_for(pagy, pagy.prev),
class: "inline-flex items-center p-2 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700", data: { turbo_frame: :_top },
data: (current_path ? { turbo_frame: "_top" } : {}) do %> class: "inline-flex items-center p-2 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" do %>
<%= lucide_icon("chevron-left", class: "w-5 h-5 text-gray-500") %> <%= lucide_icon("chevron-left", class: "w-5 h-5 text-gray-500") %>
<% end %> <% end %>
<% else %> <% else %>
@ -17,15 +18,15 @@
<div class="rounded-xl p-1 bg-gray-25"> <div class="rounded-xl p-1 bg-gray-25">
<% pagy.series.each do |series_item| %> <% pagy.series.each do |series_item| %>
<% if series_item.is_a?(Integer) %> <% if series_item.is_a?(Integer) %>
<%= link_to custom_pagy_url_for(pagy, series_item, current_path: current_path), <%= link_to pagy_url_for(pagy, series_item),
class: "rounded-md px-2 py-1 inline-flex items-center text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700", data: { turbo_frame: :_top },
data: (current_path ? { turbo_frame: "_top" } : {}) do %> class: "rounded-md px-2 py-1 inline-flex items-center text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" do %>
<%= series_item %> <%= series_item %>
<% end %> <% end %>
<% elsif series_item.is_a?(String) %> <% elsif series_item.is_a?(String) %>
<%= link_to custom_pagy_url_for(pagy, series_item, current_path: current_path), <%= link_to pagy_url_for(pagy, series_item),
class: "rounded-md px-2 py-1 bg-white border border-alpha-black-25 shadow-xs inline-flex items-center text-sm font-medium text-gray-900", data: { turbo_frame: :_top },
data: (current_path ? { turbo_frame: "_top" } : {}) do %> class: "rounded-md px-2 py-1 bg-white border border-alpha-black-25 shadow-xs inline-flex items-center text-sm font-medium text-gray-900" do %>
<%= series_item %> <%= series_item %>
<% end %> <% end %>
<% elsif series_item == :gap %> <% elsif series_item == :gap %>
@ -35,9 +36,9 @@
</div> </div>
<div> <div>
<% if pagy.next %> <% if pagy.next %>
<%= link_to custom_pagy_url_for(pagy, pagy.next, current_path: current_path), <%= link_to pagy_url_for(pagy, pagy.next),
class: "inline-flex items-center p-2 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700", data: { turbo_frame: :_top },
data: (current_path ? { turbo_frame: "_top" } : {}) do %> class: "inline-flex items-center p-2 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700" do %>
<%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %> <%= lucide_icon("chevron-right", class: "w-5 h-5 text-gray-500") %>
<% end %> <% end %>
<% else %> <% else %>
@ -48,16 +49,9 @@
</div> </div>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<%= form_with url: custom_pagy_url_for(pagy, pagy.page, current_path: current_path), <%= select_tag :per_page,
method: :get, options_for_select(["10", "20", "30", "50"], pagy.limit),
class: "flex items-center gap-4", data: { controller: "selectable-link" },
data: { controller: "auto-submit-form" } do |f| %> class: "py-1.5 pr-8 text-sm text-gray-900 font-medium border border-gray-200 rounded-lg focus:border-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900" %>
<%= f.label :per_page, t(".rows_per_page"), class: "text-sm text-gray-500" %>
<%= f.select :per_page,
options_for_select(["10", "20", "30", "50"], pagy.limit),
{},
class: "py-1.5 pr-8 text-sm text-gray-900 font-medium border border-gray-200 rounded-lg focus:border-gray-900 focus:ring-gray-900 focus-visible:ring-gray-900",
data: { "auto-submit-form-target": "auto" } %>
<% end %>
</div> </div>
</nav> </nav>

View file

@ -1,6 +1,6 @@
<%= render "accounts/show/template", <%= render "accounts/show/template",
account: @account, account: @account,
tabs: render("accounts/show/tabs", account: @account, tabs: [ tabs: render("accounts/show/tabs", account: @account, tabs: [
{ key: "activity", contents: render("accounts/show/activity", account: @account) },
{ key: "overview", contents: render("credit_cards/overview", account: @account) }, { key: "overview", contents: render("credit_cards/overview", account: @account) },
{ key: "activity", contents: render("accounts/show/activity", account: @account) }
]) %> ]) %>

View file

@ -16,8 +16,8 @@
<div class="min-h-[800px]"> <div class="min-h-[800px]">
<%= render "accounts/show/tabs", account: @account, tabs: [ <%= render "accounts/show/tabs", account: @account, tabs: [
{ key: "holdings", contents: render("investments/holdings_tab", account: @account) },
{ key: "activity", contents: render("accounts/show/activity", account: @account) }, { key: "activity", contents: render("accounts/show/activity", account: @account) },
{ key: "holdings", contents: render("investments/holdings_tab", account: @account) },
] %> ] %>
</div> </div>
<% end %> <% end %>

View file

@ -1,6 +1,6 @@
<%= render "accounts/show/template", <%= render "accounts/show/template",
account: @account, account: @account,
tabs: render("accounts/show/tabs", account: @account, tabs: [ tabs: render("accounts/show/tabs", account: @account, tabs: [
{ key: "activity", contents: render("accounts/show/activity", account: @account) },
{ key: "overview", contents: render("loans/overview", account: @account) }, { key: "overview", contents: render("loans/overview", account: @account) },
{ key: "activity", contents: render("accounts/show/activity", account: @account) }
]) %> ]) %>

View file

@ -2,6 +2,6 @@
account: @account, account: @account,
header: render("accounts/show/header", account: @account, subtitle: @account.property.address), header: render("accounts/show/header", account: @account, subtitle: @account.property.address),
tabs: render("accounts/show/tabs", account: @account, tabs: [ tabs: render("accounts/show/tabs", account: @account, tabs: [
{ key: "activity", contents: render("accounts/show/activity", account: @account) },
{ key: "overview", contents: render("properties/overview", account: @account) }, { key: "overview", contents: render("properties/overview", account: @account) },
{ key: "activity", contents: render("accounts/show/activity", account: @account) }
]) %> ]) %>

View file

@ -1,4 +1,4 @@
<div class="space-y-4 h-full flex flex-col overflow-y-auto"> <div class="space-y-4 h-full flex flex-col overflow-y-auto" data-controller="focus-record" data-focus-record-id-value="<%= @focused_record ? dom_id(@focused_record) : nil %>">
<%= render "header" %> <%= render "header" %>
<%= render "summary", totals: @totals %> <%= render "summary", totals: @totals %>

View file

@ -3,6 +3,8 @@
scope: :q, scope: :q,
method: :get, method: :get,
data: { controller: "auto-submit-form" } do |form| %> data: { controller: "auto-submit-form" } do |form| %>
<%= hidden_field_tag :per_page, params[:per_page] %>
<div class="flex gap-2 mb-4"> <div class="flex gap-2 mb-4">
<div class="grow"> <div class="grow">
<div class="flex items-center px-3 py-2 gap-2 border border-gray-200 rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900"> <div class="flex items-center px-3 py-2 gap-2 border border-gray-200 rounded-lg focus-within:ring-gray-100 focus-within:border-gray-900">

View file

@ -33,7 +33,7 @@
<div class="flex justify-between items-center gap-2 bg-white p-3"> <div class="flex justify-between items-center gap-2 bg-white p-3">
<div> <div>
<% if @q.present? %> <% if @q.present? %>
<%= link_to t(".clear_filters"), transactions_path, class: "btn btn--ghost" %> <%= link_to t(".clear_filters"), transactions_path(clear_filters: true), class: "btn btn--ghost" %>
<% end %> <% end %>
</div> </div>

View file

@ -41,7 +41,7 @@
</div> </div>
<% end %> <% end %>
<%= link_to transactions_path_without_param(param_key, param_value), data: { id: "clear-param-btn", turbo: false }, class: "flex items-center" do %> <%= button_to clear_filter_transactions_path(param_key: param_key, param_value: param_value), method: :delete, data: { turbo: false }, class: "flex items-center" do %>
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %> <%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
<% end %> <% end %>
</li> </li>

View file

@ -1,6 +1,6 @@
<%= render "accounts/show/template", <%= render "accounts/show/template",
account: @account, account: @account,
tabs: render("accounts/show/tabs", account: @account, tabs: [ tabs: render("accounts/show/tabs", account: @account, tabs: [
{ key: "activity", contents: render("accounts/show/activity", account: @account) },
{ key: "overview", contents: render("vehicles/overview", account: @account) }, { key: "overview", contents: render("vehicles/overview", account: @account) },
{ key: "activity", contents: render("accounts/show/activity", account: @account) }
]) %> ]) %>

View file

@ -6,16 +6,17 @@ en:
transfer: transfer:
attributes: attributes:
base: base:
inflow_cannot_be_in_multiple_transfers: Inflow transaction cannot be
part of multiple transfers
must_be_from_different_accounts: Transfer must have different accounts must_be_from_different_accounts: Transfer must have different accounts
must_be_from_same_family: Transfer must be from the same family
must_be_within_date_range: Transfer transaction dates must be within must_be_within_date_range: Transfer transaction dates must be within
4 days of each other 4 days of each other
must_have_opposite_amounts: Transfer transactions must have opposite must_have_opposite_amounts: Transfer transactions must have opposite
amounts amounts
must_have_single_currency: Transfer must have a single currency must_have_single_currency: Transfer must have a single currency
must_be_from_same_family: Transfer must be from the same family outflow_cannot_be_in_multiple_transfers: Outflow transaction cannot
inflow_cannot_be_in_multiple_transfers: Inflow transaction cannot be part of multiple transfers be part of multiple transfers
outflow_cannot_be_in_multiple_transfers: Outflow transaction cannot be part of multiple transfers
transfer: transfer:
name: Transfer to %{to_account} name: Transfer to %{to_account}
payment_name: Payment to %{to_account} payment_name: Payment to %{to_account}

View file

@ -9,17 +9,6 @@ en:
empty: empty:
description: Try adding an entry, editing filters or refining your search description: Try adding an entry, editing filters or refining your search
title: No entries found title: No entries found
index:
amount: Amount
balance: Balance
date: Date
entries: entries
entry: entry
new: New
new_balance: New balance
new_transaction: New transaction
no_entries: No entries found
title: Activity
loading: loading:
loading: Loading entries... loading: Loading entries...
update: update:

View file

@ -34,6 +34,17 @@ en:
title: How would you like to add it? title: How would you like to add it?
title: What would you like to add? title: What would you like to add?
show: show:
activity:
amount: Amount
balance: Balance
date: Date
entries: entries
entry: entry
new: New
new_balance: New balance
new_transaction: New transaction
no_entries: No entries found
title: Activity
chart: chart:
balance: Balance balance: Balance
owed: Amount owed owed: Amount owed

View file

@ -1,8 +1,5 @@
--- ---
en: en:
application:
pagination:
rows_per_page: Rows per page
number: number:
currency: currency:
format: format:

View file

@ -15,10 +15,10 @@ en:
form: form:
placeholder: Category name placeholder: Category name
index: index:
categories: Categories
bootstrap: Use default categories bootstrap: Use default categories
categories_incomes: Income categories categories: Categories
categories_expenses: Expense categories categories_expenses: Expense categories
categories_incomes: Income categories
empty: No categories found empty: No categories found
new: New category new: New category
menu: menu:

View file

@ -15,8 +15,8 @@ en:
description: Issue Description description: Issue Description
sidebar: sidebar:
accounts: Accounts accounts: Accounts
budgeting: Budgeting
dashboard: Dashboard dashboard: Dashboard
new_account: New account new_account: New account
portfolio: Portfolio portfolio: Portfolio
transactions: Transactions transactions: Transactions
budgeting: Budgeting

View file

@ -82,8 +82,6 @@ Rails.application.routes.draw do
namespace :account do namespace :account do
resources :holdings, only: %i[index new show destroy] resources :holdings, only: %i[index new show destroy]
resources :entries, only: :index
resources :transactions, only: %i[show new create update destroy] do resources :transactions, only: %i[show new create update destroy] do
resource :transfer_match, only: %i[new create] resource :transfer_match, only: %i[new create]
resource :category, only: :update, controller: :transaction_categories resource :category, only: :update, controller: :transaction_categories
@ -109,7 +107,11 @@ Rails.application.routes.draw do
end end
end end
resources :transactions, only: :index resources :transactions, only: :index do
collection do
delete :clear_filter
end
end
# Convenience routes for polymorphic paths # Convenience routes for polymorphic paths
# Example: account_path(Account.new(accountable: Depository.new)) => /depositories/123 # Example: account_path(Account.new(accountable: Depository.new)) => /depositories/123

View file

@ -0,0 +1,5 @@
class StoreTransactionFiltersInSession < ActiveRecord::Migration[7.2]
def change
add_column :sessions, :prev_transaction_page_params, :jsonb, default: {}
end
end

3
db/schema.rb generated
View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_01_24_224316) do ActiveRecord::Schema[7.2].define(version: 2025_01_28_203303) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -569,6 +569,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_01_24_224316) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.uuid "active_impersonator_session_id" t.uuid "active_impersonator_session_id"
t.datetime "subscribed_at" t.datetime "subscribed_at"
t.jsonb "prev_transaction_page_params", default: {}
t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id" t.index ["active_impersonator_session_id"], name: "index_sessions_on_active_impersonator_session_id"
t.index ["user_id"], name: "index_sessions_on_user_id" t.index ["user_id"], name: "index_sessions_on_user_id"
end end

View file

@ -5,7 +5,7 @@ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
Capybara.default_max_wait_time = 5 Capybara.default_max_wait_time = 5
end end
driven_by :selenium, using: ENV["CI"].present? ? :headless_chrome : :chrome, screen_size: [ 1400, 1400 ] driven_by :selenium, using: ENV["CI"].present? ? :headless_chrome : ENV.fetch("E2E_BROWSER", :chrome).to_sym, screen_size: [ 1400, 1400 ]
private private

View file

@ -1,13 +0,0 @@
require "test_helper"
class Account::EntriesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in @user = users(:family_admin)
@entry = account_entries(:transaction)
end
test "gets index" do
get account_entries_path(account_id: @entry.account.id)
assert_response :success
end
end

View file

@ -79,7 +79,7 @@ class TradesTest < ApplicationSystemTestCase
end end
def visit_account_portfolio def visit_account_portfolio
visit account_path(@account) visit account_path(@account, tab: "holdings")
end end
def select_combobox_option(text) def select_combobox_option(text)

View file

@ -6,7 +6,7 @@ class TransactionsTest < ApplicationSystemTestCase
Account::Entry.delete_all # clean slate Account::Entry.delete_all # clean slate
@uncategorized_transaction = create_transaction("one", 12.days.ago.to_date, 100) create_transaction("one", 12.days.ago.to_date, 100)
create_transaction("two", 10.days.ago.to_date, 100) create_transaction("two", 10.days.ago.to_date, 100)
create_transaction("three", 9.days.ago.to_date, 100) create_transaction("three", 9.days.ago.to_date, 100)
create_transaction("four", 8.days.ago.to_date, 100) create_transaction("four", 8.days.ago.to_date, 100)
@ -15,7 +15,7 @@ class TransactionsTest < ApplicationSystemTestCase
create_transaction("seven", 4.days.ago.to_date, 100) create_transaction("seven", 4.days.ago.to_date, 100)
create_transaction("eight", 3.days.ago.to_date, 100) create_transaction("eight", 3.days.ago.to_date, 100)
create_transaction("nine", 1.days.ago.to_date, 100) create_transaction("nine", 1.days.ago.to_date, 100)
create_transaction("ten", 1.days.ago.to_date, 100) @uncategorized_transaction = create_transaction("ten", 1.days.ago.to_date, 100)
create_transaction("eleven", Date.current, 100, category: categories(:food_and_drink), tags: [ tags(:one) ], merchant: merchants(:amazon)) create_transaction("eleven", Date.current, 100, category: categories(:food_and_drink), tags: [ tags(:one) ], merchant: merchants(:amazon))
@transactions = @user.family.entries @transactions = @user.family.entries
@ -124,13 +124,13 @@ class TransactionsTest < ApplicationSystemTestCase
assert_text "No entries found" assert_text "No entries found"
within "ul#transaction-search-filters" do within "ul#transaction-search-filters" do
find("li", text: account.name).first("a").click find("li", text: account.name).first("button").click
find("li", text: "on or after #{10.days.ago.to_date}").first("a").click find("li", text: "on or after #{10.days.ago.to_date}").first("button").click
find("li", text: "on or before #{1.day.ago.to_date}").first("a").click find("li", text: "on or before #{1.day.ago.to_date}").first("button").click
find("li", text: "Income").first("a").click find("li", text: "Income").first("button").click
find("li", text: "less than 200").first("a").click find("li", text: "less than 200").first("button").click
find("li", text: category.name).first("a").click find("li", text: category.name).first("button").click
find("li", text: merchant.name).first("a").click find("li", text: merchant.name).first("button").click
end end
assert_selector "#" + dom_id(@transaction), count: 1 assert_selector "#" + dom_id(@transaction), count: 1