strapi-plugin-comments 2.0.0-beta.2 → 2.0.0-beta.6

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 (53) hide show
  1. package/README.md +31 -5
  2. package/admin/src/components/DiscussionThread/index.js +10 -2
  3. package/admin/src/components/DiscussionThreadItem/index.js +25 -6
  4. package/admin/src/components/DiscussionThreadItem/styles.js +1 -0
  5. package/admin/src/components/DiscussionThreadItemActions/index.js +69 -56
  6. package/admin/src/components/DiscussionThreadItemActions/styles.js +0 -4
  7. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +81 -0
  8. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/styles.js +6 -0
  9. package/admin/src/components/DiscussionThreadItemWarnings/index.js +26 -16
  10. package/admin/src/components/Nav/index.js +5 -1
  11. package/admin/src/components/ReportsReviewModal/index.js +5 -2
  12. package/admin/src/components/ReportsReviewTable/index.js +69 -57
  13. package/admin/src/components/icons/index.js +6 -0
  14. package/admin/src/components/icons/lock.js +22 -0
  15. package/admin/src/components/icons/review.js +18 -0
  16. package/admin/src/components/icons/unlock.js +20 -0
  17. package/admin/src/index.js +1 -1
  18. package/admin/src/pages/App/index.js +20 -9
  19. package/admin/src/pages/App/reducer/index.js +0 -1
  20. package/admin/src/pages/App/reducer/selectors.js +1 -1
  21. package/admin/src/pages/App/utils/api.js +2 -10
  22. package/admin/src/pages/Details/components/DetailsEntity/index.js +66 -0
  23. package/admin/src/pages/Details/components/DetailsFilters/index.js +37 -0
  24. package/admin/src/pages/Details/components/DetailsFilters/styles.js +8 -0
  25. package/admin/src/pages/Details/index.js +75 -42
  26. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +210 -0
  27. package/admin/src/pages/Discover/components/DiscoverTableRow/styles.js +8 -0
  28. package/admin/src/pages/Discover/index.js +69 -102
  29. package/admin/src/pages/Discover/utils/api.js +2 -8
  30. package/admin/src/pages/{Details/utils → utils}/api.js +15 -3
  31. package/admin/src/permissions.js +10 -8
  32. package/admin/src/translations/en.json +17 -4
  33. package/admin/src/utils/constants.js +3 -6
  34. package/admin/src/utils/getMessage.js +10 -3
  35. package/admin/src/utils/getRelatedGroups.js +1 -3
  36. package/admin/src/utils/handleAPIError.js +1 -1
  37. package/admin/src/utils/index.js +4 -2
  38. package/admin/src/utils/renderEntryTitle.js +14 -0
  39. package/admin/src/utils/resolveCommentStatus.js +30 -0
  40. package/admin/src/utils/resolveCommentStatusColor.js +22 -0
  41. package/package.json +7 -3
  42. package/permissions.js +19 -0
  43. package/server/bootstrap.js +38 -2
  44. package/server/config/index.js +4 -3
  45. package/server/controllers/admin.js +3 -3
  46. package/server/controllers/client.js +11 -2
  47. package/server/services/admin.js +69 -13
  48. package/server/services/client.js +23 -15
  49. package/server/services/common.js +40 -69
  50. package/server/services/utils/functions.js +7 -14
  51. package/server/utils/constants.js +3 -3
  52. package/strapi-server.js +33 -0
  53. package/admin/src/components/EntityDetails/index.js +0 -53
package/README.md CHANGED
@@ -50,9 +50,16 @@ Enjoy 🎉
50
50
 
51
51
  Complete installation requirements are exact same as for Strapi itself and can be found in the documentation under [Installation Requirements](https://docs.strapi.io/developer-docs/latest/getting-started/introduction.html).
52
52
 
53
+ **Minimum environment requirements**
54
+ - Node.js `>=14.x.x`
55
+ - NPM `>=7.x.x`
56
+
57
+ In our minimum support we're following [official Node.js releases timelines](https://nodejs.org/en/about/releases/).
58
+
53
59
  **Supported Strapi versions**:
54
60
 
55
- - Strapi v4.0.2 (recently tested)
61
+ - Strapi v4.0.5 (recently tested)
62
+ - Strapi v4.x
56
63
 
57
64
  (This plugin is not working with v3.x and not may work with the older Strapi v4 versions, but these are not tested nor officially supported at this time.)
58
65
 
@@ -74,8 +81,13 @@ To setup amend default plugin configuration we recommend to put following snippe
74
81
  ```js
75
82
  ...
76
83
  comments: {
77
- badWords: false,
78
- moderatorRoles: ["Authenticated"]
84
+ badWords: false,
85
+ moderatorRoles: ["Authenticated"],
86
+ approvalFlow: ['api::page.page'],
87
+ entryLabel: {
88
+ '*': ['Title', 'title', 'Name', 'name', 'Subject', 'subject'],
89
+ 'api::page.page': ['MyField'],
90
+ },
79
91
  },
80
92
  ...
81
93
  ```
@@ -83,11 +95,27 @@ To setup amend default plugin configuration we recommend to put following snippe
83
95
  ### Properties
84
96
  - `badWords` - Enabled support for [bad words filtering](https://www.npmjs.com/package/bad-words). Can be turned off or overwritten using [options reference](https://www.npmjs.com/package/bad-words#constructor). Default value: `true`.
85
97
  - `moderatorRoles` - Optional list of names of roles. Users with those roles will be notified by email when a new abuse report is created. This feature requires a built-in [Strapi email plugin](https://docs.strapi.io/developer-docs/latest/plugins/email.html) configured.
98
+ - `approvalFlow` - list of Content Types which are supporting approval flow. Values must be in format like `'api::<collection name>.<content type name>'`. For not included, posted comments are going to be immediately visible.
99
+ - `entryLabel` - ordered list of property names per Content Type to generate related entity label. Keys must be in format like `'api::<collection name>.<content type name>'`. Default formatting set as `*`.
86
100
 
87
101
  ## Additional GQL Configuration
88
102
 
89
103
  > To be done
90
104
 
105
+ ## RBAC
106
+ Plugin provides granular permissions based on Strapi RBAC functionality.
107
+
108
+ ### Mandatory permissions
109
+ For any role different than **Super Admin**, to access the **Comments panel** you must set following permissions:
110
+ - _Plugins_ -> _Content-type-builder_ -> _Read_ - gives you ability to fetch Content Type schema
111
+ - _Plugins_ -> _Comments_ -> _Comments: Read_ - gives you the basic read access to **Comments Panel**
112
+
113
+ ### Optional permissions
114
+ Feature / Capability focused permissions:
115
+ - _Plugins_ -> _Comments_ -> _Comments: Moderate_ - allows you to block, unblock, approve &amp; reject comments
116
+ - _Plugins_ -> _Comments_ -> _Reports: Read_ - allows you to see the list of issued abuse reports against comments
117
+ - _Plugins_ -> _Comments_ -> _Reports: Moderate_ - allows you to review (resolve) issued abuse reports against comments
118
+
91
119
  ## Public API Comment model
92
120
 
93
121
  ### Generic (non Strapi User)
@@ -215,7 +243,6 @@ Posts a Comment related to specified instance of Content Type like for example `
215
243
  },
216
244
  "content": "My sample response",
217
245
  "threadOf": 2, // id of comment we would like to start / continue the thread (Optional)
218
- "related": "api::page.page:1"
219
246
  }
220
247
  ```
221
248
 
@@ -225,7 +252,6 @@ Posts a Comment related to specified instance of Content Type like for example `
225
252
  "authorUser": 1, // id of a author user. Optional in case of 'enableUsers: true' in the plugin configuration
226
253
  "content": "My sample response",
227
254
  "threadOf": 2, // id of comment we would like to start / continue the thread (Optional)
228
- "related": "api::page.page:1"
229
255
  }
230
256
  ```
231
257
 
@@ -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,15 @@ 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 />) }
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}
45
+ root={isNil(rootThread)}
43
46
  blockedThread={rootThread?.blockedThread || item.blockedThread}
44
47
  isSelected={isSelected}
45
48
  isThreadAuthor={isThreadAuthor}
@@ -52,6 +55,11 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
52
55
  DiscussionThread.propTypes = {
53
56
  level: PropTypes.array.isRequired,
54
57
  selected: PropTypes.object.isRequired,
58
+ allowedActions: PropTypes.shape({
59
+ canModerate: PropTypes.bool,
60
+ canAccessReports: PropTypes.bool,
61
+ canReviewReports: PropTypes.bool,
62
+ }),
55
63
  isReloading: PropTypes.bool,
56
64
  };
57
65
 
@@ -20,8 +20,22 @@ import DiscussionThreadItemActions from '../DiscussionThreadItemActions';
20
20
  import { getMessage, getUrl } from '../../utils';
21
21
  import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
22
22
 
23
- const DiscussionThreadItem = (item) => {
24
- const { id, isSelected, isThreadAuthor, content, root, preview, blocked, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
23
+ const DiscussionThreadItem = (props) => {
24
+ const {
25
+ isSelected,
26
+ isThreadAuthor,
27
+ content,
28
+ root,
29
+ preview,
30
+ pinned,
31
+ blocked,
32
+ removed,
33
+ blockedThread,
34
+ gotThread,
35
+ threadFirstItemId,
36
+ authorUser,
37
+ authorAvatar,
38
+ authorName } = props;
25
39
 
26
40
  let background = 'neutral100';
27
41
  let borderColor = 'neutral200';
@@ -62,16 +76,16 @@ const DiscussionThreadItem = (item) => {
62
76
  <Box as={Flex} grow={1} alignItems="center">
63
77
  <Typography variant="omega">{ content }</Typography>
64
78
  </Box>
65
- { !preview && (<DiscussionThreadItemActions { ...item } root={root} />) }
79
+ { !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
66
80
  </DiscussionThreadItemBoxContent>
67
- { !(preview || isBlocked) && (<DiscussionThreadItemWarnings { ...item } />) }
81
+ { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
68
82
  </Flex>
69
83
  <Divider background={dividerColor} />
70
- <DiscussionThreadItemMeta { ...item } />
84
+ <DiscussionThreadItemMeta { ...props } />
71
85
  </DiscussionThreadItemBox>
72
86
  </DiscussionThreadItemContent>
73
87
 
74
- { (gotThread && !preview) && (<DiscussionThreadItemLinkBox direction="row" alignItems="flex-end">
88
+ { (gotThread && !(removed || preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
75
89
  <Link to={getUrl(`discover/${threadFirstItemId}`)} endIcon={<ArrowRight />}>
76
90
  { getMessage('page.details.panel.discussion.nav.drilldown') }
77
91
  </Link>
@@ -81,6 +95,11 @@ const DiscussionThreadItem = (item) => {
81
95
 
82
96
  DiscussionThreadItem.propTypes = {
83
97
  content: PropTypes.string.isRequired,
98
+ allowedActions: PropTypes.shape({
99
+ canModerate: PropTypes.bool,
100
+ canAccessReports: PropTypes.bool,
101
+ canReviewReports: PropTypes.bool,
102
+ }),
84
103
  };
85
104
 
86
105
  export default DiscussionThreadItem;
@@ -6,6 +6,7 @@ export const AvatarWrapper = styled(Box)``;
6
6
  export const InitialsWrapper = styled(Box)``;
7
7
 
8
8
  export const DiscussionThreadItemLinkBox = styled(Flex)`
9
+ width: 100%;
9
10
  margin-top: .5rem;
10
11
  margin-bottom: .5rem;
11
12
  `
@@ -4,21 +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 { Lock, Check, Cross, Key } 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';
19
+ import pluginId from '../../pluginId';
20
+ import { LockIcon, UnlockIcon } from '../icons';
21
+ import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
20
22
 
21
- const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatus, root }) => {
23
+ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, reports = [], approvalStatus, root, allowedActions: { canModerate, canAccessReports, canReviewReports } }) => {
22
24
 
23
25
  const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
24
26
  const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
@@ -31,7 +33,7 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
31
33
  await queryClient.invalidateQueries('get-details-data');
32
34
  toggleNotification({
33
35
  type: 'success',
34
- message,
36
+ message: `${pluginId}.${message}`,
35
37
  });
36
38
  stateAction(false);
37
39
  unlockApp();
@@ -69,82 +71,93 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
69
71
  const gotApprovalFlow = !isNil(approvalStatus);
70
72
  const needsApproval = gotApprovalFlow && (approvalStatus === 'PENDING');
71
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;
72
78
 
73
79
  const resolveStatusBadge = () => {
74
- let color = 'primary';
75
- let label = '';
76
-
77
- if (isBlocked) {
78
- color = 'danger';
79
- label = 'blocked';
80
- } else if (gotApprovalFlow) {
81
- label = approvalStatus.toLowerCase();
82
- switch (approvalStatus) {
83
- case 'APPROVED':
84
- color = 'success';
85
- break;
86
- case 'REJECTED':
87
- color = 'danger';
88
- break;
89
- default:
90
- color = 'alternative';
91
- break;
92
- };
93
- }
94
-
80
+ const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
95
81
  return {
96
- badgeVisible: isBlocked || gotApprovalFlow,
97
- badgeColor: color,
98
- badgeLabel: label,
99
- }
100
- }
82
+ badgeVisible: isBlocked || gotApprovalFlow || removed,
83
+ badgeColor: resolveCommentStatusColor(status),
84
+ badgeLabel: status,
85
+ };
86
+ };
101
87
 
102
88
  const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
103
89
 
104
90
  const handleBlockClick = () => setBlockConfirmationVisible(true);
105
91
  const handleBlockConfirm = async () => {
106
- lockApp();
107
- blockItemMutation.mutate(id);
92
+ if (canModerate) {
93
+ lockApp();
94
+ blockItemMutation.mutate(id);
95
+ }
108
96
  };
109
97
  const handleBlockCancel = () => {
110
98
  setBlockConfirmationVisible(false);
111
99
  };
112
100
  const handleUnblockClick = async () => {
113
- lockApp();
114
- unblockItemMutation.mutate(id);
101
+ if (canModerate) {
102
+ lockApp();
103
+ unblockItemMutation.mutate(id);
104
+ }
115
105
  };
116
106
 
117
107
  const handleBlockThreadClick = () => setBlockThreadConfirmationVisible(true);
118
108
  const handleBlockThreadConfirm = async () => {
119
- lockApp();
120
- blockItemThreadMutation.mutate(id);
109
+ if (canModerate) {
110
+ lockApp();
111
+ blockItemThreadMutation.mutate(id);
112
+ }
121
113
  };
122
114
  const handleBlockThreadCancel = () => {
123
115
  setBlockThreadConfirmationVisible(false);
124
116
  };
125
117
  const handleUnblockThreadClick = async () => {
126
- lockApp();
127
- unblockItemThreadMutation.mutate(id);
118
+ if (canModerate) {
119
+ lockApp();
120
+ unblockItemThreadMutation.mutate(id);
121
+ }
128
122
  };
129
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
+
130
138
  return (<>
131
139
  <DiscussionThreadItemActionsWrapper as={Flex} direction="row">
132
- { badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.details.panel.discussion.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>)}
133
- { !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<Lock />} label={getMessage('page.details.actions.comment.block', 'Block')} />)}
134
- { !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<Key />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} />)}
135
- { (!blockedThread && root) && (<Button onClick={handleBlockThreadClick} startIcon={<Lock />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
136
- { (blockedThread && root) && (<Button onClick={handleUnblockThreadClick} startIcon={<Key />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
137
- { needsApproval && (<DiscussionThreadItemActionsGroupWrapper>
138
- <IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} />
139
- <IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} />
140
- </DiscussionThreadItemActionsGroupWrapper>) }
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" />) }
141
154
  </DiscussionThreadItemActionsWrapper>
142
155
  { !blocked && (<ConfirmationDialog
143
156
  isVisible={blockConfirmationVisible}
144
157
  isActionAsync={blockItemMutation.isLoading}
145
158
  header={getMessage('page.details.actions.comment.block.confirmation.header')}
146
159
  labelConfirm={getMessage('page.details.actions.comment.block.confirmation.button.confirm')}
147
- iconConfirm={<Lock />}
160
+ iconConfirm={<LockIcon />}
148
161
  onConfirm={handleBlockConfirm}
149
162
  onCancel={handleBlockCancel}>
150
163
  { getMessage('page.details.actions.comment.block.confirmation.description') }
@@ -154,7 +167,7 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
154
167
  isActionAsync={blockItemThreadMutation.isLoading}
155
168
  header={getMessage('page.details.actions.thread.block.confirmation.header')}
156
169
  labelConfirm={getMessage('page.details.actions.thread.block.confirmation.button.confirm')}
157
- iconConfirm={<Lock />}
170
+ iconConfirm={<LockIcon />}
158
171
  onConfirm={handleBlockThreadConfirm}
159
172
  onCancel={handleBlockThreadCancel}>
160
173
  { getMessage('page.details.actions.thread.block.confirmation.description') }
@@ -167,9 +180,9 @@ DiscussionThreadItemActions.propTypes = {
167
180
  blocked: PropTypes.bool.isRequired,
168
181
  blockedThread: PropTypes.bool.isRequired,
169
182
  approvalStatus: PropTypes.oneOfType([PropTypes.nullable, PropTypes.oneOf(['PENDING', 'APPROVED', 'REJECTED'])]),
170
- onBlockClick: PropTypes.func.isRequired,
171
- onUnBlockClick: PropTypes.func.isRequired,
172
- onApprovalGroupClick: PropTypes.func.isRequired,
183
+ allowedActions: PropTypes.shape({
184
+ canModerate: PropTypes.bool,
185
+ }),
173
186
  };
174
187
 
175
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
+ `;
@@ -14,18 +14,20 @@ import { Button } from '@strapi/design-system/Button';
14
14
  import { Divider } from '@strapi/design-system/Divider';
15
15
  import { Flex } from '@strapi/design-system/Flex';
16
16
  import { Typography } from '@strapi/design-system/Typography';
17
- import { ArrowRight, Lock } from '@strapi/icons';
18
17
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
19
18
  import { getMessage, handleAPIError } from '../../utils';
20
- import { blockItem, blockItemThread, resolveReport } from '../../pages/Details/utils/api';
19
+ import { blockItem, blockItemThread, resolveReport } from '../../pages/utils/api';
21
20
  import { DiscussionThreadItemWarningsWrapper } from './styles';
22
21
  import ReportsReviewModal from '../ReportsReviewModal';
23
22
  import ReportsReviewTable from '../ReportsReviewTable';
23
+ import pluginId from '../../pluginId';
24
+ import { LockIcon, ReviewIcon } from '../icons';
24
25
 
25
26
  const DiscussionThreadItemWarnings = (item) => {
26
- const { id, reports } = item;
27
+ const { id, reports, gotThread, allowedActions: { canModerate, canAccessReports, canReviewReports }} = item;
27
28
 
28
29
  const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
30
+ const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
29
31
 
30
32
  const toggleNotification = useNotification();
31
33
  const queryClient = useQueryClient();
@@ -37,7 +39,7 @@ const DiscussionThreadItemWarnings = (item) => {
37
39
  }
38
40
  toggleNotification({
39
41
  type: 'success',
40
- message,
42
+ message: `${pluginId}.${message}`,
41
43
  });
42
44
  stateAction(false);
43
45
  unlockApp();
@@ -82,6 +84,8 @@ const DiscussionThreadItemWarnings = (item) => {
82
84
  setReportsReviewVisible(false);
83
85
  };
84
86
 
87
+ const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
88
+
85
89
  const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
86
90
  const openReports = reports?.filter(_ => !_.resolved);
87
91
 
@@ -90,29 +94,29 @@ const DiscussionThreadItemWarnings = (item) => {
90
94
  }
91
95
 
92
96
  return (<>
93
- <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>
94
98
  <Box as={Flex} paddingRight={2} direction="row" grow={1}>
95
99
  <Badge textColor="neutral0" backgroundColor="warning600">{ openReports.length }</Badge>
96
100
  <Box paddingLeft={2}>
97
101
  <Typography textColor="warning600" fontWeight="bold">{getMessage(`page.details.panel.discussion.warnings.reports.description`, 'abuse reports')}</Typography>
98
102
  </Box>
99
103
  </Box>
100
- <Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ArrowRight />}>
104
+ <Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ReviewIcon />}>
101
105
  { getMessage(`page.details.actions.comment.report.review`, 'review') }
102
106
  </Button>
103
- </DiscussionThreadItemWarningsWrapper>
104
- <ReportsReviewModal
107
+ </DiscussionThreadItemWarningsWrapper>) }
108
+ { (canAccessReports || canReviewReports) && (<ReportsReviewModal
105
109
  isVisible={reportsReviewVisible}
106
110
  isActionAsync={isLoading}
107
111
  onClose={handleReportsReviewClose}
108
- startActions={<>
109
- <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<Lock />}>
112
+ startActions={canModerate && (<>
113
+ <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
110
114
  { getMessage(`page.details.actions.comment.block`, 'Block comment') }
111
115
  </Button>
112
- <Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<Lock />}>
116
+ { gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
113
117
  { getMessage(`page.details.actions.thread.block`, 'Block thread') }
114
- </Button>
115
- </>}
118
+ </Button>) }
119
+ </>)}
116
120
  item={item}>
117
121
  <Typography variant="sigma" textColor="neutral600" id="reports-list">
118
122
  { getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
@@ -121,16 +125,22 @@ const DiscussionThreadItemWarnings = (item) => {
121
125
  <Divider />
122
126
  </Box>
123
127
  <ReportsReviewTable
124
- commentId={item.id}
128
+ commentId={id}
125
129
  items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
126
130
  mutation={resolveReportMutation}
131
+ allowedActions={{ canAccessReports, canReviewReports }}
132
+ onBlockButtonsStateChange={handleBlockButtonsStateChange}
127
133
  />
128
- </ReportsReviewModal>
134
+ </ReportsReviewModal>) }
129
135
  </>);
130
136
  };
131
137
 
132
138
  DiscussionThreadItemWarnings.propTypes = {
133
-
139
+ allowedActions: PropTypes.shape({
140
+ canModerate: PropTypes.bool,
141
+ canAccessReports: PropTypes.bool,
142
+ canReviewReports: PropTypes.bool,
143
+ }),
134
144
  };
135
145
 
136
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>
@@ -13,7 +13,6 @@ import DiscussionThreadItem from '../DiscussionThreadItem';
13
13
 
14
14
  const ReportsReviewModal = ({
15
15
  isVisible = false,
16
- isActionAsync = false,
17
16
  item,
18
17
  children,
19
18
  onClose,
@@ -34,11 +33,15 @@ const ReportsReviewModal = ({
34
33
 
35
34
  ReportsReviewModal.propTypes = {
36
35
  isVisible: PropTypes.bool,
37
- isActionAsync: PropTypes.bool,
38
36
  startActions: PropTypes.array,
39
37
  endActions: PropTypes.array,
40
38
  item: PropTypes.object.isRequired,
41
39
  children: PropTypes.array.isRequired,
40
+ allowedActions: PropTypes.shape({
41
+ canModerate: PropTypes.bool,
42
+ canAccessReports: PropTypes.bool,
43
+ canReviewReports: PropTypes.bool,
44
+ }),
42
45
  onClose: PropTypes.func.isRequired,
43
46
  };
44
47