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' }
])
})
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' }
])
})
})

@ -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({

@ -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) {

@ -3,9 +3,12 @@ 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, params) do
def index(conn, %{"type" => "JSON"} = params) do
full_options =
[
necessity_by_association: %{
:transactions => :optional,
@ -13,6 +16,54 @@ defmodule BlockScoutWeb.BlockController do
}
]
|> 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)
end

@ -14,11 +14,22 @@
<%= render BlockScoutWeb.BlockView, "_tile.html", block: block, block_type: @block_type %>
<% end %>
</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 %>
<%= link(
gettext("Older"),
class: "button button-secondary button-sm float-right mt-3",
"data-selector": "next-page-button",
to: block_path(
@conn,
:index,

@ -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

Loading…
Cancel
Save