strapi-plugin-comments 2.0.0-beta.5 → 2.0.0-rc.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 (60) hide show
  1. package/README.md +472 -118
  2. package/admin/src/components/DiscussionThread/index.js +9 -3
  3. package/admin/src/components/DiscussionThreadItem/index.js +24 -11
  4. package/admin/src/components/DiscussionThreadItemActions/index.js +45 -18
  5. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +20 -11
  6. package/admin/src/components/DiscussionThreadItemMeta/index.js +16 -5
  7. package/admin/src/components/DiscussionThreadItemWarnings/index.js +15 -9
  8. package/admin/src/components/ReportsReviewModal/index.js +5 -0
  9. package/admin/src/components/ReportsReviewTable/index.js +60 -52
  10. package/admin/src/components/icons/review.js +1 -1
  11. package/admin/src/index.js +1 -1
  12. package/admin/src/pages/Details/components/DetailsEntity/index.js +2 -2
  13. package/admin/src/pages/Details/components/DetailsFilters/index.js +1 -0
  14. package/admin/src/pages/Details/index.js +43 -37
  15. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +38 -11
  16. package/admin/src/pages/Discover/index.js +72 -59
  17. package/admin/src/permissions.js +10 -8
  18. package/admin/src/translations/en.json +2 -0
  19. package/admin/src/utils/index.js +2 -0
  20. package/admin/src/utils/renderInitials.js +5 -0
  21. package/content-types/comment.js +0 -4
  22. package/package.json +4 -2
  23. package/permissions.js +19 -0
  24. package/public/assets/logo.png +0 -0
  25. package/public/assets/preview.png +0 -0
  26. package/server/bootstrap.js +38 -2
  27. package/server/config/index.js +7 -2
  28. package/server/controllers/client.js +11 -21
  29. package/server/controllers/utils/parsers.js +19 -0
  30. package/server/graphql/index.js +26 -0
  31. package/server/graphql/mutations/createAbuseReport.js +25 -0
  32. package/server/graphql/mutations/createComment.js +25 -0
  33. package/server/graphql/mutations/index.js +21 -0
  34. package/server/graphql/mutations/removeComment.js +25 -0
  35. package/server/graphql/mutations/updateComment.js +25 -0
  36. package/server/graphql/queries/findAllFlat.js +29 -0
  37. package/server/graphql/queries/findAllInHierarchy.js +31 -0
  38. package/server/graphql/queries/index.js +19 -0
  39. package/server/graphql/resolvers-config.js +15 -0
  40. package/server/graphql/types/comment-author.js +9 -0
  41. package/server/graphql/types/comment-nested.js +15 -0
  42. package/server/graphql/types/comment.js +15 -0
  43. package/server/graphql/types/create-comment-author.js +9 -0
  44. package/server/graphql/types/create-comment.js +9 -0
  45. package/server/graphql/types/create-report.js +9 -0
  46. package/server/graphql/types/id.js +24 -0
  47. package/server/graphql/types/identify-comment-author.js +6 -0
  48. package/server/graphql/types/index.js +16 -0
  49. package/server/graphql/types/remove-comment.js +8 -0
  50. package/server/graphql/types/report-reason.js +10 -0
  51. package/server/graphql/types/report.js +11 -0
  52. package/server/graphql/types/update-comment.js +9 -0
  53. package/server/register.js +3 -1
  54. package/server/services/admin.js +20 -16
  55. package/server/services/client.js +72 -49
  56. package/server/services/common.js +23 -12
  57. package/server/services/graphql.js +190 -0
  58. package/server/services/index.js +2 -0
  59. package/server/services/utils/functions.js +30 -11
  60. package/strapi-server.js +33 -0
@@ -18,7 +18,7 @@ import { getMessage, getUrl } from '../../utils';
18
18
  import LoadingIndicatorOverlay from '../LoadingIndicatorOverlay';
19
19
  import { LoadingIndicatorContainer } from '../LoadingIndicatorOverlay/styles';
20
20
 
21
- const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
21
+ const DiscussionThread = ({ level = [], selected = {}, allowedActions, isReloading }) => {
22
22
  const rootThread = selected?.threadOf;
23
23
  return (<LoadingIndicatorContainer as={Box} padding={4}>
24
24
  { isReloading && <LoadingIndicatorOverlay />}
@@ -34,13 +34,14 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
34
34
  <Divider />
35
35
  </Box>
36
36
  <Flex as="ul" direction="column" alignItems="flex-start">
37
- { rootThread && (<DiscussionThreadItem {...rootThread} isThreadAuthor root pinned />) }
37
+ { rootThread && (<DiscussionThreadItem {...rootThread} allowedActions={allowedActions} isThreadAuthor root pinned />) }
38
38
  { level.map(item => {
39
39
  const isSelected = selected.id === item.id;
40
- const isThreadAuthor = !isNil(selected?.threadOf?.authorId) && (selected?.threadOf?.authorId === item.authorId);
40
+ const isThreadAuthor = !isNil(selected?.threadOf?.author?.id) && (selected?.threadOf?.author?.id === item?.author?.id);
41
41
  return (<DiscussionThreadItem
42
42
  key={`comment-${item.id}`}
43
43
  {...item}
44
+ allowedActions={allowedActions}
44
45
  root={isNil(rootThread)}
45
46
  blockedThread={rootThread?.blockedThread || item.blockedThread}
46
47
  isSelected={isSelected}
@@ -54,6 +55,11 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
54
55
  DiscussionThread.propTypes = {
55
56
  level: PropTypes.array.isRequired,
56
57
  selected: PropTypes.object.isRequired,
58
+ allowedActions: PropTypes.shape({
59
+ canModerate: PropTypes.bool,
60
+ canAccessReports: PropTypes.bool,
61
+ canReviewReports: PropTypes.bool,
62
+ }),
57
63
  isReloading: PropTypes.bool,
58
64
  };
59
65
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  import React from 'react';
8
8
  import PropTypes from 'prop-types';
9
- import { first } from 'lodash';
10
9
  import { Avatar, Initials } from '@strapi/design-system/Avatar';
11
10
  import { Box } from '@strapi/design-system/Box';
12
11
  import { Divider } from '@strapi/design-system/Divider';
@@ -17,11 +16,23 @@ import { ArrowRight } from '@strapi/icons';
17
16
  import DiscussionThreadItemMeta from '../DiscussionThreadItemMeta';
18
17
  import { AvatarWrapper, DiscussionThreadItemBox, DiscussionThreadItemBoxContent, DiscussionThreadItemContent, DiscussionThreadItemLeft, DiscussionThreadItemLinkBox, DiscussionThreadItemRight, DiscussionThreadItemRoot } from './styles';
19
18
  import DiscussionThreadItemActions from '../DiscussionThreadItemActions';
20
- import { getMessage, getUrl } from '../../utils';
19
+ import { getMessage, getUrl, renderInitials } from '../../utils';
21
20
  import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
22
21
 
23
- const DiscussionThreadItem = (item) => {
24
- const { id, isSelected, isThreadAuthor, content, root, preview, pinned, blocked, removed, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
22
+ const DiscussionThreadItem = (props) => {
23
+ const {
24
+ isSelected,
25
+ isThreadAuthor,
26
+ content,
27
+ root,
28
+ preview,
29
+ pinned,
30
+ blocked,
31
+ removed,
32
+ blockedThread,
33
+ gotThread,
34
+ threadFirstItemId,
35
+ author = {} } = props;
25
36
 
26
37
  let background = 'neutral100';
27
38
  let borderColor = 'neutral200';
@@ -45,11 +56,8 @@ const DiscussionThreadItem = (item) => {
45
56
  StyledComponent = DiscussionThreadItemRoot;
46
57
  }
47
58
 
48
- const avatar = authorAvatar || authorUser?.avatar;
49
- const name = authorName || `${authorUser?.firstName} ${authorUser?.lastName}`;
59
+ const { name, avatar } = author;
50
60
  const isBlocked = blocked || blockedThread;
51
-
52
- const renderInitials = value => value.split(' ').map(_ => first(_)).join('').toUpperCase();
53
61
 
54
62
  return (<StyledComponent as="li">
55
63
  <DiscussionThreadItemContent as={Flex} direction="row">
@@ -62,12 +70,12 @@ const DiscussionThreadItem = (item) => {
62
70
  <Box as={Flex} grow={1} alignItems="center">
63
71
  <Typography variant="omega">{ content }</Typography>
64
72
  </Box>
65
- { !preview && (<DiscussionThreadItemActions { ...item } root={root || pinned} />) }
73
+ { !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
66
74
  </DiscussionThreadItemBoxContent>
67
- { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...item } gotThread={gotThread || root} />) }
75
+ { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
68
76
  </Flex>
69
77
  <Divider background={dividerColor} />
70
- <DiscussionThreadItemMeta { ...item } />
78
+ <DiscussionThreadItemMeta { ...props } />
71
79
  </DiscussionThreadItemBox>
72
80
  </DiscussionThreadItemContent>
73
81
 
@@ -81,6 +89,11 @@ const DiscussionThreadItem = (item) => {
81
89
 
82
90
  DiscussionThreadItem.propTypes = {
83
91
  content: PropTypes.string.isRequired,
92
+ allowedActions: PropTypes.shape({
93
+ canModerate: PropTypes.bool,
94
+ canAccessReports: PropTypes.bool,
95
+ canReviewReports: PropTypes.bool,
96
+ }),
84
97
  };
85
98
 
86
99
  export default DiscussionThreadItem;
@@ -7,7 +7,7 @@
7
7
  import React, { useState } from 'react';
8
8
  import PropTypes from 'prop-types';
9
9
  import { useMutation, useQueryClient } from 'react-query';
10
- import { isNil } from 'lodash';
10
+ import { isNil, isEmpty } from 'lodash';
11
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';
@@ -20,7 +20,7 @@ import pluginId from '../../pluginId';
20
20
  import { LockIcon, UnlockIcon } from '../icons';
21
21
  import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
22
22
 
23
- const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, approvalStatus, root }) => {
23
+ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, reports = [], approvalStatus, root, allowedActions: { canModerate, canAccessReports, canReviewReports } }) => {
24
24
 
25
25
  const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
26
26
  const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
@@ -72,9 +72,12 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
72
72
  const needsApproval = gotApprovalFlow && (approvalStatus === 'PENDING');
73
73
  const isBlocked = blocked || blockedThread;
74
74
  const isRejected = gotApprovalFlow && (approvalStatus === 'REJECTED');
75
+ const openReports = reports?.filter(_ => !_.resolved);
76
+ const hasReports = !isEmpty(openReports);
77
+ const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
75
78
 
76
79
  const resolveStatusBadge = () => {
77
- const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus });
80
+ const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
78
81
  return {
79
82
  badgeVisible: isBlocked || gotApprovalFlow || removed,
80
83
  badgeColor: resolveCommentStatusColor(status),
@@ -86,44 +89,68 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
86
89
 
87
90
  const handleBlockClick = () => setBlockConfirmationVisible(true);
88
91
  const handleBlockConfirm = async () => {
89
- lockApp();
90
- blockItemMutation.mutate(id);
92
+ if (canModerate) {
93
+ lockApp();
94
+ blockItemMutation.mutate(id);
95
+ }
91
96
  };
92
97
  const handleBlockCancel = () => {
93
98
  setBlockConfirmationVisible(false);
94
99
  };
95
100
  const handleUnblockClick = async () => {
96
- lockApp();
97
- unblockItemMutation.mutate(id);
101
+ if (canModerate) {
102
+ lockApp();
103
+ unblockItemMutation.mutate(id);
104
+ }
98
105
  };
99
106
 
100
107
  const handleBlockThreadClick = () => setBlockThreadConfirmationVisible(true);
101
108
  const handleBlockThreadConfirm = async () => {
102
- lockApp();
103
- blockItemThreadMutation.mutate(id);
109
+ if (canModerate) {
110
+ lockApp();
111
+ blockItemThreadMutation.mutate(id);
112
+ }
104
113
  };
105
114
  const handleBlockThreadCancel = () => {
106
115
  setBlockThreadConfirmationVisible(false);
107
116
  };
108
117
  const handleUnblockThreadClick = async () => {
109
- lockApp();
110
- unblockItemThreadMutation.mutate(id);
118
+ if (canModerate) {
119
+ lockApp();
120
+ unblockItemThreadMutation.mutate(id);
121
+ }
111
122
  };
112
123
 
113
- if (removed || isRejected) {
124
+ if (removed || isRejected || !canModerate) {
114
125
  return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
115
- <DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>
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>
116
135
  </DiscussionThreadItemActionsWrapper>);
117
136
  }
118
137
 
119
138
  return (<>
120
139
  <DiscussionThreadItemActionsWrapper as={Flex} direction="row">
121
- { badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>)}
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>)}
122
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' } : {}} />)}
123
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' } : {}} />)}
124
151
  { (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
125
152
  { (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
126
- { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} queryToInvalidate="get-details-data" />) }
153
+ { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
127
154
  </DiscussionThreadItemActionsWrapper>
128
155
  { !blocked && (<ConfirmationDialog
129
156
  isVisible={blockConfirmationVisible}
@@ -153,9 +180,9 @@ DiscussionThreadItemActions.propTypes = {
153
180
  blocked: PropTypes.bool.isRequired,
154
181
  blockedThread: PropTypes.bool.isRequired,
155
182
  approvalStatus: PropTypes.oneOfType([PropTypes.nullable, PropTypes.oneOf(['PENDING', 'APPROVED', 'REJECTED'])]),
156
- onBlockClick: PropTypes.func.isRequired,
157
- onUnBlockClick: PropTypes.func.isRequired,
158
- onApprovalGroupClick: PropTypes.func.isRequired,
183
+ allowedActions: PropTypes.shape({
184
+ canModerate: PropTypes.bool,
185
+ }),
159
186
  };
160
187
 
161
188
  export default DiscussionThreadItemActions;
@@ -15,13 +15,13 @@ import { DiscussionThreadItemActionsGroupWrapper } from './styles';
15
15
  import { approveItem, rejectItem } from '../../pages/utils/api';
16
16
  import pluginId from '../../pluginId';
17
17
 
18
- const DiscussionThreadItemApprovalFlowActions = ({ id, queryToInvalidate }) => {
18
+ const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canModerate }, queryToInvalidate }) => {
19
19
 
20
20
  const toggleNotification = useNotification();
21
21
  const queryClient = useQueryClient();
22
22
  const { lockApp, unlockApp } = useOverlayBlocker();
23
23
 
24
- const onSuccess = (message, stateAction = () => {}) => async () => {
24
+ const onSuccess = (message) => async () => {
25
25
  if (queryToInvalidate) {
26
26
  await queryClient.invalidateQueries(queryToInvalidate);
27
27
  }
@@ -45,26 +45,35 @@ const DiscussionThreadItemApprovalFlowActions = ({ id, queryToInvalidate }) => {
45
45
  refetchActive: false,
46
46
  });
47
47
 
48
-
49
48
  const handleApproveClick = () => {
50
- lockApp();
51
- approveItemMutation.mutate(id);
49
+ if (canModerate) {
50
+ lockApp();
51
+ approveItemMutation.mutate(id);
52
+ }
52
53
  };
53
54
 
54
55
  const handleRejectClick = () => {
55
- lockApp();
56
- rejectItemMutation.mutate(id);
56
+ if (canModerate) {
57
+ lockApp();
58
+ rejectItemMutation.mutate(id);
59
+ }
57
60
  };
58
61
 
59
- return (<DiscussionThreadItemActionsGroupWrapper>
60
- <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
61
- <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
62
- </DiscussionThreadItemActionsGroupWrapper>);
62
+ if (canModerate) {
63
+ return (<DiscussionThreadItemActionsGroupWrapper>
64
+ <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
65
+ <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
66
+ </DiscussionThreadItemActionsGroupWrapper>);
67
+ }
68
+ return null;
63
69
  };
64
70
 
65
71
  DiscussionThreadItemApprovalFlowActions.propTypes = {
66
72
  id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
67
73
  queryToInvalidate: PropTypes.string,
74
+ allowedActions: PropTypes.shape({
75
+ canModerate: PropTypes.bool,
76
+ }),
68
77
  wrapped: PropTypes.bool,
69
78
  };
70
79
 
@@ -9,22 +9,33 @@ import PropTypes from 'prop-types';
9
9
  import { useIntl } from 'react-intl';
10
10
  import { Box } from '@strapi/design-system/Box';
11
11
  import { Flex } from '@strapi/design-system/Flex';
12
+ import { Stack } from '@strapi/design-system/Stack';
12
13
  import { Typography } from '@strapi/design-system/Typography';
13
14
 
14
- const DiscussionThreadItemMeta = ({ authorName, authorEmail, createdAt, updatedAt }) => {
15
+ const DiscussionThreadItemMeta = ({ author, createdAt, updatedAt }) => {
15
16
 
16
17
  const { formatDate } = useIntl();
17
18
 
18
19
  const dateTime = formatDate(updatedAt || createdAt, { dateStyle: 'short', timeStyle: 'short' });
20
+ const { name } = author;
19
21
 
20
- return (<Flex as={Box} paddingTop={2} direction="row" justifyContent="space-between">
21
- <Typography variant="pi" fontWeight="bold">{ authorName }</Typography>
22
- <Typography variant="pi">{ dateTime }</Typography>
22
+ return (<Flex as={Box} paddingTop={2} direction="row">
23
+ <Stack size={2} horizontal style={{ flexGrow: 1, justifyContent: 'space-between'}}>
24
+ <Typography variant="pi" fontWeight="bold">{ name }</Typography>
25
+ <Typography variant="pi">{ dateTime }</Typography>
26
+ </Stack>
23
27
  </Flex>);
24
28
  };
25
29
 
26
30
  DiscussionThreadItemMeta.propTypes = {
27
-
31
+ author: PropTypes.shape({
32
+ id: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
33
+ name: PropTypes.string.isRequired,
34
+ email: PropTypes.string,
35
+ avatar: PropTypes.string,
36
+ }).isRequired,
37
+ createdAt: PropTypes.string.isRequired,
38
+ updatedAt: PropTypes.string,
28
39
  };
29
40
 
30
41
  export default DiscussionThreadItemMeta;
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import React, { useState } from 'react';
8
+ import PropTypes from 'prop-types';
8
9
  import { useMutation, useQueryClient } from 'react-query';
9
10
  import { isEmpty, orderBy } from 'lodash';
10
11
  import { Badge } from '@strapi/design-system/Badge';
@@ -23,7 +24,7 @@ import pluginId from '../../pluginId';
23
24
  import { LockIcon, ReviewIcon } from '../icons';
24
25
 
25
26
  const DiscussionThreadItemWarnings = (item) => {
26
- const { id, reports, gotThread } = item;
27
+ const { id, reports, gotThread, allowedActions: { canModerate, canAccessReports, canReviewReports }} = item;
27
28
 
28
29
  const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
29
30
  const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
@@ -93,7 +94,7 @@ const DiscussionThreadItemWarnings = (item) => {
93
94
  }
94
95
 
95
96
  return (<>
96
- <DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
97
+ { canAccessReports && (<DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
97
98
  <Box as={Flex} paddingRight={2} direction="row" grow={1}>
98
99
  <Badge textColor="neutral0" backgroundColor="warning600">{ openReports.length }</Badge>
99
100
  <Box paddingLeft={2}>
@@ -103,19 +104,19 @@ const DiscussionThreadItemWarnings = (item) => {
103
104
  <Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ReviewIcon />}>
104
105
  { getMessage(`page.details.actions.comment.report.review`, 'review') }
105
106
  </Button>
106
- </DiscussionThreadItemWarningsWrapper>
107
- <ReportsReviewModal
107
+ </DiscussionThreadItemWarningsWrapper>) }
108
+ { (canAccessReports || canReviewReports) && (<ReportsReviewModal
108
109
  isVisible={reportsReviewVisible}
109
110
  isActionAsync={isLoading}
110
111
  onClose={handleReportsReviewClose}
111
- startActions={<>
112
+ startActions={canModerate && (<>
112
113
  <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
113
114
  { getMessage(`page.details.actions.comment.block`, 'Block comment') }
114
115
  </Button>
115
116
  { gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
116
117
  { getMessage(`page.details.actions.thread.block`, 'Block thread') }
117
118
  </Button>) }
118
- </>}
119
+ </>)}
119
120
  item={item}>
120
121
  <Typography variant="sigma" textColor="neutral600" id="reports-list">
121
122
  { getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
@@ -124,17 +125,22 @@ const DiscussionThreadItemWarnings = (item) => {
124
125
  <Divider />
125
126
  </Box>
126
127
  <ReportsReviewTable
127
- commentId={item.id}
128
+ commentId={id}
128
129
  items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
129
130
  mutation={resolveReportMutation}
131
+ allowedActions={{ canAccessReports, canReviewReports }}
130
132
  onBlockButtonsStateChange={handleBlockButtonsStateChange}
131
133
  />
132
- </ReportsReviewModal>
134
+ </ReportsReviewModal>) }
133
135
  </>);
134
136
  };
135
137
 
136
138
  DiscussionThreadItemWarnings.propTypes = {
137
-
139
+ allowedActions: PropTypes.shape({
140
+ canModerate: PropTypes.bool,
141
+ canAccessReports: PropTypes.bool,
142
+ canReviewReports: PropTypes.bool,
143
+ }),
138
144
  };
139
145
 
140
146
  export default DiscussionThreadItemWarnings;
@@ -37,6 +37,11 @@ const ReportsReviewModal = ({
37
37
  endActions: PropTypes.array,
38
38
  item: PropTypes.object.isRequired,
39
39
  children: PropTypes.array.isRequired,
40
+ allowedActions: PropTypes.shape({
41
+ canModerate: PropTypes.bool,
42
+ canAccessReports: PropTypes.bool,
43
+ canReviewReports: PropTypes.bool,
44
+ }),
40
45
  onClose: PropTypes.func.isRequired,
41
46
  };
42
47
 
@@ -24,9 +24,9 @@ const ReportsReviewTable = ({
24
24
  commentId,
25
25
  items,
26
26
  mutation,
27
+ allowedActions: { canAccessReports, canReviewReports },
27
28
  onBlockButtonsStateChange,
28
29
  }) => {
29
-
30
30
  const [storedItems, setStoredItems] = useState([]);
31
31
 
32
32
  const { formatDate } = useIntl();
@@ -59,12 +59,12 @@ const ReportsReviewTable = ({
59
59
  };
60
60
 
61
61
  const handleClickResolve = async (reportId) => {
62
+ if (canReviewReports) {
62
63
  lockApp();
63
64
  const item = await mutation.mutateAsync({
64
65
  id: commentId,
65
66
  reportId,
66
67
  });
67
- console.log(item);
68
68
  if (item) {
69
69
  const updatedItems = storedItems.map(_ => ({
70
70
  ..._,
@@ -73,6 +73,7 @@ const ReportsReviewTable = ({
73
73
  setStoredItems(updatedItems);
74
74
  onBlockButtonsStateChange(updatedItems.filter(_ => !_.resolved).length === 0);
75
75
  }
76
+ }
76
77
  }
77
78
 
78
79
  useEffect(() => {
@@ -85,61 +86,68 @@ const ReportsReviewTable = ({
85
86
 
86
87
  const COL_COUNT = 6;
87
88
 
88
- return (<Table colCount={COL_COUNT} rowCount={storedItems.length}>
89
- <Thead>
90
- <Tr>
91
- <Th>
92
- <BaseCheckbox aria-label="Select all entries" />
93
- </Th>
94
- <Th>
95
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.reason') }</Typography>
96
- </Th>
97
- <Th>
98
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.content') }</Typography>
99
- </Th>
100
- <Th>
101
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.createdAt') }</Typography>
102
- </Th>
103
- <Th>
104
- <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.status') }</Typography>
105
- </Th>
106
- <Th>
107
- <VisuallyHidden>{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions') }</VisuallyHidden>
108
- </Th>
109
- </Tr>
110
- </Thead>
111
- <Tbody>
112
- {storedItems.map(entry => <Tr key={entry.id}>
113
- <Td>
114
- <BaseCheckbox aria-label={`Select report`} />
115
- </Td>
116
- <Td>
117
- <Typography textColor="neutral800">{renderReason(entry.reason)}</Typography>
118
- </Td>
119
- <Td>
120
- <Typography textColor="neutral800">{entry.content}</Typography>
121
- </Td>
122
- <Td>
123
- <Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
124
- </Td>
125
- <Td>
126
- <Typography textColor="neutral800">{ renderStatus(entry.resolved) }</Typography>
127
- </Td>
128
- <Td>
129
- { !entry.resolved && (<Flex direction="column" alignItems="flex-end">
130
- <Button onClick={() => handleClickResolve(entry.id)} startIcon={<Check />} variant="success">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }</Button>
131
- </Flex>) }
132
- </Td>
133
- </Tr>)}
134
- </Tbody>
135
- {/* <TFooter /> */}
136
- </Table>)
89
+ if (canAccessReports) {
90
+ return (<Table colCount={COL_COUNT} rowCount={storedItems.length}>
91
+ <Thead>
92
+ <Tr>
93
+ <Th>
94
+ <BaseCheckbox aria-label="Select all entries" />
95
+ </Th>
96
+ <Th>
97
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.reason') }</Typography>
98
+ </Th>
99
+ <Th>
100
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.content') }</Typography>
101
+ </Th>
102
+ <Th>
103
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.createdAt') }</Typography>
104
+ </Th>
105
+ <Th>
106
+ <Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.status') }</Typography>
107
+ </Th>
108
+ { canReviewReports && (<Th>
109
+ <VisuallyHidden>{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions') }</VisuallyHidden>
110
+ </Th>)}
111
+ </Tr>
112
+ </Thead>
113
+ <Tbody>
114
+ {storedItems.map(entry => <Tr key={entry.id}>
115
+ <Td>
116
+ <BaseCheckbox aria-label={`Select report`} />
117
+ </Td>
118
+ <Td>
119
+ <Typography textColor="neutral800">{renderReason(entry.reason)}</Typography>
120
+ </Td>
121
+ <Td>
122
+ <Typography textColor="neutral800">{entry.content}</Typography>
123
+ </Td>
124
+ <Td>
125
+ <Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
126
+ </Td>
127
+ <Td>
128
+ <Typography textColor="neutral800">{ renderStatus(entry.resolved) }</Typography>
129
+ </Td>
130
+ { canReviewReports && (<Td>
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>
133
+ </Flex>) }
134
+ </Td>)}
135
+ </Tr>)}
136
+ </Tbody>
137
+ {/* <TFooter /> */}
138
+ </Table>);
139
+ }
140
+ return null;
137
141
  };
138
142
 
139
143
  ReportsReviewTable.propTypes = {
140
- commentId: PropTypes.string.isRequired,
144
+ commentId: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
141
145
  items: PropTypes.array.isRequired,
142
146
  mutation: PropTypes.func.isRequired,
147
+ allowedActions: PropTypes.shape({
148
+ canAccessReports: PropTypes.bool,
149
+ canReviewReports: PropTypes.bool,
150
+ }),
143
151
  onBlockButtonsStateChange: PropTypes.func.isRequired,
144
152
  };
145
153
 
@@ -11,7 +11,7 @@ const ReviewIcon = ({ width, height, size = 16 }) =>
11
11
  style={{ width: `${width || size}px`, height: `${height || size}px`, }}
12
12
  xmlns="http://www.w3.org/2000/svg">
13
13
  <g style={ {transform: `scale(${(width || size)/initSize})` } }>
14
- <path fill-rule="evenodd" clip-rule="evenodd" d="M0 4a4 4 0 014-4h8a4 4 0 014 4v8a4 4 0 01-4 4H4a4 4 0 01-4-4V4zm6.996.165a1.017 1.017 0 112.012 0L8 11 6.996 4.165zM8 11a1 1 0 110 2 1 1 0 010-2z" />
14
+ <path fillRule="evenodd" clipRule="evenodd" d="M0 4a4 4 0 014-4h8a4 4 0 014 4v8a4 4 0 01-4 4H4a4 4 0 01-4-4V4zm6.996.165a1.017 1.017 0 112.012 0L8 11 6.996 4.165zM8 11a1 1 0 110 2 1 1 0 010-2z" />
15
15
  </g>
16
16
  </svg>
17
17
 
@@ -23,7 +23,7 @@ export default {
23
23
 
24
24
  return component;
25
25
  },
26
- permissions: pluginPermissions,
26
+ permissions: pluginPermissions.access,
27
27
  });
28
28
  app.addReducers(reducers);
29
29
  app.registerPlugin({
@@ -30,7 +30,7 @@ const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
30
30
  FIELDS_LIMIT
31
31
  );
32
32
 
33
- const formatLabel = label => label.split('_')
33
+ const formatLabel = (label = '') => label.split('_')
34
34
  .map(_ => capitalize(_))
35
35
  .join(' ');
36
36
 
@@ -47,7 +47,7 @@ const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
47
47
  <Typography>{ data[entityLabelKey] }</Typography>
48
48
  </Flex>
49
49
  { itemKeys.map(_ =>
50
- (<Flex direction="column" alignItems="flex-start">
50
+ (<Flex key={`prop_${_}`} direction="column" alignItems="flex-start">
51
51
  <Typography fontWeight="bold">{ formatLabel(_) }</Typography>
52
52
  <Typography>{ data[_] }</Typography>
53
53
  </Flex>))
@@ -20,6 +20,7 @@ const DetailsFilters = ({ data, onChange }) => {
20
20
  <Stack size={2} horizontal>
21
21
  <Typography variant="epsilon">{getMessage('page.details.filters.label', '')}</Typography>
22
22
  <Switch
23
+ label={getMessage('page.details.filters.removed.label', 'Comments visibility')}
23
24
  onLabel={getMessage('page.details.filters.removed.show', 'Show')}
24
25
  offLabel={getMessage('page.details.filters.removed.hide', 'Hide')}
25
26
  selected={removed} onChange={() => handleOnChange('removed', !removed)}