From 056f0648feaffbea7f47a3f5114be119f17539f0 Mon Sep 17 00:00:00 2001 From: jimmay5469 Date: Mon, 5 Nov 2018 15:18:27 -0500 Subject: [PATCH] Infinite scroll on blocks page --- .../assets/__tests__/pages/blocks.js | 39 ++++++---- .../assets/js/pages/address.js | 2 +- .../block_scout_web/assets/js/pages/blocks.js | 78 +++++++++++++++++-- .../controllers/block_controller.ex | 53 ++++++++++++- .../templates/block/index.html.eex | 11 +++ .../controllers/block_controller_test.exs | 7 +- 6 files changed, 164 insertions(+), 26 deletions(-) diff --git a/apps/block_scout_web/assets/__tests__/pages/blocks.js b/apps/block_scout_web/assets/__tests__/pages/blocks.js index 099bbd762a..f73121614b 100644 --- a/apps/block_scout_web/assets/__tests__/pages/blocks.js +++ b/apps/block_scout_web/assets/__tests__/pages/blocks.js @@ -70,20 +70,6 @@ describe('RECEIVED_NEW_BLOCK', () => { { blockNumber: 1, blockHtml: 'test' } ]) }) - test('on page 2+', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true - }) - const action = { - type: 'RECEIVED_NEW_BLOCK', - msgs: [{ - blockHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.blocks).toEqual([]) - }) test('inserts place holders if block received out of order', () => { window.localized = {} const state = Object.assign({}, initialState, { @@ -154,3 +140,28 @@ describe('RECEIVED_NEW_BLOCK', () => { ]) }) }) + +describe('RECEIVED_NEXT_PAGE', () => { + test('with new block page', () => { + const state = Object.assign({}, initialState, { + loadingNextPage: true, + nextPageUrl: '1', + blocks: [{ blockNumber: 2, blockHtml: 'test 2' }] + }) + const action = { + type: 'RECEIVED_NEXT_PAGE', + msg: { + nextPageUrl: '2', + blocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + } + } + const output = reducer(state, action) + + expect(output.loadingNextPage).toEqual(false) + expect(output.nextPageUrl).toEqual('2') + expect(output.blocks).toEqual([ + { blockNumber: 2, blockHtml: 'test 2' }, + { blockNumber: 1, blockHtml: 'test 1' } + ]) + }) +}) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 32c6e6f464..cd80fb4153 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -356,7 +356,7 @@ if ($addressDetailsPage.length) { msg: humps.camelizeKeys(msg) })) - $('[data-selector="transactions-list"]').length && onScrollBottom(function loadMoreTransactions () { + $('[data-selector="transactions-list"]').length && onScrollBottom(() => { const { loadingNextPage, nextPageUrl, pagingError } = store.getState() if (!loadingNextPage && nextPageUrl && !pagingError) { store.dispatch({ diff --git a/apps/block_scout_web/assets/js/pages/blocks.js b/apps/block_scout_web/assets/js/pages/blocks.js index 1bf4b32209..19119d2f2d 100644 --- a/apps/block_scout_web/assets/js/pages/blocks.js +++ b/apps/block_scout_web/assets/js/pages/blocks.js @@ -1,9 +1,9 @@ import $ from 'jquery' import _ from 'lodash' -import URI from 'urijs' import humps from 'humps' import socket from '../socket' import { createStore, connectElements } from '../lib/redux_helpers.js' +import { onScrollBottom } from '../lib/utils' import listMorph from '../lib/list_morph' export const initialState = { @@ -11,14 +11,15 @@ export const initialState = { blocks: [], - beyondPageOne: null + loadingNextPage: false, + pagingError: false, + nextPageUrl: null } export const reducer = withMissingBlocks(baseReducer) function baseReducer (state = initialState, action) { switch (action.type) { - case 'PAGE_LOAD': case 'ELEMENTS_LOAD': { return Object.assign({}, state, _.omit(action, 'type')) } @@ -28,7 +29,7 @@ function baseReducer (state = initialState, action) { }) } case 'RECEIVED_NEW_BLOCK': { - if (state.channelDisconnected || state.beyondPageOne) return state + if (state.channelDisconnected) return state if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) { return Object.assign({}, state, { @@ -43,6 +44,27 @@ function baseReducer (state = initialState, action) { }) } } + case 'LOADING_NEXT_PAGE': { + return Object.assign({}, state, { + loadingNextPage: true + }) + } + case 'PAGING_ERROR': { + return Object.assign({}, state, { + loadingNextPage: false, + pagingError: true + }) + } + case 'RECEIVED_NEXT_PAGE': { + return Object.assign({}, state, { + loadingNextPage: false, + nextPageUrl: action.msg.nextPageUrl, + blocks: [ + ...state.blocks, + ...action.msg.blocks + ] + }) + } default: return state } @@ -88,16 +110,35 @@ const elements = { const newElements = _.map(state.blocks, ({ blockHtml }) => $(blockHtml)[0]) listMorph(container, newElements, { key: 'dataset.blockNumber' }) } + }, + '[data-selector="next-page-button"]': { + load ($el) { + return { + nextPageUrl: `${$el.hide().attr('href')}&type=JSON` + } + } + }, + '[data-selector="loading-next-page"]': { + render ($el, state) { + if (state.loadingNextPage) { + $el.show() + } else { + $el.hide() + } + } + }, + '[data-selector="paging-error-message"]': { + render ($el, state) { + if (state.pagingError) { + $el.show() + } + } } } const $blockListPage = $('[data-page="block-list"]') if ($blockListPage.length) { const store = createStore(reducer) - store.dispatch({ - type: 'PAGE_LOAD', - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber - }) connectElements({ store, elements }) const blocksChannel = socket.channel(`blocks:new_block`, {}) @@ -109,6 +150,27 @@ if ($blockListPage.length) { type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) + + onScrollBottom(() => { + const { loadingNextPage, nextPageUrl, pagingError } = store.getState() + if (!loadingNextPage && nextPageUrl && !pagingError) { + store.dispatch({ + type: 'LOADING_NEXT_PAGE' + }) + $.get(nextPageUrl) + .done(msg => { + store.dispatch({ + type: 'RECEIVED_NEXT_PAGE', + msg: humps.camelizeKeys(msg) + }) + }) + .fail(() => { + store.dispatch({ + type: 'PAGING_ERROR' + }) + }) + } + }) } export function placeHolderBlock (blockNumber) { diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex index 388b9d86cd..fd28b57596 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex @@ -3,7 +3,58 @@ defmodule BlockScoutWeb.BlockController do import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + alias BlockScoutWeb.BlockView alias Explorer.Chain + alias Phoenix.View + + def index(conn, %{"type" => "JSON"} = params) do + full_options = + [ + necessity_by_association: %{ + :transactions => :optional, + [miner: :names] => :optional + } + ] + |> Keyword.merge(paging_options(params)) + + blocks_plus_one = Chain.list_blocks(full_options) + {blocks, next_page} = split_list_by_page(blocks_plus_one) + + next_page_url = + case next_page_params(next_page, blocks, params) do + nil -> + nil + + next_page_params -> + block_path( + conn, + :index, + next_page_params + ) + end + + block_type = Keyword.get(full_options, :block_type, "Block") + + json( + conn, + %{ + blocks: + Enum.map(blocks, fn block -> + %{ + block_number: block.number, + block_html: + View.render_to_string( + BlockView, + "_tile.html", + block: block, + block_type: block_type + ) + } + end), + next_page_url: next_page_url + } + ) + end def index(conn, params) do [ @@ -12,7 +63,7 @@ defmodule BlockScoutWeb.BlockController do [miner: :names] => :optional } ] - |> Keyword.merge(paging_options(params)) + |> Keyword.merge(paging_options(%{})) |> handle_render(conn, params) end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex index 11ea8142e8..69f4852c63 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex @@ -14,11 +14,22 @@ <%= render BlockScoutWeb.BlockView, "_tile.html", block: block, block_type: @block_type %> <% end %> + + <%= if @next_page_params do %> <%= link( gettext("Older"), class: "button button-secondary button-sm float-right mt-3", + "data-selector": "next-page-button", to: block_path( @conn, :index, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs index c5dffdb4e3..290ae06a13 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs @@ -68,12 +68,15 @@ defmodule BlockScoutWeb.BlockControllerTest do conn = get(conn, block_path(conn, :index), %{ + "type" => "JSON", "block_number" => Integer.to_string(block.number) }) + {:ok, %{"blocks" => blocks}} = conn.resp_body |> Poison.decode() + actual_block_ids = - conn.assigns.blocks - |> Enum.map(& &1.number) + blocks + |> Enum.map(& &1["block_number"]) |> Enum.reverse() assert second_page_block_ids == actual_block_ids