From 67705db1da928cd3f53765c21b4e1fb724d0a41d Mon Sep 17 00:00:00 2001 From: Hel Nershing Thapa Date: Thu, 29 Jun 2023 16:40:43 +0545 Subject: [PATCH] Handle project comments fetching and posting - Added `useCommentsQuery` hook to fetch project's questions and comments, updating the implementation to match the states from the new hook. - Replaced the existing implementation for posting comments with the `useMutation` hook, eliminating the use of the `useAsync` hook. - Implemented UI components to display loading and error network states for questions and comments. - Renamed the function from updateComments to refetchComments for clarity and consistency. --- frontend/src/api/questionsAndComments.js | 29 +++++++++++ .../src/components/projectDetail/messages.js | 4 ++ .../projectDetail/questionsAndComments.js | 52 +++++++++++-------- .../tests/questionsAndComments.test.js | 30 +++++++---- 4 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 frontend/src/api/questionsAndComments.js diff --git a/frontend/src/api/questionsAndComments.js b/frontend/src/api/questionsAndComments.js new file mode 100644 index 0000000000..4ae677f677 --- /dev/null +++ b/frontend/src/api/questionsAndComments.js @@ -0,0 +1,29 @@ +import { useQuery } from '@tanstack/react-query'; +import { useSelector } from 'react-redux'; + +import api from './apiClient'; + +export const useCommentsQuery = (projectId, page) => { + const token = useSelector((state) => state.auth.token); + const locale = useSelector((state) => state.preferences['locale']); + + const getComments = ({ signal }) => { + return api(token, locale).get(`projects/${projectId}/comments/`, { + signal, + params: { + perPage: 5, + page, + }, + }); + }; + + return useQuery({ + queryKey: ['questions-and-comments', projectId, page], + queryFn: getComments, + select: (data) => data.data, + }); +}; + +export const postComment = (projectId, comment, token, locale = 'en') => { + return api(token, locale).post(`projects/${projectId}/comments/`, { message: comment }); +}; diff --git a/frontend/src/components/projectDetail/messages.js b/frontend/src/components/projectDetail/messages.js index 4b022249d3..0ddec62d28 100644 --- a/frontend/src/components/projectDetail/messages.js +++ b/frontend/src/components/projectDetail/messages.js @@ -133,6 +133,10 @@ export default defineMessages({ id: 'project.detail.questionsAndComments.login', defaultMessage: 'Log in to be able to post comments.', }, + errorLoadingComments: { + id: 'project.detail.questionsAndComments.fetching.error', + defaultMessage: 'An error occured while loading questions and comments.', + }, post: { id: 'project.detail.questionsAndComments.button', defaultMessage: 'Post', diff --git a/frontend/src/components/projectDetail/questionsAndComments.js b/frontend/src/components/projectDetail/questionsAndComments.js index 4ae7c54099..6150d97469 100644 --- a/frontend/src/components/projectDetail/questionsAndComments.js +++ b/frontend/src/components/projectDetail/questionsAndComments.js @@ -1,10 +1,11 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; +import { useMutation } from '@tanstack/react-query'; +import ReactPlaceholder from 'react-placeholder'; import messages from './messages'; import { RelativeTimeWithUnit } from '../../utils/formattedRelativeTime'; -import { useAsync } from '../../hooks/UseAsync'; import { PaginatorLine } from '../paginator'; import { Button } from '../button'; import { Alert } from '../alert'; @@ -12,28 +13,29 @@ import { CommentInputField } from '../comments/commentInput'; import { MessageStatus } from '../comments/status'; import { UserAvatar } from '../user/avatar'; import { htmlFromMarkdown, formatUserNamesToLink } from '../../utils/htmlFromMarkdown'; -import { pushToLocalJSONAPI, fetchLocalJSONAPI } from '../../network/genericJSONRequest'; -import { useFetchWithAbort } from '../../hooks/UseFetch'; +import { fetchLocalJSONAPI } from '../../network/genericJSONRequest'; import { useEditProjectAllowed } from '../../hooks/UsePermissions'; import { DeleteButton } from '../teamsAndOrgs/management'; +import { postComment, useCommentsQuery } from '../../api/questionsAndComments'; import './styles.scss'; -export const PostProjectComment = ({ projectId, updateComments, contributors }) => { +export const PostProjectComment = ({ projectId, refetchComments, contributors }) => { const token = useSelector((state) => state.auth.token); + const locale = useSelector((state) => state.preferences['locale']); const [comment, setComment] = useState(''); - const saveComment = () => { - return pushToLocalJSONAPI( - `projects/${projectId}/comments/`, - JSON.stringify({ message: comment }), - token, - ).then(() => { - updateComments(); + const mutation = useMutation({ + mutationFn: () => postComment(projectId, comment, token, locale), + onSuccess: () => { + refetchComments(); setComment(''); - }); + }, + }); + + const saveComment = () => { + mutation.mutate({ message: comment }); }; - const saveCommentAsync = useAsync(saveComment); return (
@@ -51,16 +53,16 @@ export const PostProjectComment = ({ projectId, updateComments, contributors })
- +
); @@ -76,9 +78,7 @@ export const QuestionsAndComments = ({ project, contributors, titleClass }) => { setPage(val); }; - const [, , comments, refetch] = useFetchWithAbort( - `projects/${projectId}/comments/?perPage=5&page=${page}`, - ); + const { data: comments, status: commentsStatus, refetch } = useCommentsQuery(projectId, page); return (
@@ -86,7 +86,15 @@ export const QuestionsAndComments = ({ project, contributors, titleClass }) => {
- {comments?.chat?.length ? ( + {commentsStatus === 'loading' ? ( + + ) : commentsStatus === 'error' ? ( +
+ + + +
+ ) : comments?.chat.length ? ( { {token ? ( ) : ( diff --git a/frontend/src/components/projectDetail/tests/questionsAndComments.test.js b/frontend/src/components/projectDetail/tests/questionsAndComments.test.js index 3c5e9608c2..5724423618 100644 --- a/frontend/src/components/projectDetail/tests/questionsAndComments.test.js +++ b/frontend/src/components/projectDetail/tests/questionsAndComments.test.js @@ -4,7 +4,11 @@ import { render, screen, act, waitFor } from '@testing-library/react'; import { store } from '../../../store'; -import { ReduxIntlProviders, renderWithRouter } from '../../../utils/testWithIntl'; +import { + QueryClientProviders, + ReduxIntlProviders, + renderWithRouter, +} from '../../../utils/testWithIntl'; import { getProjectSummary, projectComments } from '../../../network/tests/mockData/projects'; import { QuestionsAndComments, PostProjectComment, CommentList } from '../questionsAndComments'; @@ -12,9 +16,11 @@ describe('test if QuestionsAndComments component', () => { const project = getProjectSummary(1); it('only renders text asking user to log in for non-logged in user', () => { render( - - - , + + + + + , ); expect(screen.getByText('Log in to be able to post comments.')).toBeInTheDocument(); }); @@ -22,9 +28,11 @@ describe('test if QuestionsAndComments component', () => { it('renders tabs for writing and previewing comments', async () => { const user = userEvent.setup(); render( - - - , + + + + + , ); const previewBtn = screen.getByRole('button', { name: /preview/i }); expect(screen.getAllByRole('button').length).toBe(11); @@ -41,9 +49,11 @@ describe('test if QuestionsAndComments component', () => { store.dispatch({ type: 'SET_TOKEN', token: '123456', role: 'ADMIN' }); }); const { user } = renderWithRouter( - - - , + + + + + , ); await waitFor(() => expect(screen.getByText('hello world')).toBeInTheDocument()); const textarea = screen.getByRole('textbox');