strapi-plugin-comments 2.0.0-beta.4 → 2.0.0-rc.1

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 (75) hide show
  1. package/README.md +421 -72
  2. package/admin/src/components/DiscussionThread/index.js +9 -2
  3. package/admin/src/components/DiscussionThreadItem/index.js +27 -12
  4. package/admin/src/components/DiscussionThreadItemActions/index.js +61 -50
  5. package/admin/src/components/DiscussionThreadItemActions/styles.js +0 -4
  6. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +81 -0
  7. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/styles.js +6 -0
  8. package/admin/src/components/DiscussionThreadItemMeta/index.js +16 -5
  9. package/admin/src/components/DiscussionThreadItemWarnings/index.js +16 -10
  10. package/admin/src/components/Nav/index.js +5 -1
  11. package/admin/src/components/ReportsReviewModal/index.js +5 -0
  12. package/admin/src/components/ReportsReviewTable/index.js +60 -52
  13. package/admin/src/components/icons/review.js +2 -2
  14. package/admin/src/index.js +1 -1
  15. package/admin/src/pages/Details/components/DetailsEntity/index.js +66 -0
  16. package/admin/src/pages/Details/components/DetailsFilters/index.js +37 -0
  17. package/admin/src/pages/Details/components/DetailsFilters/styles.js +8 -0
  18. package/admin/src/pages/Details/index.js +75 -42
  19. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +69 -48
  20. package/admin/src/pages/Discover/components/DiscoverTableRow/styles.js +8 -0
  21. package/admin/src/pages/Discover/index.js +72 -56
  22. package/admin/src/pages/{Details/utils → utils}/api.js +15 -3
  23. package/admin/src/permissions.js +10 -8
  24. package/admin/src/translations/en.json +17 -4
  25. package/admin/src/utils/constants.js +3 -6
  26. package/admin/src/utils/getMessage.js +1 -1
  27. package/admin/src/utils/getRelatedGroups.js +1 -3
  28. package/admin/src/utils/index.js +5 -1
  29. package/admin/src/utils/renderEntryTitle.js +14 -0
  30. package/admin/src/utils/renderInitials.js +5 -0
  31. package/admin/src/utils/resolveCommentStatus.js +30 -0
  32. package/admin/src/utils/resolveCommentStatusColor.js +22 -0
  33. package/package.json +8 -3
  34. package/permissions.js +19 -0
  35. package/public/assets/logo.png +0 -0
  36. package/public/assets/preview.png +0 -0
  37. package/server/bootstrap.js +38 -2
  38. package/server/config/index.js +9 -3
  39. package/server/controllers/admin.js +3 -3
  40. package/server/controllers/client.js +4 -18
  41. package/server/controllers/utils/parsers.js +17 -0
  42. package/server/graphql/index.js +26 -0
  43. package/server/graphql/mutations/createAbuseReport.js +20 -0
  44. package/server/graphql/mutations/createComment.js +20 -0
  45. package/server/graphql/mutations/index.js +21 -0
  46. package/server/graphql/mutations/removeComment.js +20 -0
  47. package/server/graphql/mutations/updateComment.js +20 -0
  48. package/server/graphql/queries/findAllFlat.js +29 -0
  49. package/server/graphql/queries/findAllInHierarchy.js +31 -0
  50. package/server/graphql/queries/index.js +19 -0
  51. package/server/graphql/resolvers-config.js +10 -0
  52. package/server/graphql/types/comment-author-user.js +9 -0
  53. package/server/graphql/types/comment-author.js +9 -0
  54. package/server/graphql/types/comment-nested.js +16 -0
  55. package/server/graphql/types/comment.js +16 -0
  56. package/server/graphql/types/create-comment-author.js +9 -0
  57. package/server/graphql/types/create-comment.js +9 -0
  58. package/server/graphql/types/create-report.js +9 -0
  59. package/server/graphql/types/id.js +24 -0
  60. package/server/graphql/types/identify-comment-author.js +6 -0
  61. package/server/graphql/types/index.js +17 -0
  62. package/server/graphql/types/remove-comment.js +8 -0
  63. package/server/graphql/types/report-reason.js +10 -0
  64. package/server/graphql/types/report.js +11 -0
  65. package/server/graphql/types/update-comment.js +9 -0
  66. package/server/register.js +3 -1
  67. package/server/services/admin.js +42 -15
  68. package/server/services/client.js +54 -38
  69. package/server/services/common.js +38 -67
  70. package/server/services/graphql.js +190 -0
  71. package/server/services/index.js +2 -0
  72. package/server/services/utils/functions.js +24 -17
  73. package/server/utils/constants.js +3 -3
  74. package/strapi-server.js +33 -0
  75. package/admin/src/components/EntityDetails/index.js +0 -53
@@ -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,12 +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
40
  const isThreadAuthor = !isNil(selected?.threadOf?.authorId) && (selected?.threadOf?.authorId === item.authorId);
41
41
  return (<DiscussionThreadItem
42
+ key={`comment-${item.id}`}
42
43
  {...item}
44
+ allowedActions={allowedActions}
43
45
  root={isNil(rootThread)}
44
46
  blockedThread={rootThread?.blockedThread || item.blockedThread}
45
47
  isSelected={isSelected}
@@ -53,6 +55,11 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
53
55
  DiscussionThread.propTypes = {
54
56
  level: PropTypes.array.isRequired,
55
57
  selected: PropTypes.object.isRequired,
58
+ allowedActions: PropTypes.shape({
59
+ canModerate: PropTypes.bool,
60
+ canAccessReports: PropTypes.bool,
61
+ canReviewReports: PropTypes.bool,
62
+ }),
56
63
  isReloading: PropTypes.bool,
57
64
  };
58
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,24 @@ 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, 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
+ authorUser,
36
+ author } = props;
25
37
 
26
38
  let background = 'neutral100';
27
39
  let borderColor = 'neutral200';
@@ -45,11 +57,9 @@ const DiscussionThreadItem = (item) => {
45
57
  StyledComponent = DiscussionThreadItemRoot;
46
58
  }
47
59
 
48
- const avatar = authorAvatar || authorUser?.avatar;
49
- const name = authorName || `${authorUser?.firstName} ${authorUser?.lastName}`;
60
+ const avatar = author?.avatar || authorUser?.avatar;
61
+ const name = author?.name || `${authorUser?.firstName} ${authorUser?.lastName}`;
50
62
  const isBlocked = blocked || blockedThread;
51
-
52
- const renderInitials = value => value.split(' ').map(_ => first(_)).join('').toUpperCase();
53
63
 
54
64
  return (<StyledComponent as="li">
55
65
  <DiscussionThreadItemContent as={Flex} direction="row">
@@ -62,16 +72,16 @@ const DiscussionThreadItem = (item) => {
62
72
  <Box as={Flex} grow={1} alignItems="center">
63
73
  <Typography variant="omega">{ content }</Typography>
64
74
  </Box>
65
- { !preview && (<DiscussionThreadItemActions { ...item } root={root || pinned} />) }
75
+ { !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
66
76
  </DiscussionThreadItemBoxContent>
67
- { !(preview || isBlocked) && (<DiscussionThreadItemWarnings { ...item } gotThread={gotThread || root} />) }
77
+ { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
68
78
  </Flex>
69
79
  <Divider background={dividerColor} />
70
- <DiscussionThreadItemMeta { ...item } />
80
+ <DiscussionThreadItemMeta { ...props } />
71
81
  </DiscussionThreadItemBox>
72
82
  </DiscussionThreadItemContent>
73
83
 
74
- { (gotThread && !(preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
84
+ { (gotThread && !(removed || preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
75
85
  <Link to={getUrl(`discover/${threadFirstItemId}`)} endIcon={<ArrowRight />}>
76
86
  { getMessage('page.details.panel.discussion.nav.drilldown') }
77
87
  </Link>
@@ -81,6 +91,11 @@ const DiscussionThreadItem = (item) => {
81
91
 
82
92
  DiscussionThreadItem.propTypes = {
83
93
  content: PropTypes.string.isRequired,
94
+ allowedActions: PropTypes.shape({
95
+ canModerate: PropTypes.bool,
96
+ canAccessReports: PropTypes.bool,
97
+ canReviewReports: PropTypes.bool,
98
+ }),
84
99
  };
85
100
 
86
101
  export default DiscussionThreadItem;
@@ -4,23 +4,23 @@
4
4
  *
5
5
  */
6
6
 
7
- import React, { useState, useEffect } from 'react';
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';
14
- import { Check, Cross } from '@strapi/icons';
15
14
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
16
- import { getMessage, handleAPIError } from '../../utils';
17
- import { DiscussionThreadItemActionsBadge, DiscussionThreadItemActionsGroupWrapper, DiscussionThreadItemActionsWrapper } from './styles';
15
+ import { getMessage, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
16
+ import { DiscussionThreadItemActionsBadge, DiscussionThreadItemActionsWrapper } from './styles';
18
17
  import ConfirmationDialog from '../ConfirmationDialog';
19
- import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/Details/utils/api';
18
+ import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/utils/api';
20
19
  import pluginId from '../../pluginId';
21
20
  import { LockIcon, UnlockIcon } from '../icons';
21
+ import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
22
22
 
23
- const DiscussionThreadItemActions = ({ id, blocked, 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);
@@ -71,75 +71,86 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, gotThread, pi
71
71
  const gotApprovalFlow = !isNil(approvalStatus);
72
72
  const needsApproval = gotApprovalFlow && (approvalStatus === 'PENDING');
73
73
  const isBlocked = blocked || blockedThread;
74
+ const isRejected = gotApprovalFlow && (approvalStatus === 'REJECTED');
75
+ const openReports = reports?.filter(_ => !_.resolved);
76
+ const hasReports = !isEmpty(openReports);
77
+ const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
74
78
 
75
79
  const resolveStatusBadge = () => {
76
- let color = 'primary';
77
- let label = '';
78
-
79
- if (isBlocked) {
80
- color = 'danger';
81
- label = 'blocked';
82
- } else if (gotApprovalFlow) {
83
- label = approvalStatus.toLowerCase();
84
- switch (approvalStatus) {
85
- case 'APPROVED':
86
- color = 'success';
87
- break;
88
- case 'REJECTED':
89
- color = 'danger';
90
- break;
91
- default:
92
- color = 'alternative';
93
- break;
94
- };
95
- }
96
-
80
+ const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
97
81
  return {
98
- badgeVisible: isBlocked || gotApprovalFlow,
99
- badgeColor: color,
100
- badgeLabel: label,
101
- }
102
- }
82
+ badgeVisible: isBlocked || gotApprovalFlow || removed,
83
+ badgeColor: resolveCommentStatusColor(status),
84
+ badgeLabel: status,
85
+ };
86
+ };
103
87
 
104
88
  const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
105
89
 
106
90
  const handleBlockClick = () => setBlockConfirmationVisible(true);
107
91
  const handleBlockConfirm = async () => {
108
- lockApp();
109
- blockItemMutation.mutate(id);
92
+ if (canModerate) {
93
+ lockApp();
94
+ blockItemMutation.mutate(id);
95
+ }
110
96
  };
111
97
  const handleBlockCancel = () => {
112
98
  setBlockConfirmationVisible(false);
113
99
  };
114
100
  const handleUnblockClick = async () => {
115
- lockApp();
116
- unblockItemMutation.mutate(id);
101
+ if (canModerate) {
102
+ lockApp();
103
+ unblockItemMutation.mutate(id);
104
+ }
117
105
  };
118
106
 
119
107
  const handleBlockThreadClick = () => setBlockThreadConfirmationVisible(true);
120
108
  const handleBlockThreadConfirm = async () => {
121
- lockApp();
122
- blockItemThreadMutation.mutate(id);
109
+ if (canModerate) {
110
+ lockApp();
111
+ blockItemThreadMutation.mutate(id);
112
+ }
123
113
  };
124
114
  const handleBlockThreadCancel = () => {
125
115
  setBlockThreadConfirmationVisible(false);
126
116
  };
127
117
  const handleUnblockThreadClick = async () => {
128
- lockApp();
129
- unblockItemThreadMutation.mutate(id);
118
+ if (canModerate) {
119
+ lockApp();
120
+ unblockItemThreadMutation.mutate(id);
121
+ }
130
122
  };
131
123
 
124
+ if (removed || isRejected || !canModerate) {
125
+ 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>
135
+ </DiscussionThreadItemActionsWrapper>);
136
+ }
137
+
132
138
  return (<>
133
139
  <DiscussionThreadItemActionsWrapper as={Flex} direction="row">
134
- { badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.details.panel.discussion.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>)}
135
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' } : {}} />)}
136
- { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} />)}
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' } : {}} />)}
137
151
  { (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
138
152
  { (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
139
- { needsApproval && (<DiscussionThreadItemActionsGroupWrapper>
140
- <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} />
141
- <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} />
142
- </DiscussionThreadItemActionsGroupWrapper>) }
153
+ { needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
143
154
  </DiscussionThreadItemActionsWrapper>
144
155
  { !blocked && (<ConfirmationDialog
145
156
  isVisible={blockConfirmationVisible}
@@ -169,9 +180,9 @@ DiscussionThreadItemActions.propTypes = {
169
180
  blocked: PropTypes.bool.isRequired,
170
181
  blockedThread: PropTypes.bool.isRequired,
171
182
  approvalStatus: PropTypes.oneOfType([PropTypes.nullable, PropTypes.oneOf(['PENDING', 'APPROVED', 'REJECTED'])]),
172
- onBlockClick: PropTypes.func.isRequired,
173
- onUnBlockClick: PropTypes.func.isRequired,
174
- onApprovalGroupClick: PropTypes.func.isRequired,
183
+ allowedActions: PropTypes.shape({
184
+ canModerate: PropTypes.bool,
185
+ }),
175
186
  };
176
187
 
177
188
  export default DiscussionThreadItemActions;
@@ -15,8 +15,4 @@ export const DiscussionThreadItemActionsWrapper = styled(Box)`
15
15
  margin-top: .175rem;
16
16
  margin-right: .5rem;
17
17
  }
18
- `;
19
-
20
- export const DiscussionThreadItemActionsGroupWrapper = styled(IconButtonGroup)`
21
- margin-left: .5rem;
22
18
  `;
@@ -0,0 +1,81 @@
1
+ /**
2
+ *
3
+ * Approval Flow Actions
4
+ *
5
+ */
6
+
7
+ import React from 'react';
8
+ import PropTypes from 'prop-types';
9
+ import { useMutation, useQueryClient } from 'react-query';
10
+ import { IconButton } from '@strapi/design-system/IconButton';
11
+ import { Check, Cross } from '@strapi/icons';
12
+ import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
13
+ import { getMessage, handleAPIError } from '../../utils';
14
+ import { DiscussionThreadItemActionsGroupWrapper } from './styles';
15
+ import { approveItem, rejectItem } from '../../pages/utils/api';
16
+ import pluginId from '../../pluginId';
17
+
18
+ const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canModerate }, queryToInvalidate }) => {
19
+
20
+ const toggleNotification = useNotification();
21
+ const queryClient = useQueryClient();
22
+ const { lockApp, unlockApp } = useOverlayBlocker();
23
+
24
+ const onSuccess = (message) => async () => {
25
+ if (queryToInvalidate) {
26
+ await queryClient.invalidateQueries(queryToInvalidate);
27
+ }
28
+ toggleNotification({
29
+ type: 'success',
30
+ message: `${pluginId}.${message}`,
31
+ });
32
+ unlockApp();
33
+ };
34
+
35
+ const onError = err => { handleAPIError(err, toggleNotification); };
36
+
37
+ const approveItemMutation = useMutation(approveItem, {
38
+ onSuccess: onSuccess('page.details.actions.comment.approve.confirmation.success'),
39
+ onError,
40
+ refetchActive: false,
41
+ });
42
+ const rejectItemMutation = useMutation(rejectItem, {
43
+ onSuccess: onSuccess('page.details.actions.comment.reject.confirmation.success'),
44
+ onError,
45
+ refetchActive: false,
46
+ });
47
+
48
+ const handleApproveClick = () => {
49
+ if (canModerate) {
50
+ lockApp();
51
+ approveItemMutation.mutate(id);
52
+ }
53
+ };
54
+
55
+ const handleRejectClick = () => {
56
+ if (canModerate) {
57
+ lockApp();
58
+ rejectItemMutation.mutate(id);
59
+ }
60
+ };
61
+
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;
69
+ };
70
+
71
+ DiscussionThreadItemApprovalFlowActions.propTypes = {
72
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
73
+ queryToInvalidate: PropTypes.string,
74
+ allowedActions: PropTypes.shape({
75
+ canModerate: PropTypes.bool,
76
+ }),
77
+ wrapped: PropTypes.bool,
78
+ };
79
+
80
+ export default DiscussionThreadItemApprovalFlowActions;
81
+
@@ -0,0 +1,6 @@
1
+ import styled from 'styled-components';
2
+ import { IconButtonGroup } from '@strapi/design-system/IconButton';
3
+
4
+ export const DiscussionThreadItemActionsGroupWrapper = styled(IconButtonGroup)`
5
+ margin-left: .5rem;
6
+ `;
@@ -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';
@@ -15,7 +16,7 @@ import { Flex } from '@strapi/design-system/Flex';
15
16
  import { Typography } from '@strapi/design-system/Typography';
16
17
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
17
18
  import { getMessage, handleAPIError } from '../../utils';
18
- import { blockItem, blockItemThread, resolveReport } from '../../pages/Details/utils/api';
19
+ import { blockItem, blockItemThread, resolveReport } from '../../pages/utils/api';
19
20
  import { DiscussionThreadItemWarningsWrapper } from './styles';
20
21
  import ReportsReviewModal from '../ReportsReviewModal';
21
22
  import ReportsReviewTable from '../ReportsReviewTable';
@@ -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;
@@ -18,7 +18,11 @@ import getMessage from '../../utils/getMessage';
18
18
  // import PropTypes from 'prop-types';
19
19
  import getUrl from '../../utils/getUrl';
20
20
 
21
- const Nav = ({ hasNewComments, hasNewReports }) => {
21
+ const Nav = ({ visible = false, hasNewComments, hasNewReports }) => {
22
+ if (!visible) {
23
+ return null;
24
+ }
25
+
22
26
  return <SubNav ariaLabel="Comments sub nav">
23
27
  <SubNavHeader label={getMessage('plugin.name')} />
24
28
  <SubNavSections>
@@ -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