strapi-plugin-comments 2.0.0-rc.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +109 -67
  2. package/__mocks__/initSetup.js +53 -0
  3. package/admin/src/components/ActionButton/styles.js +10 -0
  4. package/admin/src/components/DiscussionThread/index.js +3 -7
  5. package/admin/src/components/DiscussionThreadItem/index.js +25 -70
  6. package/admin/src/components/DiscussionThreadItem/styles.js +4 -79
  7. package/admin/src/components/DiscussionThreadItemActions/index.js +82 -40
  8. package/admin/src/components/DiscussionThreadItemActions/styles.js +1 -12
  9. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +2 -3
  10. package/admin/src/components/DiscussionThreadItemFooter/index.js +47 -0
  11. package/admin/src/components/DiscussionThreadItemFooter/styles.js +13 -0
  12. package/admin/src/components/DiscussionThreadItemReviewAction/index.js +131 -0
  13. package/admin/src/components/IconButton/styles.js +14 -0
  14. package/admin/src/components/ReportsReviewModal/index.js +1 -1
  15. package/admin/src/components/ReportsReviewTable/index.js +12 -6
  16. package/admin/src/components/StatusBadge/index.js +17 -0
  17. package/admin/src/components/StatusBadge/styles.js +17 -0
  18. package/admin/src/pages/Details/components/DetailsEntity/index.js +42 -24
  19. package/admin/src/pages/Details/components/DetailsFilters/index.js +8 -10
  20. package/admin/src/pages/Details/index.js +6 -15
  21. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +30 -67
  22. package/admin/src/translations/en.json +13 -15
  23. package/admin/src/utils/resolveCommentStatus.js +4 -4
  24. package/content-types/comment.js +0 -4
  25. package/package.json +8 -5
  26. package/server/controllers/client.js +11 -6
  27. package/server/controllers/utils/__tests__/functions.test.js +2 -2
  28. package/server/controllers/utils/parsers.js +6 -2
  29. package/server/graphql/mutations/createAbuseReport.js +8 -3
  30. package/server/graphql/mutations/createComment.js +8 -3
  31. package/server/graphql/mutations/removeComment.js +8 -3
  32. package/server/graphql/mutations/updateComment.js +8 -3
  33. package/server/graphql/queries/findAllFlat.js +4 -2
  34. package/server/graphql/queries/findAllInHierarchy.js +3 -3
  35. package/server/graphql/resolvers-config.js +13 -8
  36. package/server/graphql/types/comment-nested.js +1 -1
  37. package/server/graphql/types/comment.js +0 -1
  38. package/server/graphql/types/create-comment-author.js +1 -1
  39. package/server/graphql/types/identify-comment-author.js +1 -1
  40. package/server/graphql/types/index.js +0 -1
  41. package/server/services/__tests__/common.test.js +165 -0
  42. package/server/services/admin.js +10 -7
  43. package/server/services/client.js +45 -31
  44. package/server/services/common.js +71 -19
  45. package/server/services/utils/__tests__/functions.test.js +9 -73
  46. package/server/services/utils/functions.js +23 -34
  47. package/server/utils/__tests__/functions.test.js +17 -0
  48. package/server/utils/constants.js +1 -0
  49. package/.circleci/config.yml +0 -48
  50. package/.eslintrc +0 -35
  51. package/.github/pull_request_template.md +0 -13
  52. package/.nvmrc +0 -1
  53. package/admin/src/components/DiscussionThreadItemMeta/index.js +0 -42
  54. package/admin/src/components/DiscussionThreadItemWarnings/index.js +0 -147
  55. package/admin/src/components/DiscussionThreadItemWarnings/styles.js +0 -7
  56. package/codecov.yml +0 -4
  57. package/public/assets/logo.png +0 -0
  58. package/public/assets/preview.png +0 -0
  59. package/server/graphql/types/comment-author-user.js +0 -9
@@ -1,88 +1,13 @@
1
1
  import styled from 'styled-components';
2
2
  import { Box } from '@strapi/design-system/Box';
3
- import { Flex } from '@strapi/design-system/Flex';
4
3
 
5
- export const AvatarWrapper = styled(Box)``;
6
- export const InitialsWrapper = styled(Box)``;
7
-
8
- export const DiscussionThreadItemLinkBox = styled(Flex)`
4
+ export const DiscussionThreadFullsize = styled(Box)`
9
5
  width: 100%;
10
- margin-top: .5rem;
11
- margin-bottom: .5rem;
12
6
  `
13
-
14
- const DiscussionThreadItemStyled = styled(Box)`
15
- display: flex;
16
- flex-direction: column;
17
- width: 100%;
18
- margin-bottom: 1rem;
19
-
20
- ${AvatarWrapper},
21
- ${InitialsWrapper} {
22
- align-self: flex-start;
23
- margin-top: .4rem;
24
- }
25
- `;
26
-
27
- export const DiscussionThreadItemContent = styled(Box)`
28
- max-width: 45%;
29
- `;
30
-
31
- export const DiscussionThreadItemLeft = styled(DiscussionThreadItemStyled)`
7
+ export const DiscussionThreadItemContent = styled(DiscussionThreadFullsize)`
32
8
  align-items: flex-start;
33
- padding-left: 4rem;
34
- text-align: left;
35
-
36
- ${DiscussionThreadItemContent} {
37
- align-self: flex-start;
38
- }
39
-
40
- ${AvatarWrapper},
41
- ${InitialsWrapper} {
42
- margin-right: 1rem;
43
- }
44
9
  `;
45
10
 
46
- export const DiscussionThreadItemRight = styled(DiscussionThreadItemStyled)`
47
- align-items: flex-end;
48
- padding-right: 4rem;
49
- text-align: left;
50
-
51
- ${DiscussionThreadItemContent} {
52
- align-self: flex-end;
53
- flex-direction: row-reverse;
54
- }
55
-
56
- ${AvatarWrapper},
57
- ${InitialsWrapper} {
58
- margin-left: 1rem;
59
- }
60
-
61
- ${DiscussionThreadItemLinkBox} {
62
- margin-right: 3rem;
63
- }
64
- `;
65
-
66
- export const DiscussionThreadItemBox = styled(Box)`
67
-
11
+ export const DiscussionThreadItemContainer = styled(DiscussionThreadFullsize)`
12
+ align-items: flex-start;
68
13
  `;
69
-
70
- export const DiscussionThreadItemBoxContent = styled(Box)`
71
- width: 100%;
72
- align-items: stretch;
73
- `
74
-
75
- export const DiscussionThreadItemRoot = styled(DiscussionThreadItemLeft)`
76
- padding-left: 0;
77
- padding-right: 0;
78
-
79
-
80
- ${DiscussionThreadItemContent} {
81
- width: 100%;
82
- max-width: 100%;
83
- }
84
-
85
- ${DiscussionThreadItemBox} {
86
- flex-grow: 1;
87
- }
88
- `;
@@ -7,24 +7,42 @@
7
7
  import React, { useState } from 'react';
8
8
  import PropTypes from 'prop-types';
9
9
  import { useMutation, useQueryClient } from 'react-query';
10
+ import { useHistory } from 'react-router-dom';
10
11
  import { isNil, isEmpty } from 'lodash';
11
- import { Button } from '@strapi/design-system/Button';
12
12
  import { Flex } from '@strapi/design-system/Flex';
13
13
  import { IconButton } from '@strapi/design-system/IconButton';
14
14
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
15
- import { getMessage, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
16
- import { DiscussionThreadItemActionsBadge, DiscussionThreadItemActionsWrapper } from './styles';
15
+ import { Eye } from '@strapi/icons';
16
+ import { getMessage, getUrl, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
17
+ import { DiscussionThreadItemActionsWrapper } from './styles';
17
18
  import ConfirmationDialog from '../ConfirmationDialog';
18
19
  import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/utils/api';
19
20
  import pluginId from '../../pluginId';
20
21
  import { LockIcon, UnlockIcon } from '../icons';
21
22
  import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
22
-
23
- const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, reports = [], approvalStatus, root, allowedActions: { canModerate, canAccessReports, canReviewReports } }) => {
23
+ import StatusBadge from '../StatusBadge';
24
+ import { IconButtonGroupStyled } from '../IconButton/styles';
25
+ import { ActionButton } from '../ActionButton/styles';
26
+ import DiscussionThreadItemReviewAction from '../DiscussionThreadItemReviewAction';
27
+
28
+ const DiscussionThreadItemActions = ({ allowedActions: { canModerate, canAccessReports, canReviewReports }, ...item }) => {
29
+ const { id,
30
+ blocked,
31
+ removed,
32
+ blockedThread,
33
+ gotThread,
34
+ threadItemsCount,
35
+ threadFirstItemId,
36
+ pinned,
37
+ preview,
38
+ reports = [],
39
+ approvalStatus } = item;
24
40
 
25
41
  const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
26
42
  const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
43
+ const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(blockedThread);
27
44
 
45
+ const { push } = useHistory();
28
46
  const toggleNotification = useNotification();
29
47
  const queryClient = useQueryClient();
30
48
  const { lockApp, unlockApp } = useOverlayBlocker();
@@ -75,17 +93,20 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
75
93
  const openReports = reports?.filter(_ => !_.resolved);
76
94
  const hasReports = !isEmpty(openReports);
77
95
  const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
78
-
79
- const resolveStatusBadge = () => {
80
- const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
81
- return {
82
- badgeVisible: isBlocked || gotApprovalFlow || removed,
83
- badgeColor: resolveCommentStatusColor(status),
84
- badgeLabel: status,
85
- };
86
- };
87
-
88
- const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
96
+ const hasActiveThread = gotThread && !(removed || preview || pinned || blockedThread);
97
+ const isStatusBadgeVisible = isBlocked || reviewFlowEnabled;
98
+
99
+ const renderStatus = (props) => {
100
+ const status = resolveCommentStatus({ ...props, reviewFlowEnabled });
101
+ const color = resolveCommentStatusColor(status);
102
+
103
+ return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}700`} color={color}>{ getMessage({
104
+ id: `page.common.item.status.${status}`,
105
+ props: {
106
+ count: openReports.length
107
+ }
108
+ }, status) }</StatusBadge>);
109
+ };
89
110
 
90
111
  const handleBlockClick = () => setBlockConfirmationVisible(true);
91
112
  const handleBlockConfirm = async () => {
@@ -121,36 +142,57 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
121
142
  }
122
143
  };
123
144
 
145
+ const handleDrilldownClick = () => {
146
+ if (hasActiveThread) {
147
+ push(getUrl(`discover/${threadFirstItemId}`));
148
+ }
149
+ };
150
+
151
+ const handleBlockActionClick = async (mutation, onCallback) => {
152
+ lockApp();
153
+ await mutation.mutateAsync(id);
154
+ onCallback();
155
+ };
156
+
157
+ const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
158
+
159
+ const anyGroupButtonsVisible = needsApproval || reviewFlowEnabled || !blockedThread;
160
+ const isLoading = unblockItemMutation.isLoading || blockItemMutation.isLoading || blockItemThreadMutation.isLoading || unblockItemThreadMutation.isLoading;
161
+
124
162
  if (removed || isRejected || !canModerate) {
125
163
  return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
126
- <DiscussionThreadItemActionsBadge
127
- color={badgeColor}
128
- backgroundColor={`${badgeColor}100`}
129
- textColor={`${badgeColor}600`}>
130
- { getMessage({
131
- id: `page.common.item.status.${badgeLabel}`,
132
- props: { count: openReports.length },
133
- }, badgeLabel)}
134
- </DiscussionThreadItemActionsBadge>
164
+ { renderStatus(item) }
135
165
  </DiscussionThreadItemActionsWrapper>);
136
166
  }
137
167
 
138
168
  return (<>
139
169
  <DiscussionThreadItemActionsWrapper as={Flex} direction="row">
140
- { badgeVisible && (<DiscussionThreadItemActionsBadge
141
- color={badgeColor}
142
- backgroundColor={`${badgeColor}100`}
143
- textColor={`${badgeColor}600`}>
144
- { getMessage({
145
- id: `page.common.item.status.${badgeLabel}`,
146
- props: { count: openReports.length },
147
- }, badgeLabel)}
148
- </DiscussionThreadItemActionsBadge>)}
149
- { !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} style={(!blockedThread && root) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
150
- { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
151
- { (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
152
- { (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
153
- { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
170
+ { isStatusBadgeVisible && (renderStatus(item))}
171
+ { (!blockedThread && (gotThread || pinned)) && (<ActionButton onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</ActionButton>) }
172
+ { (blockedThread && (gotThread || pinned)) && (<ActionButton onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</ActionButton>) }
173
+ { anyGroupButtonsVisible && (<IconButtonGroupStyled isSingle withMargin>
174
+ { !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} />)}
175
+ { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} />)}
176
+ { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
177
+ <DiscussionThreadItemReviewAction
178
+ item={item}
179
+ queryToInvalidate="get-details-data"
180
+ areBlockButtonsDisabled={blockButtonsDisabled}
181
+ isLoading={isLoading}
182
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
183
+ blockItemMutation={blockItemMutation}
184
+ blockItemThreadMutation={blockItemThreadMutation}
185
+ onBlockButtonsStateChange={handleBlockButtonsStateChange}
186
+ onBlockActionClick={handleBlockActionClick}
187
+ />
188
+ </IconButtonGroupStyled>)}
189
+ { hasActiveThread && (<IconButtonGroupStyled isSingle withMargin>
190
+ <IconButton
191
+ onClick={handleDrilldownClick}
192
+ icon={<Eye />}
193
+ label={getMessage('page.details.panel.discussion.nav.drilldown', 'Drilldown thread')}
194
+ style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />
195
+ </IconButtonGroupStyled>) }
154
196
  </DiscussionThreadItemActionsWrapper>
155
197
  { !blocked && (<ConfirmationDialog
156
198
  isVisible={blockConfirmationVisible}
@@ -162,7 +204,7 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
162
204
  onCancel={handleBlockCancel}>
163
205
  { getMessage('page.details.actions.comment.block.confirmation.description') }
164
206
  </ConfirmationDialog>) }
165
- { (!blockedThread && root) && (<ConfirmationDialog
207
+ { !blockedThread && (<ConfirmationDialog
166
208
  isVisible={blockThreadConfirmationVisible}
167
209
  isActionAsync={blockItemThreadMutation.isLoading}
168
210
  header={getMessage('page.details.actions.thread.block.confirmation.header')}
@@ -1,18 +1,7 @@
1
1
  import styled from 'styled-components';
2
2
  import { Box } from '@strapi/design-system/Box';
3
- import { Badge } from '@strapi/design-system/Badge';
4
- import { IconButtonGroup } from '@strapi/design-system/IconButton';
5
-
6
- export const DiscussionThreadItemActionsBadge = styled(Badge)`
7
- border: 1px ${ ({ theme, color }) => theme.colors[`${color}200`] } solid;
8
- `;
9
3
 
10
4
  export const DiscussionThreadItemActionsWrapper = styled(Box)`
11
5
  margin-left: 1rem;
12
- align-items: start;
13
-
14
- ${DiscussionThreadItemActionsBadge} {
15
- margin-top: .175rem;
16
- margin-right: .5rem;
17
- }
6
+ align-items: flex-start;
18
7
  `;
@@ -11,7 +11,6 @@ import { IconButton } from '@strapi/design-system/IconButton';
11
11
  import { Check, Cross } from '@strapi/icons';
12
12
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
13
13
  import { getMessage, handleAPIError } from '../../utils';
14
- import { DiscussionThreadItemActionsGroupWrapper } from './styles';
15
14
  import { approveItem, rejectItem } from '../../pages/utils/api';
16
15
  import pluginId from '../../pluginId';
17
16
 
@@ -60,10 +59,10 @@ const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canMode
60
59
  };
61
60
 
62
61
  if (canModerate) {
63
- return (<DiscussionThreadItemActionsGroupWrapper>
62
+ return (<>
64
63
  <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
65
64
  <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
66
- </DiscussionThreadItemActionsGroupWrapper>);
65
+ </>);
67
66
  }
68
67
  return null;
69
68
  };
@@ -0,0 +1,47 @@
1
+ /**
2
+ *
3
+ * Discussion thread - Item footer
4
+ *
5
+ */
6
+
7
+ import React from 'react';
8
+ import PropTypes from 'prop-types';
9
+ import { useIntl } from 'react-intl';
10
+ import { Avatar, Initials } from '@strapi/design-system/Avatar';
11
+ import { Box } from '@strapi/design-system/Box';
12
+ import { Typography } from '@strapi/design-system/Typography';
13
+ import { renderInitials } from '../../utils';
14
+ import { DiscussionThreadItemFooterMeta, DiscussionThreadItemFooterStyled } from './styles';
15
+
16
+ const DiscussionThreadItemFooter = ({ children, author, createdAt, updatedAt }) => {
17
+
18
+ const { formatDate } = useIntl();
19
+
20
+ const dateTime = formatDate(updatedAt || createdAt, { dateStyle: 'medium', timeStyle: 'short' });
21
+ const { name, avatar } = author;
22
+
23
+
24
+ return (<DiscussionThreadItemFooterStyled as={Box} paddingTop={2} direction="row">
25
+ <DiscussionThreadItemFooterMeta size={3} horizontal>
26
+ { avatar ? <Avatar src={avatar} alt={name} /> : <Initials>{ renderInitials(name) }</Initials> }
27
+ <Typography variant="pi" fontWeight="bold" textColor="neutral800">{ name }</Typography>
28
+ <Typography variant="pi" textColor="neutral600">{ dateTime }</Typography>
29
+ </DiscussionThreadItemFooterMeta>
30
+ { children }
31
+ </DiscussionThreadItemFooterStyled>);
32
+ };
33
+
34
+ DiscussionThreadItemFooter.propTypes = {
35
+ author: PropTypes.shape({
36
+ id: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
37
+ name: PropTypes.string.isRequired,
38
+ email: PropTypes.string,
39
+ avatar: PropTypes.string,
40
+ }).isRequired,
41
+ createdAt: PropTypes.string.isRequired,
42
+ updatedAt: PropTypes.string,
43
+ children: PropTypes.array,
44
+ };
45
+
46
+ export default DiscussionThreadItemFooter;
47
+
@@ -0,0 +1,13 @@
1
+ import styled from 'styled-components';
2
+ import { Flex } from '@strapi/design-system/Flex';
3
+ import { Stack } from '@strapi/design-system/Stack';
4
+
5
+ export const DiscussionThreadItemFooterStyled = styled(Flex)`
6
+ width: 100%;
7
+ `;
8
+
9
+ export const DiscussionThreadItemFooterMeta = styled(Stack)`
10
+ * + * {
11
+ margin-left: 8px;
12
+ }
13
+ `;
@@ -0,0 +1,131 @@
1
+ import React, { useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { useMutation, useQueryClient } from 'react-query';
4
+ import { isNil, isEmpty, orderBy } from 'lodash';
5
+ import { Button } from '@strapi/design-system/Button';
6
+ import { IconButton } from '@strapi/design-system/IconButton';
7
+ import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
8
+ import { ReviewIcon, LockIcon } from '../icons';
9
+ import { resolveReport } from '../../pages/utils/api';
10
+ import { getMessage, handleAPIError } from '../../utils';
11
+ import ReportsReviewTable from '../ReportsReviewTable';
12
+ import ReportsReviewModal from '../ReportsReviewModal';
13
+ import pluginId from '../../pluginId';
14
+
15
+ const DiscussionThreadItemReviewAction = ({
16
+ item,
17
+ isAnyActionLoading: isAnyActionLoading,
18
+ queryToInvalidate,
19
+ areBlockButtonsDisabled,
20
+ allowedActions: { canModerate, canAccessReports, canReviewReports },
21
+ blockItemMutation,
22
+ blockItemThreadMutation,
23
+ onBlockButtonsStateChange,
24
+ onBlockActionClick }) => {
25
+
26
+ const { reports } = item;
27
+
28
+ const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
29
+
30
+ const toggleNotification = useNotification();
31
+ const queryClient = useQueryClient();
32
+ const { unlockApp } = useOverlayBlocker();
33
+
34
+ const onSuccess = (message, stateAction = () => {}, indalidate = true) => async () => {
35
+ if (indalidate) {
36
+ await queryClient.invalidateQueries(queryToInvalidate);
37
+ }
38
+ toggleNotification({
39
+ type: 'success',
40
+ message: `${pluginId}.${message}`,
41
+ });
42
+ stateAction(false);
43
+ unlockApp();
44
+ };
45
+
46
+ const onError = err => { handleAPIError(err, toggleNotification); };
47
+
48
+ const resolveReportMutation = useMutation(resolveReport, {
49
+ onSuccess: onSuccess('page.details.panel.discussion.warnings.reports.dialog.confirmation.success', () => {}, false),
50
+ onError,
51
+ refetchActive: false,
52
+ });
53
+
54
+ const handleBlockActionClick = () => setReportsReviewVisible(false);
55
+
56
+ const handleReportsReviewClick = () => {
57
+ if (canAccessReports) {
58
+ setReportsReviewVisible(true);
59
+ }
60
+ };
61
+ const handleBlockItemClick = async () => {
62
+ if (canModerate) {
63
+ await onBlockActionClick(blockItemMutation, handleBlockActionClick);
64
+ }
65
+ };
66
+ const handleBlockItemThreadClick = async () => {
67
+ if (canModerate) {
68
+ await onBlockActionClick(blockItemThreadMutation, handleBlockActionClick);
69
+ }
70
+ };
71
+
72
+ const handleReportsReviewClose = async () => {
73
+ await queryClient.invalidateQueries(queryToInvalidate);
74
+ setReportsReviewVisible(false);
75
+ };
76
+
77
+ const isLoading = isAnyActionLoading && resolveReportMutation.isLoading;
78
+ const openReports = reports?.filter(_ => !_.resolved);
79
+ const hasReports = !isEmpty(openReports);
80
+ const reviewFlowEnabled = (canAccessReports && hasReports) && !(item.blocked || item.blockedThread);
81
+
82
+ if (reviewFlowEnabled) {
83
+ return (<>
84
+ <IconButton onClick={handleReportsReviewClick} label={ getMessage('page.discover.table.reports.review') } icon={<ReviewIcon />} />
85
+ <ReportsReviewModal
86
+ isVisible={reportsReviewVisible}
87
+ isActionAsync={isLoading}
88
+ allowedActions={{ canModerate, canAccessReports, canReviewReports }}
89
+ onClose={handleReportsReviewClose}
90
+ startActions={<Button onClick={handleReportsReviewClose} variant="tertiary">
91
+ { getMessage('compontents.confirmation.dialog.button.cancel', 'Cancel') }
92
+ </Button>}
93
+ endActions={canModerate && (<>
94
+ <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={areBlockButtonsDisabled}>
95
+ { getMessage(`page.details.actions.comment.block`, 'Block comment') }
96
+ </Button>
97
+ { item.gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={areBlockButtonsDisabled}>
98
+ { getMessage(`page.details.actions.thread.block`, 'Block thread') }
99
+ </Button>) }
100
+ </>)}
101
+ item={item}>
102
+ <ReportsReviewTable
103
+ commentId={item.id}
104
+ items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
105
+ mutation={resolveReportMutation}
106
+ allowedActions={{ canAccessReports, canReviewReports }}
107
+ onBlockButtonsStateChange={onBlockButtonsStateChange}
108
+ />
109
+ </ReportsReviewModal>
110
+ </>);
111
+ }
112
+ return null;
113
+ };
114
+
115
+ DiscussionThreadItemReviewAction.propTypes = {
116
+ item: PropTypes.object.isRequired,
117
+ isLoading: PropTypes.bool,
118
+ queryToInvalidate: PropTypes.string.isRequired,
119
+ areBlockButtonsDisabled: PropTypes.bool,
120
+ allowedActions: PropTypes.shape({
121
+ canModerate: PropTypes.bool,
122
+ canAccessReports: PropTypes.bool,
123
+ canReviewReports: PropTypes.bool,
124
+ }),
125
+ blockItemMutation: PropTypes.func.isRequired,
126
+ blockItemThreadMutation: PropTypes.func.isRequired,
127
+ onBlockButtonsStateChange: PropTypes.func.isRequired,
128
+ onBlockActionClick: PropTypes.func.isRequired,
129
+ };
130
+
131
+ export default DiscussionThreadItemReviewAction;
@@ -0,0 +1,14 @@
1
+
2
+ import styled from 'styled-components';
3
+ import { IconButtonGroup } from '@strapi/design-system/IconButton';
4
+
5
+ export const IconButtonGroupStyled = styled(IconButtonGroup)`
6
+ ${ ({ isSingle }) => isSingle && (`
7
+ span:first-child button {
8
+ border-radius: 4px;
9
+ }
10
+ `)}
11
+ ${ ({ withMargin }) => withMargin && (`
12
+ margin-left: .5rem;
13
+ `)}
14
+ `;
@@ -24,7 +24,7 @@ const ReportsReviewModal = ({
24
24
  <Typography fontWeight="bold" textColor="neutral800" as="h2" id="title">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.header', 'Open reports') }</Typography>
25
25
  </ModalHeader>
26
26
  <ModalBody>
27
- <DiscussionThreadItem {...item} root preview isSelected={false} />
27
+ <DiscussionThreadItem as="div" {...item} root preview isSelected={true} />
28
28
  { children }
29
29
  </ModalBody>
30
30
  <ModalFooter variant="tertiary" startActions={startActions} endActions={endActions} />
@@ -8,8 +8,6 @@
8
8
  import { useIntl } from 'react-intl';
9
9
  import PropTypes from 'prop-types';
10
10
  import { isEmpty } from 'lodash';
11
- import { Badge } from '@strapi/design-system/Badge';
12
- import { Button } from '@strapi/design-system/Button';
13
11
  import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
14
12
  import { Flex } from '@strapi/design-system/Flex';
15
13
  import { Table, Thead, Tbody, TFooter, Tr, Th, Td } from '@strapi/design-system/Table';
@@ -19,6 +17,8 @@
19
17
  import { useOverlayBlocker } from '@strapi/helper-plugin';
20
18
  import { getMessage } from '../../utils';
21
19
  import { REPORT_REASON, REPORT_STATUS } from '../../utils/constants';
20
+ import StatusBadge from '../StatusBadge';
21
+ import { ActionButton } from '../ActionButton/styles';
22
22
 
23
23
  const ReportsReviewTable = ({
24
24
  commentId,
@@ -43,7 +43,7 @@ const ReportsReviewTable = ({
43
43
  case REPORT_STATUS.OPEN:
44
44
  color = 'danger';
45
45
  };
46
- return (<Badge backgroundColor={`${color}100`} textColor={`${color}600`}>{ getMessage(`page.details.panel.discussion.warnings.reports.dialog.status.${status}`, status) }</Badge>);
46
+ return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}600`} color={color}>{ getMessage(`page.details.panel.discussion.warnings.reports.dialog.status.${status}`, status) }</StatusBadge>);
47
47
  };
48
48
 
49
49
  const renderReason = reason => {
@@ -55,7 +55,7 @@ const ReportsReviewTable = ({
55
55
  case REPORT_REASON.BAD_LANGUAGE:
56
56
  color = 'warning';
57
57
  };
58
- return (<Badge backgroundColor={`${color}100`} textColor={`${color}600`}>{ getMessage(`page.details.panel.discussion.warnings.reports.dialog.reason.${reason}`, reason) }</Badge>);
58
+ return (<StatusBadge backgroundColor={`${color}100`} textColor={`${color}600`} color={color}>{ getMessage(`page.details.panel.discussion.warnings.reports.dialog.reason.${reason}`, reason) }</StatusBadge>);
59
59
  };
60
60
 
61
61
  const handleClickResolve = async (reportId) => {
@@ -125,11 +125,17 @@ const ReportsReviewTable = ({
125
125
  <Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
126
126
  </Td>
127
127
  <Td>
128
- <Typography textColor="neutral800">{ renderStatus(entry.resolved) }</Typography>
128
+ { renderStatus(entry.resolved) }
129
129
  </Td>
130
130
  { canReviewReports && (<Td>
131
131
  { !entry.resolved && (<Flex direction="column" alignItems="flex-end">
132
- <Button onClick={() => handleClickResolve(entry.id)} startIcon={<Check />} variant="success">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }</Button>
132
+ <ActionButton
133
+ isSingle
134
+ onClick={() => handleClickResolve(entry.id)}
135
+ startIcon={<Check />}
136
+ variant="success">
137
+ { getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }
138
+ </ActionButton>
133
139
  </Flex>) }
134
140
  </Td>)}
135
141
  </Tr>)}
@@ -0,0 +1,17 @@
1
+ /**
2
+ *
3
+ * Status badge
4
+ *
5
+ */
6
+
7
+ import React from 'react';
8
+ import { Typography } from '@strapi/design-system/Typography';
9
+ import { StatusBadge as StatusBadgeStyled } from './styles';
10
+
11
+ const StatusBadge = ({ children, textColor, ...rest }) => (<StatusBadgeStyled {...rest} >
12
+ <Typography variant="omega" fontWeight="semibold" textColor={textColor}>
13
+ { children }
14
+ </Typography>
15
+ </StatusBadgeStyled>);
16
+
17
+ export default StatusBadge;
@@ -0,0 +1,17 @@
1
+ import styled from 'styled-components';
2
+ import { Badge } from '@strapi/design-system/Badge';
3
+
4
+ export const StatusBadge = styled(Badge)`
5
+ padding: 5px 8px;
6
+ border: 1px ${ ({ theme, color }) => theme.colors[`${color}200`] } solid;
7
+
8
+ overflow: hidden;
9
+
10
+ text-overflow: ellipsis;
11
+
12
+ cursor: default;
13
+
14
+ span {
15
+ text-transform: none;
16
+ }
17
+ `;