Infinite scroll on blocks page

pull/1058/head
jimmay5469 6 years ago
parent 57ef9bbe19
commit 056f0648fe
  1. 39
      apps/block_scout_web/assets/__tests__/pages/blocks.js
  2. 2
      apps/block_scout_web/assets/js/pages/address.js
  3. 78
      apps/block_scout_web/assets/js/pages/blocks.js
  4. 53
      apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex
  5. 11
      apps/block_scout_web/lib/block_scout_web/templates/block/index.html.eex
  6. 7
      apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs

@ -70,20 +70,6 @@ describe('RECEIVED_NEW_BLOCK', () => {
{ blockNumber: 1, blockHtml: 'test' } { 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', () => { test('inserts place holders if block received out of order', () => {
window.localized = {} window.localized = {}
const state = Object.assign({}, initialState, { 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' }
])
})
})

@ -356,7 +356,7 @@ if ($addressDetailsPage.length) {
msg: humps.camelizeKeys(msg) msg: humps.camelizeKeys(msg)
})) }))
$('[data-selector="transactions-list"]').length && onScrollBottom(function loadMoreTransactions () { $('[data-selector="transactions-list"]').length && onScrollBottom(() => {
const { loadingNextPage, nextPageUrl, pagingError } = store.getState() const { loadingNextPage, nextPageUrl, pagingError } = store.getState()
if (!loadingNextPage && nextPageUrl && !pagingError) { if (!loadingNextPage && nextPageUrl && !pagingError) {
store.dispatch({ store.dispatch({

@ -1,9 +1,9 @@
import $ from 'jquery' import $ from 'jquery'
import _ from 'lodash' import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import socket from '../socket' import socket from '../socket'
import { createStore, connectElements } from '../lib/redux_helpers.js' import { createStore, connectElements } from '../lib/redux_helpers.js'
import { onScrollBottom } from '../lib/utils'
import listMorph from '../lib/list_morph' import listMorph from '../lib/list_morph'
export const initialState = { export const initialState = {
@ -11,14 +11,15 @@ export const initialState = {
blocks: [], blocks: [],
beyondPageOne: null loadingNextPage: false,
pagingError: false,
nextPageUrl: null
} }
export const reducer = withMissingBlocks(baseReducer) export const reducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) { function baseReducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': { case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type')) return Object.assign({}, state, _.omit(action, 'type'))
} }
@ -28,7 +29,7 @@ function baseReducer (state = initialState, action) {
}) })
} }
case 'RECEIVED_NEW_BLOCK': { 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) { if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
return Object.assign({}, state, { 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: default:
return state return state
} }
@ -88,16 +110,35 @@ const elements = {
const newElements = _.map(state.blocks, ({ blockHtml }) => $(blockHtml)[0]) const newElements = _.map(state.blocks, ({ blockHtml }) => $(blockHtml)[0])
listMorph(container, newElements, { key: 'dataset.blockNumber' }) 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"]') const $blockListPage = $('[data-page="block-list"]')
if ($blockListPage.length) { if ($blockListPage.length) {
const store = createStore(reducer) const store = createStore(reducer)
store.dispatch({
type: 'PAGE_LOAD',
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber
})
connectElements({ store, elements }) connectElements({ store, elements })
const blocksChannel = socket.channel(`blocks:new_block`, {}) const blocksChannel = socket.channel(`blocks:new_block`, {})
@ -109,6 +150,27 @@ if ($blockListPage.length) {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg) 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) { export function placeHolderBlock (blockNumber) {

@ -3,9 +3,12 @@ defmodule BlockScoutWeb.BlockController do
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias BlockScoutWeb.BlockView
alias Explorer.Chain alias Explorer.Chain
alias Phoenix.View
def index(conn, params) do def index(conn, %{"type" => "JSON"} = params) do
full_options =
[ [
necessity_by_association: %{ necessity_by_association: %{
:transactions => :optional, :transactions => :optional,
@ -13,6 +16,54 @@ defmodule BlockScoutWeb.BlockController do
} }
] ]
|> Keyword.merge(paging_options(params)) |> 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
[
necessity_by_association: %{
:transactions => :optional,
[miner: :names] => :optional
}
]
|> Keyword.merge(paging_options(%{}))
|> handle_render(conn, params) |> handle_render(conn, params)
end end

@ -14,11 +14,22 @@
<%= render BlockScoutWeb.BlockView, "_tile.html", block: block, block_type: @block_type %> <%= render BlockScoutWeb.BlockView, "_tile.html", block: block, block_type: @block_type %>
<% end %> <% end %>
</span> </span>
<div data-selector="loading-next-page" class="tile tile-muted text-center mt-3" style="display: none;">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<%= gettext("Loading") %>...
</div>
<div data-selector="paging-error-message" class="alert alert-danger text-center mt-3" style="display: none;">
<%= gettext("Error trying to fetch next page.") %>
</div>
<%= if @next_page_params do %> <%= if @next_page_params do %>
<%= link( <%= link(
gettext("Older"), gettext("Older"),
class: "button button-secondary button-sm float-right mt-3", class: "button button-secondary button-sm float-right mt-3",
"data-selector": "next-page-button",
to: block_path( to: block_path(
@conn, @conn,
:index, :index,

@ -68,12 +68,15 @@ defmodule BlockScoutWeb.BlockControllerTest do
conn = conn =
get(conn, block_path(conn, :index), %{ get(conn, block_path(conn, :index), %{
"type" => "JSON",
"block_number" => Integer.to_string(block.number) "block_number" => Integer.to_string(block.number)
}) })
{:ok, %{"blocks" => blocks}} = conn.resp_body |> Poison.decode()
actual_block_ids = actual_block_ids =
conn.assigns.blocks blocks
|> Enum.map(& &1.number) |> Enum.map(& &1["block_number"])
|> Enum.reverse() |> Enum.reverse()
assert second_page_block_ids == actual_block_ids assert second_page_block_ids == actual_block_ids

Loading…
Cancel
Save