From 1d3827ed10a3e210dce175e528d43da0d3515048 Mon Sep 17 00:00:00 2001 From: Felipe Trzaskowski Date: Wed, 13 Oct 2021 15:35:50 -0300 Subject: [PATCH 1/3] feat: add posts pagination [BD-38] [TNL-8810] [BB-4993] --- src/discussions/posts/PostsView.jsx | 29 ++++++++++++++++++++---- src/discussions/posts/data/redux.test.js | 29 ++++++++++++++++++++++++ src/discussions/posts/data/selectors.js | 2 ++ src/discussions/posts/data/slices.js | 2 ++ src/discussions/posts/messages.js | 11 +++++++++ 5 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/discussions/posts/messages.js diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx index b6eb369a..54858e84 100644 --- a/src/discussions/posts/PostsView.jsx +++ b/src/discussions/posts/PostsView.jsx @@ -4,13 +4,15 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { AppContext } from '@edx/frontend-platform/react'; -import { Spinner } from '@edx/paragon'; +import { Button, Spinner } from '@edx/paragon'; import { RequestStatus } from '../../data/constants'; import { selectAllThreads, selectThreadFilters, + selectThreadNextPage, selectThreadSorting, selectTopicThreads, selectUserThreads, @@ -18,9 +20,10 @@ import { } from './data/selectors'; import { fetchThreads } from './data/thunks'; import PostFilterBar from './post-filter-bar/PostFilterBar'; +import messages from './messages'; import { PostLink } from './post'; -function PostsView({ showOwnPosts }) { +function PostsView({ showOwnPosts, intl }) { const { courseId, topicId, @@ -30,6 +33,7 @@ function PostsView({ showOwnPosts }) { const { authenticatedUser } = useContext(AppContext); const orderBy = useSelector(selectThreadSorting()); const filters = useSelector(selectThreadFilters()); + const nextPage = useSelector(selectThreadNextPage()); const loadingStatus = useSelector(threadsLoadingStatus()); let posts = []; @@ -48,6 +52,16 @@ function PostsView({ showOwnPosts }) { })); }, [courseId, orderBy, filters]); + const loadMorePosts = async () => { + if (nextPage) { + dispatch(fetchThreads(courseId, { + orderBy, + filters, + page: nextPage, + })); + } + }; + return (
@@ -56,10 +70,16 @@ function PostsView({ showOwnPosts }) { {posts.map(post => ())}
)} - {loadingStatus === RequestStatus.IN_PROGRESS && ( + {loadingStatus === RequestStatus.IN_PROGRESS ? (
+ ) : ( + nextPage && ( + + ) )} ); @@ -67,10 +87,11 @@ function PostsView({ showOwnPosts }) { PostsView.propTypes = { showOwnPosts: PropTypes.bool, + intl: intlShape.isRequired, }; PostsView.defaultProps = { showOwnPosts: false, }; -export default PostsView; +export default injectIntl(PostsView); diff --git a/src/discussions/posts/data/redux.test.js b/src/discussions/posts/data/redux.test.js index 0e1d7148..39738764 100644 --- a/src/discussions/posts/data/redux.test.js +++ b/src/discussions/posts/data/redux.test.js @@ -58,6 +58,35 @@ describe('Threads/Posts data layer tests', () => { .toEqual('test-topic'); }); + test('successfully processes threads pagination', async () => { + const mockPage = page => axiosMock + .onGet(threadsApiUrl) + .reply(200, Factory.build('threadsResult', null, { + page, + count: 5, + pageSize: 3, + })); + + mockPage(1); + await executeThunk(fetchThreads(courseId), store.dispatch, store.getState); + expect(store.getState().threads.pages) + .toEqual([ + ['thread-1', 'thread-2', 'thread-3'], + ]); + expect(store.getState().threads.nextPage) + .toEqual(2); + + mockPage(2); + await executeThunk(fetchThreads(courseId, { page: 2 }), store.dispatch, store.getState); + expect(store.getState().threads.pages) + .toEqual([ + ['thread-1', 'thread-2', 'thread-3'], + ['thread-4', 'thread-5'], + ]); + expect(store.getState().threads.nextPage) + .toBeNull(); + }); + test('successfully processes single thread', async () => { const threadId = 'thread-1'; axiosMock.onGet(`${threadsApiUrl}${threadId}/`) diff --git a/src/discussions/posts/data/selectors.js b/src/discussions/posts/data/selectors.js index 2f8098c6..ed110d9f 100644 --- a/src/discussions/posts/data/selectors.js +++ b/src/discussions/posts/data/selectors.js @@ -47,6 +47,8 @@ export const selectThreadSorting = () => state => state.threads.sortedBy; export const selectThreadFilters = () => state => state.threads.filters; +export const selectThreadNextPage = () => state => state.threads.nextPage; + export const selectAuthorAvatars = author => state => ( state.threads.avatars?.[author]?.profile.image ); diff --git a/src/discussions/posts/data/slices.js b/src/discussions/posts/data/slices.js index 8f402d53..1d1a3bef 100644 --- a/src/discussions/posts/data/slices.js +++ b/src/discussions/posts/data/slices.js @@ -24,6 +24,7 @@ const threadsSlice = createSlice({ }, pages: [], threadDraft: null, + nextPage: null, totalPages: null, totalThreads: null, postStatus: RequestStatus.SUCCESSFUL, @@ -47,6 +48,7 @@ const threadsSlice = createSlice({ state.threadsById = { ...state.threadsById, ...payload.threadsById }; state.threadsInTopic = { ...state.threadsInTopic, ...payload.threadsInTopic }; state.avatars = { ...state.avatars, ...payload.avatars }; + state.nextPage = (payload.page < payload.pagination.numPages) ? payload.page + 1 : null; state.totalPages = payload.pagination.numPages; state.totalThreads = payload.pagination.count; }, diff --git a/src/discussions/posts/messages.js b/src/discussions/posts/messages.js new file mode 100644 index 00000000..15a2a36c --- /dev/null +++ b/src/discussions/posts/messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + loadMorePosts: { + id: 'discussions.posts.loadMorePosts', + defaultMessage: 'Load more posts', + description: 'Button to load more forum posts', + }, +}); + +export default messages; From b8e3b8626490a7eb3e7aac16b1eae613fdb7c873 Mon Sep 17 00:00:00 2001 From: Felipe Trzaskowski Date: Mon, 18 Oct 2021 14:02:55 -0300 Subject: [PATCH 2/3] fix: posts pagination is loaded by scrolling Implemented a ScrollThreshold reusable component to detect scrolling events, which can be used in the place of a Button. Removed the i18n entry created for the Button, which is no longer needed. --- src/components/ScrollThreshold.jsx | 41 +++++++++++++++++++++++++++++ src/discussions/posts/PostsView.jsx | 14 ++++------ src/discussions/posts/messages.js | 11 -------- 3 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 src/components/ScrollThreshold.jsx delete mode 100644 src/discussions/posts/messages.js diff --git a/src/components/ScrollThreshold.jsx b/src/components/ScrollThreshold.jsx new file mode 100644 index 00000000..be8272b5 --- /dev/null +++ b/src/components/ScrollThreshold.jsx @@ -0,0 +1,41 @@ +import React, { + useEffect, + useRef, +} from 'react'; +import PropTypes from 'prop-types'; + +function ScrollThreshold({ onScroll }) { + const elementRef = useRef(null); + + useEffect(() => { + if (!elementRef.current) { + return undefined; + } + + // create the observer + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + onScroll(); + } + }, + ); + + observer.observe(elementRef.current); + + // cleanup callback + return () => { + observer.disconnect(); + }; + }, [elementRef]); + + return ( +
+ ); +} + +ScrollThreshold.propTypes = { + onScroll: PropTypes.func.isRequired, +}; + +export default ScrollThreshold; diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx index 54858e84..0948b21c 100644 --- a/src/discussions/posts/PostsView.jsx +++ b/src/discussions/posts/PostsView.jsx @@ -4,10 +4,10 @@ import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { AppContext } from '@edx/frontend-platform/react'; -import { Button, Spinner } from '@edx/paragon'; +import { Spinner } from '@edx/paragon'; +import ScrollThreshold from '../../components/ScrollThreshold'; import { RequestStatus } from '../../data/constants'; import { selectAllThreads, @@ -20,10 +20,9 @@ import { } from './data/selectors'; import { fetchThreads } from './data/thunks'; import PostFilterBar from './post-filter-bar/PostFilterBar'; -import messages from './messages'; import { PostLink } from './post'; -function PostsView({ showOwnPosts, intl }) { +function PostsView({ showOwnPosts }) { const { courseId, topicId, @@ -76,9 +75,7 @@ function PostsView({ showOwnPosts, intl }) {
) : ( nextPage && ( - + ) )} @@ -87,11 +84,10 @@ function PostsView({ showOwnPosts, intl }) { PostsView.propTypes = { showOwnPosts: PropTypes.bool, - intl: intlShape.isRequired, }; PostsView.defaultProps = { showOwnPosts: false, }; -export default injectIntl(PostsView); +export default PostsView; diff --git a/src/discussions/posts/messages.js b/src/discussions/posts/messages.js deleted file mode 100644 index 15a2a36c..00000000 --- a/src/discussions/posts/messages.js +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - loadMorePosts: { - id: 'discussions.posts.loadMorePosts', - defaultMessage: 'Load more posts', - description: 'Button to load more forum posts', - }, -}); - -export default messages; From 3a71253e341f85141652fb5147e27231bc186ba7 Mon Sep 17 00:00:00 2001 From: Felipe Trzaskowski Date: Tue, 19 Oct 2021 11:03:51 -0300 Subject: [PATCH 3/3] fix: move the loading indicator and scroll threshold inside the posts list --- src/discussions/posts/PostsView.jsx | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/discussions/posts/PostsView.jsx b/src/discussions/posts/PostsView.jsx index 0948b21c..0ad2730c 100644 --- a/src/discussions/posts/PostsView.jsx +++ b/src/discussions/posts/PostsView.jsx @@ -64,20 +64,20 @@ function PostsView({ showOwnPosts }) { return (
- {posts && posts.length > 0 && ( -
- {posts.map(post => ())} -
- )} - {loadingStatus === RequestStatus.IN_PROGRESS ? ( -
- -
- ) : ( - nextPage && ( - - ) - )} +
+ {posts && posts.map(post => ( + + ))} + {loadingStatus === RequestStatus.IN_PROGRESS ? ( +
+ +
+ ) : ( + nextPage && ( + + ) + )} +
); }