strapi-plugin-comments 2.0.0-beta.5 → 2.0.0-rc.2
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 +472 -118
- package/admin/src/components/DiscussionThread/index.js +9 -3
- package/admin/src/components/DiscussionThreadItem/index.js +24 -11
- package/admin/src/components/DiscussionThreadItemActions/index.js +45 -18
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +20 -11
- package/admin/src/components/DiscussionThreadItemMeta/index.js +16 -5
- 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 +38 -11
- package/admin/src/pages/Discover/index.js +72 -59
- package/admin/src/permissions.js +10 -8
- package/admin/src/translations/en.json +2 -0
- package/admin/src/utils/index.js +2 -0
- package/admin/src/utils/renderInitials.js +5 -0
- package/content-types/comment.js +0 -4
- package/package.json +4 -2
- package/permissions.js +19 -0
- package/public/assets/logo.png +0 -0
- package/public/assets/preview.png +0 -0
- package/server/bootstrap.js +38 -2
- package/server/config/index.js +7 -2
- package/server/controllers/client.js +11 -21
- package/server/controllers/utils/parsers.js +19 -0
- package/server/graphql/index.js +26 -0
- package/server/graphql/mutations/createAbuseReport.js +25 -0
- package/server/graphql/mutations/createComment.js +25 -0
- package/server/graphql/mutations/index.js +21 -0
- package/server/graphql/mutations/removeComment.js +25 -0
- package/server/graphql/mutations/updateComment.js +25 -0
- package/server/graphql/queries/findAllFlat.js +29 -0
- package/server/graphql/queries/findAllInHierarchy.js +31 -0
- package/server/graphql/queries/index.js +19 -0
- package/server/graphql/resolvers-config.js +15 -0
- package/server/graphql/types/comment-author.js +9 -0
- package/server/graphql/types/comment-nested.js +15 -0
- package/server/graphql/types/comment.js +15 -0
- package/server/graphql/types/create-comment-author.js +9 -0
- package/server/graphql/types/create-comment.js +9 -0
- package/server/graphql/types/create-report.js +9 -0
- package/server/graphql/types/id.js +24 -0
- package/server/graphql/types/identify-comment-author.js +6 -0
- package/server/graphql/types/index.js +16 -0
- package/server/graphql/types/remove-comment.js +8 -0
- package/server/graphql/types/report-reason.js +10 -0
- package/server/graphql/types/report.js +11 -0
- package/server/graphql/types/update-comment.js +9 -0
- package/server/register.js +3 -1
- package/server/services/admin.js +20 -16
- package/server/services/client.js +72 -49
- package/server/services/common.js +23 -12
- package/server/services/graphql.js +190 -0
- package/server/services/index.js +2 -0
- package/server/services/utils/functions.js +30 -11
- package/strapi-server.js +33 -0
|
@@ -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
|
-
const isThreadAuthor = !isNil(selected?.threadOf?.
|
|
40
|
+
const isThreadAuthor = !isNil(selected?.threadOf?.author?.id) && (selected?.threadOf?.author?.id === item?.author?.id);
|
|
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
|
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
import PropTypes from 'prop-types';
|
|
9
|
-
import { first } from 'lodash';
|
|
10
9
|
import { Avatar, Initials } from '@strapi/design-system/Avatar';
|
|
11
10
|
import { Box } from '@strapi/design-system/Box';
|
|
12
11
|
import { Divider } from '@strapi/design-system/Divider';
|
|
@@ -17,11 +16,23 @@ import { ArrowRight } from '@strapi/icons';
|
|
|
17
16
|
import DiscussionThreadItemMeta from '../DiscussionThreadItemMeta';
|
|
18
17
|
import { AvatarWrapper, DiscussionThreadItemBox, DiscussionThreadItemBoxContent, DiscussionThreadItemContent, DiscussionThreadItemLeft, DiscussionThreadItemLinkBox, DiscussionThreadItemRight, DiscussionThreadItemRoot } from './styles';
|
|
19
18
|
import DiscussionThreadItemActions from '../DiscussionThreadItemActions';
|
|
20
|
-
import { getMessage, getUrl } from '../../utils';
|
|
19
|
+
import { getMessage, getUrl, renderInitials } from '../../utils';
|
|
21
20
|
import DiscussionThreadItemWarnings from '../DiscussionThreadItemWarnings';
|
|
22
21
|
|
|
23
|
-
const DiscussionThreadItem = (
|
|
24
|
-
const {
|
|
22
|
+
const DiscussionThreadItem = (props) => {
|
|
23
|
+
const {
|
|
24
|
+
isSelected,
|
|
25
|
+
isThreadAuthor,
|
|
26
|
+
content,
|
|
27
|
+
root,
|
|
28
|
+
preview,
|
|
29
|
+
pinned,
|
|
30
|
+
blocked,
|
|
31
|
+
removed,
|
|
32
|
+
blockedThread,
|
|
33
|
+
gotThread,
|
|
34
|
+
threadFirstItemId,
|
|
35
|
+
author = {} } = props;
|
|
25
36
|
|
|
26
37
|
let background = 'neutral100';
|
|
27
38
|
let borderColor = 'neutral200';
|
|
@@ -45,11 +56,8 @@ const DiscussionThreadItem = (item) => {
|
|
|
45
56
|
StyledComponent = DiscussionThreadItemRoot;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
const avatar =
|
|
49
|
-
const name = authorName || `${authorUser?.firstName} ${authorUser?.lastName}`;
|
|
59
|
+
const { name, avatar } = author;
|
|
50
60
|
const isBlocked = blocked || blockedThread;
|
|
51
|
-
|
|
52
|
-
const renderInitials = value => value.split(' ').map(_ => first(_)).join('').toUpperCase();
|
|
53
61
|
|
|
54
62
|
return (<StyledComponent as="li">
|
|
55
63
|
<DiscussionThreadItemContent as={Flex} direction="row">
|
|
@@ -62,12 +70,12 @@ const DiscussionThreadItem = (item) => {
|
|
|
62
70
|
<Box as={Flex} grow={1} alignItems="center">
|
|
63
71
|
<Typography variant="omega">{ content }</Typography>
|
|
64
72
|
</Box>
|
|
65
|
-
{ !preview && (<DiscussionThreadItemActions { ...
|
|
73
|
+
{ !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
|
|
66
74
|
</DiscussionThreadItemBoxContent>
|
|
67
|
-
{ !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...
|
|
75
|
+
{ !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
|
|
68
76
|
</Flex>
|
|
69
77
|
<Divider background={dividerColor} />
|
|
70
|
-
<DiscussionThreadItemMeta { ...
|
|
78
|
+
<DiscussionThreadItemMeta { ...props } />
|
|
71
79
|
</DiscussionThreadItemBox>
|
|
72
80
|
</DiscussionThreadItemContent>
|
|
73
81
|
|
|
@@ -81,6 +89,11 @@ const DiscussionThreadItem = (item) => {
|
|
|
81
89
|
|
|
82
90
|
DiscussionThreadItem.propTypes = {
|
|
83
91
|
content: PropTypes.string.isRequired,
|
|
92
|
+
allowedActions: PropTypes.shape({
|
|
93
|
+
canModerate: PropTypes.bool,
|
|
94
|
+
canAccessReports: PropTypes.bool,
|
|
95
|
+
canReviewReports: PropTypes.bool,
|
|
96
|
+
}),
|
|
84
97
|
};
|
|
85
98
|
|
|
86
99
|
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
|
|
|
@@ -9,22 +9,33 @@ import PropTypes from 'prop-types';
|
|
|
9
9
|
import { useIntl } from 'react-intl';
|
|
10
10
|
import { Box } from '@strapi/design-system/Box';
|
|
11
11
|
import { Flex } from '@strapi/design-system/Flex';
|
|
12
|
+
import { Stack } from '@strapi/design-system/Stack';
|
|
12
13
|
import { Typography } from '@strapi/design-system/Typography';
|
|
13
14
|
|
|
14
|
-
const DiscussionThreadItemMeta = ({
|
|
15
|
+
const DiscussionThreadItemMeta = ({ author, createdAt, updatedAt }) => {
|
|
15
16
|
|
|
16
17
|
const { formatDate } = useIntl();
|
|
17
18
|
|
|
18
19
|
const dateTime = formatDate(updatedAt || createdAt, { dateStyle: 'short', timeStyle: 'short' });
|
|
20
|
+
const { name } = author;
|
|
19
21
|
|
|
20
|
-
return (<Flex as={Box} paddingTop={2} direction="row"
|
|
21
|
-
<
|
|
22
|
-
|
|
22
|
+
return (<Flex as={Box} paddingTop={2} direction="row">
|
|
23
|
+
<Stack size={2} horizontal style={{ flexGrow: 1, justifyContent: 'space-between'}}>
|
|
24
|
+
<Typography variant="pi" fontWeight="bold">{ name }</Typography>
|
|
25
|
+
<Typography variant="pi">{ dateTime }</Typography>
|
|
26
|
+
</Stack>
|
|
23
27
|
</Flex>);
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
DiscussionThreadItemMeta.propTypes = {
|
|
27
|
-
|
|
31
|
+
author: PropTypes.shape({
|
|
32
|
+
id: PropTypes.oneOfType(PropTypes.string, PropTypes.number).isRequired,
|
|
33
|
+
name: PropTypes.string.isRequired,
|
|
34
|
+
email: PropTypes.string,
|
|
35
|
+
avatar: PropTypes.string,
|
|
36
|
+
}).isRequired,
|
|
37
|
+
createdAt: PropTypes.string.isRequired,
|
|
38
|
+
updatedAt: PropTypes.string,
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
export default DiscussionThreadItemMeta;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
8
9
|
import { useMutation, useQueryClient } from 'react-query';
|
|
9
10
|
import { isEmpty, orderBy } from 'lodash';
|
|
10
11
|
import { Badge } from '@strapi/design-system/Badge';
|
|
@@ -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)}
|