strapi-plugin-comments 2.0.0-beta.1 → 2.0.0-beta.5

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 +16 -5
  2. package/admin/src/components/DiscussionThread/index.js +3 -1
  3. package/admin/src/components/DiscussionThreadItem/index.js +4 -4
  4. package/admin/src/components/DiscussionThreadItem/styles.js +1 -0
  5. package/admin/src/components/DiscussionThreadItemActions/index.js +30 -44
  6. package/admin/src/components/DiscussionThreadItemActions/styles.js +0 -4
  7. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +72 -0
  8. package/admin/src/components/DiscussionThreadItemApprovalFlowActions/styles.js +6 -0
  9. package/admin/src/components/DiscussionThreadItemWarnings/index.js +13 -9
  10. package/admin/src/components/Nav/index.js +5 -1
  11. package/admin/src/components/ReportsReviewModal/index.js +0 -2
  12. package/admin/src/components/ReportsReviewTable/index.js +9 -5
  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 +6 -4
  18. package/admin/src/pages/App/index.js +42 -3
  19. package/admin/src/pages/App/reducer/actions.js +8 -0
  20. package/admin/src/pages/App/reducer/constants.js +4 -0
  21. package/admin/src/pages/App/reducer/index.js +26 -0
  22. package/admin/src/pages/App/reducer/init.js +7 -0
  23. package/admin/src/pages/App/reducer/selectors.js +35 -0
  24. package/admin/src/pages/App/utils/api.js +12 -0
  25. package/admin/src/pages/Details/components/DetailsEntity/index.js +66 -0
  26. package/admin/src/pages/Details/components/DetailsFilters/index.js +36 -0
  27. package/admin/src/pages/Details/components/DetailsFilters/styles.js +8 -0
  28. package/admin/src/pages/Details/index.js +41 -14
  29. package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +190 -0
  30. package/admin/src/pages/Discover/components/DiscoverTableRow/styles.js +8 -0
  31. package/admin/src/pages/Discover/index.js +25 -50
  32. package/admin/src/pages/Discover/utils/api.js +2 -11
  33. package/admin/src/pages/{Details/utils → utils}/api.js +15 -3
  34. package/admin/src/reducers.js +8 -0
  35. package/admin/src/translations/en.json +16 -4
  36. package/admin/src/utils/constants.js +3 -6
  37. package/admin/src/utils/getMessage.js +10 -3
  38. package/admin/src/utils/getRelatedGroups.js +1 -3
  39. package/admin/src/utils/handleAPIError.js +1 -1
  40. package/admin/src/utils/index.js +4 -2
  41. package/admin/src/utils/renderEntryTitle.js +14 -0
  42. package/admin/src/utils/resolveCommentStatus.js +30 -0
  43. package/admin/src/utils/resolveCommentStatusColor.js +22 -0
  44. package/package.json +7 -3
  45. package/server/config/index.js +6 -1
  46. package/server/controllers/admin.js +8 -24
  47. package/server/controllers/client.js +11 -2
  48. package/server/services/admin.js +75 -14
  49. package/server/services/client.js +22 -14
  50. package/server/services/common.js +41 -70
  51. package/server/services/utils/functions.js +7 -14
  52. package/server/utils/constants.js +3 -3
  53. package/admin/src/components/EntityDetails/index.js +0 -53
package/README.md CHANGED
@@ -50,9 +50,15 @@ 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.4 (recently tested)
56
62
 
57
63
  (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
64
 
@@ -74,8 +80,13 @@ To setup amend default plugin configuration we recommend to put following snippe
74
80
  ```js
75
81
  ...
76
82
  comments: {
77
- badWords: false,
78
- moderatorRoles: ["Authenticated"]
83
+ badWords: false,
84
+ moderatorRoles: ["Authenticated"],
85
+ approvalFlow: ['api::page.page'],
86
+ entryLabel: {
87
+ '*': ['Title', 'title', 'Name', 'name', 'Subject', 'subject'],
88
+ 'api::page.page': ['MyField'],
89
+ },
79
90
  },
80
91
  ...
81
92
  ```
@@ -83,6 +94,8 @@ To setup amend default plugin configuration we recommend to put following snippe
83
94
  ### Properties
84
95
  - `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
96
  - `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.
97
+ - `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.
98
+ - `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
99
 
87
100
  ## Additional GQL Configuration
88
101
 
@@ -215,7 +228,6 @@ Posts a Comment related to specified instance of Content Type like for example `
215
228
  },
216
229
  "content": "My sample response",
217
230
  "threadOf": 2, // id of comment we would like to start / continue the thread (Optional)
218
- "related": "api::page.page:1"
219
231
  }
220
232
  ```
221
233
 
@@ -225,7 +237,6 @@ Posts a Comment related to specified instance of Content Type like for example `
225
237
  "authorUser": 1, // id of a author user. Optional in case of 'enableUsers: true' in the plugin configuration
226
238
  "content": "My sample response",
227
239
  "threadOf": 2, // id of comment we would like to start / continue the thread (Optional)
228
- "related": "api::page.page:1"
229
240
  }
230
241
  ```
231
242
 
@@ -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 />) }
37
+ { rootThread && (<DiscussionThreadItem {...rootThread} 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
+ root={isNil(rootThread)}
43
45
  blockedThread={rootThread?.blockedThread || item.blockedThread}
44
46
  isSelected={isSelected}
45
47
  isThreadAuthor={isThreadAuthor}
@@ -21,7 +21,7 @@ import { getMessage, getUrl } from '../../utils';
21
21
  import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
22
22
 
23
23
  const DiscussionThreadItem = (item) => {
24
- const { id, isSelected, isThreadAuthor, content, root, preview, blocked, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
24
+ const { id, isSelected, isThreadAuthor, content, root, preview, pinned, blocked, removed, blockedThread, gotThread, threadFirstItemId, authorUser, authorAvatar, authorName } = item;
25
25
 
26
26
  let background = 'neutral100';
27
27
  let borderColor = 'neutral200';
@@ -62,16 +62,16 @@ const DiscussionThreadItem = (item) => {
62
62
  <Box as={Flex} grow={1} alignItems="center">
63
63
  <Typography variant="omega">{ content }</Typography>
64
64
  </Box>
65
- { !preview && (<DiscussionThreadItemActions { ...item } root={root} />) }
65
+ { !preview && (<DiscussionThreadItemActions { ...item } root={root || pinned} />) }
66
66
  </DiscussionThreadItemBoxContent>
67
- { !(preview || isBlocked) && (<DiscussionThreadItemWarnings { ...item } />) }
67
+ { !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...item } gotThread={gotThread || root} />) }
68
68
  </Flex>
69
69
  <Divider background={dividerColor} />
70
70
  <DiscussionThreadItemMeta { ...item } />
71
71
  </DiscussionThreadItemBox>
72
72
  </DiscussionThreadItemContent>
73
73
 
74
- { (gotThread && !preview) && (<DiscussionThreadItemLinkBox direction="row" alignItems="flex-end">
74
+ { (gotThread && !(removed || preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
75
75
  <Link to={getUrl(`discover/${threadFirstItemId}`)} endIcon={<ArrowRight />}>
76
76
  { getMessage('page.details.panel.discussion.nav.drilldown') }
77
77
  </Link>
@@ -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
10
  import { isNil } 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, approvalStatus, root }) => {
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,35 +71,16 @@ 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');
72
75
 
73
76
  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
-
77
+ const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus });
95
78
  return {
96
- badgeVisible: isBlocked || gotApprovalFlow,
97
- badgeColor: color,
98
- badgeLabel: label,
99
- }
100
- }
79
+ badgeVisible: isBlocked || gotApprovalFlow || removed,
80
+ badgeColor: resolveCommentStatusColor(status),
81
+ badgeLabel: status,
82
+ };
83
+ };
101
84
 
102
85
  const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
103
86
 
@@ -127,24 +110,27 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
127
110
  unblockItemThreadMutation.mutate(id);
128
111
  };
129
112
 
113
+ if (removed || isRejected) {
114
+ return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
115
+ <DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>
116
+ </DiscussionThreadItemActionsWrapper>);
117
+ }
118
+
130
119
  return (<>
131
120
  <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>) }
121
+ { badgeVisible && (<DiscussionThreadItemActionsBadge color={badgeColor} backgroundColor={`${badgeColor}100`} textColor={`${badgeColor}600`}>{getMessage(`page.common.item.status.${badgeLabel}`, badgeLabel)}</DiscussionThreadItemActionsBadge>)}
122
+ { !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
+ { !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
+ { (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
125
+ { (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" />) }
141
127
  </DiscussionThreadItemActionsWrapper>
142
128
  { !blocked && (<ConfirmationDialog
143
129
  isVisible={blockConfirmationVisible}
144
130
  isActionAsync={blockItemMutation.isLoading}
145
131
  header={getMessage('page.details.actions.comment.block.confirmation.header')}
146
132
  labelConfirm={getMessage('page.details.actions.comment.block.confirmation.button.confirm')}
147
- iconConfirm={<Lock />}
133
+ iconConfirm={<LockIcon />}
148
134
  onConfirm={handleBlockConfirm}
149
135
  onCancel={handleBlockCancel}>
150
136
  { getMessage('page.details.actions.comment.block.confirmation.description') }
@@ -154,7 +140,7 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
154
140
  isActionAsync={blockItemThreadMutation.isLoading}
155
141
  header={getMessage('page.details.actions.thread.block.confirmation.header')}
156
142
  labelConfirm={getMessage('page.details.actions.thread.block.confirmation.button.confirm')}
157
- iconConfirm={<Lock />}
143
+ iconConfirm={<LockIcon />}
158
144
  onConfirm={handleBlockThreadConfirm}
159
145
  onCancel={handleBlockThreadCancel}>
160
146
  { getMessage('page.details.actions.thread.block.confirmation.description') }
@@ -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,72 @@
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, queryToInvalidate }) => {
19
+
20
+ const toggleNotification = useNotification();
21
+ const queryClient = useQueryClient();
22
+ const { lockApp, unlockApp } = useOverlayBlocker();
23
+
24
+ const onSuccess = (message, stateAction = () => {}) => 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
+
49
+ const handleApproveClick = () => {
50
+ lockApp();
51
+ approveItemMutation.mutate(id);
52
+ };
53
+
54
+ const handleRejectClick = () => {
55
+ lockApp();
56
+ rejectItemMutation.mutate(id);
57
+ };
58
+
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>);
63
+ };
64
+
65
+ DiscussionThreadItemApprovalFlowActions.propTypes = {
66
+ id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
67
+ queryToInvalidate: PropTypes.string,
68
+ wrapped: PropTypes.bool,
69
+ };
70
+
71
+ export default DiscussionThreadItemApprovalFlowActions;
72
+
@@ -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
+ `;
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import React, { useState } from 'react';
8
- import PropTypes from 'prop-types';
9
8
  import { useMutation, useQueryClient } from 'react-query';
10
9
  import { isEmpty, orderBy } from 'lodash';
11
10
  import { Badge } from '@strapi/design-system/Badge';
@@ -14,18 +13,20 @@ import { Button } from '@strapi/design-system/Button';
14
13
  import { Divider } from '@strapi/design-system/Divider';
15
14
  import { Flex } from '@strapi/design-system/Flex';
16
15
  import { Typography } from '@strapi/design-system/Typography';
17
- import { ArrowRight, Lock } from '@strapi/icons';
18
16
  import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
19
17
  import { getMessage, handleAPIError } from '../../utils';
20
- import { blockItem, blockItemThread, resolveReport } from '../../pages/Details/utils/api';
18
+ import { blockItem, blockItemThread, resolveReport } from '../../pages/utils/api';
21
19
  import { DiscussionThreadItemWarningsWrapper } from './styles';
22
20
  import ReportsReviewModal from '../ReportsReviewModal';
23
21
  import ReportsReviewTable from '../ReportsReviewTable';
22
+ import pluginId from '../../pluginId';
23
+ import { LockIcon, ReviewIcon } from '../icons';
24
24
 
25
25
  const DiscussionThreadItemWarnings = (item) => {
26
- const { id, reports } = item;
26
+ const { id, reports, gotThread } = item;
27
27
 
28
28
  const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
29
+ const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
29
30
 
30
31
  const toggleNotification = useNotification();
31
32
  const queryClient = useQueryClient();
@@ -37,7 +38,7 @@ const DiscussionThreadItemWarnings = (item) => {
37
38
  }
38
39
  toggleNotification({
39
40
  type: 'success',
40
- message,
41
+ message: `${pluginId}.${message}`,
41
42
  });
42
43
  stateAction(false);
43
44
  unlockApp();
@@ -82,6 +83,8 @@ const DiscussionThreadItemWarnings = (item) => {
82
83
  setReportsReviewVisible(false);
83
84
  };
84
85
 
86
+ const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
87
+
85
88
  const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
86
89
  const openReports = reports?.filter(_ => !_.resolved);
87
90
 
@@ -97,7 +100,7 @@ const DiscussionThreadItemWarnings = (item) => {
97
100
  <Typography textColor="warning600" fontWeight="bold">{getMessage(`page.details.panel.discussion.warnings.reports.description`, 'abuse reports')}</Typography>
98
101
  </Box>
99
102
  </Box>
100
- <Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ArrowRight />}>
103
+ <Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ReviewIcon />}>
101
104
  { getMessage(`page.details.actions.comment.report.review`, 'review') }
102
105
  </Button>
103
106
  </DiscussionThreadItemWarningsWrapper>
@@ -106,12 +109,12 @@ const DiscussionThreadItemWarnings = (item) => {
106
109
  isActionAsync={isLoading}
107
110
  onClose={handleReportsReviewClose}
108
111
  startActions={<>
109
- <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<Lock />}>
112
+ <Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
110
113
  { getMessage(`page.details.actions.comment.block`, 'Block comment') }
111
114
  </Button>
112
- <Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<Lock />}>
115
+ { gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
113
116
  { getMessage(`page.details.actions.thread.block`, 'Block thread') }
114
- </Button>
117
+ </Button>) }
115
118
  </>}
116
119
  item={item}>
117
120
  <Typography variant="sigma" textColor="neutral600" id="reports-list">
@@ -124,6 +127,7 @@ const DiscussionThreadItemWarnings = (item) => {
124
127
  commentId={item.id}
125
128
  items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
126
129
  mutation={resolveReportMutation}
130
+ onBlockButtonsStateChange={handleBlockButtonsStateChange}
127
131
  />
128
132
  </ReportsReviewModal>
129
133
  </>);
@@ -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,7 +33,6 @@ 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,
@@ -23,7 +23,8 @@ import { REPORT_REASON, REPORT_STATUS } from '../../utils/constants';
23
23
  const ReportsReviewTable = ({
24
24
  commentId,
25
25
  items,
26
- mutation
26
+ mutation,
27
+ onBlockButtonsStateChange,
27
28
  }) => {
28
29
 
29
30
  const [storedItems, setStoredItems] = useState([]);
@@ -65,10 +66,12 @@ const ReportsReviewTable = ({
65
66
  });
66
67
  console.log(item);
67
68
  if (item) {
68
- setStoredItems(storedItems.map(_ => ({
69
- ..._,
70
- resolved: reportId === _.id ? true : _.resolved,
71
- })));
69
+ const updatedItems = storedItems.map(_ => ({
70
+ ..._,
71
+ resolved: reportId === _.id ? true : _.resolved,
72
+ }));
73
+ setStoredItems(updatedItems);
74
+ onBlockButtonsStateChange(updatedItems.filter(_ => !_.resolved).length === 0);
72
75
  }
73
76
  }
74
77
 
@@ -137,6 +140,7 @@ ReportsReviewTable.propTypes = {
137
140
  commentId: PropTypes.string.isRequired,
138
141
  items: PropTypes.array.isRequired,
139
142
  mutation: PropTypes.func.isRequired,
143
+ onBlockButtonsStateChange: PropTypes.func.isRequired,
140
144
  };
141
145
 
142
146
  export default ReportsReviewTable;
@@ -0,0 +1,6 @@
1
+ import CommentsIcon from './comments';
2
+ import ReviewIcon from './review';
3
+ import LockIcon from './lock';
4
+ import UnlockIcon from './unlock';
5
+
6
+ export { CommentsIcon, ReviewIcon, LockIcon, UnlockIcon };
@@ -0,0 +1,22 @@
1
+
2
+ import React from 'react';
3
+
4
+ const initSize = 330;
5
+
6
+ const LockIcon = ({ width, height, size = 16 }) =>
7
+ <svg
8
+ width={width || size}
9
+ height={height || size}
10
+ viewBox={`0 0 ${width || size} ${height || size}`}
11
+ style={{ width: `${width || size}px`, height: `${height || size}px`, }}
12
+ xmlns="http://www.w3.org/2000/svg">
13
+ <g style={ { transform: `scale(${(width || size)/initSize})` } }>
14
+ <path d="M165,330c63.411,0,115-51.589,115-115c0-29.771-11.373-56.936-30-77.379V85c0-46.869-38.131-85-85-85
15
+ S80.001,38.131,80.001,85v52.619C61.373,158.064,50,185.229,50,215C50,278.411,101.589,330,165,330z M180,219.986V240
16
+ c0,8.284-6.716,15-15,15s-15-6.716-15-15v-20.014c-6.068-4.565-10-11.824-10-19.986c0-13.785,11.215-25,25-25s25,11.215,25,25
17
+ C190,208.162,186.068,215.421,180,219.986z M110.001,85c0-30.327,24.673-55,54.999-55c30.327,0,55,24.673,55,55v29.029
18
+ C203.652,105.088,184.91,100,165,100c-19.909,0-38.651,5.088-54.999,14.028V85z"/>
19
+ </g>
20
+ </svg>
21
+
22
+ export default LockIcon;
@@ -0,0 +1,18 @@
1
+
2
+ import React from 'react';
3
+
4
+ const initSize = 16;
5
+
6
+ const ReviewIcon = ({ width, height, size = 16 }) =>
7
+ <svg
8
+ width={width || size}
9
+ height={height || size}
10
+ viewBox={`0 0 ${width || size} ${height || size}`}
11
+ style={{ width: `${width || size}px`, height: `${height || size}px`, }}
12
+ xmlns="http://www.w3.org/2000/svg">
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" />
15
+ </g>
16
+ </svg>
17
+
18
+ export default ReviewIcon;
@@ -0,0 +1,20 @@
1
+
2
+ import React from 'react';
3
+
4
+ const initSize = 330;
5
+
6
+ const UnlockIcon = ({ width, height, size = 16 }) =>
7
+ <svg
8
+ width={width || size}
9
+ height={height || size}
10
+ viewBox={`0 0 ${width || size} ${height || size}`}
11
+ style={{ width: `${width || size}px`, height: `${height || size}px`, }}
12
+ xmlns="http://www.w3.org/2000/svg">
13
+ <g style={ { transform: `scale(${(width || size)/initSize})` } }>
14
+ <path d="M15,160c8.283,0,15-6.716,15-15V85c0-30.327,24.672-55,55-55c30.326,0,55,24.673,55,55v42.893
15
+ c-24.479,21.105-40,52.327-40,87.107c0,63.411,51.588,115,115,115c63.41,0,115-51.589,115-115s-51.59-115-115-115
16
+ c-15.961,0-31.172,3.271-45,9.174V85c0-46.869-38.131-85-85-85S0,38.131,0,85v60C0,153.284,6.715,160,15,160z"/>
17
+ </g>
18
+ </svg>
19
+
20
+ export default UnlockIcon;
@@ -4,6 +4,7 @@ import pluginId from './pluginId';
4
4
  import Initializer from './components/Initializer';
5
5
  import PluginIcon from './components/PluginIcon';
6
6
  import pluginPermissions from './permissions';
7
+ import reducers from './reducers';
7
8
 
8
9
  const name = pluginPkg.strapi.name;
9
10
 
@@ -24,6 +25,7 @@ export default {
24
25
  },
25
26
  permissions: pluginPermissions,
26
27
  });
28
+ app.addReducers(reducers);
27
29
  app.registerPlugin({
28
30
  id: pluginId,
29
31
  initializer: Initializer,
@@ -33,10 +35,10 @@ export default {
33
35
  },
34
36
 
35
37
  bootstrap(app) {
36
- app.injectContentManagerComponent('editView', 'informations', {
37
- name: 'comments-link',
38
- Component: () => 'TODO: Comments count',
39
- });
38
+ // app.injectContentManagerComponent('editView', 'informations', {
39
+ // name: 'comments-link',
40
+ // Component: () => 'TODO: Comments count',
41
+ // });
40
42
  },
41
43
 
42
44
  async registerTrads({ locales }) {
@@ -6,15 +6,41 @@
6
6
  */
7
7
 
8
8
  import React from 'react';
9
+ import { connect } from 'react-redux';
10
+ import { bindActionCreators, compose } from 'redux';
9
11
  import { Switch, Route } from 'react-router-dom';
10
- import { NotFound } from '@strapi/helper-plugin';
12
+ import { useQuery } from 'react-query';
13
+ import {
14
+ NotFound,
15
+ LoadingIndicatorPage,
16
+ useNotification } from '@strapi/helper-plugin';
17
+
11
18
  import ComingSoonPage from '../ComingSoonPage';
12
19
  import getUrl from '../../utils/getUrl';
13
20
  import Discover from '../Discover';
14
21
  import Details from '../Details';
22
+ import { fetchConfig } from './utils/api';
23
+ import { setConfig } from './reducer/actions';
24
+ import makeAppView from './reducer/selectors';
25
+
26
+ const App = ({ setConfig }) => {
27
+
28
+ const toggleNotification = useNotification();
29
+
30
+ const { isLoading, isFetching } = useQuery(
31
+ 'get-config', () => fetchConfig(toggleNotification),
32
+ {
33
+ initialData: { },
34
+ onSuccess: (response) => {
35
+ setConfig(response);
36
+ }
37
+ }
38
+ );
15
39
 
40
+ if (isLoading || isFetching) {
41
+ return (<LoadingIndicatorPage />)
42
+ }
16
43
 
17
- const App = () => {
18
44
  return (
19
45
  <div>
20
46
  <Switch>
@@ -29,4 +55,17 @@ const App = () => {
29
55
  );
30
56
  };
31
57
 
32
- export default App;
58
+ const mapStateToProps = makeAppView();
59
+
60
+ export function mapDispatchToProps(dispatch) {
61
+ return bindActionCreators(
62
+ { setConfig },
63
+ dispatch
64
+ );
65
+ }
66
+ const withConnect = connect(
67
+ mapStateToProps,
68
+ mapDispatchToProps
69
+ );
70
+
71
+ export default compose(withConnect)(App);