Skip to content

Commit

Permalink
Merge pull request #319 from cofacts/articlereplyfeedbacks
Browse files Browse the repository at this point in the history
Reasons dialog in <ArticleReplyFeedbacks> component
  • Loading branch information
MrOrz authored Sep 13, 2020
2 parents fa0b2c7 + 52d6a11 commit 9e4f20f
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 183 deletions.
5 changes: 4 additions & 1 deletion components/AppLayout/Widgets/Avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { makeStyles, withStyles } from '@material-ui/core/styles';
import { Badge } from '@material-ui/core';
import { TYPE_ICON } from 'constants/replyType';

const NULL_USER_IMG =
'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp';

const useStyles = makeStyles({
root: {
width: size => size,
Expand Down Expand Up @@ -78,7 +81,7 @@ function Avatar({
let avatar = (
<img
className={cx(classes.root, className)}
src={user?.avatarUrl}
src={user ? user.avatarUrl : NULL_USER_IMG}
alt=""
{...rest}
/>
Expand Down
1 change: 0 additions & 1 deletion components/ArticleReply/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ const ArticleReply = React.memo(
{showFeedback && (
<ArticleReplyFeedbackControl
articleReply={articleReply}
reply={reply}
className={classes.feedbacks}
/>
)}
Expand Down
128 changes: 23 additions & 105 deletions components/ArticleReplyFeedbackControl/ArticleReplyFeedbackControl.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import React, { useState } from 'react';
import React, { useState, useCallback, useRef } from 'react';
import { t } from 'ttag';
import gql from 'graphql-tag';
import { useMutation } from '@apollo/react-hooks';
import { makeStyles, withStyles } from '@material-ui/core/styles';
import { Tabs, Tab, Box, Popover, Typography } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { Popover, Typography } from '@material-ui/core';
import Snackbar from '@material-ui/core/Snackbar';
import CloseIcon from '@material-ui/icons/Close';
import Avatar from 'components/AppLayout/Widgets/Avatar';
import { ThumbUpIcon, ThumbDownIcon } from 'components/icons';
import ReasonsDisplay from './ReasonsDisplay';
import ButtonGroupDisplay from './ButtonGroupDisplay';
import cx from 'clsx';

Expand All @@ -31,11 +30,6 @@ const useStyles = makeStyles(theme => ({
outline: 'none',
color: theme.palette.secondary[100],
},
feedbacks: {
marginTop: 16,
maxHeight: 300,
overflow: 'auto',
},
popupTitle: {
fontSize: 18,
marginBottom: 24,
Expand Down Expand Up @@ -67,41 +61,6 @@ const useStyles = makeStyles(theme => ({
},
}));

const CustomTab = withStyles({
root: {
position: 'relative',
minHeight: 0,
},
wrapper: {
'& > svg': {
position: 'absolute',
left: 0,
},
},
})(Tab);

const Feedback = withStyles(theme => ({
root: {
marginTop: 16,
display: 'flex',
borderBottom: `1px solid ${theme.palette.secondary[100]}`,
alignItems: ({ comment }) => (comment ? 'flex-start' : 'center'),
paddingBottom: 10,
},
name: {
color: ({ comment }) =>
comment ? theme.palette.primary[500] : theme.palette.secondary[300],
},
}))(({ comment, user, classes }) => (
<div className={classes.root}>
<Avatar user={user} size={48} />
<Box px={2}>
<div className={classes.name}>{user.name}</div>
<div>{comment}</div>
</Box>
</div>
));

// Subset of fields that needs to be updated after login
//
const ArticleReplyFeedbackControlDataForUser = gql`
Expand All @@ -116,25 +75,15 @@ const ArticleReplyFeedbackControlDataForUser = gql`

const ArticleReplyFeedbackControlData = gql`
fragment ArticleReplyFeedbackControlData on ArticleReply {
positiveFeedbackCount
negativeFeedbackCount
ownVote
feedbacks {
id
comment
vote
user {
id
name
...AvatarData
}
}
articleId
replyId
...ArticleReplyFeedbackControlDataForUser
...ButtonGroupDisplayArticleReply
...ReasonsDisplayData
}
${ArticleReplyFeedbackControlDataForUser}
${ButtonGroupDisplay.fragments.ButtonGroupDisplayArticleReply}
${Avatar.fragments.AvatarData}
${ReasonsDisplay.fragments.ReasonsDisplayData}
`;

export const CREATE_REPLY_FEEDBACK = gql`
Expand Down Expand Up @@ -163,13 +112,14 @@ export const CREATE_REPLY_FEEDBACK = gql`
* Isolated because not all use case have reply nested under articleReply.
* @param {string?} props.className
*/
function ArticleReplyFeedbackControl({ articleReply, reply = {}, className }) {
function ArticleReplyFeedbackControl({ articleReply, className }) {
const classes = useStyles();
const [vote, setVote] = useState(null);
const [reason, setReason] = useState('');
const [reasonsPopoverAnchorEl, setReasonsPopoverAnchorEl] = useState(null);
const [votePopoverAnchorEl, setVotePopoverAnchorEl] = useState(null);
const [tab, setTab] = useState(0);
const reasonsPopoverRef = useRef();

const [showReorderSnack, setReorderSnackShow] = useState(false);
const [createReplyFeedback, { loading: updatingReplyFeedback }] = useMutation(
CREATE_REPLY_FEEDBACK,
Expand Down Expand Up @@ -202,6 +152,12 @@ function ArticleReplyFeedbackControl({ articleReply, reply = {}, className }) {
setVote(null);
};

const handleReasonReposition = useCallback(() => {
if (reasonsPopoverRef.current) {
reasonsPopoverRef.current.updatePosition();
}
}, []);

return (
<div className={cx(classes.root, className)}>
<ButtonGroupDisplay
Expand All @@ -211,9 +167,9 @@ function ArticleReplyFeedbackControl({ articleReply, reply = {}, className }) {
onReasonClick={openReasonsPopover}
/>
<Popover
id={`reply-reasons-${reply.id}`}
open={!!reasonsPopoverAnchorEl}
anchorEl={reasonsPopoverAnchorEl}
action={reasonsPopoverRef}
onClose={closeReasonsPopover}
anchorOrigin={{
vertical: 'top',
Expand All @@ -228,46 +184,14 @@ function ArticleReplyFeedbackControl({ articleReply, reply = {}, className }) {
>
<CloseIcon />
</button>
{reply.text}
<Tabs
value={tab}
onChange={(e, value) => setTab(value)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
>
<CustomTab
icon={<ThumbUpIcon />}
label={t`Helpful ${articleReply.positiveFeedbackCount}`}
/>
<CustomTab
icon={<ThumbDownIcon />}
label={t`Not Helpful ${articleReply.negativeFeedbackCount}`}
{!!reasonsPopoverAnchorEl && (
<ReasonsDisplay
articleReply={articleReply}
onSizeChange={handleReasonReposition}
/>
</Tabs>
<Box
display={tab === 0 ? 'block' : 'none'}
className={classes.feedbacks}
>
{articleReply.feedbacks
.filter(({ vote, user }) => vote === 'UPVOTE' && user)
.map(feedback => (
<Feedback key={feedback.id} {...feedback} />
))}
</Box>
<Box
display={tab === 1 ? 'block' : 'none'}
className={classes.feedbacks}
>
{articleReply.feedbacks
.filter(({ vote, user }) => vote === 'DOWNVOTE' && user)
.map(feedback => (
<Feedback key={feedback.id} {...feedback} />
))}
</Box>
)}
</Popover>
<Popover
id={`feedback-reasons-${reply.id}`}
open={!!votePopoverAnchorEl}
anchorEl={votePopoverAnchorEl}
onClose={closeVotePopover}
Expand Down Expand Up @@ -325,12 +249,6 @@ function ArticleReplyFeedbackControl({ articleReply, reply = {}, className }) {
ArticleReplyFeedbackControl.fragments = {
ArticleReplyFeedbackControlData,
ArticleReplyFeedbackControlDataForUser,
ArticleReplyFeedbackControlReply: gql`
fragment ArticleReplyFeedbackControlReply on Reply {
id
text
}
`,
};

export default ArticleReplyFeedbackControl;
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { MockedProvider } from '@apollo/react-testing';
import ArticleReplyFeedbackControl from './';
import { CREATE_REPLY_FEEDBACK } from './ArticleReplyFeedbackControl';
import { LOAD_FEEDBACKS } from './ReasonsDisplay';
import { USER_QUERY } from 'lib/useCurrentUser';

// Mocked objects
Expand All @@ -13,25 +14,22 @@ const mockArticleReply = {
positiveFeedbackCount: 1,
negativeFeedbackCount: 1,
ownVote: null,
feedbacks: [
{ id: 'feedback1', comment: 'test comment', vote: 'UPVOTE', user: null },
{
id: 'feedback1',
comment: 'test comment',
vote: 'DOWNVOTE',
user: {
id: 'webUser1',
name: 'Web User',
avatarUrl: 'https://placekitten.com/100/100',
},
},
],
};

const mockReply = {
id: mockArticleReply.replyId,
text: 'Text reply text',
};
const mockArticleReplyFeedbacks = [
{ id: 'feedback1', comment: 'test comment', vote: 'UPVOTE', user: null },
{
id: 'feedback1',
comment: 'test comment',
vote: 'DOWNVOTE',
user: {
id: 'webUser1',
name: 'Web User',
level: 12,
avatarUrl: 'https://placekitten.com/100/100',
},
},
];

// MockedProvider mocks
//
Expand All @@ -51,6 +49,7 @@ const mocks = [
data: {
CreateOrUpdateArticleReplyFeedback: {
...mockArticleReply,
feedbacks: mockArticleReplyFeedbacks,
positiveFeedbackCount: mockArticleReply.positiveFeedbackCount + 1,
},
},
Expand All @@ -71,11 +70,35 @@ const mocks = [
data: {
CreateOrUpdateArticleReplyFeedback: {
...mockArticleReply,
feedbacks: mockArticleReplyFeedbacks,
negativeFeedbackCount: mockArticleReply.negativeFeedbackCount + 1,
},
},
},
},
// Load reply mock
{
request: {
query: LOAD_FEEDBACKS,
variables: {
articleId: mockArticleReply.articleId,
replyId: mockArticleReply.replyId,
},
},
result: {
data: {
ListArticleReplyFeedbacks: {
edges: mockArticleReplyFeedbacks.map(feedback => ({
node: feedback,
})),
},
GetReply: {
id: mockArticleReply.replyId,
text: 'Text reply text',
},
},
},
},
];

// current user mock - other user
Expand All @@ -84,7 +107,11 @@ const otherUserMock = {
request: { query: USER_QUERY },
result: {
data: {
GetUser: { id: 'other user' },
GetUser: {
id: 'other user',
name: 'Other User',
avatarUrl: 'https://placekitten.com/84/84',
},
},
},
};
Expand All @@ -111,17 +138,13 @@ export default {
};

export const WithArticleReplyAndReplySet = () => (
<MockedProvider mocks={[...mocks, otherUserMock]}>
<MockedProvider mocks={[...mocks, otherUserMock]} addTypename={false}>
<>
<p>Not voted yet</p>
<ArticleReplyFeedbackControl
articleReply={mockArticleReply}
reply={mockReply}
/>
<ArticleReplyFeedbackControl articleReply={mockArticleReply} />
<p>Upvoted</p>
<ArticleReplyFeedbackControl
articleReply={{ ...mockArticleReply, ownVote: 'UPVOTE' }}
reply={mockReply}
/>
</>
</MockedProvider>
Expand Down
Loading

0 comments on commit 9e4f20f

Please sign in to comment.