strapi-plugin-comments 2.0.0-beta.4 → 2.0.0-rc.1
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 +421 -72
- package/admin/src/components/DiscussionThread/index.js +9 -2
- package/admin/src/components/DiscussionThreadItem/index.js +27 -12
- package/admin/src/components/DiscussionThreadItemActions/index.js +61 -50
- package/admin/src/components/DiscussionThreadItemActions/styles.js +0 -4
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/index.js +81 -0
- package/admin/src/components/DiscussionThreadItemApprovalFlowActions/styles.js +6 -0
- package/admin/src/components/DiscussionThreadItemMeta/index.js +16 -5
- package/admin/src/components/DiscussionThreadItemWarnings/index.js +16 -10
- package/admin/src/components/Nav/index.js +5 -1
- 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 +2 -2
- package/admin/src/index.js +1 -1
- package/admin/src/pages/Details/components/DetailsEntity/index.js +66 -0
- package/admin/src/pages/Details/components/DetailsFilters/index.js +37 -0
- package/admin/src/pages/Details/components/DetailsFilters/styles.js +8 -0
- package/admin/src/pages/Details/index.js +75 -42
- package/admin/src/pages/Discover/components/DiscoverTableRow/index.js +69 -48
- package/admin/src/pages/Discover/components/DiscoverTableRow/styles.js +8 -0
- package/admin/src/pages/Discover/index.js +72 -56
- package/admin/src/pages/{Details/utils → utils}/api.js +15 -3
- package/admin/src/permissions.js +10 -8
- package/admin/src/translations/en.json +17 -4
- package/admin/src/utils/constants.js +3 -6
- package/admin/src/utils/getMessage.js +1 -1
- package/admin/src/utils/getRelatedGroups.js +1 -3
- package/admin/src/utils/index.js +5 -1
- package/admin/src/utils/renderEntryTitle.js +14 -0
- package/admin/src/utils/renderInitials.js +5 -0
- package/admin/src/utils/resolveCommentStatus.js +30 -0
- package/admin/src/utils/resolveCommentStatusColor.js +22 -0
- package/package.json +8 -3
- 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 +9 -3
- package/server/controllers/admin.js +3 -3
- package/server/controllers/client.js +4 -18
- package/server/controllers/utils/parsers.js +17 -0
- package/server/graphql/index.js +26 -0
- package/server/graphql/mutations/createAbuseReport.js +20 -0
- package/server/graphql/mutations/createComment.js +20 -0
- package/server/graphql/mutations/index.js +21 -0
- package/server/graphql/mutations/removeComment.js +20 -0
- package/server/graphql/mutations/updateComment.js +20 -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 +10 -0
- package/server/graphql/types/comment-author-user.js +9 -0
- package/server/graphql/types/comment-author.js +9 -0
- package/server/graphql/types/comment-nested.js +16 -0
- package/server/graphql/types/comment.js +16 -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 +17 -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 +42 -15
- package/server/services/client.js +54 -38
- package/server/services/common.js +38 -67
- package/server/services/graphql.js +190 -0
- package/server/services/index.js +2 -0
- package/server/services/utils/functions.js +24 -17
- package/server/utils/constants.js +3 -3
- package/strapi-server.js +33 -0
- package/admin/src/components/EntityDetails/index.js +0 -53
|
@@ -18,7 +18,7 @@ import { getMessage, getUrl } from '../../utils';
|
|
|
18
18
|
import LoadingIndicatorOverlay from '../LoadingIndicatorOverlay';
|
|
19
19
|
import { LoadingIndicatorContainer } from '../LoadingIndicatorOverlay/styles';
|
|
20
20
|
|
|
21
|
-
const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
|
|
21
|
+
const DiscussionThread = ({ level = [], selected = {}, allowedActions, isReloading }) => {
|
|
22
22
|
const rootThread = selected?.threadOf;
|
|
23
23
|
return (<LoadingIndicatorContainer as={Box} padding={4}>
|
|
24
24
|
{ isReloading && <LoadingIndicatorOverlay />}
|
|
@@ -34,12 +34,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
|
+
key={`comment-${item.id}`}
|
|
42
43
|
{...item}
|
|
44
|
+
allowedActions={allowedActions}
|
|
43
45
|
root={isNil(rootThread)}
|
|
44
46
|
blockedThread={rootThread?.blockedThread || item.blockedThread}
|
|
45
47
|
isSelected={isSelected}
|
|
@@ -53,6 +55,11 @@ const DiscussionThread = ({ level = [], selected = {}, isReloading }) => {
|
|
|
53
55
|
DiscussionThread.propTypes = {
|
|
54
56
|
level: PropTypes.array.isRequired,
|
|
55
57
|
selected: PropTypes.object.isRequired,
|
|
58
|
+
allowedActions: PropTypes.shape({
|
|
59
|
+
canModerate: PropTypes.bool,
|
|
60
|
+
canAccessReports: PropTypes.bool,
|
|
61
|
+
canReviewReports: PropTypes.bool,
|
|
62
|
+
}),
|
|
56
63
|
isReloading: PropTypes.bool,
|
|
57
64
|
};
|
|
58
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,24 @@ 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
|
+
authorUser,
|
|
36
|
+
author } = props;
|
|
25
37
|
|
|
26
38
|
let background = 'neutral100';
|
|
27
39
|
let borderColor = 'neutral200';
|
|
@@ -45,11 +57,9 @@ const DiscussionThreadItem = (item) => {
|
|
|
45
57
|
StyledComponent = DiscussionThreadItemRoot;
|
|
46
58
|
}
|
|
47
59
|
|
|
48
|
-
const avatar =
|
|
49
|
-
const name =
|
|
60
|
+
const avatar = author?.avatar || authorUser?.avatar;
|
|
61
|
+
const name = author?.name || `${authorUser?.firstName} ${authorUser?.lastName}`;
|
|
50
62
|
const isBlocked = blocked || blockedThread;
|
|
51
|
-
|
|
52
|
-
const renderInitials = value => value.split(' ').map(_ => first(_)).join('').toUpperCase();
|
|
53
63
|
|
|
54
64
|
return (<StyledComponent as="li">
|
|
55
65
|
<DiscussionThreadItemContent as={Flex} direction="row">
|
|
@@ -62,16 +72,16 @@ const DiscussionThreadItem = (item) => {
|
|
|
62
72
|
<Box as={Flex} grow={1} alignItems="center">
|
|
63
73
|
<Typography variant="omega">{ content }</Typography>
|
|
64
74
|
</Box>
|
|
65
|
-
{ !preview && (<DiscussionThreadItemActions { ...
|
|
75
|
+
{ !preview && (<DiscussionThreadItemActions { ...props } root={root || pinned} />) }
|
|
66
76
|
</DiscussionThreadItemBoxContent>
|
|
67
|
-
{ !(preview || isBlocked) && (<DiscussionThreadItemWarnings { ...
|
|
77
|
+
{ !(preview || isBlocked || removed) && (<DiscussionThreadItemWarnings { ...props } gotThread={gotThread || root} />) }
|
|
68
78
|
</Flex>
|
|
69
79
|
<Divider background={dividerColor} />
|
|
70
|
-
<DiscussionThreadItemMeta { ...
|
|
80
|
+
<DiscussionThreadItemMeta { ...props } />
|
|
71
81
|
</DiscussionThreadItemBox>
|
|
72
82
|
</DiscussionThreadItemContent>
|
|
73
83
|
|
|
74
|
-
{ (gotThread && !(preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
|
|
84
|
+
{ (gotThread && !(removed || preview || pinned)) && (<DiscussionThreadItemLinkBox direction="column" alignItems="flex-end">
|
|
75
85
|
<Link to={getUrl(`discover/${threadFirstItemId}`)} endIcon={<ArrowRight />}>
|
|
76
86
|
{ getMessage('page.details.panel.discussion.nav.drilldown') }
|
|
77
87
|
</Link>
|
|
@@ -81,6 +91,11 @@ const DiscussionThreadItem = (item) => {
|
|
|
81
91
|
|
|
82
92
|
DiscussionThreadItem.propTypes = {
|
|
83
93
|
content: PropTypes.string.isRequired,
|
|
94
|
+
allowedActions: PropTypes.shape({
|
|
95
|
+
canModerate: PropTypes.bool,
|
|
96
|
+
canAccessReports: PropTypes.bool,
|
|
97
|
+
canReviewReports: PropTypes.bool,
|
|
98
|
+
}),
|
|
84
99
|
};
|
|
85
100
|
|
|
86
101
|
export default DiscussionThreadItem;
|
|
@@ -4,23 +4,23 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useState
|
|
7
|
+
import React, { useState } from 'react';
|
|
8
8
|
import PropTypes from 'prop-types';
|
|
9
9
|
import { useMutation, useQueryClient } from 'react-query';
|
|
10
|
-
import { isNil } from 'lodash';
|
|
10
|
+
import { isNil, isEmpty } from 'lodash';
|
|
11
11
|
import { Button } from '@strapi/design-system/Button';
|
|
12
12
|
import { Flex } from '@strapi/design-system/Flex';
|
|
13
13
|
import { IconButton } from '@strapi/design-system/IconButton';
|
|
14
|
-
import { Check, Cross } from '@strapi/icons';
|
|
15
14
|
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
16
|
-
import { getMessage, handleAPIError } from '../../utils';
|
|
17
|
-
import { DiscussionThreadItemActionsBadge,
|
|
15
|
+
import { getMessage, handleAPIError, resolveCommentStatus, resolveCommentStatusColor } from '../../utils';
|
|
16
|
+
import { DiscussionThreadItemActionsBadge, DiscussionThreadItemActionsWrapper } from './styles';
|
|
18
17
|
import ConfirmationDialog from '../ConfirmationDialog';
|
|
19
|
-
import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/
|
|
18
|
+
import { blockItem, blockItemThread, unblockItem, unblockItemThread } from '../../pages/utils/api';
|
|
20
19
|
import pluginId from '../../pluginId';
|
|
21
20
|
import { LockIcon, UnlockIcon } from '../icons';
|
|
21
|
+
import DiscussionThreadItemApprovalFlowActions from '../DiscussionThreadItemApprovalFlowActions';
|
|
22
22
|
|
|
23
|
-
const DiscussionThreadItemActions = ({ id, blocked, 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);
|
|
@@ -71,75 +71,86 @@ const DiscussionThreadItemActions = ({ id, blocked, blockedThread, gotThread, pi
|
|
|
71
71
|
const gotApprovalFlow = !isNil(approvalStatus);
|
|
72
72
|
const needsApproval = gotApprovalFlow && (approvalStatus === 'PENDING');
|
|
73
73
|
const isBlocked = blocked || blockedThread;
|
|
74
|
+
const isRejected = gotApprovalFlow && (approvalStatus === 'REJECTED');
|
|
75
|
+
const openReports = reports?.filter(_ => !_.resolved);
|
|
76
|
+
const hasReports = !isEmpty(openReports);
|
|
77
|
+
const reviewFlowEnabled = (canAccessReports || canReviewReports) && hasReports;
|
|
74
78
|
|
|
75
79
|
const resolveStatusBadge = () => {
|
|
76
|
-
|
|
77
|
-
let label = '';
|
|
78
|
-
|
|
79
|
-
if (isBlocked) {
|
|
80
|
-
color = 'danger';
|
|
81
|
-
label = 'blocked';
|
|
82
|
-
} else if (gotApprovalFlow) {
|
|
83
|
-
label = approvalStatus.toLowerCase();
|
|
84
|
-
switch (approvalStatus) {
|
|
85
|
-
case 'APPROVED':
|
|
86
|
-
color = 'success';
|
|
87
|
-
break;
|
|
88
|
-
case 'REJECTED':
|
|
89
|
-
color = 'danger';
|
|
90
|
-
break;
|
|
91
|
-
default:
|
|
92
|
-
color = 'alternative';
|
|
93
|
-
break;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
80
|
+
const status = resolveCommentStatus({ removed, blocked, blockedThread, approvalStatus, reviewFlowEnabled: !isBlocked && reviewFlowEnabled });
|
|
97
81
|
return {
|
|
98
|
-
badgeVisible: isBlocked || gotApprovalFlow,
|
|
99
|
-
badgeColor:
|
|
100
|
-
badgeLabel:
|
|
101
|
-
}
|
|
102
|
-
}
|
|
82
|
+
badgeVisible: isBlocked || gotApprovalFlow || removed,
|
|
83
|
+
badgeColor: resolveCommentStatusColor(status),
|
|
84
|
+
badgeLabel: status,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
103
87
|
|
|
104
88
|
const {badgeVisible, badgeColor, badgeLabel } = resolveStatusBadge();
|
|
105
89
|
|
|
106
90
|
const handleBlockClick = () => setBlockConfirmationVisible(true);
|
|
107
91
|
const handleBlockConfirm = async () => {
|
|
108
|
-
|
|
109
|
-
|
|
92
|
+
if (canModerate) {
|
|
93
|
+
lockApp();
|
|
94
|
+
blockItemMutation.mutate(id);
|
|
95
|
+
}
|
|
110
96
|
};
|
|
111
97
|
const handleBlockCancel = () => {
|
|
112
98
|
setBlockConfirmationVisible(false);
|
|
113
99
|
};
|
|
114
100
|
const handleUnblockClick = async () => {
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
if (canModerate) {
|
|
102
|
+
lockApp();
|
|
103
|
+
unblockItemMutation.mutate(id);
|
|
104
|
+
}
|
|
117
105
|
};
|
|
118
106
|
|
|
119
107
|
const handleBlockThreadClick = () => setBlockThreadConfirmationVisible(true);
|
|
120
108
|
const handleBlockThreadConfirm = async () => {
|
|
121
|
-
|
|
122
|
-
|
|
109
|
+
if (canModerate) {
|
|
110
|
+
lockApp();
|
|
111
|
+
blockItemThreadMutation.mutate(id);
|
|
112
|
+
}
|
|
123
113
|
};
|
|
124
114
|
const handleBlockThreadCancel = () => {
|
|
125
115
|
setBlockThreadConfirmationVisible(false);
|
|
126
116
|
};
|
|
127
117
|
const handleUnblockThreadClick = async () => {
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
if (canModerate) {
|
|
119
|
+
lockApp();
|
|
120
|
+
unblockItemThreadMutation.mutate(id);
|
|
121
|
+
}
|
|
130
122
|
};
|
|
131
123
|
|
|
124
|
+
if (removed || isRejected || !canModerate) {
|
|
125
|
+
return (<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
126
|
+
<DiscussionThreadItemActionsBadge
|
|
127
|
+
color={badgeColor}
|
|
128
|
+
backgroundColor={`${badgeColor}100`}
|
|
129
|
+
textColor={`${badgeColor}600`}>
|
|
130
|
+
{ getMessage({
|
|
131
|
+
id: `page.common.item.status.${badgeLabel}`,
|
|
132
|
+
props: { count: openReports.length },
|
|
133
|
+
}, badgeLabel)}
|
|
134
|
+
</DiscussionThreadItemActionsBadge>
|
|
135
|
+
</DiscussionThreadItemActionsWrapper>);
|
|
136
|
+
}
|
|
137
|
+
|
|
132
138
|
return (<>
|
|
133
139
|
<DiscussionThreadItemActionsWrapper as={Flex} direction="row">
|
|
134
|
-
{ 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>)}
|
|
135
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' } : {}} />)}
|
|
136
|
-
{ !blockedThread && blocked && (<IconButton onClick={handleUnblockClick} loading={unblockItemMutation.isLoading} icon={<UnlockIcon />} label={getMessage('page.details.actions.comment.unblock', 'Unblock')} />)}
|
|
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' } : {}} />)}
|
|
137
151
|
{ (!blockedThread && (gotThread || pinned)) && (<Button onClick={handleBlockThreadClick} startIcon={<LockIcon />} loading={blockItemThreadMutation.isLoading} variant="danger">{ getMessage('page.details.actions.thread.block', 'Block thread') }</Button>) }
|
|
138
152
|
{ (blockedThread && (gotThread || pinned)) && (<Button onClick={handleUnblockThreadClick} startIcon={<UnlockIcon />} loading={unblockItemThreadMutation.isLoading} variant="success">{ getMessage('page.details.actions.thread.unblock', 'Unblock thread') }</Button>) }
|
|
139
|
-
{ needsApproval && (<
|
|
140
|
-
<IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} />
|
|
141
|
-
<IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} />
|
|
142
|
-
</DiscussionThreadItemActionsGroupWrapper>) }
|
|
153
|
+
{ needsApproval && (<DiscussionThreadItemApprovalFlowActions id={id} allowedActions={{ canModerate }} queryToInvalidate="get-details-data" />) }
|
|
143
154
|
</DiscussionThreadItemActionsWrapper>
|
|
144
155
|
{ !blocked && (<ConfirmationDialog
|
|
145
156
|
isVisible={blockConfirmationVisible}
|
|
@@ -169,9 +180,9 @@ DiscussionThreadItemActions.propTypes = {
|
|
|
169
180
|
blocked: PropTypes.bool.isRequired,
|
|
170
181
|
blockedThread: PropTypes.bool.isRequired,
|
|
171
182
|
approvalStatus: PropTypes.oneOfType([PropTypes.nullable, PropTypes.oneOf(['PENDING', 'APPROVED', 'REJECTED'])]),
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
183
|
+
allowedActions: PropTypes.shape({
|
|
184
|
+
canModerate: PropTypes.bool,
|
|
185
|
+
}),
|
|
175
186
|
};
|
|
176
187
|
|
|
177
188
|
export default DiscussionThreadItemActions;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Approval Flow Actions
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
import { useMutation, useQueryClient } from 'react-query';
|
|
10
|
+
import { IconButton } from '@strapi/design-system/IconButton';
|
|
11
|
+
import { Check, Cross } from '@strapi/icons';
|
|
12
|
+
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
13
|
+
import { getMessage, handleAPIError } from '../../utils';
|
|
14
|
+
import { DiscussionThreadItemActionsGroupWrapper } from './styles';
|
|
15
|
+
import { approveItem, rejectItem } from '../../pages/utils/api';
|
|
16
|
+
import pluginId from '../../pluginId';
|
|
17
|
+
|
|
18
|
+
const DiscussionThreadItemApprovalFlowActions = ({ id, allowedActions: { canModerate }, queryToInvalidate }) => {
|
|
19
|
+
|
|
20
|
+
const toggleNotification = useNotification();
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
const { lockApp, unlockApp } = useOverlayBlocker();
|
|
23
|
+
|
|
24
|
+
const onSuccess = (message) => async () => {
|
|
25
|
+
if (queryToInvalidate) {
|
|
26
|
+
await queryClient.invalidateQueries(queryToInvalidate);
|
|
27
|
+
}
|
|
28
|
+
toggleNotification({
|
|
29
|
+
type: 'success',
|
|
30
|
+
message: `${pluginId}.${message}`,
|
|
31
|
+
});
|
|
32
|
+
unlockApp();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const onError = err => { handleAPIError(err, toggleNotification); };
|
|
36
|
+
|
|
37
|
+
const approveItemMutation = useMutation(approveItem, {
|
|
38
|
+
onSuccess: onSuccess('page.details.actions.comment.approve.confirmation.success'),
|
|
39
|
+
onError,
|
|
40
|
+
refetchActive: false,
|
|
41
|
+
});
|
|
42
|
+
const rejectItemMutation = useMutation(rejectItem, {
|
|
43
|
+
onSuccess: onSuccess('page.details.actions.comment.reject.confirmation.success'),
|
|
44
|
+
onError,
|
|
45
|
+
refetchActive: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const handleApproveClick = () => {
|
|
49
|
+
if (canModerate) {
|
|
50
|
+
lockApp();
|
|
51
|
+
approveItemMutation.mutate(id);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleRejectClick = () => {
|
|
56
|
+
if (canModerate) {
|
|
57
|
+
lockApp();
|
|
58
|
+
rejectItemMutation.mutate(id);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (canModerate) {
|
|
63
|
+
return (<DiscussionThreadItemActionsGroupWrapper>
|
|
64
|
+
<IconButton icon={<Check />} label={getMessage('page.details.actions.comment.approve', 'Approve')} onClick={handleApproveClick} />
|
|
65
|
+
<IconButton icon={<Cross />} label={getMessage('page.details.actions.comment.reject', 'Reject')} onClick={handleRejectClick} />
|
|
66
|
+
</DiscussionThreadItemActionsGroupWrapper>);
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
DiscussionThreadItemApprovalFlowActions.propTypes = {
|
|
72
|
+
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
73
|
+
queryToInvalidate: PropTypes.string,
|
|
74
|
+
allowedActions: PropTypes.shape({
|
|
75
|
+
canModerate: PropTypes.bool,
|
|
76
|
+
}),
|
|
77
|
+
wrapped: PropTypes.bool,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default DiscussionThreadItemApprovalFlowActions;
|
|
81
|
+
|
|
@@ -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';
|
|
@@ -15,7 +16,7 @@ import { Flex } from '@strapi/design-system/Flex';
|
|
|
15
16
|
import { Typography } from '@strapi/design-system/Typography';
|
|
16
17
|
import { useNotification, useOverlayBlocker } from '@strapi/helper-plugin';
|
|
17
18
|
import { getMessage, handleAPIError } from '../../utils';
|
|
18
|
-
import { blockItem, blockItemThread, resolveReport } from '../../pages/
|
|
19
|
+
import { blockItem, blockItemThread, resolveReport } from '../../pages/utils/api';
|
|
19
20
|
import { DiscussionThreadItemWarningsWrapper } from './styles';
|
|
20
21
|
import ReportsReviewModal from '../ReportsReviewModal';
|
|
21
22
|
import ReportsReviewTable from '../ReportsReviewTable';
|
|
@@ -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;
|
|
@@ -18,7 +18,11 @@ import getMessage from '../../utils/getMessage';
|
|
|
18
18
|
// import PropTypes from 'prop-types';
|
|
19
19
|
import getUrl from '../../utils/getUrl';
|
|
20
20
|
|
|
21
|
-
const Nav = ({ hasNewComments, hasNewReports }) => {
|
|
21
|
+
const Nav = ({ visible = false, hasNewComments, hasNewReports }) => {
|
|
22
|
+
if (!visible) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
22
26
|
return <SubNav ariaLabel="Comments sub nav">
|
|
23
27
|
<SubNavHeader label={getMessage('plugin.name')} />
|
|
24
28
|
<SubNavSections>
|
|
@@ -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
|
|