From f6d88bf724f4ed8c2a2f2bf12c0b19563ad6432f Mon Sep 17 00:00:00 2001 From: David Sevilla Martin Date: Sun, 10 Jan 2021 10:56:26 -0500 Subject: [PATCH] Create Pagination util & make DiscussionListState and DiscussionList use it I'm 100% sure that this code can be improved by a ton. Just pushing this so we can potentially work off of it. Some stuff taken from PR #1829. Does *not* have changing URL query parameter - this code is already a disaster. --- js/src/common/utils/Pagination.ts | 80 ++++++++++++++++++++++ js/src/forum/components/DiscussionList.js | 27 ++++++-- js/src/forum/states/DiscussionListState.js | 77 +++++++++++---------- 3 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 js/src/common/utils/Pagination.ts diff --git a/js/src/common/utils/Pagination.ts b/js/src/common/utils/Pagination.ts new file mode 100644 index 000000000..7db5550f1 --- /dev/null +++ b/js/src/common/utils/Pagination.ts @@ -0,0 +1,80 @@ +export default class Pagination { + private readonly loadFunction: (page: number) => Promise; + + public loading = { + prev: false, + next: false, + }; + + public page: number; + + public data: { [page: number]: T } = {}; + + public pages: { + first: number; + last: number; + }; + + constructor(load: (page: number) => Promise, page: number = 1) { + this.loadFunction = load; + this.page = page; + + this.pages = { + first: page, + last: page, + }; + } + + clear() { + this.data = {}; + } + + refresh(page: number) { + this.clear(); + + this.page = page; + this.pages.last = page - 1; + this.pages.first = page; + + return this.loadNext(); + } + + loadNext() { + this.loading.next = true; + const page = this.pages.last + 1; + + return this.load( + page, + () => (this.loading.next = false), + () => (this.pages.last = this.page = page) + ); + } + + loadPrev() { + this.loading.prev = true; + const page = this.pages.first - 1; + + return this.load( + page, + () => (this.loading.prev = false), + () => (this.pages.first = this.page = page) + ); + } + + private load(page, done, success) { + return this.loadFunction(page) + .then((out) => { + done(); + success(); + + this.data[this.page] = out; + + return out; + }) + .catch((err) => { + done(); + + return Promise.reject(err); + }); + } +} diff --git a/js/src/forum/components/DiscussionList.js b/js/src/forum/components/DiscussionList.js index 9ca3f5866..730352dc9 100644 --- a/js/src/forum/components/DiscussionList.js +++ b/js/src/forum/components/DiscussionList.js @@ -21,13 +21,7 @@ export default class DiscussionList extends Component { if (state.isLoading()) { loading = LoadingIndicator.component(); } else if (state.moreResults) { - loading = Button.component( - { - className: 'Button', - onclick: state.loadMore.bind(state), - }, - app.translator.trans('core.forum.discussion_list.load_more_button') - ); + loading = this.getLoadButton('more', state.loadMore.bind(state)); } if (state.empty()) { @@ -35,8 +29,18 @@ export default class DiscussionList extends Component { return
{Placeholder.component({ text })}
; } + console.log(state); + return (
+ {state.isLoadingPrev() ? ( + + ) : state.pagination.pages.first !== 1 ? ( +
{this.getLoadButton('prev', state.loadPrev.bind(state))}
+ ) : ( + '' + )} +
    {state.discussions.map((discussion) => { return ( @@ -46,8 +50,17 @@ export default class DiscussionList extends Component { ); })}
+
{loading}
); } + + getLoadButton(key, onclick) { + return ( + + ); + } } diff --git a/js/src/forum/states/DiscussionListState.js b/js/src/forum/states/DiscussionListState.js index 57f4396a8..4d802e6c6 100644 --- a/js/src/forum/states/DiscussionListState.js +++ b/js/src/forum/states/DiscussionListState.js @@ -1,4 +1,8 @@ +import Pagination from '../../common/utils/Pagination'; + export default class DiscussionListState { + static DISCUSSIONS_PER_PAGE = 20; + constructor(params = {}, app = window.app) { this.params = params; @@ -8,7 +12,7 @@ export default class DiscussionListState { this.moreResults = false; - this.loading = false; + this.pagination = new Pagination(this.load.bind(this)); } /** @@ -82,33 +86,16 @@ export default class DiscussionListState { * This can be used to refresh discussions without loading animations. */ refresh({ deferClear = false } = {}) { - this.loading = true; + this.pagination.loading.next = true; if (!deferClear) { this.clear(); } - return this.loadResults().then( - (results) => { - // This ensures that any changes made while waiting on this request - // are ignored. Otherwise, we could get duplicate discussions. - // We don't use `this.clear()` to avoid an unnecessary redraw. - this.discussions = []; - this.parseResults(results); - }, - () => { - this.loading = false; - m.redraw(); - } - ); + return this.pagination.refresh(Number(m.route.param('page')) || 1).then(this.parse.bind(this)); } - /** - * Load a new page of discussion results. - * - * @param offset The index to start the page at. - */ - loadResults(offset) { + load(page) { const preloadedDiscussions = this.app.preloadedApiDocument(); if (preloadedDiscussions) { @@ -116,44 +103,54 @@ export default class DiscussionListState { } const params = this.requestParams(); - params.page = { offset }; + params.page = { offset: DiscussionListState.DISCUSSIONS_PER_PAGE * (page - 1) }; params.include = params.include.join(','); return this.app.store.find('discussions', params); } - /** - * Load the next page of discussion results. - */ - loadMore() { - this.loading = true; + loadPrev() { + return this.pagination.loadPrev().then(this.parse.bind(this)); + } - this.loadResults(this.discussions.length).then(this.parseResults.bind(this)); + loadMore() { + return this.pagination.loadNext().then(this.parse.bind(this)); } /** * Parse results and append them to the discussion list. */ - parseResults(results) { - this.discussions.push(...results); + parse() { + const discussions = []; + const { first, last } = this.pagination.pages; - this.loading = false; - this.moreResults = !!results.payload.links && !!results.payload.links.next; + for (let page = first; page <= last; page++) { + const results = this.pagination.data[page]; + + if (Array.isArray(results)) discussions.push(...results); + } + + this.discussions = discussions; + + const results = this.pagination.data[last]; + this.moreResults = !!results.payload.links.next; m.redraw(); - return results; + return discussions; } /** * Remove a discussion from the list if it is present. */ removeDiscussion(discussion) { - const index = this.discussions.indexOf(discussion); + Object.keys(this.pagination.data).forEach((key) => { + const index = this.pagination.data[key].indexOf(discussion); - if (index !== -1) { - this.discussions.splice(index, 1); - } + this.pagination.data[key].splice(index, 1); + }); + + this.parse(); m.redraw(); } @@ -177,7 +174,11 @@ export default class DiscussionListState { * Are discussions currently being loaded? */ isLoading() { - return this.loading; + return this.pagination.loading.next; + } + + isLoadingPrev() { + return this.pagination.loading.prev; } /**