strapi-plugin-comments 2.0.0-beta.5 → 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.
- package/README.md +16 -1
- package/admin/src/components/DiscussionThread/index.js +8 -2
- package/admin/src/components/DiscussionThreadItem/index.js +24 -5
- package/admin/src/components/DiscussionThreadItemActions/index.js +45 -18
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +20 -11
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +15 -9
- package/admin/src/components/ReportsReviewModal/index.js +5 -0
- package/admin/src/components/ReportsReviewTable/index.js +60 -52
- package/admin/src/components/icons/review.js +1 -1
- package/admin/src/index.js +1 -1
- package/admin/src/pages/Details/components/DetailsEntity/index.js +2 -2
- package/admin/src/pages/Details/components/DetailsFilters/index.js +1 -0
- package/admin/src/pages/Details/index.js +43 -37
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +30 -10
- package/admin/src/pages/Discover/index.js +67 -57
- package/admin/src/permissions.js +10 -8
- package/admin/src/translations/en.json +1 -0
- package/package.json +1 -1
- package/permissions.js +19 -0
- package/server/bootstrap.js +38 -2
- package/server/config/index.js +2 -2
- package/server/services/admin.js +2 -1
- package/server/services/client.js +1 -1
- package/strapi-server.js +33 -0
package/README.md
CHANGED
|
@@ -58,7 +58,8 @@ In our minimum support we're following [official Node.js releases timelines](htt
|
|
|
58
58
|
|
|
59
59
|
**Supported Strapi versions**:
|
|
60
60
|
|
|
61
|
-
- Strapi v4.0.
|
|
61
|
+
- Strapi v4.0.5 (recently tested)
|
|
62
|
+
- Strapi v4.x
|
|
62
63
|
|
|
63
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.)
|
|
64
65
|
|
|
@@ -101,6 +102,20 @@ To setup amend default plugin configuration we recommend to put following snippe
|
|
|
101
102
|
|
|
102
103
|
> To be done
|
|
103
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 & 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
|
+
|
|
104
119
|
## Public API Comment model
|
|
105
120
|
|
|
106
121
|
### Generic (non Strapi User)
|
|
@@ -18,7 +18,7 @@ import { getMessage, getUrl } from '../../utils';
|
|
|
18
18
|
import LoadingIndicatorOverlay from '../LoadingIndicatorOverlay';
|
|
19
19
|
import { LoadingIndicatorContainer } from '../LoadingIndicatorOverlay/styles';
|
|
20
20
|
|
|
21
|
-
const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
|
|
21
|
+
const DiscussionThread = ({ level = [], selected = {}, allowedActions, isReloading }) => {
|
|
22
22
|
const rootThread = selected?.threadOf;
|
|
23
23
|
return (<LoadingIndicatorContainer as={Box} padding={4}>
|
|
24
24
|
{ isReloading && <LoadingIndicatorOverlay />}
|
|
@@ -34,13 +34,14 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
|
|
|
34
34
|
<Divider />
|
|
35
35
|
</Box>
|
|
36
36
|
<Flex as="ul" direction="column" alignItems="flex-start">
|
|
37
|
-
{ rootThread && (<DiscussionThreadItem {...rootThread} isThreadAuthor root pinned />) }
|
|
37
|
+
{ rootThread && (<DiscussionThreadItem {...rootThread} allowedActions={allowedActions} isThreadAuthor root pinned />) }
|
|
38
38
|
{ level.map(item => {
|
|
39
39
|
const isSelected = selected.id === item.id;
|
|
40
40
|
const isThreadAuthor = !isNil(selected?.threadOf?.authorId) && (selected?.threadOf?.authorId === item.authorId);
|
|
41
41
|
return (<DiscussionThreadItem
|
|
42
42
|
key={`comment-${item.id}`}
|
|
43
43
|
{...item}
|
|
44
|
+
allowedActions={allowedActions}
|
|
44
45
|
root={isNil(rootThread)}
|
|
45
46
|
blockedThread={rootThread?.blockedThread || item.blockedThread}
|
|
46
47
|
isSelected={isSelected}
|
|
@@ -54,6 +55,11 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
|
|
|
54
55
|
DiscussionThread.propTypes = {
|
|
55
56
|
level: PropTypes.array.isRequired,
|
|
56
57
|
selected: PropTypes.object.isRequired,
|
|
58
|
+
allowedActions: PropTypes.shape({
|
|
59
|
+
canModerate: PropTypes.bool,
|
|
60
|
+
canAccessReports: PropTypes.bool,
|
|
61
|
+
canReviewReports: PropTypes.bool,
|
|
62
|
+
}),
|
|
57
63
|
isReloading: PropTypes.bool,
|
|
58
64
|
};
|
|
59
65
|
|
|
@@ -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 = (
|
|
24
|
-
const {
|
|
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,12 +76,12 @@ 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 { ...
|
|
79
|
+
{ !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
|
|
66
80
|
</DiscussionThreadItemBoxContent>
|
|
67
|
-
{ !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...
|
|
81
|
+
{ !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
|
|
68
82
|
</Flex>
|
|
69
83
|
<Divider background={dividerColor} />
|
|
70
|
-
<DiscussionThreadItemMeta { ...
|
|
84
|
+
<DiscussionThreadItemMeta { ...props } />
|
|
71
85
|
</DiscussionThreadItemBox>
|
|
72
86
|
</DiscussionThreadItemContent>
|
|
73
87
|
|
|
@@ -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;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
8
|
import PropTypes from 'prop-types';
|
|
9
9
|
import { useMutation, useQueryClient } from 'react-query';
|
|
10
|
-
import { isNil } from 'lodash';
|
|
10
|
+
import { isNil, isEmpty } from 'lodash';
|
|
11
11
|
import { Button } from '@strapi/design-system/Button';
|
|
12
12
|
import { Flex } from '@strapi/design-system/Flex';
|
|
13
13
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
@@ -20,7 +20,7 @@ import pluginId from '../../pluginId';
|
|
|
20
20
|
import { LockIcon, UnlockIcon } from '../icons';
|
|
21
21
|
import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
|
|
22
22
|
|
|
23
|
-
const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, approvalStatus, root }) => {
|
|
23
|
+
const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotThread, pinned, reports = [], approvalStatus, root, allowedActions: { canModerate, canAccessReports, canReviewReports } }) => {
|
|
24
24
|
|
|
25
25
|
const [blockConfirmationVisible, setBlockConfirmationVisible] = useState(false);
|
|
26
26
|
const [blockThreadConfirmationVisible, setBlockThreadConfirmationVisible] = useState(false);
|
|
@@ -72,9 +72,12 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
|
|
|
72
72
|
const needsApproval = gotApprovalFlow && (approvalStatus === 'PENDING');
|
|
73
73
|
const isBlocked = blocked || blockedThread;
|
|
74
74
|
const isRejected = gotApprovalFlow && (approvalStatus === 'REJECTED');
|
|
75
|
+
const openReports = reports?.filter(_ => !_.resolved);
|
|
76
|
+
const hasReports = !isEmpty(openReports);
|
|
77
|
+
const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
|
|
75
78
|
|
|
76
79
|
const resolveStatusBadge = () => {
|
|
77
|
-
const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus });
|
|
80
|
+
const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
|
|
78
81
|
return {
|
|
79
82
|
badgeVisible: isBlocked || gotApprovalFlow || removed,
|
|
80
83
|
badgeColor: resolveCommentStatusColor(status),
|
|
@@ -86,44 +89,68 @@ const DiscussionThreadItemActions = ({ id, blocked, removed, blockedThread, gotT
|
|
|
86
89
|
|
|
87
90
|
const handleBlockClick = () => setBlockConfirmationVisible(true);
|
|
88
91
|
const handleBlockConfirm = async () => {
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
if (canModerate) {
|
|
93
|
+
lockApp();
|
|
94
|
+
blockItemMutation.mutate(id);
|
|
95
|
+
}
|
|
91
96
|
};
|
|
92
97
|
const handleBlockCancel = () => {
|
|
93
98
|
setBlockConfirmationVisible(false);
|
|
94
99
|
};
|
|
95
100
|
const handleUnblockClick = async () => {
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
if (canModerate) {
|
|
102
|
+
lockApp();
|
|
103
|
+
unblockItemMutation.mutate(id);
|
|
104
|
+
}
|
|
98
105
|
};
|
|
99
106
|
|
|
100
107
|
const handleBlockThreadClick = () => setBlockThreadConfirmationVisible(true);
|
|
101
108
|
const handleBlockThreadConfirm = async () => {
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
if (canModerate) {
|
|
110
|
+
lockApp();
|
|
111
|
+
blockItemThreadMutation.mutate(id);
|
|
112
|
+
}
|
|
104
113
|
};
|
|
105
114
|
const handleBlockThreadCancel = () => {
|
|
106
115
|
setBlockThreadConfirmationVisible(false);
|
|
107
116
|
};
|
|
108
117
|
const handleUnblockThreadClick = async () => {
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
if (canModerate) {
|
|
119
|
+
lockApp();
|
|
120
|
+
unblockItemThreadMutation.mutate(id);
|
|
121
|
+
}
|
|
111
122
|
};
|
|
112
123
|
|
|
113
|
-
if (removed || isRejected) {
|
|
124
|
+
if (removed || isRejected || !canModerate) {
|
|
114
125
|
return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
115
|
-
<DiscussionThreadItemActionsBadge
|
|
126
|
+
<DiscussionThreadItemActionsBadge
|
|
127
|
+
color={badgeColor}
|
|
128
|
+
backgroundColor={`${badgeColor}100`}
|
|
129
|
+
textColor={`${badgeColor}600`}>
|
|
130
|
+
{ getMessage({
|
|
131
|
+
id: `page.common.item.status.${badgeLabel}`,
|
|
132
|
+
props: { count: openReports.length },
|
|
133
|
+
}, badgeLabel)}
|
|
134
|
+
</DiscussionThreadItemActionsBadge>
|
|
116
135
|
</DiscussionThreadItemActionsWrapper>);
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
return (<>
|
|
120
139
|
<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
121
|
-
{ badgeVisible && (<DiscussionThreadItemActionsBadge
|
|
140
|
+
{ badgeVisible && (<DiscussionThreadItemActionsBadge
|
|
141
|
+
color={badgeColor}
|
|
142
|
+
backgroundColor={`${badgeColor}100`}
|
|
143
|
+
textColor={`${badgeColor}600`}>
|
|
144
|
+
{ getMessage({
|
|
145
|
+
id: `page.common.item.status.${badgeLabel}`,
|
|
146
|
+
props: { count: openReports.length },
|
|
147
|
+
}, badgeLabel)}
|
|
148
|
+
</DiscussionThreadItemActionsBadge>)}
|
|
122
149
|
{ !blockedThread && !(blocked || needsApproval) && (<IconButton onClick={handleBlockClick} loading={blockItemMutation.isLoading} icon={<LockIcon />} label={getMessage('page.details.actions.comment.block', 'Block')} style={(!blockedThread && root) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
|
|
123
150
|
{ !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} style={(blocked && !blockedThread) ? { marginTop: '1px', marginRight: '.5rem' } : {}} />)}
|
|
124
151
|
{ (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
|
|
125
152
|
{ (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
|
|
126
|
-
{ needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} queryToInvalidate="get-details-data" />) }
|
|
153
|
+
{ needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
|
|
127
154
|
</DiscussionThreadItemActionsWrapper>
|
|
128
155
|
{ !blocked && (<ConfirmationDialog
|
|
129
156
|
isVisible={blockConfirmationVisible}
|
|
@@ -153,9 +180,9 @@ DiscussionThreadItemActions.propTypes = {
|
|
|
153
180
|
blocked: PropTypes.bool.isRequired,
|
|
154
181
|
blockedThread: PropTypes.bool.isRequired,
|
|
155
182
|
approvalStatus: PropTypes.oneOfType([PropTypes.nullable, PropTypes.oneOf(['PENDING', 'APPROVED', 'REJECTED'])]),
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
allowedActions: PropTypes.shape({
|
|
184
|
+
canModerate: PropTypes.bool,
|
|
185
|
+
}),
|
|
159
186
|
};
|
|
160
187
|
|
|
161
188
|
export default DiscussionThreadItemActions;
|
|
@@ -15,13 +15,13 @@ import { DiscussionThreadItemActionsGroupWrapper } from './styles';
|
|
|
15
15
|
import { approveItem, rejectItem } from '../../pages/utils/api';
|
|
16
16
|
import pluginId from '../../pluginId';
|
|
17
17
|
|
|
18
|
-
const DiscussionThreadItemApprovalFlowActions = ({ id, queryToInvalidate }) => {
|
|
18
|
+
const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canModerate }, queryToInvalidate }) => {
|
|
19
19
|
|
|
20
20
|
const toggleNotification = useNotification();
|
|
21
21
|
const queryClient = useQueryClient();
|
|
22
22
|
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
23
23
|
|
|
24
|
-
const onSuccess = (message
|
|
24
|
+
const onSuccess = (message) => async () => {
|
|
25
25
|
if (queryToInvalidate) {
|
|
26
26
|
await queryClient.invalidateQueries(queryToInvalidate);
|
|
27
27
|
}
|
|
@@ -45,26 +45,35 @@ const DiscussionThreadItemApprovalFlowActions = ({ id, queryToInvalidate }) => {
|
|
|
45
45
|
refetchActive: false,
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
49
48
|
const handleApproveClick = () => {
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
if (canModerate) {
|
|
50
|
+
lockApp();
|
|
51
|
+
approveItemMutation.mutate(id);
|
|
52
|
+
}
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
const handleRejectClick = () => {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (canModerate) {
|
|
57
|
+
lockApp();
|
|
58
|
+
rejectItemMutation.mutate(id);
|
|
59
|
+
}
|
|
57
60
|
};
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
if (canModerate) {
|
|
63
|
+
return (<DiscussionThreadItemActionsGroupWrapper>
|
|
64
|
+
<IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
|
|
65
|
+
<IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
|
|
66
|
+
</DiscussionThreadItemActionsGroupWrapper>);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
63
69
|
};
|
|
64
70
|
|
|
65
71
|
DiscussionThreadItemApprovalFlowActions.propTypes = {
|
|
66
72
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
67
73
|
queryToInvalidate: PropTypes.string,
|
|
74
|
+
allowedActions: PropTypes.shape({
|
|
75
|
+
canModerate: PropTypes.bool,
|
|
76
|
+
}),
|
|
68
77
|
wrapped: PropTypes.bool,
|
|
69
78
|
};
|
|
70
79
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
8
9
|
import { useMutation, useQueryClient } from 'react-query';
|
|
9
10
|
import { isEmpty, orderBy } from 'lodash';
|
|
10
11
|
import { Badge } from '@strapi/design-system/Badge';
|
|
@@ -23,7 +24,7 @@ import pluginId from '../../pluginId';
|
|
|
23
24
|
import { LockIcon, ReviewIcon } from '../icons';
|
|
24
25
|
|
|
25
26
|
const DiscussionThreadItemWarnings = (item) => {
|
|
26
|
-
const { id, reports, gotThread } = item;
|
|
27
|
+
const { id, reports, gotThread, allowedActions: { canModerate, canAccessReports, canReviewReports }} = item;
|
|
27
28
|
|
|
28
29
|
const [reportsReviewVisible, setReportsReviewVisible] = useState(false);
|
|
29
30
|
const [blockButtonsDisabled, setBlockButtonsDisabled] = useState(false);
|
|
@@ -93,7 +94,7 @@ const DiscussionThreadItemWarnings = (item) => {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
return (<>
|
|
96
|
-
<DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
|
|
97
|
+
{ canAccessReports && (<DiscussionThreadItemWarningsWrapper as={Flex} direction="row" padding={2} background="danger100" borderColor="danger200" border={1} hasRadius>
|
|
97
98
|
<Box as={Flex} paddingRight={2} direction="row" grow={1}>
|
|
98
99
|
<Badge textColor="neutral0" backgroundColor="warning600">{ openReports.length }</Badge>
|
|
99
100
|
<Box paddingLeft={2}>
|
|
@@ -103,19 +104,19 @@ const DiscussionThreadItemWarnings = (item) => {
|
|
|
103
104
|
<Button onClick={handleReportsReviewClick} variant="ghost" endIcon={<ReviewIcon />}>
|
|
104
105
|
{ getMessage(`page.details.actions.comment.report.review`, 'review') }
|
|
105
106
|
</Button>
|
|
106
|
-
</DiscussionThreadItemWarningsWrapper>
|
|
107
|
-
<ReportsReviewModal
|
|
107
|
+
</DiscussionThreadItemWarningsWrapper>) }
|
|
108
|
+
{ (canAccessReports || canReviewReports) && (<ReportsReviewModal
|
|
108
109
|
isVisible={reportsReviewVisible}
|
|
109
110
|
isActionAsync={isLoading}
|
|
110
111
|
onClose={handleReportsReviewClose}
|
|
111
|
-
startActions={<>
|
|
112
|
+
startActions={canModerate && (<>
|
|
112
113
|
<Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
113
114
|
{ getMessage(`page.details.actions.comment.block`, 'Block comment') }
|
|
114
115
|
</Button>
|
|
115
116
|
{ gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
116
117
|
{ getMessage(`page.details.actions.thread.block`, 'Block thread') }
|
|
117
118
|
</Button>) }
|
|
118
|
-
</>}
|
|
119
|
+
</>)}
|
|
119
120
|
item={item}>
|
|
120
121
|
<Typography variant="sigma" textColor="neutral600" id="reports-list">
|
|
121
122
|
{ getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
|
|
@@ -124,17 +125,22 @@ const DiscussionThreadItemWarnings = (item) => {
|
|
|
124
125
|
<Divider />
|
|
125
126
|
</Box>
|
|
126
127
|
<ReportsReviewTable
|
|
127
|
-
commentId={
|
|
128
|
+
commentId={id}
|
|
128
129
|
items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
|
|
129
130
|
mutation={resolveReportMutation}
|
|
131
|
+
allowedActions={{ canAccessReports, canReviewReports }}
|
|
130
132
|
onBlockButtonsStateChange={handleBlockButtonsStateChange}
|
|
131
133
|
/>
|
|
132
|
-
</ReportsReviewModal>
|
|
134
|
+
</ReportsReviewModal>) }
|
|
133
135
|
</>);
|
|
134
136
|
};
|
|
135
137
|
|
|
136
138
|
DiscussionThreadItemWarnings.propTypes = {
|
|
137
|
-
|
|
139
|
+
allowedActions: PropTypes.shape({
|
|
140
|
+
canModerate: PropTypes.bool,
|
|
141
|
+
canAccessReports: PropTypes.bool,
|
|
142
|
+
canReviewReports: PropTypes.bool,
|
|
143
|
+
}),
|
|
138
144
|
};
|
|
139
145
|
|
|
140
146
|
export default DiscussionThreadItemWarnings;
|
|
@@ -37,6 +37,11 @@ const ReportsReviewModal = ({
|
|
|
37
37
|
endActions: PropTypes.array,
|
|
38
38
|
item: PropTypes.object.isRequired,
|
|
39
39
|
children: PropTypes.array.isRequired,
|
|
40
|
+
allowedActions: PropTypes.shape({
|
|
41
|
+
canModerate: PropTypes.bool,
|
|
42
|
+
canAccessReports: PropTypes.bool,
|
|
43
|
+
canReviewReports: PropTypes.bool,
|
|
44
|
+
}),
|
|
40
45
|
onClose: PropTypes.func.isRequired,
|
|
41
46
|
};
|
|
42
47
|
|
|
@@ -24,9 +24,9 @@ const ReportsReviewTable = ({
|
|
|
24
24
|
commentId,
|
|
25
25
|
items,
|
|
26
26
|
mutation,
|
|
27
|
+
allowedActions: { canAccessReports, canReviewReports },
|
|
27
28
|
onBlockButtonsStateChange,
|
|
28
29
|
}) => {
|
|
29
|
-
|
|
30
30
|
const [storedItems, setStoredItems] = useState([]);
|
|
31
31
|
|
|
32
32
|
const { formatDate } = useIntl();
|
|
@@ -59,12 +59,12 @@ const ReportsReviewTable = ({
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const handleClickResolve = async (reportId) => {
|
|
62
|
+
if (canReviewReports) {
|
|
62
63
|
lockApp();
|
|
63
64
|
const item = await mutation.mutateAsync({
|
|
64
65
|
id: commentId,
|
|
65
66
|
reportId,
|
|
66
67
|
});
|
|
67
|
-
console.log(item);
|
|
68
68
|
if (item) {
|
|
69
69
|
const updatedItems = storedItems.map(_ => ({
|
|
70
70
|
..._,
|
|
@@ -73,6 +73,7 @@ const ReportsReviewTable = ({
|
|
|
73
73
|
setStoredItems(updatedItems);
|
|
74
74
|
onBlockButtonsStateChange(updatedItems.filter(_ => !_.resolved).length === 0);
|
|
75
75
|
}
|
|
76
|
+
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
useEffect(() => {
|
|
@@ -85,61 +86,68 @@ const ReportsReviewTable = ({
|
|
|
85
86
|
|
|
86
87
|
const COL_COUNT = 6;
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
89
|
+
if (canAccessReports) {
|
|
90
|
+
return (<Table colCount={COL_COUNT} rowCount={storedItems.length}>
|
|
91
|
+
<Thead>
|
|
92
|
+
<Tr>
|
|
93
|
+
<Th>
|
|
94
|
+
<BaseCheckbox aria-label="Select all entries" />
|
|
95
|
+
</Th>
|
|
96
|
+
<Th>
|
|
97
|
+
<Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.reason') }</Typography>
|
|
98
|
+
</Th>
|
|
99
|
+
<Th>
|
|
100
|
+
<Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.content') }</Typography>
|
|
101
|
+
</Th>
|
|
102
|
+
<Th>
|
|
103
|
+
<Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.createdAt') }</Typography>
|
|
104
|
+
</Th>
|
|
105
|
+
<Th>
|
|
106
|
+
<Typography variant="sigma">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.status') }</Typography>
|
|
107
|
+
</Th>
|
|
108
|
+
{ canReviewReports && (<Th>
|
|
109
|
+
<VisuallyHidden>{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions') }</VisuallyHidden>
|
|
110
|
+
</Th>)}
|
|
111
|
+
</Tr>
|
|
112
|
+
</Thead>
|
|
113
|
+
<Tbody>
|
|
114
|
+
{storedItems.map(entry => <Tr key={entry.id}>
|
|
115
|
+
<Td>
|
|
116
|
+
<BaseCheckbox aria-label={`Select report`} />
|
|
117
|
+
</Td>
|
|
118
|
+
<Td>
|
|
119
|
+
<Typography textColor="neutral800">{renderReason(entry.reason)}</Typography>
|
|
120
|
+
</Td>
|
|
121
|
+
<Td>
|
|
122
|
+
<Typography textColor="neutral800">{entry.content}</Typography>
|
|
123
|
+
</Td>
|
|
124
|
+
<Td>
|
|
125
|
+
<Typography textColor="neutral800">{ formatDate(entry.createdAt, { dateStyle: 'short', timeStyle: 'short' }) }</Typography>
|
|
126
|
+
</Td>
|
|
127
|
+
<Td>
|
|
128
|
+
<Typography textColor="neutral800">{ renderStatus(entry.resolved) }</Typography>
|
|
129
|
+
</Td>
|
|
130
|
+
{ canReviewReports && (<Td>
|
|
131
|
+
{ !entry.resolved && (<Flex direction="column" alignItems="flex-end">
|
|
132
|
+
<Button onClick={() => handleClickResolve(entry.id)} startIcon={<Check />} variant="success">{ getMessage('page.details.panel.discussion.warnings.reports.dialog.actions.resolve', 'resolve') }</Button>
|
|
133
|
+
</Flex>) }
|
|
134
|
+
</Td>)}
|
|
135
|
+
</Tr>)}
|
|
136
|
+
</Tbody>
|
|
137
|
+
{/* <TFooter /> */}
|
|
138
|
+
</Table>);
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
ReportsReviewTable.propTypes = {
|
|
140
|
-
commentId: PropTypes.string.isRequired,
|
|
144
|
+
commentId: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
|
|
141
145
|
items: PropTypes.array.isRequired,
|
|
142
146
|
mutation: PropTypes.func.isRequired,
|
|
147
|
+
allowedActions: PropTypes.shape({
|
|
148
|
+
canAccessReports: PropTypes.bool,
|
|
149
|
+
canReviewReports: PropTypes.bool,
|
|
150
|
+
}),
|
|
143
151
|
onBlockButtonsStateChange: PropTypes.func.isRequired,
|
|
144
152
|
};
|
|
145
153
|
|
|
@@ -11,7 +11,7 @@ const ReviewIcon = ({ width, height, size = 16 }) =>
|
|
|
11
11
|
style={{ width: `${width || size}px`, height: `${height || size}px`, }}
|
|
12
12
|
xmlns="http://www.w3.org/2000/svg">
|
|
13
13
|
<g style={ {transform: `scale(${(width || size)/initSize})` } }>
|
|
14
|
-
<path
|
|
14
|
+
<path fillRule="evenodd" clipRule="evenodd" d="M0 4a4 4 0 014-4h8a4 4 0 014 4v8a4 4 0 01-4 4H4a4 4 0 01-4-4V4zm6.996.165a1.017 1.017 0 112.012 0L8 11 6.996 4.165zM8 11a1 1 0 110 2 1 1 0 010-2z" />
|
|
15
15
|
</g>
|
|
16
16
|
</svg>
|
|
17
17
|
|
package/admin/src/index.js
CHANGED
|
@@ -30,7 +30,7 @@ const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
|
|
|
30
30
|
FIELDS_LIMIT
|
|
31
31
|
);
|
|
32
32
|
|
|
33
|
-
const formatLabel = label => label.split('_')
|
|
33
|
+
const formatLabel = (label = '') => label.split('_')
|
|
34
34
|
.map(_ => capitalize(_))
|
|
35
35
|
.join(' ');
|
|
36
36
|
|
|
@@ -47,7 +47,7 @@ const DetailsEntity = ({ data = {}, schema = {}, config = {} }) => {
|
|
|
47
47
|
<Typography>{ data[entityLabelKey] }</Typography>
|
|
48
48
|
</Flex>
|
|
49
49
|
{ itemKeys.map(_ =>
|
|
50
|
-
(<Flex direction="column" alignItems="flex-start">
|
|
50
|
+
(<Flex key={`prop_${_}`} direction="column" alignItems="flex-start">
|
|
51
51
|
<Typography fontWeight="bold">{ formatLabel(_) }</Typography>
|
|
52
52
|
<Typography>{ data[_] }</Typography>
|
|
53
53
|
</Flex>))
|
|
@@ -20,6 +20,7 @@ const DetailsFilters = ({ data, onChange }) => {
|
|
|
20
20
|
<Stack size={2} horizontal>
|
|
21
21
|
<Typography variant="epsilon">{getMessage('page.details.filters.label', '')}</Typography>
|
|
22
22
|
<Switch
|
|
23
|
+
label={getMessage('page.details.filters.removed.label', 'Comments visibility')}
|
|
23
24
|
onLabel={getMessage('page.details.filters.removed.show', 'Show')}
|
|
24
25
|
offLabel={getMessage('page.details.filters.removed.hide', 'Hide')}
|
|
25
26
|
selected={removed} onChange={() => handleOnChange('removed', !removed)}
|
|
@@ -50,13 +50,16 @@ const Details = ({ config }) => {
|
|
|
50
50
|
const trackUsageRef = useRef(trackUsage);
|
|
51
51
|
const toggleNotification = useNotification();
|
|
52
52
|
|
|
53
|
-
const viewPermissions = useMemo(() => {
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const viewPermissions = useMemo(() => ({
|
|
54
|
+
access: pluginPermissions.access,
|
|
55
|
+
moderate: pluginPermissions.moderate,
|
|
56
|
+
accessReports: pluginPermissions.reports,
|
|
57
|
+
reviewReports: pluginPermissions.reportsReview,
|
|
58
|
+
}), []);
|
|
56
59
|
|
|
57
60
|
const {
|
|
58
61
|
isLoading: isLoadingForPermissions,
|
|
59
|
-
allowedActions: {
|
|
62
|
+
allowedActions: { canAccess, canModerate, canAccessReports, canReviewReports },
|
|
60
63
|
} = useRBAC(viewPermissions);
|
|
61
64
|
|
|
62
65
|
const [filters, setFilters] = useState({});
|
|
@@ -64,7 +67,7 @@ const Details = ({ config }) => {
|
|
|
64
67
|
const regexUID = new RegExp(config.regex.uid);
|
|
65
68
|
|
|
66
69
|
const { isLoading: isLoadingForData, data, isFetching } = useQuery(
|
|
67
|
-
['get-details-data', id, filters],
|
|
70
|
+
['get-details-data', id, filters, canAccess],
|
|
68
71
|
() => fetchDetailsData(id, filters, toggleNotification),
|
|
69
72
|
{
|
|
70
73
|
initialData: { },
|
|
@@ -75,7 +78,7 @@ const Details = ({ config }) => {
|
|
|
75
78
|
const entityUidValid = entity?.uid && regexUID.test(entity.uid);
|
|
76
79
|
|
|
77
80
|
const { data: contentTypeData } = useQuery(
|
|
78
|
-
['get-additional-data', entity?.uid],
|
|
81
|
+
['get-additional-data', entity?.uid, canAccess],
|
|
79
82
|
() => fetchContentTypeData(entity?.uid, toggleNotification),
|
|
80
83
|
{
|
|
81
84
|
enabled: !!entity?.uid,
|
|
@@ -87,37 +90,40 @@ const Details = ({ config }) => {
|
|
|
87
90
|
|
|
88
91
|
const isLoading = isLoadingForData || isFetching;
|
|
89
92
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
93
|
+
if (canAccess) {
|
|
94
|
+
return <Box background="neutral100">
|
|
95
|
+
<Layout>
|
|
96
|
+
{(isLoading || isLoadingForPermissions) && isEmpty(data) ? (<LoadingIndicatorPage />) : (
|
|
97
|
+
<>
|
|
98
|
+
<HeaderLayout
|
|
99
|
+
navigationAction={
|
|
100
|
+
<Link startIcon={<ArrowLeft />} to={getUrl(`discover`)}>
|
|
101
|
+
{ getMessage('HeaderLayout.link.go-back', 'Back', false) }
|
|
102
|
+
</Link>}
|
|
103
|
+
title={ getMessage('page.details.header') }
|
|
104
|
+
subtitle={ getMessage('page.details.header.description') }
|
|
105
|
+
primaryAction={<DetailsFilters data={filters} onChange={handleChangeFilters} />}
|
|
106
|
+
secondaryAction={<Box as={Flex}> </Box>}
|
|
107
|
+
as="h2" />
|
|
108
|
+
<ContentLayout>
|
|
109
|
+
<TwoColsLayout
|
|
110
|
+
startCol={<DiscussionThread level={level} selected={selected} isReloading={isLoading} allowedActions={{ canModerate, canAccessReports, canReviewReports }} />}
|
|
111
|
+
endCol={ <Stack size={2}>
|
|
112
|
+
<DiscussionStatus item={selected} />
|
|
113
|
+
{ !contentTypeData?.schema ? (<Box padding={4}>
|
|
114
|
+
<Loader small>{ getMessage('page.details.panel.loading', 'Loading...') }</Loader>
|
|
115
|
+
</Box>) :
|
|
116
|
+
(<DetailsEntity data={entity} schema={contentTypeData?.schema} config={config} />)
|
|
117
|
+
}
|
|
118
|
+
</Stack>
|
|
119
|
+
}
|
|
120
|
+
/>
|
|
121
|
+
</ContentLayout>
|
|
122
|
+
</>) }
|
|
123
|
+
</Layout>
|
|
124
|
+
</Box>;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
const mapStateToProps = makeAppView();
|
|
@@ -25,7 +25,7 @@ import { TableLink } from './styles';
|
|
|
25
25
|
import renderEntryTitle from '../../../../utils/renderEntryTitle';
|
|
26
26
|
import DiscussionThreadItemApprovalFlowActions from '../../../../components/DiscussionThreadItemApprovalFlowActions';
|
|
27
27
|
|
|
28
|
-
const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
28
|
+
const DiscoverTableRow = ({ config, item, allowedActions: { canModerate, canAccessReports, canReviewReports }, onClick }) => {
|
|
29
29
|
|
|
30
30
|
const { id, reports } = item;
|
|
31
31
|
|
|
@@ -74,14 +74,22 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
|
74
74
|
refetchActive: false,
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
const handleReportsReviewClick = () =>
|
|
77
|
+
const handleReportsReviewClick = () => {
|
|
78
|
+
if (canAccessReports) {
|
|
79
|
+
setReportsReviewVisible(true);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
78
82
|
const handleBlockItemClick = async () => {
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
if (canModerate) {
|
|
84
|
+
lockApp();
|
|
85
|
+
blockItemMutation.mutate(id);
|
|
86
|
+
}
|
|
81
87
|
};
|
|
82
88
|
const handleBlockItemThreadClick = async () => {
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
if (canModerate) {
|
|
90
|
+
lockApp();
|
|
91
|
+
blockItemThreadMutation.mutate(id);
|
|
92
|
+
}
|
|
85
93
|
};
|
|
86
94
|
const handleReportsReviewClose = async () => {
|
|
87
95
|
await queryClient.invalidateQueries('get-data');
|
|
@@ -91,7 +99,7 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
|
91
99
|
const isLoading = blockItemMutation.isLoading || blockItemThreadMutation.isLoading || resolveReportMutation.isLoading;
|
|
92
100
|
const openReports = reports?.filter(_ => !_.resolved);
|
|
93
101
|
const hasReports = !isEmpty(openReports);
|
|
94
|
-
const reviewFlowEnabled = hasReports && !(item.blocked || item.blockedThread);
|
|
102
|
+
const reviewFlowEnabled = (canAccessReports && hasReports) && !(item.blocked || item.blockedThread);
|
|
95
103
|
|
|
96
104
|
const handleClick = () => onClick();
|
|
97
105
|
const handleBlockButtonsStateChange = disabled => setBlockButtonsDisabled(disabled);
|
|
@@ -146,22 +154,28 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
|
146
154
|
<Flex direction="column" alignItems="flex-end">
|
|
147
155
|
<Stack size={actionItemsCount} horizontal>
|
|
148
156
|
{ reviewFlowEnabled && (<IconButton onClick={handleReportsReviewClick} label={ getMessage('page.discover.table.reports.review') } icon={<ReviewIcon />} />)}
|
|
149
|
-
{ needsApproval && (
|
|
157
|
+
{ (canModerate && needsApproval) && (
|
|
158
|
+
<DiscussionThreadItemApprovalFlowActions
|
|
159
|
+
id={item.id}
|
|
160
|
+
allowedActions={{ canModerate }}
|
|
161
|
+
queryToInvalidate="get-data" />
|
|
162
|
+
)}
|
|
150
163
|
<IconButton onClick={handleClick} label={ getMessage('page.discover.table.action.display') } icon={<Eye />} />
|
|
151
164
|
</Stack>
|
|
152
165
|
</Flex>
|
|
153
166
|
{ reviewFlowEnabled && (<ReportsReviewModal
|
|
154
167
|
isVisible={reportsReviewVisible}
|
|
155
168
|
isActionAsync={isLoading}
|
|
169
|
+
allowedActions={{ canModerate, canAccessReports, canReviewReports }}
|
|
156
170
|
onClose={handleReportsReviewClose}
|
|
157
|
-
startActions={<>
|
|
171
|
+
startActions={canModerate && (<>
|
|
158
172
|
<Button onClick={handleBlockItemClick} variant="danger-light" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
159
173
|
{ getMessage(`page.details.actions.comment.block`, 'Block comment') }
|
|
160
174
|
</Button>
|
|
161
175
|
{ item.gotThread && (<Button onClick={handleBlockItemThreadClick} variant="danger" startIcon={<LockIcon />} disabled={blockButtonsDisabled}>
|
|
162
176
|
{ getMessage(`page.details.actions.thread.block`, 'Block thread') }
|
|
163
177
|
</Button>) }
|
|
164
|
-
</>}
|
|
178
|
+
</>)}
|
|
165
179
|
item={item}>
|
|
166
180
|
<Typography variant="sigma" textColor="neutral600" id="reports-list">
|
|
167
181
|
{ getMessage('page.details.panel.discussion.warnings.reports.list', 'List') }
|
|
@@ -173,6 +187,7 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
|
173
187
|
commentId={item.id}
|
|
174
188
|
items={orderBy(reports, ['resolved', 'createdAt'], ['DESC', 'DESC'])}
|
|
175
189
|
mutation={resolveReportMutation}
|
|
190
|
+
allowedActions={{ canAccessReports, canReviewReports }}
|
|
176
191
|
onBlockButtonsStateChange={handleBlockButtonsStateChange}
|
|
177
192
|
/>
|
|
178
193
|
</ReportsReviewModal>) }
|
|
@@ -184,6 +199,11 @@ const DiscoverTableRow = ({ config, item, onClick }) => {
|
|
|
184
199
|
DiscoverTableRow.propTypes = {
|
|
185
200
|
config: PropTypes.object.isRequired,
|
|
186
201
|
item: PropTypes.object.isRequired,
|
|
202
|
+
allowedActions: PropTypes.shape({
|
|
203
|
+
canModerate: PropTypes.bool,
|
|
204
|
+
canAccessReports: PropTypes.bool,
|
|
205
|
+
canReviewReports: PropTypes.bool,
|
|
206
|
+
}),
|
|
187
207
|
onClick: PropTypes.func.isRequired,
|
|
188
208
|
};
|
|
189
209
|
|
|
@@ -51,17 +51,20 @@ const Discover = ({ config }) => {
|
|
|
51
51
|
const [{ query: queryParams }] = useQueryParams();
|
|
52
52
|
const _q = queryParams?._q || '';
|
|
53
53
|
|
|
54
|
-
const viewPermissions = useMemo(() => {
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const viewPermissions = useMemo(() => ({
|
|
55
|
+
access: pluginPermissions.access,
|
|
56
|
+
moderate: pluginPermissions.moderate,
|
|
57
|
+
accessReports: pluginPermissions.reports,
|
|
58
|
+
reviewReports: pluginPermissions.reportsReview,
|
|
59
|
+
}), []);
|
|
57
60
|
|
|
58
61
|
const {
|
|
59
62
|
isLoading: isLoadingForPermissions,
|
|
60
|
-
allowedActions: {
|
|
63
|
+
allowedActions: { canAccess, canModerate, canAccessReports, canReviewReports },
|
|
61
64
|
} = useRBAC(viewPermissions);
|
|
62
65
|
|
|
63
66
|
const { isLoading: isLoadingForData, data: { result, pagination = {} }, isFetching } = useQuery(
|
|
64
|
-
['get-data', queryParams],
|
|
67
|
+
['get-data', queryParams, canAccess],
|
|
65
68
|
() => fetchData(queryParams, toggleNotification),
|
|
66
69
|
{
|
|
67
70
|
initialData: { },
|
|
@@ -86,62 +89,69 @@ const Discover = ({ config }) => {
|
|
|
86
89
|
},
|
|
87
90
|
};
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
92
|
const emptyContent = _q ? 'search' : 'comments';
|
|
92
93
|
|
|
93
94
|
const COL_COUNT = 7;
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
96
|
+
if (canAccess) {
|
|
97
|
+
return <Box background="neutral100">
|
|
98
|
+
<Layout>
|
|
99
|
+
{isLoading || isLoadingForPermissions ? (<LoadingIndicatorPage />) : (
|
|
100
|
+
<>
|
|
101
|
+
<HeaderLayout title={ getMessage('page.discover.header') } subtitle={ `${total} ${ getMessage('page.discover.header.count')}` } as="h2" />
|
|
102
|
+
<ActionLayout
|
|
103
|
+
startActions={<>
|
|
104
|
+
<SearchURLQuery
|
|
105
|
+
label={ getMessage('search.label', 'Search', false) }
|
|
106
|
+
/>
|
|
107
|
+
<TableFilters displayedFilters={filtersSchema} />
|
|
108
|
+
</>} />
|
|
109
|
+
<ContentLayout>
|
|
110
|
+
{ !isEmpty(result) ? (<>
|
|
111
|
+
<Table colCount={COL_COUNT} rowCount={result.length}>
|
|
112
|
+
<Thead>
|
|
113
|
+
<Tr>
|
|
114
|
+
<Th>
|
|
115
|
+
<Typography variant="sigma">{ getMessage('page.discover.table.header.id') }</Typography>
|
|
116
|
+
</Th>
|
|
117
|
+
<Th>
|
|
118
|
+
<Typography variant="sigma">{ getMessage('page.discover.table.header.message') }</Typography>
|
|
119
|
+
</Th>
|
|
120
|
+
<Th>
|
|
121
|
+
<Typography variant="sigma">{ getMessage('page.discover.table.header.thread') }</Typography>
|
|
122
|
+
</Th>
|
|
123
|
+
<Th>
|
|
124
|
+
<Typography variant="sigma">{ getMessage('page.discover.table.header.entry') }</Typography>
|
|
125
|
+
</Th>
|
|
126
|
+
<Th>
|
|
127
|
+
<Typography variant="sigma">{ getMessage('page.discover.table.header.lastUpdate') }</Typography>
|
|
128
|
+
</Th>
|
|
129
|
+
<Th>
|
|
130
|
+
<Typography variant="sigma">{ getMessage('page.discover.table.header.status') }</Typography>
|
|
131
|
+
</Th>
|
|
132
|
+
<Th>
|
|
133
|
+
<VisuallyHidden>{ getMessage('page.discover.table.header.actions') }</VisuallyHidden>
|
|
134
|
+
</Th>
|
|
135
|
+
</Tr>
|
|
136
|
+
</Thead>
|
|
137
|
+
<Tbody>
|
|
138
|
+
{result.map(entry => (<DiscoverTableRow
|
|
139
|
+
key={`comment-${entry.id}`}
|
|
140
|
+
config={config}
|
|
141
|
+
item={entry}
|
|
142
|
+
allowedActions={{ canModerate, canAccessReports, canReviewReports }}
|
|
143
|
+
onClick={() => handleClickDisplay(entry.id)}
|
|
144
|
+
/>))}
|
|
145
|
+
</Tbody>
|
|
146
|
+
</Table>
|
|
147
|
+
<TablePagination pagination={{ pageCount: pagination?.pageCount || 1 }} />
|
|
148
|
+
</>) : (<EmptyStateLayout content={emptyLayout[emptyContent]} />) }
|
|
149
|
+
</ContentLayout>
|
|
150
|
+
</>) }
|
|
151
|
+
</Layout>
|
|
152
|
+
</Box>;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
const mapStateToProps = makeAppView();
|
package/admin/src/permissions.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
const permissions = require('./../../permissions');
|
|
2
|
+
|
|
1
3
|
const pluginPermissions = {
|
|
2
4
|
access: [
|
|
3
|
-
{ action:
|
|
4
|
-
{ action:
|
|
5
|
-
{ action: 'plugin::comments.
|
|
6
|
-
{ action:
|
|
5
|
+
{ action: permissions.render(permissions.comments.read), subject: null },
|
|
6
|
+
{ action: permissions.render(permissions.reports.read), subject: null },
|
|
7
|
+
// { action: 'plugin::comments.discover.read', subject: null },
|
|
8
|
+
{ action: permissions.render(permissions.settings.read), subject: null },
|
|
7
9
|
],
|
|
8
|
-
discover: [{ action: 'plugin::comments.read', subject: null }],
|
|
9
|
-
moderate: [{ action:
|
|
10
|
-
reports: [{ action:
|
|
11
|
-
reportsReview: [{ action:
|
|
10
|
+
// discover: [{ action: 'plugin::comments.discover.read', subject: null }],
|
|
11
|
+
moderate: [{ action: permissions.render(permissions.comments.moderate), subject: null }],
|
|
12
|
+
reports: [{ action: permissions.render(permissions.reports.read), subject: null }],
|
|
13
|
+
reportsReview: [{ action: permissions.render(permissions.reports.action), subject: null }],
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export default pluginPermissions;
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"page.details.actions.comment.approve.confirmation.success": "Comment has been approved",
|
|
77
77
|
"page.details.actions.comment.reject.confirmation.success": "Comment has been rejected",
|
|
78
78
|
"page.details.filters.label": "View",
|
|
79
|
+
"page.details.filters.removed.visibility": "Comments visibility",
|
|
79
80
|
"page.details.filters.removed.show": "Show removed",
|
|
80
81
|
"page.details.filters.removed.hide": "Hide removed",
|
|
81
82
|
"page.coming.soon": "Wait for what is coming soon...",
|
package/package.json
CHANGED
package/permissions.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
render: function(uid) {
|
|
5
|
+
return `plugin::comments.${uid}`;
|
|
6
|
+
},
|
|
7
|
+
comments: {
|
|
8
|
+
read: 'read',
|
|
9
|
+
moderate: 'moderate',
|
|
10
|
+
},
|
|
11
|
+
reports: {
|
|
12
|
+
read: 'reports.read',
|
|
13
|
+
action: 'reports.action',
|
|
14
|
+
},
|
|
15
|
+
settings: {
|
|
16
|
+
read: 'settings.read',
|
|
17
|
+
change: 'settings.change',
|
|
18
|
+
},
|
|
19
|
+
};
|
package/server/bootstrap.js
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const permissions = require('./../permissions');
|
|
4
|
+
|
|
5
|
+
module.exports = async ({ strapi }) => {
|
|
6
|
+
// Check if the plugin users-permissions is installed because the navigation needs it
|
|
7
|
+
if (Object.keys(strapi.plugins).indexOf("users-permissions") === -1) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"In order to make the comments plugin work the users-permissions plugin is required",
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
// Add permissions
|
|
13
|
+
const actions = [
|
|
14
|
+
{
|
|
15
|
+
section: "plugins",
|
|
16
|
+
displayName: "Comments: Read",
|
|
17
|
+
uid: permissions.comments.read,
|
|
18
|
+
pluginName: "comments",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
section: "plugins",
|
|
22
|
+
displayName: "Comments: Moderate",
|
|
23
|
+
uid: permissions.comments.moderate,
|
|
24
|
+
pluginName: "comments",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
section: "plugins",
|
|
28
|
+
displayName: "Reports: Read",
|
|
29
|
+
uid: permissions.reports.read,
|
|
30
|
+
pluginName: "comments",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
section: "plugins",
|
|
34
|
+
displayName: "Reports: Moderate",
|
|
35
|
+
uid: permissions.reports.action,
|
|
36
|
+
pluginName: "comments",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
await strapi.admin.services.permission.actionProvider.registerMany(actions);
|
|
5
41
|
};
|
package/server/config/index.js
CHANGED
package/server/services/admin.js
CHANGED
|
@@ -88,12 +88,13 @@ const { APPROVAL_STATUS, REGEX } = require('./../utils/constants')
|
|
|
88
88
|
.map(_ => filterOurResolvedReports(this.getCommonService().sanitizeCommentEntity(_)))
|
|
89
89
|
.map(_ => this.getCommonService().mergeRelatedEntityTo(_, relatedEntities));
|
|
90
90
|
|
|
91
|
+
const pageCount = Math.floor(total / pageSize);
|
|
91
92
|
return {
|
|
92
93
|
result,
|
|
93
94
|
pagination: {
|
|
94
95
|
page: page,
|
|
95
96
|
pageSize: pageSize,
|
|
96
|
-
pageCount:
|
|
97
|
+
pageCount: total % pageSize === 0 ? pageCount : pageCount + 1,
|
|
97
98
|
total,
|
|
98
99
|
},
|
|
99
100
|
};
|
package/strapi-server.js
CHANGED
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { isArray, isEmpty, isString } = require('lodash');
|
|
4
|
+
|
|
3
5
|
const server = require('./server');
|
|
4
6
|
const contentTypes = require('./content-types');
|
|
7
|
+
const { REGEX } = require('./server/utils/constants');
|
|
5
8
|
|
|
6
9
|
module.exports = () => ({
|
|
7
10
|
...server,
|
|
8
11
|
contentTypes,
|
|
12
|
+
config: {
|
|
13
|
+
...server.config,
|
|
14
|
+
default: ({ env }) => ({
|
|
15
|
+
...server.config.default,
|
|
16
|
+
}),
|
|
17
|
+
validator: (config) => {
|
|
18
|
+
// Check moderatorRoles values
|
|
19
|
+
const moderatorRolesValid = config.moderatorRoles
|
|
20
|
+
.filter(_ => !isString(_)).length === 0;
|
|
21
|
+
if (!moderatorRolesValid) {
|
|
22
|
+
throw new Error(`'moderatorRoles' must roles keys`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check approvalFlow values
|
|
26
|
+
const approvalFlowValid = config.approvalFlow
|
|
27
|
+
.filter(_ => !REGEX.uid.test(_)).length === 0;
|
|
28
|
+
if (!approvalFlowValid) {
|
|
29
|
+
throw new Error(`'approvalFlow' must contain Content Types identifiers in the format like 'api::<collection name>.<content type name>'`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check entryLabel keys and values
|
|
33
|
+
const entryLabelKeysValid = Object.keys(config.entryLabel)
|
|
34
|
+
.filter(_ => _ !== '*')
|
|
35
|
+
.filter(_ => !(REGEX.uid.test(_) && isArray(config.entryLabel[_]) && !isEmpty(config.entryLabel[_]))).length === 0;
|
|
36
|
+
|
|
37
|
+
if (!entryLabelKeysValid) {
|
|
38
|
+
throw new Error(`'entryLabel' must contain records in the format like 'api::<collection name>.<content type name>': ['Field1', 'field_2']`);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
},
|
|
9
42
|
});
|