From d3c47e2f86940b652e3a9d9dc9c6073b5d59bcd1 Mon Sep 17 00:00:00 2001 From: Amanda Sposito Date: Thu, 20 Sep 2018 16:33:15 -0300 Subject: [PATCH] Add 'Token Inventory' page and tab control * The pagination at the 'Tokens.InventoryController' doesn't follow the 'BlockScoutWeb.Chain', because we have two different 'TokenTransfers' listings that are ordered in different ways. The 'Transfers' page is ordered from the most recent to the older and the 'Inventory' page is ordered according to the 'token_id'. --- .../assets/css/components/_tile.scss | 27 ++++++++++ .../lib/block_scout_web/chain.ex | 4 ++ .../tokens/inventory_controller.ex | 52 +++++++++++++++++++ .../lib/block_scout_web/router.ex | 7 +++ .../tokens/inventory/_token.html.eex | 28 ++++++++++ .../templates/tokens/inventory/index.html.eex | 42 +++++++++++++++ .../templates/tokens/overview/_tabs.html.eex | 18 +++++++ .../views/tokens/inventory_view.ex | 5 ++ .../views/tokens/overview_view.ex | 6 ++- .../views/tokens/overview_view_test.exs | 14 +++++ 10 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex diff --git a/apps/block_scout_web/assets/css/components/_tile.scss b/apps/block_scout_web/assets/css/components/_tile.scss index 9f07317c08..d7a83abdf0 100644 --- a/apps/block_scout_web/assets/css/components/_tile.scss +++ b/apps/block_scout_web/assets/css/components/_tile.scss @@ -63,6 +63,28 @@ } } + &-unique-token { + border-left: 4px solid $orange; + padding: 35px 0; + + .tile-label { + color: $orange; + } + } + + &-unique-token-image{ + border-left: 4px solid $orange; + padding: 0; + + .tile-label { + color: $orange; + } + + & .tile-content { + padding: 45px 0; + } + } + &-internal-transaction { border-left: 4px solid $teal; @@ -150,3 +172,8 @@ margin: 0; } } + +.tile-image { + max-width: 140px; + max-height: 140px; +} diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index c41229acec..d65d11de7d 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -29,6 +29,10 @@ defmodule BlockScoutWeb.Chain do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} + def default_paging_options do + @default_paging_options + end + def current_filter(%{paging_options: paging_options} = params) do params |> Map.get("filter") diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex new file mode 100644 index 0000000000..a6a19a8523 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex @@ -0,0 +1,52 @@ +defmodule BlockScoutWeb.Tokens.InventoryController do + use BlockScoutWeb, :controller + + alias Explorer.Chain + alias Explorer.Chain.TokenTransfer + + import BlockScoutWeb.Chain, only: [split_list_by_page: 1, default_paging_options: 0] + + def index(conn, %{"token_id" => address_hash_string} = params) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, token} <- Chain.token_from_address_hash(address_hash) do + unique_tokens = + Chain.address_to_unique_tokens( + token.contract_address_hash, + unique_tokens_paging_options(params) + ) + + {unique_tokens_paginated, next_page} = split_list_by_page(unique_tokens) + + render( + conn, + "index.html", + token: token, + unique_tokens: unique_tokens_paginated, + total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash), + total_token_holders: Chain.count_token_holders_from_token_hash(address_hash), + next_page_params: unique_tokens_next_page(next_page, unique_tokens_paginated, params) + ) + else + :error -> + not_found(conn) + + {:error, :not_found} -> + not_found(conn) + end + end + + defp unique_tokens_paging_options(%{"unique_token" => token_id}), + do: [paging_options: %{default_paging_options() | key: {token_id}}] + + defp unique_tokens_paging_options(_params), do: [paging_options: default_paging_options()] + + defp unique_tokens_next_page([], _list, _params), do: nil + + defp unique_tokens_next_page(_, list, params) do + Map.merge(params, paging_params(List.last(list))) + end + + defp paging_params(%TokenTransfer{token_id: token_id}) do + %{"unique_token" => Decimal.to_integer(token_id)} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 93e6532cd3..cc18314f4b 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -135,6 +135,13 @@ defmodule BlockScoutWeb.Router do only: [:index], as: :holder ) + + resources( + "/inventory", + Tokens.InventoryController, + only: [:index], + as: :inventory + ) end resources( diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex new file mode 100644 index 0000000000..88c36e5158 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex @@ -0,0 +1,28 @@ + +
+
+
+ + <%= gettext "Unique Token" %> +
+ +
+ + <%= gettext "Token ID" %>: + + <%= @token_transfer.token_id %> + + + + + <%= gettext "Owner Address" %>: + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @token_transfer.to_address, + contract: false %> + + +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex new file mode 100644 index 0000000000..8b2fdcb5b7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex @@ -0,0 +1,42 @@ +
+ <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + total_token_holders: @total_token_holders, + conn: @conn + ) %> + +
+
+
+ <%= render OverviewView, "_tabs.html", assigns %> +
+ +
+

<%= gettext "Inventory" %>

+ + <%= if Enum.any?(@unique_tokens) do %> + <%= for token_transfer <- @unique_tokens do %> + <%= render "_token.html", token_transfer: token_transfer %> + <% end %> + <% else %> +
+ + <%= gettext "There are no tokens." %> + +
+ <% end %> + + <%= if @next_page_params do %> + <%= link( + gettext("Next Page"), + class: "button button-secondary button-small float-right mt-4", + to: token_inventory_path(@conn, :index, @token.contract_address_hash, @next_page_params) + ) %> + <% end %> +
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex index b5dc0dc9b8..b2ca5f3d18 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex @@ -25,6 +25,16 @@ to: token_holder_path(@conn, :index, @token.contract_address_hash) ) %> + + <%= if display_inventory?(@token) do %> + + <% end %> @@ -56,6 +66,14 @@ class: "dropdown-item #{tab_status("token_holders", @conn.request_path)}", to: token_holder_path(@conn, :index, @token.contract_address_hash) ) %> + + <%= if display_inventory?(@token) do %> + <%= link( + gettext("Inventory"), + class: "dropdown-item #{tab_status("inventory", @conn.request_path)}", + to: token_inventory_path(@conn, :index, @token.contract_address_hash) + ) %> + <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex new file mode 100644 index 0000000000..6c3f3a6845 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tokens.InventoryView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.OverviewView +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex index 80bb51a04e..f36bca8b80 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do alias Explorer.Chain.Token alias BlockScoutWeb.Tokens.TransferView - @tabs ["token_transfers", "token_holders", "read_contract"] + @tabs ["token_transfers", "token_holders", "read_contract", "inventory"] def decimals?(%Token{decimals: nil}), do: false def decimals?(%Token{decimals: _}), do: true @@ -34,4 +34,8 @@ defmodule BlockScoutWeb.Tokens.OverviewView do defp tab_name(["token_transfers"]), do: gettext("Token Transfers") defp tab_name(["token_holders"]), do: gettext("Token Holders") defp tab_name(["read_contract"]), do: gettext("Read Contract") + defp tab_name(["inventory"]), do: gettext("Inventory") + + def display_inventory?(%Token{type: "ERC-721"}), do: true + def display_inventory?(_), do: false end diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs index 5f18eff56a..22f1e81299 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs @@ -64,4 +64,18 @@ defmodule BlockScoutWeb.Tokens.OverviewViewTest do assert OverviewView.current_tab_name(read_contract_path) == "Read Contract" end end + + describe "display_inventory?/1" do + test "returns true when token is unique" do + token = insert(:token, type: "ERC-721") + + assert OverviewView.display_inventory?(token) == true + end + + test "returns false when token is not unique" do + token = insert(:token, type: "ERC-20") + + assert OverviewView.display_inventory?(token) == false + end + end end