Merge pull request #25 from open-craft/felipetrz/bb-4993-add-post-pagination
feat: add pagination for posts [BD-38] [TNL-8810] [BB-4993]
This commit is contained in:
41
src/components/ScrollThreshold.jsx
Normal file
41
src/components/ScrollThreshold.jsx
Normal file
@@ -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 (
|
||||
<div ref={elementRef} />
|
||||
);
|
||||
}
|
||||
|
||||
ScrollThreshold.propTypes = {
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ScrollThreshold;
|
||||
@@ -7,10 +7,12 @@ import { useParams } from 'react-router';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
|
||||
import ScrollThreshold from '../../components/ScrollThreshold';
|
||||
import { RequestStatus } from '../../data/constants';
|
||||
import {
|
||||
selectAllThreads,
|
||||
selectThreadFilters,
|
||||
selectThreadNextPage,
|
||||
selectThreadSorting,
|
||||
selectTopicThreads,
|
||||
selectUserThreads,
|
||||
@@ -30,6 +32,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,19 +51,33 @@ function PostsView({ showOwnPosts }) {
|
||||
}));
|
||||
}, [courseId, orderBy, filters]);
|
||||
|
||||
const loadMorePosts = async () => {
|
||||
if (nextPage) {
|
||||
dispatch(fetchThreads(courseId, {
|
||||
orderBy,
|
||||
filters,
|
||||
page: nextPage,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="discussion-posts d-flex flex-column">
|
||||
<PostFilterBar filterSelfPosts={showOwnPosts} />
|
||||
{posts && posts.length > 0 && (
|
||||
<div className="list-group list-group-flush">
|
||||
{posts.map(post => (<PostLink post={post} key={post.id} />))}
|
||||
</div>
|
||||
)}
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS && (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
)}
|
||||
<div className="list-group list-group-flush">
|
||||
{posts && posts.map(post => (
|
||||
<PostLink post={post} key={post.id} />
|
||||
))}
|
||||
{loadingStatus === RequestStatus.IN_PROGRESS ? (
|
||||
<div className="d-flex justify-content-center p-4">
|
||||
<Spinner animation="border" variant="primary" size="lg" />
|
||||
</div>
|
||||
) : (
|
||||
nextPage && (
|
||||
<ScrollThreshold onScroll={loadMorePosts} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}/`)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user