• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cofacts / rumors-site / 19682723090

25 Nov 2025 10:15AM UTC coverage: 67.864%. First build
19682723090

push

github

web-flow
Merge pull request #614 from lancatlin/remove-vote

feat: add remove vote feature

400 of 721 branches covered (55.48%)

Branch coverage included in aggregate %.

3 of 7 new or added lines in 2 files covered. (42.86%)

960 of 1283 relevant lines covered (74.82%)

11.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

46.81
/components/ArticleReplyFeedbackControl/ArticleReplyFeedbackControl.js
1
import React, { useState, useCallback, useRef } from 'react';
2
import { t } from 'ttag';
3
import gql from 'graphql-tag';
4
import { useMutation } from '@apollo/react-hooks';
5
import { makeStyles } from '@material-ui/core/styles';
6
import { Popover, Typography } from '@material-ui/core';
7
import Snackbar from '@material-ui/core/Snackbar';
8
import Button from '@material-ui/core/Button';
9
import CloseIcon from '@material-ui/icons/Close';
10
import ReasonsDisplay from './ReasonsDisplay';
11
import ButtonGroupDisplay from './ButtonGroupDisplay';
12
import cx from 'clsx';
13

14
const useStyles = makeStyles(theme => ({
1✔
15
  root: {
16
    display: 'flex',
17
  },
18
  popover: {
19
    position: 'relative',
20
    width: 420,
21
    maxWidth: '90vw',
22
    padding: 32,
23
  },
24
  closeButton: {
25
    background: theme.palette.common.white,
26
    cursor: 'pointer',
27
    position: 'absolute',
28
    right: 6,
29
    top: 10,
30
    border: 'none',
31
    outline: 'none',
32
    color: theme.palette.secondary[100],
33
  },
34
  popupTitle: {
35
    fontSize: 18,
36
    marginBottom: 24,
37
  },
38
  textarea: {
39
    padding: 15,
40
    width: '100%',
41
    borderRadius: 8,
42
    border: `1px solid ${theme.palette.secondary[100]}`,
43
    outline: 'none',
44
    '&:focus': {
45
      border: `1px solid ${theme.palette.primary[500]}`,
46
    },
47
  },
48
  textCenter: { textAlign: 'center' },
49
  sendButton: {
50
    marginTop: 10,
51
    borderRadius: 30,
52
  },
53
}));
54

55
// Subset of fields that needs to be updated after login
56
//
57
const ArticleReplyFeedbackControlDataForUser = gql`
1✔
58
  fragment ArticleReplyFeedbackControlDataForUser on ArticleReply {
59
    articleId
60
    replyId
61
    ownVote
62
    ...ButtonGroupDisplayArticleReplyForUser
63
  }
64
  ${ButtonGroupDisplay.fragments.ButtonGroupDisplayArticleReplyForUser}
65
`;
66

67
const ArticleReplyFeedbackControlData = gql`
1✔
68
  fragment ArticleReplyFeedbackControlData on ArticleReply {
69
    articleId
70
    replyId
71
    ...ArticleReplyFeedbackControlDataForUser
72
    ...ButtonGroupDisplayArticleReply
73
    ...ReasonsDisplayData
74
  }
75
  ${ArticleReplyFeedbackControlDataForUser}
76
  ${ButtonGroupDisplay.fragments.ButtonGroupDisplayArticleReply}
77
  ${ReasonsDisplay.fragments.ReasonsDisplayData}
78
`;
79

80
export const CREATE_REPLY_FEEDBACK = gql`
1✔
81
  mutation CreateOrUpdateArticleReplyFeedback(
82
    $articleId: String!
83
    $replyId: String!
84
    $vote: FeedbackVote!
85
    $comment: String
86
  ) {
87
    CreateOrUpdateArticleReplyFeedback(
88
      articleId: $articleId
89
      replyId: $replyId
90
      vote: $vote
91
      comment: $comment
92
    ) {
93
      ...ArticleReplyFeedbackControlData
94
    }
95
  }
96
  ${ArticleReplyFeedbackControlData}
97
`;
98

99
/**
100
 *
101
 * @param {ArticleReply} props.articleReply - ArticleReply from API
102
 * @param {Reply} props.reply - the reply instance of current articleReply.
103
 *   Isolated because not all use case have reply nested under articleReply.
104
 * @param {string?} props.className
105
 */
106
function ArticleReplyFeedbackControl({ articleReply, className }) {
107
  const classes = useStyles();
2✔
108
  const [vote, setVote] = useState(null);
2✔
109
  const [reason, setReason] = useState('');
2✔
110
  const [reasonsPopoverAnchorEl, setReasonsPopoverAnchorEl] = useState(null);
2✔
111
  const [votePopoverAnchorEl, setVotePopoverAnchorEl] = useState(null);
2✔
112
  const reasonsPopoverRef = useRef();
2✔
113

114
  const [showReorderSnack, setReorderSnackShow] = useState(false);
2✔
115
  const [createReplyFeedback, { loading: updatingReplyFeedback }] = useMutation(
2✔
116
    CREATE_REPLY_FEEDBACK,
117
    {
118
      refetchQueries: ['LoadArticlePage'], // Update article reply order
119
      awaitRefetchQueries: true,
120
      onCompleted() {
121
        closeVotePopover();
×
122
        setReason('');
×
NEW
123
        if (vote !== null) {
×
124
          // Do not show when removing vote
NEW
125
          setReorderSnackShow(true);
×
126
        }
127
      },
128
    }
129
  );
130

131
  const openReasonsPopover = event => {
2✔
132
    setReasonsPopoverAnchorEl(event.currentTarget);
×
133
  };
134

135
  const closeReasonsPopover = () => {
2✔
136
    setReasonsPopoverAnchorEl(null);
×
137
  };
138

139
  const openVotePopover = (event, value) => {
2✔
140
    setVotePopoverAnchorEl(event.currentTarget);
×
141
    setVote(value);
×
142
  };
143

144
  const closeVotePopover = () => {
2✔
145
    setVotePopoverAnchorEl(null);
×
146
    setVote(null);
×
147
  };
148

149
  const removeVote = useCallback(() => {
2✔
NEW
150
    setVote(null);
×
NEW
151
    createReplyFeedback({
×
152
      variables: {
153
        articleId: articleReply.articleId,
154
        replyId: articleReply.replyId,
155
        vote: 'NEUTRAL',
156
        comment: '',
157
      },
158
    });
159
  }, [createReplyFeedback, articleReply.articleId, articleReply.replyId]);
160

161
  const handleReasonReposition = useCallback(() => {
2✔
162
    if (reasonsPopoverRef.current) {
×
163
      reasonsPopoverRef.current.updatePosition();
×
164
    }
165
  }, []);
166

167
  return (
2✔
168
    <div className={cx(classes.root, className)}>
169
      <ButtonGroupDisplay
170
        articleReply={articleReply}
171
        onVoteUp={e => openVotePopover(e, 'UPVOTE')}
×
172
        onVoteDown={e => openVotePopover(e, 'DOWNVOTE')}
×
173
        onRemoveVote={removeVote}
174
        onReasonClick={openReasonsPopover}
175
      />
176
      <Popover
177
        open={!!reasonsPopoverAnchorEl}
178
        anchorEl={reasonsPopoverAnchorEl}
179
        action={reasonsPopoverRef}
180
        onClose={closeReasonsPopover}
181
        anchorOrigin={{
182
          vertical: 'top',
183
          horizontal: 'center',
184
        }}
185
        classes={{ paper: classes.popover }}
186
      >
187
        <button
188
          type="button"
189
          className={classes.closeButton}
190
          onClick={closeReasonsPopover}
191
        >
192
          <CloseIcon />
193
        </button>
194
        {!!reasonsPopoverAnchorEl && (
2!
195
          <ReasonsDisplay
196
            articleReply={articleReply}
197
            onSizeChange={handleReasonReposition}
198
          />
199
        )}
200
      </Popover>
201
      <Popover
202
        open={!!votePopoverAnchorEl}
203
        anchorEl={votePopoverAnchorEl}
204
        onClose={closeVotePopover}
205
        anchorOrigin={{
206
          vertical: 'top',
207
          horizontal: 'center',
208
        }}
209
        classes={{ paper: classes.popover }}
210
      >
211
        <button
212
          type="button"
213
          className={classes.closeButton}
214
          onClick={closeVotePopover}
215
        >
216
          <CloseIcon />
217
        </button>
218
        <Typography className={classes.popupTitle}>
219
          {vote === 'UPVOTE'
2!
220
            ? t`Do you have any thing to add?`
221
            : t`Why do you think it is not useful?`}
222
        </Typography>
223
        <textarea
224
          className={classes.textarea}
225
          value={reason}
226
          onChange={e => setReason(e.target.value)}
×
227
          rows={10}
228
        />
229
        <div className={classes.textCenter}>
230
          <Button
231
            className={classes.sendButton}
232
            color="primary"
233
            variant="contained"
234
            disableElevation
235
            disabled={updatingReplyFeedback}
236
            onClick={() => {
237
              createReplyFeedback({
×
238
                variables: {
239
                  articleId: articleReply.articleId,
240
                  replyId: articleReply.replyId,
241
                  vote,
242
                  comment: reason,
243
                },
244
              });
245
            }}
246
          >
247
            {t`Send`}
248
          </Button>
249
        </div>
250
      </Popover>
251
      <Snackbar
252
        open={showReorderSnack}
253
        onClose={() => setReorderSnackShow(false)}
×
254
        message={t`Thank you for the feedback.`}
255
      ></Snackbar>
256
    </div>
257
  );
258
}
259

260
ArticleReplyFeedbackControl.fragments = {
1✔
261
  ArticleReplyFeedbackControlData,
262
  ArticleReplyFeedbackControlDataForUser,
263
};
264

265
export default ArticleReplyFeedbackControl;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc