Skip to content

Commit

Permalink
✨ Add pagination for transactions
Browse files Browse the repository at this point in the history
I was unable to make infinite scrolling work, so I resorted to prev page/next page buttons instead.

The `hero-arrow-left-circle-mini` icon would not display correctly, so I submitted [a PR](tailwindlabs/heroicons#1211) upstream and am now using my fork.
  • Loading branch information
randycoulman committed Jul 20, 2024
1 parent 4737c8f commit 39b2636
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 34 deletions.
67 changes: 58 additions & 9 deletions lib/freedom_account_web/components/fund_transaction/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,38 @@ defmodule FreedomAccountWeb.FundTransaction.Index do
alias FreedomAccount.Transactions
alias Phoenix.LiveComponent

@per_page 50
@keep_limit @per_page * 3
@page_size 10

@doc false
@spec page_size :: pos_integer()
def page_size, do: @page_size

@impl LiveComponent
def update(assigns, socket) do
%{fund: fund} = assigns
{transactions, %Paging{}} = Transactions.list_fund_transactions(fund, per_page: @per_page)
{transactions, %Paging{} = paging} = Transactions.list_fund_transactions(fund, per_page: @page_size)

{:ok,
socket
|> assign(assigns)
|> stream(:transactions, transactions, limit: @keep_limit, reset: true)}
|> assign(:paging, paging)
|> assign(:transactions, transactions)
|> stream(:transactions, transactions, reset: true)}
end

@impl LiveComponent
def render(assigns) do
~H"""
<div>
<.table id="fund-transactions" row_id={fn {id, _txn} -> id end} rows={@streams.transactions}>
<:col :let={{_id, txn}} label="Date"><span><%= txn.date %></span></:col>
<:col :let={{_id, txn}} label="Memo"><span><%= txn.memo %></span></:col>
<:col :let={{_id, txn}} label="Out">
<.table id="fund-transactions" row_id={&"txn-#{&1.id}"} rows={@transactions}>
<:col :let={txn} label="Date"><span><%= txn.date %></span></:col>
<:col :let={txn} label="Memo"><span><%= txn.memo %></span></:col>
<:col :let={txn} label="Out">
<span :if={Money.negative?(txn.amount)} data-role="withdrawal">
<%= MoneyUtils.negate(txn.amount) %>
</span>
</:col>
<:col :let={{_id, txn}} label="In">
<:col :let={txn} label="In">
<span :if={Money.positive?(txn.amount)} data-role="deposit"><%= txn.amount %></span>
</:col>
<:empty_state>
Expand All @@ -44,7 +49,51 @@ defmodule FreedomAccountWeb.FundTransaction.Index do
</div>
</:empty_state>
</.table>
<.button
disabled={is_nil(@paging.prev_cursor)}
phx-click="prev-page"
phx-disable-with="Loading..."
phx-target={@myself}
type="button"
>
<.icon name="hero-arrow-left-circle-mini" /> Previous Page
</.button>
<.button
disabled={is_nil(@paging.next_cursor)}
phx-click="next-page"
phx-disable-with="Loading..."
phx-target={@myself}
type="button"
>
Next Page <.icon name="hero-arrow-right-circle-mini" />
</.button>
</div>
"""
end

@impl LiveComponent
def handle_event("next-page", _params, socket) do
%{fund: fund, paging: paging} = socket.assigns

{transactions, %Paging{} = next_paging} =
Transactions.list_fund_transactions(fund, next_cursor: paging.next_cursor, per_page: @page_size)

{:noreply,
socket
|> assign(:paging, next_paging)
|> assign(:transactions, transactions)}
end

@impl LiveComponent
def handle_event("prev-page", _params, socket) do
%{fund: fund, paging: paging} = socket.assigns

{transactions, %Paging{} = next_paging} =
Transactions.list_fund_transactions(fund, prev_cursor: paging.prev_cursor, per_page: @page_size)

{:noreply,
socket
|> assign(:paging, next_paging)
|> assign(:transactions, transactions)}
end
end
9 changes: 8 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,15 @@ defmodule FreedomAccount.MixProject do
{:faker, "~> 0.18.0", only: :test},
{:floki, ">= 0.35.3", only: :test},
{:gettext, "~> 0.24.0"},
# {:heroicons,
# github: "tailwindlabs/heroicons", tag: "v2.1.5", sparse: "optimized", app: false, compile: false, depth: 1},
{:heroicons,
github: "tailwindlabs/heroicons", tag: "v2.1.1", sparse: "optimized", app: false, compile: false, depth: 1},
github: "randycoulman/heroicons",
branch: "fix-arrow-left-circle-20",
sparse: "optimized",
app: false,
compile: false,
depth: 1},
{:jason, "~> 1.2"},
{:mix_test_interactive, "~> 3.0", only: :dev, runtime: false},
{:paginator, "~> 1.2"},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
"gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"heroicons": {:git, "https://github.com/randycoulman/heroicons.git", "9bf00d1b2de659d8e9fe18ee81db4c0236b198d7", [branch: "fix-arrow-left-circle-20", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
Expand Down
70 changes: 70 additions & 0 deletions test/freedom_account_web/components/fund_transaction_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule FreedomAccountWeb.FundTransactionTest do
use FreedomAccountWeb.ConnCase, async: true

alias FreedomAccount.Factory
alias FreedomAccount.MoneyUtils
alias FreedomAccountWeb.FundTransaction

setup [:create_account, :create_fund]

describe "Index" do
test "shows message when fund has no transactions", %{conn: conn, fund: fund} do
conn
|> visit(~p"/funds/#{fund}")
|> assert_has("#no-transactions")
end

test "displays transactions", %{conn: conn, account: account, fund: fund} do
deposit = Factory.deposit(fund)
[deposit_line_item] = deposit.line_items
withdrawal = Factory.withdrawal(account, fund)
[withdrawal_line_item] = withdrawal.line_items

conn
|> visit(~p"/funds/#{fund}")
|> assert_has(table_cell(), text: "#{deposit.date}")
|> assert_has(table_cell(), text: deposit.memo)
|> assert_has(role("deposit"), text: "#{deposit_line_item.amount}")
|> assert_has(table_cell(), text: "#{withdrawal.date}")
|> assert_has(table_cell(), text: withdrawal.memo)
|> assert_has(role("withdrawal"), text: "#{MoneyUtils.negate(withdrawal_line_item.amount)}")
end

test "paginates transactions", %{conn: conn, fund: fund} do
page_size = FundTransaction.Index.page_size()
count = round(page_size * 2.5)

transactions =
for i <- 1..count do
Factory.deposit(fund, date: :local |> Timex.today() |> Timex.shift(days: i * -1))
end

[page1, page2, page3] = Enum.chunk_every(transactions, page_size)

conn
|> visit(~p"/funds/#{fund}")
|> assert_has_all_transactions(page1)
|> assert_has(disabled("button"), text: "Previous Page")
|> assert_has(enabled("button"), text: "Next Page")
|> click_button("Next Page")
|> assert_has_all_transactions(page2)
|> assert_has(enabled("button"), text: "Previous Page")
|> assert_has(enabled("button"), text: "Next Page")
|> click_button("Next Page")
|> assert_has_all_transactions(page3)
|> assert_has(enabled("button"), text: "Previous Page")
|> assert_has(disabled("button"), text: "Next Page")
|> click_button("Previous Page")
|> click_button("Previous Page")
|> assert_has_all_transactions(page1)
|> assert_has(disabled("button"), text: "Previous Page")
|> assert_has(enabled("button"), text: "Next Page")
end

defp assert_has_all_transactions(session, transactions) do
Enum.reduce(transactions, session, fn txn, session ->
assert_has(session, table_cell(), text: "#{txn.date}")
end)
end
end
end
23 changes: 0 additions & 23 deletions test/freedom_account_web/live/fund_live_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ defmodule FreedomAccountWeb.FundLiveTest do

alias FreedomAccount.Factory
alias FreedomAccount.Funds
alias FreedomAccount.MoneyUtils
alias Phoenix.HTML.Safe

describe "Index" do
Expand Down Expand Up @@ -123,28 +122,6 @@ defmodule FreedomAccountWeb.FundLiveTest do
|> assert_has(heading(), text: Safe.to_iodata(fund))
end

test "shows message when fund has no transactions", %{conn: conn, fund: fund} do
conn
|> visit(~p"/funds/#{fund}")
|> assert_has("#no-transactions")
end

test "displays transactions", %{conn: conn, account: account, fund: fund} do
deposit = Factory.deposit(fund)
[deposit_line_item] = deposit.line_items
withdrawal = Factory.withdrawal(account, fund)
[withdrawal_line_item] = withdrawal.line_items

conn
|> visit(~p"/funds/#{fund}")
|> assert_has(table_cell(), text: "#{deposit.date}")
|> assert_has(table_cell(), text: deposit.memo)
|> assert_has(role("deposit"), text: "#{deposit_line_item.amount}")
|> assert_has(table_cell(), text: "#{withdrawal.date}")
|> assert_has(table_cell(), text: withdrawal.memo)
|> assert_has(role("withdrawal"), text: "#{MoneyUtils.negate(withdrawal_line_item.amount)}")
end

test "updates fund within modal", %{conn: conn, fund: fund} do
%{icon: icon, name: name} = Factory.fund_attrs()
Factory.deposit(fund)
Expand Down
6 changes: 6 additions & 0 deletions test/support/element_selectors.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ defmodule FreedomAccountWeb.ElementSelectors do
@spec action_link(selector()) :: selector()
def action_link(selector), do: "#{selector} a"

@spec disabled(selector()) :: selector()
def disabled(selector), do: "#{selector}[disabled]"

@spec enabled(selector()) :: selector()
def enabled(selector), do: "#{selector}:not([disabled])"

@spec field_error(selector()) :: selector()
def field_error(selector), do: "#{selector} ~ [name='error']"

Expand Down

0 comments on commit 39b2636

Please sign in to comment.