strapi-plugin-comments 2.0.0-beta.2 → 2.0.0-beta.3
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.
- package/README.md +7 -2
- package/admin/src/components/DiscussionThread/index.js +2 -1
- package/admin/src/components/DiscussionThreadItem/index.js +4 -4
- package/admin/src/components/DiscussionThreadItem/styles.js +1 -0
- package/admin/src/components/DiscussionThreadItemActions/index.js +11 -9
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +12 -8
- package/admin/src/components/ReportsReviewModal/index.js +0 -2
- package/admin/src/components/ReportsReviewTable/index.js +9 -5
- package/admin/src/components/icons/index.js +6 -0
- package/admin/src/components/icons/lock.js +22 -0
- package/admin/src/components/icons/review.js +18 -0
- package/admin/src/components/icons/unlock.js +20 -0
- package/admin/src/pages/App/index.js +20 -9
- package/admin/src/pages/App/reducer/index.js +0 -1
- package/admin/src/pages/App/reducer/selectors.js +1 -1
- package/admin/src/pages/App/utils/api.js +2 -10
- package/admin/src/pages/Details/index.js +1 -1
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +196 -0
- package/admin/src/pages/Discover/index.js +6 -52
- package/admin/src/pages/Discover/utils/api.js +2 -8
- package/admin/src/translations/en.json +2 -1
- package/admin/src/utils/getMessage.js +10 -3
- package/admin/src/utils/handleAPIError.js +1 -1
- package/package.json +1 -1
- package/server/controllers/client.js +10 -2
- package/server/services/admin.js +34 -5
- package/server/services/client.js +1 -1
- package/server/services/common.js +12 -6
package/README.md
CHANGED
|
@@ -74,8 +74,12 @@ To setup amend default plugin configuration we recommend to put following snippe
|
|
|
74
74
|
```js
|
|
75
75
|
...
|
|
76
76
|
comments: {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
badWords: false,
|
|
78
|
+
moderatorRoles: ["Authenticated"],
|
|
79
|
+
entryLabel: {
|
|
80
|
+
'*': ['Title', 'title', 'Name', 'name', 'Subject', 'subject'],
|
|
81
|
+
'api::page.page': ['MyField'],
|
|
82
|
+
},
|
|
79
83
|
},
|
|
80
84
|
...
|
|
81
85
|
```
|
|
@@ -83,6 +87,7 @@ To setup amend default plugin configuration we recommend to put following snippe
|
|
|
83
87
|
### Properties
|
|
84
88
|
- `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
89
|
- `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.
|
|
90
|
+
- `entryLabel` - ordered list of property names per Content Type to generate related entity label. Default set as `*`.
|
|
86
91
|
|
|
87
92
|
## Additional GQL Configuration
|
|
88
93
|
|
|
@@ -34,12 +34,13 @@ 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
42
|
{...item}
|
|
43
|
+
root={isNil(rootThread)}
|
|
43
44
|
blockedThread={rootThread?.blockedThread || item.blockedThread}
|
|
44
45
|
isSelected={isSelected}
|
|
45
46
|
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, 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) && (<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="
|
|
74
|
+
{ (gotThread && !(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>
|
|
@@ -11,14 +11,16 @@ 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 {
|
|
14
|
+
import { Check, Cross } from '@strapi/icons';
|
|
15
15
|
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
16
16
|
import { getMessage, handleAPIError } from '../../utils';
|
|
17
17
|
import { DiscussionThreadItemActionsBadge, DiscussionThreadItemActionsGroupWrapper, DiscussionThreadItemActionsWrapper } from './styles';
|
|
18
18
|
import ConfirmationDialog from '../ConfirmationDialog';
|
|
19
19
|
import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/Details/utils/api';
|
|
20
|
+
import pluginId from '../../pluginId';
|
|
21
|
+
import { LockIcon, UnlockIcon } from '../icons';
|
|
20
22
|
|
|
21
|
-
const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatus, root }) => {
|
|
23
|
+
const DiscussionThreadItemActions = ({ id, blocked, 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();
|
|
@@ -130,10 +132,10 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
|
|
|
130
132
|
return (<>
|
|
131
133
|
<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
132
134
|
{ 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={<
|
|
134
|
-
{ !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<
|
|
135
|
-
{ (!blockedThread &&
|
|
136
|
-
{ (blockedThread &&
|
|
135
|
+
{ !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')} />)}
|
|
137
|
+
{ (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
|
|
138
|
+
{ (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
|
|
137
139
|
{ needsApproval && (<DiscussionThreadItemActionsGroupWrapper>
|
|
138
140
|
<IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} />
|
|
139
141
|
<IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} />
|
|
@@ -144,7 +146,7 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
|
|
|
144
146
|
isActionAsync={blockItemMutation.isLoading}
|
|
145
147
|
header={getMessage('page.details.actions.comment.block.confirmation.header')}
|
|
146
148
|
labelConfirm={getMessage('page.details.actions.comment.block.confirmation.button.confirm')}
|
|
147
|
-
iconConfirm={<
|
|
149
|
+
iconConfirm={<LockIcon />}
|
|
148
150
|
onConfirm={handleBlockConfirm}
|
|
149
151
|
onCancel={handleBlockCancel}>
|
|
150
152
|
{ getMessage('page.details.actions.comment.block.confirmation.description') }
|
|
@@ -154,7 +156,7 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, approvalStatu
|
|
|
154
156
|
isActionAsync={blockItemThreadMutation.isLoading}
|
|
155
157
|
header={getMessage('page.details.actions.thread.block.confirmation.header')}
|
|
156
158
|
labelConfirm={getMessage('page.details.actions.thread.block.confirmation.button.confirm')}
|
|
157
|
-
iconConfirm={<
|
|
159
|
+
iconConfirm={<LockIcon />}
|
|
158
160
|
onConfirm={handleBlockThreadConfirm}
|
|
159
161
|
onCancel={handleBlockThreadCancel}>
|
|
160
162
|
{ getMessage('page.details.actions.thread.block.confirmation.description') }
|
|
@@ -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
18
|
import { blockItem, blockItemThread, resolveReport } from '../../pages/Details/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={<
|
|
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={<
|
|
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={<
|
|
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
|
</>);
|
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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,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 = 24;
|
|
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 strokeWidth="1" d="M5.5,21 C7.98528137,21 10,18.9852814 10,16.5 C10,14.0147186 7.98528137,12 5.5,12 C3.01471863,12 1,14.0147186 1,16.5 C1,18.9852814 3.01471863,21 5.5,21 Z M1,16 L1,7 L1,6.5 C1,4.01471863 3.01471863,2 5.5,2 L6,2 M23,16 L23,7 L23,6.5 C23,4.01471863 20.9852814,2 18.5,2 L18,2 M18.5,21 C20.9852814,21 23,18.9852814 23,16.5 C23,14.0147186 20.9852814,12 18.5,12 C16.0147186,12 14,14.0147186 14,16.5 C14,18.9852814 16.0147186,21 18.5,21 Z M10,17 C10,17 10,15 12,15 C14,15 14,17 14,17"/>
|
|
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;
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React
|
|
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
12
|
import { useQuery } from 'react-query';
|
|
11
13
|
import {
|
|
@@ -18,23 +20,19 @@ import getUrl from '../../utils/getUrl';
|
|
|
18
20
|
import Discover from '../Discover';
|
|
19
21
|
import Details from '../Details';
|
|
20
22
|
import { fetchConfig } from './utils/api';
|
|
21
|
-
import initReducer from './reducer/init';
|
|
22
|
-
import reducer, { initialState } from './reducer';
|
|
23
23
|
import { setConfig } from './reducer/actions';
|
|
24
|
+
import makeAppView from './reducer/selectors';
|
|
24
25
|
|
|
25
|
-
const App = () => {
|
|
26
|
+
const App = ({ setConfig }) => {
|
|
26
27
|
|
|
27
28
|
const toggleNotification = useNotification();
|
|
28
|
-
const [reducerState, dispatch] = useReducer(reducer, initialState, () =>
|
|
29
|
-
initReducer(initialState)
|
|
30
|
-
);
|
|
31
29
|
|
|
32
30
|
const { isLoading, isFetching } = useQuery(
|
|
33
31
|
'get-config', () => fetchConfig(toggleNotification),
|
|
34
32
|
{
|
|
35
33
|
initialData: { },
|
|
36
34
|
onSuccess: (response) => {
|
|
37
|
-
|
|
35
|
+
setConfig(response);
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
);
|
|
@@ -57,4 +55,17 @@ const App = () => {
|
|
|
57
55
|
);
|
|
58
56
|
};
|
|
59
57
|
|
|
60
|
-
|
|
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);
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isEmpty } from 'lodash';
|
|
3
|
-
|
|
4
|
-
import { getApiURL, axiosInstance } from '../../../utils';
|
|
1
|
+
import { getApiURL, axiosInstance, handleAPIError } from '../../../utils';
|
|
5
2
|
|
|
6
3
|
// eslint-disable-next-line import/prefer-default-export
|
|
7
4
|
export const fetchConfig = async (toggleNotification) => {
|
|
@@ -10,11 +7,6 @@ export const fetchConfig = async (toggleNotification) => {
|
|
|
10
7
|
|
|
11
8
|
return data;
|
|
12
9
|
} catch (err) {
|
|
13
|
-
toggleNotification
|
|
14
|
-
type: 'warning',
|
|
15
|
-
message: { id: 'notification.error' },
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
throw new Error('error');
|
|
10
|
+
handleAPIError(err, toggleNotification);
|
|
19
11
|
}
|
|
20
12
|
};
|
|
@@ -83,7 +83,7 @@ const Details = () => {
|
|
|
83
83
|
<HeaderLayout
|
|
84
84
|
navigationAction={
|
|
85
85
|
<Link startIcon={<ArrowLeft />} to={getUrl(`discover`)}>
|
|
86
|
-
{ getMessage('
|
|
86
|
+
{ getMessage('HeaderLayout.link.go-back', 'Back', false) }
|
|
87
87
|
</Link>}
|
|
88
88
|
title={ getMessage('page.details.header') }
|
|
89
89
|
subtitle={ getMessage('page.details.header.description') } as="h2" />
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
4
|
+
import { useMutation, useQueryClient } from 'react-query';
|
|
5
|
+
import { first, isEmpty, orderBy } from 'lodash';
|
|
6
|
+
import { Badge } from '@strapi/design-system/Badge';
|
|
7
|
+
import { Box } from '@strapi/design-system/Box';
|
|
8
|
+
import { Button } from '@strapi/design-system/Button';
|
|
9
|
+
import { Divider } from '@strapi/design-system/Divider';
|
|
10
|
+
import { Flex } from '@strapi/design-system/Flex';
|
|
11
|
+
import { IconButton } from '@strapi/design-system/IconButton';
|
|
12
|
+
import { Link } from '@strapi/design-system/Link';
|
|
13
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
14
|
+
import { Tr, Td } from '@strapi/design-system/Table';
|
|
15
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
16
|
+
import { Eye, Lock } from '@strapi/icons';
|
|
17
|
+
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
18
|
+
import { getMessage, handleAPIError } from '../../../../utils';
|
|
19
|
+
import { COMMENT_STATUS } from '../../../../utils/constants';
|
|
20
|
+
import ReportsReviewModal from '../../../../components/ReportsReviewModal';
|
|
21
|
+
import ReportsReviewTable from '../../../../components/ReportsReviewTable';
|
|
22
|
+
import { blockItem, blockItemThread, resolveReport } from '../../../Details/utils/api';
|
|
23
|
+
import pluginId from '../../../../pluginId';
|
|
24
|
+
import { ReviewIcon, LockIcon } from '../../../../components/icons';
|
|
25
|
+
|
|
26
|
+
const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
27
|
+
|
|
28
|
+
const { id, reports } = item;
|
|
29
|
+
|
|
30
|
+
const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
|
|
31
|
+
const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
|
|
32
|
+
|
|
33
|
+
const { formatDate } = useIntl();
|
|
34
|
+
const toggleNotification = useNotification();
|
|
35
|
+
const queryClient = useQueryClient();
|
|
36
|
+
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
37
|
+
|
|
38
|
+
const onSuccess = (message, stateAction = () => {}, indalidate = true) => async () => {
|
|
39
|
+
if (indalidate) {
|
|
40
|
+
await queryClient.invalidateQueries('get-data');
|
|
41
|
+
}
|
|
42
|
+
toggleNotification({
|
|
43
|
+
type: 'success',
|
|
44
|
+
message: `${pluginId}.${message}`,
|
|
45
|
+
});
|
|
46
|
+
stateAction(false);
|
|
47
|
+
unlockApp();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onError = err => { handleAPIError(err, toggleNotification); };
|
|
51
|
+
|
|
52
|
+
const resolveReportMutation = useMutation(resolveReport, {
|
|
53
|
+
onSuccess: onSuccess('page.details.panel.discussion.warnings.reports.dialog.confirmation.success', () => {}, false),
|
|
54
|
+
onError,
|
|
55
|
+
refetchActive: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const blockItemMutation = useMutation(blockItem, {
|
|
59
|
+
onSuccess: onSuccess(
|
|
60
|
+
'page.details.actions.comment.block.confirmation.success',
|
|
61
|
+
setReportsReviewVisible
|
|
62
|
+
),
|
|
63
|
+
onError,
|
|
64
|
+
refetchActive: false,
|
|
65
|
+
});
|
|
66
|
+
const blockItemThreadMutation = useMutation(blockItemThread, {
|
|
67
|
+
onSuccess: onSuccess(
|
|
68
|
+
'page.details.actions.thread.block.confirmation.success',
|
|
69
|
+
setReportsReviewVisible
|
|
70
|
+
),
|
|
71
|
+
onError,
|
|
72
|
+
refetchActive: false,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const handleReportsReviewClick = () => setReportsReviewVisible(true);
|
|
76
|
+
const handleBlockItemClick = async () => {
|
|
77
|
+
lockApp();
|
|
78
|
+
blockItemMutation.mutate(id);
|
|
79
|
+
};
|
|
80
|
+
const handleBlockItemThreadClick = async () => {
|
|
81
|
+
lockApp();
|
|
82
|
+
blockItemThreadMutation.mutate(id);
|
|
83
|
+
};
|
|
84
|
+
const handleReportsReviewClose = async () => {
|
|
85
|
+
await queryClient.invalidateQueries('get-data');
|
|
86
|
+
setReportsReviewVisible(false);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
|
|
90
|
+
const openReports = reports?.filter(_ => !_.resolved);
|
|
91
|
+
const hasReports = !isEmpty(openReports);
|
|
92
|
+
const reviewFlowEnabled = hasReports && !(item.blocked || item.blockedThread);
|
|
93
|
+
|
|
94
|
+
const handleClick = () => onClick();
|
|
95
|
+
const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
|
|
96
|
+
|
|
97
|
+
const renderStatus = ({ blocked, blockedThread }) => {
|
|
98
|
+
let status = blocked || blockedThread ? COMMENT_STATUS.BLOCKED : COMMENT_STATUS.OPEN;
|
|
99
|
+
if (reviewFlowEnabled) {
|
|
100
|
+
status = COMMENT_STATUS.TO_REVIEW;
|
|
101
|
+
}
|
|
102
|
+
let color = 'secondary';
|
|
103
|
+
switch (status) {
|
|
104
|
+
case COMMENT_STATUS.OPEN:
|
|
105
|
+
color = 'success';
|
|
106
|
+
break;
|
|
107
|
+
case COMMENT_STATUS.TO_REVIEW:
|
|
108
|
+
color = 'warning';
|
|
109
|
+
break;
|
|
110
|
+
case COMMENT_STATUS.BLOCKED:
|
|
111
|
+
color = 'danger';
|
|
112
|
+
break;
|
|
113
|
+
};
|
|
114
|
+
return (<Badge backgroundColor={`${color}100`} textColor={`${color}600`}>{ getMessage({
|
|
115
|
+
id: `page.discover.table.header.status.${status}`,
|
|
116
|
+
props: {
|
|
117
|
+
count: openReports.length
|
|
118
|
+
}
|
|
119
|
+
}, status) }</Badge>);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const renderEntryTitle = entry => {
|
|
123
|
+
const { entryLabel } = config;
|
|
124
|
+
const rule = entry.uid in entryLabel ? entryLabel[entry.uid] : entryLabel['*'];
|
|
125
|
+
return first(
|
|
126
|
+
Object.keys(entry)
|
|
127
|
+
.filter(_ => (rule === _) || rule.includes(_))
|
|
128
|
+
.map(_ => entry[_])
|
|
129
|
+
.filter(_ => _)
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const renderEntryUrl = entry => `/content-manager/collectionType/${entry.uid}/${entry.id}`;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Tr key={item.id}>
|
|
137
|
+
<Td style={{ maxWidth: '40vw' }}>
|
|
138
|
+
<Typography textColor="neutral800" ellipsis>{item.content}</Typography>
|
|
139
|
+
</Td>
|
|
140
|
+
<Td>
|
|
141
|
+
<Typography textColor="neutral800">{item.threadOf?.id || '-'}</Typography>
|
|
142
|
+
</Td>
|
|
143
|
+
<Td>
|
|
144
|
+
<Link to={ renderEntryUrl(item.related) }>{ renderEntryTitle(item.related) }</Link>
|
|
145
|
+
</Td>
|
|
146
|
+
<Td>
|
|
147
|
+
<Typography textColor="neutral800">{ formatDate(item.updatedAt || item.createdAt, { dateStyle: 'long', timeStyle: 'short' }) }</Typography>
|
|
148
|
+
</Td>
|
|
149
|
+
<Td>
|
|
150
|
+
<Typography textColor="neutral800">{ renderStatus(item) }</Typography>
|
|
151
|
+
</Td>
|
|
152
|
+
<Td>
|
|
153
|
+
<Flex direction="column" alignItems="flex-end">
|
|
154
|
+
<Stack size={reviewFlowEnabled ? 2 : 1} horizontal>
|
|
155
|
+
{ reviewFlowEnabled && (<IconButton onClick={handleReportsReviewClick} label={ getMessage('page.discover.table.reports.review') } icon={<ReviewIcon />} />)}
|
|
156
|
+
<IconButton onClick={handleClick} label={ getMessage('page.discover.table.action.display') } icon={<Eye />} />
|
|
157
|
+
</Stack>
|
|
158
|
+
</Flex>
|
|
159
|
+
{ reviewFlowEnabled && (<ReportsReviewModal
|
|
160
|
+
isVisible={reportsReviewVisible}
|
|
161
|
+
isActionAsync={isLoading}
|
|
162
|
+
onClose={handleReportsReviewClose}
|
|
163
|
+
startActions={<>
|
|
164
|
+
<Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
165
|
+
{ getMessage(`page.details.actions.comment.block`, 'Block comment') }
|
|
166
|
+
</Button>
|
|
167
|
+
{ item.gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
168
|
+
{ getMessage(`page.details.actions.thread.block`, 'Block thread') }
|
|
169
|
+
</Button>) }
|
|
170
|
+
</>}
|
|
171
|
+
item={item}>
|
|
172
|
+
<Typography variant="sigma" textColor="neutral600" id="reports-list">
|
|
173
|
+
{ getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
|
|
174
|
+
</Typography>
|
|
175
|
+
<Box paddingTop={2} paddingBottom={6}>
|
|
176
|
+
<Divider />
|
|
177
|
+
</Box>
|
|
178
|
+
<ReportsReviewTable
|
|
179
|
+
commentId={item.id}
|
|
180
|
+
items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
|
|
181
|
+
mutation={resolveReportMutation}
|
|
182
|
+
onBlockButtonsStateChange={handleBlockButtonsStateChange}
|
|
183
|
+
/>
|
|
184
|
+
</ReportsReviewModal>) }
|
|
185
|
+
</Td>
|
|
186
|
+
</Tr>
|
|
187
|
+
);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
DiscoverTableRow.propTypes = {
|
|
191
|
+
config: PropTypes.object.isRequired,
|
|
192
|
+
item: PropTypes.object.isRequired,
|
|
193
|
+
onClick: PropTypes.func.isRequired,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export default DiscoverTableRow;
|
|
@@ -8,21 +8,15 @@ import React, { memo, useRef, useMemo } from 'react';
|
|
|
8
8
|
import { connect } from 'react-redux';
|
|
9
9
|
import { bindActionCreators, compose } from 'redux';
|
|
10
10
|
import isEqual from 'react-fast-compare';
|
|
11
|
-
import { useIntl } from 'react-intl';
|
|
12
11
|
import { useQuery } from 'react-query';
|
|
13
12
|
import { useHistory } from 'react-router-dom';
|
|
14
13
|
import { isEmpty } from 'lodash';
|
|
15
|
-
import { Badge } from '@strapi/design-system/Badge';
|
|
16
|
-
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
|
|
17
14
|
import { Box } from '@strapi/design-system/Box';
|
|
18
|
-
import { Flex } from '@strapi/design-system/Flex';
|
|
19
|
-
import { IconButton } from '@strapi/design-system/IconButton';
|
|
20
15
|
import { Layout, HeaderLayout, ActionLayout, ContentLayout } from '@strapi/design-system/Layout';
|
|
21
16
|
import { Table, Thead, Tbody, Tr, Th, Td } from '@strapi/design-system/Table';
|
|
22
17
|
import { Typography } from '@strapi/design-system/Typography';
|
|
23
18
|
import { VisuallyHidden } from '@strapi/design-system/VisuallyHidden';
|
|
24
19
|
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
|
25
|
-
import { Eye } from '@strapi/icons';
|
|
26
20
|
|
|
27
21
|
import {
|
|
28
22
|
EmptyStateLayout,
|
|
@@ -40,12 +34,11 @@ import pluginPermissions from '../../permissions';
|
|
|
40
34
|
|
|
41
35
|
import getUrl from '../../utils/getUrl';
|
|
42
36
|
import Nav from '../../components/Nav';
|
|
43
|
-
import { COMMENT_STATUS } from '../../utils/constants';
|
|
44
37
|
import TablePagination from '../../components/TablePagination';
|
|
45
38
|
import filtersSchema from './utils/filtersSchema';
|
|
46
39
|
import TableFilters from '../../components/TableFilters';
|
|
47
|
-
import
|
|
48
|
-
import
|
|
40
|
+
import makeAppView from '../App/reducer/selectors';
|
|
41
|
+
import DiscoverTableRow from './components/DiscoverTableRow';
|
|
49
42
|
|
|
50
43
|
const Discover = ({ config }) => {
|
|
51
44
|
useFocusWhenNavigate();
|
|
@@ -55,7 +48,6 @@ const Discover = ({ config }) => {
|
|
|
55
48
|
const { trackUsage } = useTracking();
|
|
56
49
|
const trackUsageRef = useRef(trackUsage);
|
|
57
50
|
const toggleNotification = useNotification();
|
|
58
|
-
const { formatDate } = useIntl();
|
|
59
51
|
const [{ query: queryParams }] = useQueryParams();
|
|
60
52
|
const _q = queryParams?._q || '';
|
|
61
53
|
|
|
@@ -69,7 +61,7 @@ const Discover = ({ config }) => {
|
|
|
69
61
|
} = useRBAC(viewPermissions);
|
|
70
62
|
|
|
71
63
|
const { isLoading: isLoadingForData, data: { result, pagination = {} }, isFetching } = useQuery(
|
|
72
|
-
['get-
|
|
64
|
+
['get-data', queryParams],
|
|
73
65
|
() => fetchData(queryParams, toggleNotification),
|
|
74
66
|
{
|
|
75
67
|
initialData: { },
|
|
@@ -94,22 +86,11 @@ const Discover = ({ config }) => {
|
|
|
94
86
|
},
|
|
95
87
|
};
|
|
96
88
|
|
|
97
|
-
|
|
98
|
-
const status = blocked || blockedThread ? COMMENT_STATUS.BLOCKED : COMMENT_STATUS.OPEN;
|
|
99
|
-
let color = 'secondary';
|
|
100
|
-
switch (status) {
|
|
101
|
-
case COMMENT_STATUS.OPEN:
|
|
102
|
-
color = 'success';
|
|
103
|
-
break;
|
|
104
|
-
case COMMENT_STATUS.BLOCKED:
|
|
105
|
-
color = 'danger';
|
|
106
|
-
};
|
|
107
|
-
return (<Badge backgroundColor={`${color}100`} textColor={`${color}600`}>{ getMessage(`page.discover.table.header.status.${status}`, status) }</Badge>);
|
|
108
|
-
};
|
|
89
|
+
|
|
109
90
|
|
|
110
91
|
const emptyContent = _q ? 'search' : 'comments';
|
|
111
92
|
|
|
112
|
-
const COL_COUNT =
|
|
93
|
+
const COL_COUNT = 6;
|
|
113
94
|
|
|
114
95
|
return <Box background="neutral100">
|
|
115
96
|
<Layout sideNav={<Nav />}>
|
|
@@ -128,9 +109,6 @@ const Discover = ({ config }) => {
|
|
|
128
109
|
<Table colCount={COL_COUNT} rowCount={result.length}>
|
|
129
110
|
<Thead>
|
|
130
111
|
<Tr>
|
|
131
|
-
<Th>
|
|
132
|
-
<BaseCheckbox aria-label="Select all entries" />
|
|
133
|
-
</Th>
|
|
134
112
|
<Th>
|
|
135
113
|
<Typography variant="sigma">{ getMessage('page.discover.table.header.message') }</Typography>
|
|
136
114
|
</Th>
|
|
@@ -152,31 +130,7 @@ const Discover = ({ config }) => {
|
|
|
152
130
|
</Tr>
|
|
153
131
|
</Thead>
|
|
154
132
|
<Tbody>
|
|
155
|
-
{result.map(entry => <
|
|
156
|
-
<Td>
|
|
157
|
-
<BaseCheckbox aria-label={`Select ${entry.content}`} />
|
|
158
|
-
</Td>
|
|
159
|
-
<Td style={{ maxWidth: '40vw' }}>
|
|
160
|
-
<Typography textColor="neutral800" ellipsis>{entry.content}</Typography>
|
|
161
|
-
</Td>
|
|
162
|
-
<Td>
|
|
163
|
-
<Typography textColor="neutral800">{entry.threadOf?.id || '-'}</Typography>
|
|
164
|
-
</Td>
|
|
165
|
-
<Td>
|
|
166
|
-
<Typography textColor="neutral800">{entry.related?.id}</Typography>
|
|
167
|
-
</Td>
|
|
168
|
-
<Td>
|
|
169
|
-
<Typography textColor="neutral800">{ formatDate(entry.updatedAt || entry.createdAt, { dateStyle: 'long', timeStyle: 'short' }) }</Typography>
|
|
170
|
-
</Td>
|
|
171
|
-
<Td>
|
|
172
|
-
<Typography textColor="neutral800">{ renderStatus(entry) }</Typography>
|
|
173
|
-
</Td>
|
|
174
|
-
<Td>
|
|
175
|
-
<Flex direction="column" alignItems="flex-end">
|
|
176
|
-
<IconButton onClick={() => handleClickDisplay(entry.id)} label={ getMessage('page.discover.table.action.display') } icon={<Eye />} />
|
|
177
|
-
</Flex>
|
|
178
|
-
</Td>
|
|
179
|
-
</Tr>)}
|
|
133
|
+
{result.map(entry => (<DiscoverTableRow config={config} item={entry} onClick={() => handleClickDisplay(entry.id)}/>))}
|
|
180
134
|
</Tbody>
|
|
181
135
|
</Table>
|
|
182
136
|
<TablePagination pagination={{ pageCount: pagination?.pageCount || 1 }} />
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { stringify } from 'qs';
|
|
2
|
-
import { isEmpty } from 'lodash';
|
|
3
2
|
|
|
4
|
-
import { getApiURL, axiosInstance } from '../../../utils';
|
|
3
|
+
import { getApiURL, axiosInstance, handleAPIError } from '../../../utils';
|
|
5
4
|
|
|
6
5
|
// eslint-disable-next-line import/prefer-default-export
|
|
7
6
|
export const fetchData = async (queryParams, toggleNotification) => {
|
|
@@ -10,11 +9,6 @@ export const fetchData = async (queryParams, toggleNotification) => {
|
|
|
10
9
|
|
|
11
10
|
return data;
|
|
12
11
|
} catch (err) {
|
|
13
|
-
toggleNotification
|
|
14
|
-
type: 'warning',
|
|
15
|
-
message: { id: 'notification.error' },
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
throw new Error('error');
|
|
12
|
+
handleAPIError(err, toggleNotification);
|
|
19
13
|
}
|
|
20
14
|
};
|
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
"page.discover.table.header.status": "Status",
|
|
18
18
|
"page.discover.table.header.status.OPEN": "OPEN",
|
|
19
19
|
"page.discover.table.header.status.BLOCKED": "BLOCKED",
|
|
20
|
-
"page.discover.table.header.status.TO_REVIEW": "TO REVIEW",
|
|
20
|
+
"page.discover.table.header.status.TO_REVIEW": "TO REVIEW ({count, number})",
|
|
21
21
|
"page.discover.table.header.actions": "Status",
|
|
22
22
|
"page.discover.table.action.display": "Display",
|
|
23
23
|
"page.discover.table.action.reports": "Reports",
|
|
24
24
|
"page.discover.table.empty": "You don't have any comments yet.",
|
|
25
25
|
"page.discover.table.empty.search": "No comments match the search.",
|
|
26
26
|
"page.discover.table.filters": "Filtering",
|
|
27
|
+
"page.discover.table.reports.review": "Review reports",
|
|
27
28
|
"page.details.header": "Discussion panel",
|
|
28
29
|
"page.details.header.description": "Drilldown and manage discussion related to the entity",
|
|
29
30
|
"page.details.panel.discussion": "Discussion",
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
import { useIntl } from 'react-intl';
|
|
3
|
+
import { isString } from 'lodash';
|
|
3
4
|
|
|
4
5
|
import pluginId from '../pluginId';
|
|
5
6
|
|
|
6
|
-
const getMessage = (
|
|
7
|
+
const getMessage = (input, defaultMessage, inPluginScope = true) => {
|
|
7
8
|
const { formatMessage } = useIntl();
|
|
9
|
+
let formattedId = ''
|
|
10
|
+
if (isString(input)) {
|
|
11
|
+
formattedId = input;
|
|
12
|
+
} else {
|
|
13
|
+
formattedId = input?.id;
|
|
14
|
+
}
|
|
8
15
|
return formatMessage({
|
|
9
|
-
id: `${inPluginScope ? pluginId : 'app.
|
|
16
|
+
id: `${inPluginScope ? pluginId : 'app.components'}.${formattedId}`,
|
|
10
17
|
defaultMessage,
|
|
11
|
-
})
|
|
18
|
+
}, input?.props || undefined)
|
|
12
19
|
};
|
|
13
20
|
|
|
14
21
|
export default getMessage;
|
package/package.json
CHANGED
|
@@ -16,8 +16,13 @@ module.exports = {
|
|
|
16
16
|
try {
|
|
17
17
|
return this.getService('common')
|
|
18
18
|
.findAllFlat({
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
query: {
|
|
20
|
+
...query,
|
|
21
|
+
related: relation,
|
|
22
|
+
},
|
|
23
|
+
populate: {
|
|
24
|
+
threadOf: true,
|
|
25
|
+
},
|
|
21
26
|
});
|
|
22
27
|
} catch (e) {
|
|
23
28
|
throwError(ctx, e);
|
|
@@ -34,6 +39,9 @@ module.exports = {
|
|
|
34
39
|
...query,
|
|
35
40
|
related: relation,
|
|
36
41
|
},
|
|
42
|
+
populate: {
|
|
43
|
+
threadOf: true,
|
|
44
|
+
},
|
|
37
45
|
dropBlockedThreads: true,
|
|
38
46
|
});
|
|
39
47
|
} catch (e) {
|
package/server/services/admin.js
CHANGED
|
@@ -55,7 +55,17 @@ const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const entities = await strapi.db.query(getModelUid('comment'))
|
|
58
|
-
.findMany(
|
|
58
|
+
.findMany({
|
|
59
|
+
...params,
|
|
60
|
+
populate: {
|
|
61
|
+
authorUser: true,
|
|
62
|
+
reports: {
|
|
63
|
+
where: {
|
|
64
|
+
resolved: false
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
59
69
|
const total = await strapi.db.query(getModelUid('comment'))
|
|
60
70
|
.count({
|
|
61
71
|
where: params.where,
|
|
@@ -78,11 +88,27 @@ const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
|
|
|
78
88
|
|
|
79
89
|
// Find single comment
|
|
80
90
|
async findOneAndThread(id) {
|
|
91
|
+
const reportsPopulation = {
|
|
92
|
+
reports: {
|
|
93
|
+
where: {
|
|
94
|
+
resolved: false,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
81
99
|
const entity = await strapi.db.query(getModelUid('comment')).findOne({
|
|
82
100
|
where: {
|
|
83
101
|
id
|
|
84
102
|
},
|
|
85
|
-
populate:
|
|
103
|
+
populate: {
|
|
104
|
+
authorUser: true,
|
|
105
|
+
threadOf: {
|
|
106
|
+
populate: {
|
|
107
|
+
...reportsPopulation
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
...reportsPopulation,
|
|
111
|
+
},
|
|
86
112
|
});
|
|
87
113
|
|
|
88
114
|
if (!entity){
|
|
@@ -109,19 +135,22 @@ const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
|
|
|
109
135
|
threadOf: levelThreadId,
|
|
110
136
|
related: entity.related,
|
|
111
137
|
},
|
|
138
|
+
populate: {
|
|
139
|
+
...reportsPopulation,
|
|
140
|
+
},
|
|
112
141
|
startingFromId: levelThreadId,
|
|
113
142
|
isAdmin: true
|
|
114
143
|
}, false);
|
|
115
144
|
|
|
116
|
-
const selectedEntity =
|
|
145
|
+
const selectedEntity = this.getCommonService().sanitizeCommentEntity(entity);
|
|
117
146
|
|
|
118
147
|
return {
|
|
119
148
|
entity: relatedEntity,
|
|
120
149
|
selected: {
|
|
121
150
|
...selectedEntity,
|
|
122
|
-
threadOf: selectedEntity.threadOf ?
|
|
151
|
+
threadOf: selectedEntity.threadOf ? selectedEntity.threadOf : null,
|
|
123
152
|
},
|
|
124
|
-
level: entitiesOnSameLevel
|
|
153
|
+
level: entitiesOnSameLevel,
|
|
125
154
|
};
|
|
126
155
|
},
|
|
127
156
|
|
|
@@ -160,7 +160,7 @@ module.exports = ({ strapi }) => ({
|
|
|
160
160
|
try {
|
|
161
161
|
await this.sendAbuseReportEmail(entity.reason, entity.content); // Could also add some info about relation
|
|
162
162
|
} catch (err) {
|
|
163
|
-
|
|
163
|
+
return entity;
|
|
164
164
|
}
|
|
165
165
|
} else {
|
|
166
166
|
throw new PluginError(500, 'Report cannot be created');
|
|
@@ -63,7 +63,7 @@ module.exports = ({ strapi }) => ({
|
|
|
63
63
|
},
|
|
64
64
|
|
|
65
65
|
// Find comments in the flat structure
|
|
66
|
-
async findAllFlat(query, isAdmin = false, relatedEntity = null) {
|
|
66
|
+
async findAllFlat({ query, populate = {} }, isAdmin = false, relatedEntity = null) {
|
|
67
67
|
const criteria = {
|
|
68
68
|
...query,
|
|
69
69
|
$or: [{ removed: { $null: true } }, { removed: false }],
|
|
@@ -73,14 +73,19 @@ module.exports = ({ strapi }) => ({
|
|
|
73
73
|
delete criteria.$or;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const
|
|
76
|
+
const defaultPopulate = {
|
|
77
|
+
authorUser: true,
|
|
78
|
+
};
|
|
77
79
|
|
|
78
80
|
const entries = await strapi.db.query(getModelUid('comment'))
|
|
79
81
|
.findMany({
|
|
80
82
|
where: {
|
|
81
83
|
...criteria,
|
|
82
84
|
},
|
|
83
|
-
populate
|
|
85
|
+
populate: {
|
|
86
|
+
...defaultPopulate,
|
|
87
|
+
...populate,
|
|
88
|
+
},
|
|
84
89
|
});
|
|
85
90
|
|
|
86
91
|
const entriesWithThreads = await Promise.all(entries.map(async _ => {
|
|
@@ -97,12 +102,12 @@ module.exports = ({ strapi }) => ({
|
|
|
97
102
|
const result = entries
|
|
98
103
|
.map(_ => {
|
|
99
104
|
const threadedItem = entriesWithThreads.find(item => item.id === _.id);
|
|
100
|
-
return
|
|
105
|
+
return this.sanitizeCommentEntity({
|
|
101
106
|
..._,
|
|
102
107
|
threadOf: query.threadOf || _.threadOf || null,
|
|
103
108
|
gotThread: (threadedItem?.itemsInTread || 0) > 0,
|
|
104
109
|
threadFirstItemId: threadedItem?.firstThreadItemId,
|
|
105
|
-
})
|
|
110
|
+
});
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
if (relatedEntities.filter(_ => _).length > 0) {
|
|
@@ -114,11 +119,12 @@ module.exports = ({ strapi }) => ({
|
|
|
114
119
|
// Find comments and create relations tree structure
|
|
115
120
|
async findAllInHierarchy ({
|
|
116
121
|
query,
|
|
122
|
+
populate = {},
|
|
117
123
|
startingFromId = null,
|
|
118
124
|
dropBlockedThreads = false,
|
|
119
125
|
isAdmin = false,
|
|
120
126
|
}, relatedEntity) {
|
|
121
|
-
const entities = await this.findAllFlat(query, isAdmin, relatedEntity);
|
|
127
|
+
const entities = await this.findAllFlat({ query, populate }, isAdmin, relatedEntity);
|
|
122
128
|
return buildNestedStructure(entities, startingFromId, 'threadOf', dropBlockedThreads, false);
|
|
123
129
|
},
|
|
124
130
|
|