Skip to content

Commit

Permalink
Handle project comments fetching and posting
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
HelNershingThapa committed Jun 29, 2023
1 parent fe2353c commit 67705db
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 32 deletions.
29 changes: 29 additions & 0 deletions frontend/src/api/questionsAndComments.js
Original file line number Diff line number Diff line change
@@ -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 });
};
4 changes: 4 additions & 0 deletions frontend/src/components/projectDetail/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
52 changes: 30 additions & 22 deletions frontend/src/components/projectDetail/questionsAndComments.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,41 @@
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';
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 (
<div className="w-100 cf mh4 pv4 bg-white center shadow-7 ba0 br1 post-comment-ctr">
Expand All @@ -51,16 +53,16 @@ export const PostProjectComment = ({ projectId, updateComments, contributors })

<div className="fl w-100 tr pt1 pr0-ns pr1 ml-auto">
<Button
onClick={() => saveCommentAsync.execute()}
onClick={() => saveComment()}
className="bg-red white f5"
disabled={comment === '' || saveCommentAsync.status === 'pending'}
loading={saveCommentAsync.status === 'pending'}
disabled={comment === ''}
loading={mutation.isLoading}
>
<FormattedMessage {...messages.post} />
</Button>
</div>
<div className="cf w-100 fr tr pr2 mt3">
<MessageStatus status={saveCommentAsync.status} comment={comment} />
<MessageStatus status={mutation.status} comment={comment} />
</div>
</div>
);
Expand All @@ -76,17 +78,23 @@ 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 (
<div className="bg-tan-dim">
<h3 className={titleClass}>
<FormattedMessage {...messages.questionsAndComments} />
</h3>
<div className="ph6-l ph4 pb5 w-100 w-70-l">
{comments?.chat?.length ? (
{commentsStatus === 'loading' ? (
<ReactPlaceholder type="media" rows={3} ready={false} />
) : commentsStatus === 'error' ? (
<div className="mb4">
<Alert type="error">
<FormattedMessage {...messages.errorLoadingComments} />
</Alert>
</div>
) : comments?.chat.length ? (
<CommentList
userCanEditProject={userCanEditProject}
projectId={projectId}
Expand All @@ -110,7 +118,7 @@ export const QuestionsAndComments = ({ project, contributors, titleClass }) => {
{token ? (
<PostProjectComment
projectId={projectId}
updateComments={refetch}
refetchComments={refetch}
contributors={contributors}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,35 @@ 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';

describe('test if QuestionsAndComments component', () => {
const project = getProjectSummary(1);
it('only renders text asking user to log in for non-logged in user', () => {
render(
<ReduxIntlProviders store={store}>
<QuestionsAndComments project={project} />
</ReduxIntlProviders>,
<QueryClientProviders>
<ReduxIntlProviders store={store}>
<QuestionsAndComments project={project} />
</ReduxIntlProviders>
</QueryClientProviders>,
);
expect(screen.getByText('Log in to be able to post comments.')).toBeInTheDocument();
});

it('renders tabs for writing and previewing comments', async () => {
const user = userEvent.setup();
render(
<ReduxIntlProviders store={store}>
<PostProjectComment projectId={1} />
</ReduxIntlProviders>,
<QueryClientProviders>
<ReduxIntlProviders store={store}>
<PostProjectComment projectId={1} />
</ReduxIntlProviders>
</QueryClientProviders>,
);
const previewBtn = screen.getByRole('button', { name: /preview/i });
expect(screen.getAllByRole('button').length).toBe(11);
Expand All @@ -41,9 +49,11 @@ describe('test if QuestionsAndComments component', () => {
store.dispatch({ type: 'SET_TOKEN', token: '123456', role: 'ADMIN' });
});
const { user } = renderWithRouter(
<ReduxIntlProviders store={store}>
<QuestionsAndComments project={project} />
</ReduxIntlProviders>,
<QueryClientProviders>
<ReduxIntlProviders store={store}>
<QuestionsAndComments project={project} />
</ReduxIntlProviders>
</QueryClientProviders>,
);
await waitFor(() => expect(screen.getByText('hello world')).toBeInTheDocument());
const textarea = screen.getByRole('textbox');
Expand Down

0 comments on commit 67705db

Please sign in to comment.