fix: fix post height and remove overstate from author label (#472)

* fix: fixed post length according to figma

* fix: remove hoverstate from author label and author icon

* style: adds tooltip in hovercard for endorsemment icon

* test: fix test cases

* test: adds test cases for descreaased coverage

* refactor: updated api call to a method for reusability

* refactor: changed test descriptions
This commit is contained in:
ayesha waris
2023-03-22 17:19:15 +05:00
committed by GitHub
parent 15aee6a534
commit 39da42ee3f
7 changed files with 124 additions and 90 deletions

View File

@@ -44,34 +44,31 @@ function AuthorLabel({
const isRetiredUser = author ? author.startsWith('retired__user') : false;
const showTextPrimary = !authorLabelMessage && !isRetiredUser && !alert;
const className = classNames('d-flex align-items-center', { 'mb-0.5': !postOrComment }, labelColor);
const showUserNameAsLink = useShowLearnersTab()
&& linkToProfile && author && author !== intl.formatMessage(messages.anonymous);
const authorName = (
<span
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
'text-gray-700': isRetiredUser,
'text-primary-500': !authorLabelMessage && !isRetiredUser,
})}
role="heading"
aria-level="2"
>
{isRetiredUser ? '[Deactivated]' : author}
</span>
);
const labelContents = (
<div className={className}>
{!alert && (
<span
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
'text-gray-700': isRetiredUser,
'text-primary-500': !authorLabelMessage && !isRetiredUser,
})}
role="heading"
aria-level="2"
>
{isRetiredUser ? '[Deactivated]' : author}
</span>
)}
<>
<OverlayTrigger
overlay={(
<Tooltip id={`endorsed-by-${author}-tooltip`}>
{author}
</Tooltip>
)}
trigger={['hover', 'focus']}
>
<div className={classNames('d-flex flex-row align-items-center', {
@@ -86,20 +83,19 @@ function AuthorLabel({
src={icon}
data-testid="author-icon"
/>
{authorLabelMessage && (
<span
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
'text-primary-500': showTextPrimary,
'text-gray-700': isRetiredUser,
})}
style={{ marginLeft: '2px' }}
>
{authorLabelMessage}
</span>
)}
</div>
</OverlayTrigger>
{authorLabelMessage && (
<span
className={classNames('mr-1.5 font-size-14 font-style font-weight-500', {
'text-primary-500': showTextPrimary,
'text-gray-700': isRetiredUser,
})}
style={{ marginLeft: '2px' }}
>
{authorLabelMessage}
</span>
)}
{postCreatedAt && (
<span
title={postCreatedAt}
@@ -112,23 +108,25 @@ function AuthorLabel({
{timeago.format(postCreatedAt, 'time-locale')}
</span>
)}
</div>
</>
);
return showUserNameAsLink
? (
<Link
data-testid="learner-posts-link"
id="learner-posts-link"
to={discussionsPath(Routes.LEARNERS.POSTS, { learnerUsername: author, courseId })(location)}
className="text-decoration-none"
style={{ width: 'fit-content' }}
>
<div className={className}>
<Link
data-testid="learner-posts-link"
id="learner-posts-link"
to={discussionsPath(Routes.LEARNERS.POSTS, { learnerUsername: author, courseId })(location)}
className="text-decoration-none"
style={{ width: 'fit-content' }}
>
{!alert && authorName}
</Link>
{labelContents}
</Link>
</div>
)
: <>{labelContents}</>;
: <div className={className}>{authorName}{labelContents}</div>;
}
AuthorLabel.propTypes = {

View File

@@ -93,12 +93,13 @@ describe('Author label', () => {
async () => {
renderComponent(author, authorLabel, linkToProfile, labelColor);
const authorElement = container.querySelector('[role=heading]');
const labelElement = authorElement.parentNode.lastChild;
const labelParentNode = authorElement.parentNode.parentNode;
const labelElement = labelParentNode.lastChild.lastChild;
const label = ['TA', 'Staff'].includes(labelElement.textContent) && labelElement.textContent;
if (linkToProfile) {
expect(authorElement.parentNode).toHaveClass(labelColor);
expect(authorElement.parentNode.lastChild).toHaveTextContent(label);
expect(labelParentNode).toHaveClass(labelColor);
expect(labelElement).toHaveTextContent(label);
} else {
expect(authorElement.parentNode.lastChild).not.toHaveTextContent(label, { exact: true });
expect(authorElement.parentNode).not.toHaveClass(labelColor, { exact: true });

View File

@@ -3,8 +3,10 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl } from '@edx/frontend-platform/i18n';
import { Button, Icon, IconButton } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Icon, IconButton, OverlayTrigger, Tooltip,
} from '@edx/paragon';
import {
StarFilled, StarOutline, ThumbUpFilled, ThumbUpOutline,
@@ -16,6 +18,7 @@ import ActionsDropdown from './ActionsDropdown';
import { DiscussionContext } from './context';
function HoverCard({
intl,
commentOrPost,
actionHandlers,
handleResponseCommentButton,
@@ -49,17 +52,26 @@ function HoverCard({
)}
{endorseIcons && (
<div className="hover-button">
<IconButton
src={endorseIcons.icon}
iconAs={Icon}
onClick={() => {
const actionFunction = actionHandlers[endorseIcons.action];
actionFunction();
}}
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
size="sm"
alt="Endorse"
/>
<OverlayTrigger
overlay={(
<Tooltip id="endorsed-icon-tooltip">
{intl.formatMessage(endorseIcons.label)}
</Tooltip>
)}
trigger={['hover', 'focus']}
>
<IconButton
src={endorseIcons.icon}
iconAs={Icon}
onClick={() => {
const actionFunction = actionHandlers[endorseIcons.action];
actionFunction();
}}
className={['endorse', 'unendorse'].includes(endorseIcons.id) ? 'text-dark-500' : 'text-success-500'}
size="sm"
alt="Endorse"
/>
</OverlayTrigger>
</div>
)}
<div className="hover-button">
@@ -98,6 +110,7 @@ function HoverCard({
}
HoverCard.propTypes = {
intl: intlShape.isRequired,
commentOrPost: PropTypes.oneOfType([commentShape, postShape]).isRequired,
actionHandlers: PropTypes.objectOf(PropTypes.func).isRequired,
handleResponseCommentButton: PropTypes.func.isRequired,

View File

@@ -97,9 +97,9 @@ function mockAxiosReturnPagedCommentsResponses() {
}
}
async function getThreadAPIResponse(threadId, topicId) {
async function getThreadAPIResponse(attr = null) {
axiosMock.onGet(`${threadsApiUrl}${discussionPostId}/`)
.reply(200, Factory.build('thread', { id: threadId, topic_id: topicId }));
.reply(200, Factory.build('thread', attr));
await executeThunk(fetchThread(discussionPostId), store.dispatch, store.getState);
}
@@ -152,14 +152,14 @@ describe('PostView', () => {
});
it('should show Topic Info for non-courseware topics', async () => {
await getThreadAPIResponse('thread-1', 'non-courseware-topic-1');
await getThreadAPIResponse({ id: 'thread-1', topic_id: 'non-courseware-topic-1' });
renderComponent(discussionPostId);
expect(await screen.findByText('Related to')).toBeInTheDocument();
expect(await screen.findByText('non-courseware-topic 1')).toBeInTheDocument();
});
it('should show Topic Info for courseware topics with category', async () => {
await getThreadAPIResponse('thread-2', 'courseware-topic-2');
await getThreadAPIResponse({ id: 'thread-2', topic_id: 'courseware-topic-2' });
renderComponent('thread-2');
expect(await screen.findByText('Related to')).toBeInTheDocument();
@@ -233,6 +233,22 @@ describe('ThreadView', () => {
renderComponent(discussionPostId);
const comment = await waitFor(() => screen.findByTestId('comment-comment-1'));
expect(within(comment).queryByTestId('comment-1')).toBeInTheDocument();
it('should not show post footer', async () => {
Factory.resetAll();
await getThreadAPIResponse({
vote_count: 0, following: false, closed: false, group_id: null,
});
renderComponent(discussionPostId);
expect(screen.queryByTestId('post-footer')).not.toBeInTheDocument();
});
it('should show post footer', async () => {
Factory.resetAll();
await getThreadAPIResponse({
vote_count: 4, following: true, closed: false, group_id: null,
});
renderComponent(discussionPostId);
expect(screen.queryByTestId('post-footer')).toBeInTheDocument();
});
it('should show and hide the editor', async () => {

View File

@@ -15,7 +15,7 @@ import { selectorForUnitSubsection, selectTopicContext } from '../../../data/sel
import { AlertBanner, Confirmation } from '../../common';
import { DiscussionContext } from '../../common/context';
import HoverCard from '../../common/HoverCard';
import { selectModerationSettings } from '../../data/selectors';
import { selectModerationSettings, selectUserHasModerationPrivileges } from '../../data/selectors';
import { selectTopic } from '../../topics/data/selectors';
import { removeThread, updateExistingThread } from '../data/thunks';
import ClosePostReasonModal from './ClosePostReasonModal';
@@ -26,7 +26,6 @@ import { postShape } from './proptypes';
function Post({
post,
preview,
intl,
handleAddResponseButton,
}) {
@@ -42,6 +41,9 @@ function Post({
const [isDeleting, showDeleteConfirmation, hideDeleteConfirmation] = useToggle(false);
const [isReporting, showReportConfirmation, hideReportConfirmation] = useToggle(false);
const [isClosing, showClosePostModal, hideClosePostModal] = useToggle(false);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const displayPostFooter = post.following || post.voteCount || post.closed
|| (post.groupId && userHasModerationPrivileges);
const handleAbusedFlag = useCallback(() => {
if (post.abuseFlagged) {
@@ -147,8 +149,8 @@ function Post({
</div>
{(topicContext || topic) && (
<div
className={classNames('mt-14px mb-1 font-style font-size-12',
{ 'w-100': enableInContextSidebar })}
className={classNames('mt-14px font-style font-size-12',
{ 'w-100': enableInContextSidebar, 'mb-1': !displayPostFooter })}
style={{ lineHeight: '20px' }}
>
<span className="text-gray-500" style={{ lineHeight: '20px' }}>{intl.formatMessage(messages.relatedTo)}{' '}</span>
@@ -170,7 +172,7 @@ function Post({
</Hyperlink>
</div>
)}
<PostFooter post={post} preview={preview} />
{displayPostFooter && <PostFooter post={post} userHasModerationPrivileges={userHasModerationPrivileges} />}
<ClosePostReasonModal
isOpen={isClosing}
onCancel={hideClosePostModal}
@@ -186,12 +188,7 @@ function Post({
Post.propTypes = {
intl: intlShape.isRequired,
post: postShape.isRequired,
preview: PropTypes.bool,
handleAddResponseButton: PropTypes.func.isRequired,
};
Post.defaultProps = {
preview: false,
};
export default injectIntl(Post);

View File

@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
@@ -9,20 +10,19 @@ import {
import { Locked, People } from '@edx/paragon/icons';
import { StarFilled, StarOutline } from '../../../components/icons';
import { selectUserHasModerationPrivileges } from '../../data/selectors';
import { updateExistingThread } from '../data/thunks';
import LikeButton from './LikeButton';
import messages from './messages';
import { postShape } from './proptypes';
function PostFooter({
post,
intl,
post,
userHasModerationPrivileges,
}) {
const dispatch = useDispatch();
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
return (
<div className="d-flex align-items-center ml-n1.5 mt-10px" style={{ lineHeight: '32px' }}>
<div className="d-flex align-items-center ml-n1.5 mt-10px" style={{ height: '32px' }} data-testid="post-footer">
{post.voteCount !== 0 && (
<LikeButton
count={post.voteCount}
@@ -83,6 +83,7 @@ function PostFooter({
>
<Icon
src={Locked}
className="text-primary-500"
style={{
width: '1rem',
height: '1rem',
@@ -99,7 +100,7 @@ function PostFooter({
PostFooter.propTypes = {
intl: intlShape.isRequired,
post: postShape.isRequired,
userHasModerationPrivileges: PropTypes.bool.isRequired,
};
export default injectIntl(PostFooter);

View File

@@ -14,11 +14,11 @@ import PostFooter from './PostFooter';
let store;
function renderComponent(post, preview = false, showNewCountLabel = false) {
function renderComponent(post, userHasModerationPrivileges = false) {
return render(
<IntlProvider locale="en">
<AppProvider store={store}>
<PostFooter post={post} preview={preview} showNewCountLabel={showNewCountLabel} />
<PostFooter post={post} userHasModerationPrivileges={userHasModerationPrivileges} />
</AppProvider>
</IntlProvider>,
);
@@ -64,23 +64,11 @@ describe('PostFooter', () => {
});
});
it("doesn't have 'new' badge when there are 0 new comments", () => {
renderComponent({ ...mockPost, unreadCommentCount: 0 });
expect(screen.queryByText('2 New')).toBeFalsy();
expect(screen.queryByText('0 New')).toBeFalsy();
});
it("doesn't has 'new' badge when the new-unread item is the post itself", () => {
// commentCount === 1 means it's just the post without any further comments
renderComponent({ ...mockPost, unreadCommentCount: 1, commentCount: 1 });
expect(screen.queryByText('1 New')).toBeFalsy();
});
it('has the cohort icon only when group information is present', () => {
renderComponent(mockPost);
expect(screen.queryByTestId('cohort-icon')).toBeFalsy();
renderComponent({ ...mockPost, groupId: 5, groupName: 'Test Cohort' });
renderComponent({ ...mockPost, groupId: 5, groupName: 'Test Cohort' }, true);
expect(screen.getByTestId('cohort-icon')).toBeTruthy();
});
@@ -104,4 +92,24 @@ describe('PostFooter', () => {
renderComponent({ ...mockPost, following: false });
expect(screen.queryByRole('button', { name: /follow/i })).not.toBeInTheDocument();
});
it('tests like button when voteCount is zero', async () => {
renderComponent({ ...mockPost, voteCount: 0 });
expect(screen.queryByRole('button', { name: /like/i })).not.toBeInTheDocument();
});
it('tests like button when voteCount is not zero', async () => {
renderComponent({ ...mockPost, voted: true, voteCount: 4 });
const likeButton = screen.getByRole('button', { name: /like/i });
await act(async () => {
fireEvent.mouseEnter(likeButton);
});
expect(screen.getByRole('tooltip')).toHaveTextContent(/unlike/i);
await act(async () => {
fireEvent.click(likeButton);
});
// clicking on the button triggers thread update.
expect(store.getState().threads.status === RequestStatus.IN_PROGRESS).toBeTruthy();
});
});